Skip to content
4 changes: 2 additions & 2 deletions api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -925,7 +925,7 @@
"ExecutorExecutionBinding": "type cbd52e7e2eb8",
"ExecutorFactory": "type 0d6f475ad3d4",
"ExecutorMaterialization": "type ceafe44da26b",
"ExecutorNodeContext": "type 7f86daa88edc",
"ExecutorNodeContext": "type 17a7348871c5",
"ExecutorProgress": "type c91983468166",
"ExecutorProgressEvent": "type 19b8d5b7224e",
"ExecutorRegistry": "type 2abca0065370",
Expand Down Expand Up @@ -1306,7 +1306,7 @@
"UntrackedCopyStats": "type 3d60a38b88e2",
"UsageEvent": "type 110014e66fac",
"VERIFY_TAIL_CHARS": "value 95999f4bd438",
"ValidationCtx": "type 45da43f45d39",
"ValidationCtx": "type 4905637394bb",
"Validator": "type c67346b2cb69",
"VerifierEnvironmentOptions": "type 8381e1b6dc2e",
"Verify": "type 4e1beeeba7ee",
Expand Down
8 changes: 8 additions & 0 deletions docs/agent-managed-compute/reliability.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ Omit `coordination.port` to allocate a separate port for each concurrent manager
Runtime does not provision a proxy or tunnel.
Remote public endpoints require HTTPS.

Before provider admission, Runtime checks each configured public endpoint with authenticated `initialize` and `tools/list` requests.
The returned grants must match that manager's exact tool names.
The check takes at most 10 seconds, or `coordination.requestTimeoutMs` when shorter, and stops on manager cancellation.
Responses are limited to 1 MiB independently of the incoming request limit, and redirects are refused.
Failure closes the listener and reports a credential-free cause before inference starts.
This verifies the operator's public route; it does not establish reachability from the provider's network.
Omitting `coordination.publicUrl` preserves local-only startup without this network check.

For same-host coordinator restart, configure `authentication.signingKeys` with an active key ID and a secret key map.
Keep the public URL, run ID, actor ID, tool grants, and verification key stable until the retained credential expires.
Stable keys support resumed coordination within the original scope deadline and any explicit credential expiry.
Expand Down
17 changes: 17 additions & 0 deletions docs/api/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -4186,6 +4186,8 @@ OPT-IN executable score for this worker, with the SAME contract the sandbox seam
has: `validate` runs while the environment is still alive, so `ValidationCtx.box` can read
files and run commands in the environment it is scoring. Every other supervised hook fires
after teardown and can only read the artifact.
`ValidationCtx.node` identifies the supervised node, including its recursion depth, so a
shared validator can apply a root-only contract without applying it to nested managers.

The verdict becomes the settled artifact's verdict. Absent, nothing changes and the leaf falls
back to its own settle verdict.
Expand Down Expand Up @@ -17717,6 +17719,8 @@ OPT-IN executable score for this worker, with the SAME contract the sandbox seam
has: `validate` runs while the environment is still alive, so `ValidationCtx.box` can read
files and run commands in the environment it is scoring. Every other supervised hook fires
after teardown and can only read the artifact.
`ValidationCtx.node` identifies the supervised node, including its recursion depth, so a
shared validator can apply a root-only contract without applying it to nested managers.

The verdict becomes the settled artifact's verdict. Absent, nothing changes and the leaf falls
back to its own settle verdict.
Expand Down Expand Up @@ -21443,6 +21447,12 @@ Kernel-owned context for the concrete supervised node a factory is constructing.

> `readonly` **nodeId**: `string`

##### depth?

> `readonly` `optional` **depth?**: `number`

Recursion depth supplied by Runtime scopes (root = 0). Standalone callers may omit it.

##### attemptId

> `readonly` **attemptId**: `string`
Expand Down Expand Up @@ -24234,6 +24244,13 @@ Live sandbox for this iteration. Validators that need execution-grounded
evidence can inspect files or run commands here instead of forcing callers
to bypass the loop kernel with raw Sandbox SDK orchestration.

##### node?

> `readonly` `optional` **node?**: [`ExecutorNodeContext`](#executornodecontext)

Detached, immutable node identity supplied by supervised provider execution.
Runtime scopes include depth (root = 0); standalone execution may omit this context.

##### signal

> **signal**: `AbortSignal`
Expand Down
1 change: 1 addition & 0 deletions scripts/check-model-execution-boundary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const dynamicNonModelFetchOwners = new Set([
'src/platform/auth.ts',
'src/platform/integrations.ts',
'src/runtime/mcp-environment.ts',
'src/runtime/supervise/coordination-preflight.ts',
'bench/src/research-shot.ts',
'bench/src/search-tool.ts',
'bench/src/benchmarks/aec-bench.ts',
Expand Down
8 changes: 8 additions & 0 deletions scripts/check-model-execution-boundary.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ describe('model execution boundary source check', () => {
expect(violations[0]?.location).toBe('3:7')
})

it('permits dynamic MCP preflight routes without admitting named model endpoints', () => {
const path = 'src/runtime/supervise/coordination-preflight.ts'
expect(checkJavaScript(path, 'await fetch(input.url)')).toEqual([])
expect(
checkJavaScript(path, "await fetch('https://router.tangle.tools/v1/chat/completions')"),
).toHaveLength(1)
})

it('rejects qualified and aliased global fetch calls', () => {
expect(
checkJavaScript(
Expand Down
17 changes: 16 additions & 1 deletion scripts/release-cohort.test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { rangeAdmits } from './lib/packed-package-test.mjs'
import {
assertReleaseCohortArtifacts,
readReleaseCohort,
Expand All @@ -17,6 +19,19 @@ afterEach(() => {
})

describe('release cohort', () => {
it.each(Object.values(readReleaseCohort().packages))(
'selects $name@$version within its workspace catalog range',
({ name, version }) => {
const { catalog } = parse(
readFileSync(new URL('../pnpm-workspace.yaml', import.meta.url), 'utf8'),
)
expect(
rangeAdmits(catalog[name], version),
`${name}@${version} must be admitted by catalog range ${catalog[name]}`,
).toBe(true)
},
)

it('reads one exact source identity for every first-party dependency', () => {
const cohort = readReleaseCohort()
expect(Object.keys(cohort.packages)).toEqual([
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/tool-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { JsonRpcMessage, JsonRpcResponse, McpToolDescriptor, McpTransport }

export type { JsonRpcMessage, JsonRpcResponse, McpToolDescriptor, McpTransport } from './protocol'

const PROTOCOL_VERSION = '2024-11-05'
export const PROTOCOL_VERSION = '2024-11-05'

/** @experimental */
export interface StdioToolServerOptions {
Expand Down
12 changes: 10 additions & 2 deletions src/runtime/environment-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ import type {
ExecutorExecutionBinding,
ExecutorFactory,
ExecutorMaterialization,
ExecutorNodeContext,
ExecutorResult,
Runtime,
Spend,
Expand Down Expand Up @@ -558,6 +559,8 @@ export interface ProviderExecutorOptions {
* has: `validate` runs while the environment is still alive, so `ValidationCtx.box` can read
* files and run commands in the environment it is scoring. Every other supervised hook fires
* after teardown and can only read the artifact.
* `ValidationCtx.node` identifies the supervised node, including its recursion depth, so a
* shared validator can apply a root-only contract without applying it to nested managers.
*
* The verdict becomes the settled artifact's verdict. Absent, nothing changes and the leaf falls
* back to its own settle verdict.
Expand Down Expand Up @@ -641,6 +644,8 @@ function createProviderExecutor(
placement?: { id: string; digest: string },
): Executor<unknown> {
const controller = new AbortController()
const node =
ctx.node === undefined ? undefined : detachedSnapshot(ctx.node, 'provider executor node')

let environment: AgentEnvironment | undefined
let artifact: ExecutorResult<unknown> | undefined
Expand All @@ -665,8 +670,8 @@ function createProviderExecutor(
'provider placement: profileForCreate cannot change the selected profile',
)
}
const executionId = retention?.executionId ?? ctx.node?.nodeId ?? `provider-run-${randomUUID()}`
const attemptId = ctx.node?.attemptId ?? newExecutionAttemptId(executionId)
const executionId = retention?.executionId ?? node?.nodeId ?? `provider-run-${randomUUID()}`
const attemptId = node?.attemptId ?? newExecutionAttemptId(executionId)
const trace = createPushTraceSource({ runId: executionId })
const providerModel = concreteProfileModel(createProfile)
// The provider owns the model call inside its environment and the create input carries no
Expand Down Expand Up @@ -726,6 +731,7 @@ function createProviderExecutor(
createProfile,
task,
signal: linked.signal,
...(node === undefined ? {} : { node }),
options,
retention,
executionId,
Expand Down Expand Up @@ -907,6 +913,7 @@ interface StreamProviderExecutorArgs {
createProfile: AgentProfile
task: unknown
signal: AbortSignal
node?: ExecutorNodeContext
options: ProviderExecutorOptions
retention?: RetainedExecutorContext
executionId: string
Expand Down Expand Up @@ -1085,6 +1092,7 @@ async function* streamProviderExecutor(
// hook fires after teardown and can only read the artifact.
const verdict = await args.options.validator?.validate(result, {
iteration: 0,
...(args.node === undefined ? {} : { node: args.node }),
box: environmentAsSandboxInstance(environment, {
requireTerminalEvent: args.options.requireTerminalEvent ?? true,
}),
Expand Down
16 changes: 15 additions & 1 deletion src/runtime/supervise/coordination-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
coordinationHttpHandler,
coordinationHttpLimits,
} from './coordination-http'
import { preflightPublicCoordination } from './coordination-preflight'
import { singleFlightTools } from './single-flight-tools'

export type { CoordinationHttpAudit, CoordinationHttpOptions } from './coordination-http'
Expand Down Expand Up @@ -567,8 +568,21 @@ export async function serveCoordinationMcp(
audiences.add(publicAddress.host)
if (host === '0.0.0.0' || host === '::') audiences.add(`127.0.0.1:${port}`)
paths.add(publicAddress.pathname)
if (configured !== undefined) {
await preflightPublicCoordination({
url,
headers,
signal: opts.scope.signal,
requestTimeoutMs,
toolNames: selectedNames,
})
}
} catch (error) {
await new Promise<void>((resolve) => server.close(() => resolve()))
closed = true
await new Promise<void>((resolve) => {
server.close(() => resolve())
server.closeAllConnections()
})
throw error
}

Expand Down
150 changes: 150 additions & 0 deletions src/runtime/supervise/coordination-preflight.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
import { afterEach, describe, expect, it } from 'vitest'
import { preflightPublicCoordination } from './coordination-preflight'

const closes: Array<() => Promise<void>> = []
afterEach(async () => {
await Promise.all(closes.splice(0).map((close) => close()))
})

async function endpoint(handler: (request: IncomingMessage, response: ServerResponse) => void) {
const server = createServer(handler)
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve))
closes.push(
() =>
new Promise((resolve) => {
server.closeAllConnections()
server.close(() => resolve())
}),
)
const address = server.address()
if (!address || typeof address === 'string') throw new Error('test endpoint did not bind')
return `http://127.0.0.1:${address.port}`
}

function inspect(url: string, signal = new AbortController().signal, requestTimeoutMs = 1000) {
return preflightPublicCoordination({
url,
headers: { Authorization: 'Bearer private-test-credential' },
signal,
requestTimeoutMs,
toolNames: ['stop'],
})
}

describe('public coordination preflight', () => {
it('performs authenticated initialization and checks the exact granted tool names', async () => {
const methods: string[] = []
const url = await endpoint((request, response) => {
expect(request.headers.authorization).toBe('Bearer private-test-credential')
let body = ''
request.on('data', (chunk) => {
body += String(chunk)
})
request.on('end', () => {
const rpc = JSON.parse(body)
methods.push(rpc.method)
response.end(
JSON.stringify({
jsonrpc: '2.0',
id: rpc.id,
result:
rpc.method === 'initialize'
? {
protocolVersion: '2024-11-05',
serverInfo: { name: 'coordination' },
capabilities: { tools: {} },
}
: { tools: [{ name: 'stop' }] },
}),
)
})
})
await inspect(url)
expect(methods).toEqual(['initialize', 'tools/list'])
})

it.each([401, 403, 404, 503])(
'refuses HTTP %s without retaining echoed secrets',
async (status) => {
const url = await endpoint((_request, response) => {
response.writeHead(status).end('private-test-credential private-response')
})
await expect(inspect(url)).rejects.toThrow(
`coordination public endpoint preflight failed: HTTP ${status}`,
)
try {
await inspect(url)
} catch (error) {
expect(String(error)).not.toContain('private-')
expect(String(error)).not.toContain(url)
expect(error).not.toHaveProperty('cause')
}
},
)

it('refuses redirects instead of forwarding actor credentials to another endpoint', async () => {
let redirected = 0
const target = await endpoint((_request, response) => {
redirected++
response.end('{}')
})
const url = await endpoint((_request, response) =>
response.writeHead(307, { location: target }).end(),
)
await expect(inspect(url)).rejects.toThrow('HTTP 307')
expect(redirected).toBe(0)
})

it.each(['not-json', '{}', '{"jsonrpc":"2.0","id":"other","result":{}}'])(
'refuses malformed MCP response %s',
async (body) => {
const url = await endpoint((_request, response) => response.end(body))
await expect(inspect(url)).rejects.toThrow('invalid MCP response')
},
)

it('bounds a hanging response body and distinguishes timeout from cancellation', async () => {
const url = await endpoint((_request, response) => {
response.writeHead(200)
response.write('{')
})
await expect(inspect(url, undefined, 40)).rejects.toThrow('timed out')
const controller = new AbortController()
const pending = inspect(url, controller.signal)
controller.abort('private-test-credential in a caller reason')
await expect(pending).rejects.toThrow('preflight failed: cancelled')
})

it('bounds response bytes before decoding or retaining an upstream body', async () => {
const url = await endpoint((_request, response) => response.end('x'.repeat(1024 * 1024 + 1)))
await expect(inspect(url)).rejects.toThrow('response too large')
})

it('rejects incomplete or extra tool grants', async () => {
const url = await endpoint((request, response) => {
let body = ''
request.on('data', (chunk) => {
body += String(chunk)
})
request.on('end', () => {
const rpc = JSON.parse(body)
response.end(
JSON.stringify({
jsonrpc: '2.0',
id: rpc.id,
result:
rpc.method === 'initialize'
? {
protocolVersion: '2024-11-05',
serverInfo: { name: 'coordination' },
capabilities: { tools: {} },
}
: { tools: [{ name: 'spawn_worker' }] },
}),
)
})
})
await expect(inspect(url)).rejects.toThrow('coordination tool grants differ')
})
})
Loading
Loading