@nylorun/runtime 0.22.0-beta → 0.24.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +161 -0
- package/README.md +95 -30
- package/dist/adapters/execution/restate.d.ts +15 -3
- package/dist/adapters/execution/restate.js +78 -84
- package/dist/adapters/replication/pgoutput.d.ts +2 -0
- package/dist/adapters/replication/pgoutput.js +6 -3
- package/dist/adapters/sandbox/local.d.ts +9 -1
- package/dist/adapters/sandbox/local.js +16 -4
- package/dist/adapters/sandbox/virtual.js +4 -1
- package/dist/adapters/streams/s2.js +1 -13
- package/dist/api/a2a/endpoint.js +5 -1
- package/dist/api/a2a/routes.d.ts +2 -0
- package/dist/api/a2a/routes.js +2 -2
- package/dist/api/a2a/tasks.js +2 -1
- package/dist/api/ag-ui/endpoint.js +4 -2
- package/dist/api/ag-ui/history.js +4 -1
- package/dist/api/ag-ui/routes.d.ts +4 -1
- package/dist/api/ag-ui/routes.js +11 -10
- package/dist/api/ag-ui/translate.js +3 -2
- package/dist/api/components.d.ts +4953 -1204
- package/dist/api/components.js +25 -16
- package/dist/api/gate/keys.js +7 -1
- package/dist/api/gate/routes.d.ts +6 -0
- package/dist/api/gate/routes.js +39 -3
- package/dist/api/http/app.js +4 -4
- package/dist/api/http/define.d.ts +6 -0
- package/dist/api/http/define.js +13 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/artifacts.js +35 -8
- package/dist/api/http/routes/coverage.d.ts +9 -0
- package/dist/api/http/routes/coverage.js +34 -0
- package/dist/api/http/routes/environments.d.ts +11 -0
- package/dist/api/http/routes/environments.js +283 -0
- package/dist/api/http/routes/mcp.d.ts +1 -1
- package/dist/api/http/routes/mcp.js +2 -2
- package/dist/api/http/routes/me.js +1 -1
- package/dist/api/http/routes/reads.d.ts +45 -9
- package/dist/api/http/routes/reads.js +54 -5
- package/dist/api/http/routes/sessions.js +11 -6
- package/dist/api/http/routes/tenant.js +32 -10
- package/dist/api/openapi.js +31 -26
- package/dist/artifacts/disk.d.ts +59 -0
- package/dist/artifacts/disk.js +563 -0
- package/dist/artifacts/export.d.ts +1 -1
- package/dist/artifacts/export.js +2 -3
- package/dist/artifacts/folders.d.ts +6 -3
- package/dist/artifacts/folders.js +17 -12
- package/dist/artifacts/service.d.ts +2 -1
- package/dist/artifacts/service.js +6 -1
- package/dist/artifacts/workspace.d.ts +1 -1
- package/dist/artifacts/workspace.js +1 -1
- package/dist/core/flow-host.d.ts +3 -22
- package/dist/core/flow-host.js +11 -37
- package/dist/core/provider.d.ts +3 -2
- package/dist/core/provider.js +5 -37
- package/dist/core/sandbox-routes.d.ts +2 -21
- package/dist/core/sandbox-routes.js +10 -43
- package/dist/execution/types.d.ts +11 -9
- package/dist/execution/types.js +5 -7
- package/dist/gates/contract.d.ts +3 -24
- package/dist/gates/contract.js +1 -36
- package/dist/gates/egress-credentials.d.ts +9 -0
- package/dist/gates/egress-credentials.js +11 -4
- package/dist/gates/egress.d.ts +28 -1
- package/dist/gates/egress.js +115 -10
- package/dist/gates/fixture-model.d.ts +28 -0
- package/dist/gates/fixture-model.js +92 -0
- package/dist/gates/handler.js +4 -4
- package/dist/gates/http-tool.d.ts +6 -2
- package/dist/gates/http-tool.js +3 -5
- package/dist/gates/in-process.d.ts +5 -0
- package/dist/gates/in-process.js +12 -1
- package/dist/gates/journal.d.ts +30 -0
- package/dist/gates/journal.js +52 -0
- package/dist/gates/mcp-handler.d.ts +4 -0
- package/dist/gates/mcp-handler.js +5 -1
- package/dist/gates/meter.d.ts +10 -2
- package/dist/gates/meter.js +20 -5
- package/dist/gates/policy.d.ts +49 -0
- package/dist/gates/policy.js +134 -0
- package/dist/gates/tenant-vaults.d.ts +7 -7
- package/dist/gates/tenant-vaults.js +5 -5
- package/dist/gates/tool-calls.d.ts +10 -5
- package/dist/gates/tool-calls.js +28 -10
- package/dist/gates/tool-client.d.ts +8 -0
- package/dist/gates/tool-client.js +27 -11
- package/dist/gates/tool-contract.d.ts +10 -68
- package/dist/gates/tool-contract.js +1 -85
- package/dist/gates/tool-gate.d.ts +1 -1
- package/dist/harness/calls.d.ts +12 -5
- package/dist/harness/calls.js +21 -12
- package/dist/harness/executors.js +12 -9
- package/dist/harness/main.js +18 -5
- package/dist/harness/pod.d.ts +34 -0
- package/dist/harness/pod.js +41 -0
- package/dist/harness/service.d.ts +0 -1
- package/dist/harness/service.js +13 -5
- package/dist/harness/setup.d.ts +27 -0
- package/dist/harness/setup.js +150 -0
- package/dist/harness/ws-client.js +3 -2
- package/dist/harness/ws-port.d.ts +1 -5
- package/dist/harness/ws-port.js +1 -5
- package/dist/harness-api/in-process.js +3 -3
- package/dist/harness-api/record.d.ts +15 -1
- package/dist/harness-api/record.js +174 -44
- package/dist/harness-api/redact.d.ts +3 -0
- package/dist/harness-api/redact.js +45 -0
- package/dist/harness-api/server.d.ts +8 -0
- package/dist/harness-api/server.js +93 -13
- package/dist/harness-api/start.d.ts +10 -3
- package/dist/harness-api/start.js +40 -10
- package/dist/harness-api/tool-journal.d.ts +96 -0
- package/dist/harness-api/tool-journal.js +102 -0
- package/dist/harness-api/tool-status.d.ts +13 -0
- package/dist/harness-api/tool-status.js +26 -0
- package/dist/harness-api/workspace.d.ts +6 -3
- package/dist/harness-api/workspace.js +27 -11
- package/dist/harness-api/ws-server.d.ts +11 -3
- package/dist/harness-api/ws-server.js +94 -15
- package/dist/host/execution.d.ts +8 -6
- package/dist/host/gates.d.ts +2 -1
- package/dist/host/gates.js +10 -2
- package/dist/host/main.js +64 -21
- package/dist/host/operate.js +67 -5
- package/dist/host/stack-config.d.ts +70 -0
- package/dist/host/stack-config.js +67 -1
- package/dist/infra/execution.d.ts +18 -1
- package/dist/infra/execution.js +28 -4
- package/dist/infra/index.d.ts +1 -1
- package/dist/keys/client.js +4 -1
- package/dist/keys/keys.d.ts +5 -5
- package/dist/keys/keys.js +5 -7
- package/dist/management-openapi.json +999 -2211
- package/dist/mcp/connect.d.ts +25 -20
- package/dist/mcp/connect.js +29 -30
- package/dist/mcp/pool.d.ts +2 -2
- package/dist/mcp/pool.js +1 -1
- package/dist/mcp/preview.d.ts +3 -3
- package/dist/mcp/preview.js +10 -20
- package/dist/mcp/snapshot.d.ts +5 -11
- package/dist/mcp/snapshot.js +6 -7
- package/dist/model/pi-model.js +8 -0
- package/dist/openapi.json +6955 -2526
- package/dist/reads/types.d.ts +16 -3
- package/dist/record/append.d.ts +3 -1
- package/dist/record/append.js +1 -0
- package/dist/record/envelope.d.ts +5 -1
- package/dist/record/envelope.js +7 -3
- package/dist/record/environment.d.ts +25 -0
- package/dist/record/environment.js +44 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/retry.d.ts +21 -0
- package/dist/retry.js +34 -0
- package/dist/sandbox/disk-fs.d.ts +95 -0
- package/dist/sandbox/disk-fs.js +206 -0
- package/dist/sandbox/join.d.ts +17 -18
- package/dist/sandbox/join.js +46 -20
- package/dist/sandbox/manager.d.ts +16 -8
- package/dist/sandbox/manager.js +33 -17
- package/dist/sandbox/placement.d.ts +13 -12
- package/dist/sandbox/placement.js +29 -12
- package/dist/sandbox/pods/client.d.ts +6 -1
- package/dist/sandbox/pods/lifecycle.d.ts +28 -27
- package/dist/sandbox/pods/lifecycle.js +14 -26
- package/dist/sandbox/pods/reconcile.js +21 -15
- package/dist/sandbox/pods/spec.d.ts +15 -13
- package/dist/sandbox/pods/spec.js +16 -20
- package/dist/sandbox/records.d.ts +2 -2
- package/dist/sandbox/records.js +3 -3
- package/dist/sandbox/resolve.d.ts +35 -27
- package/dist/sandbox/resolve.js +66 -41
- package/dist/sandbox/session-sandbox.js +1 -1
- package/dist/sandbox/setup.d.ts +61 -0
- package/dist/sandbox/setup.js +123 -0
- package/dist/sandbox/share.d.ts +11 -21
- package/dist/sandbox/share.js +12 -41
- package/dist/sandbox/tenant-config.d.ts +12 -14
- package/dist/sandbox/tenant-config.js +12 -17
- package/dist/sandbox/types.d.ts +6 -0
- package/dist/store/postgres/control.js +8 -1
- package/dist/store/postgres/drizzle/0018_wake_outbox_retries.sql +3 -0
- package/dist/store/postgres/drizzle/0019_sandbox_signal_outbox.sql +18 -0
- package/dist/store/postgres/drizzle/0020_environments.sql +63 -0
- package/dist/store/postgres/drizzle/0021_disk_versions.sql +7 -0
- package/dist/store/postgres/drizzle/0022_model_usage_timing.sql +5 -0
- package/dist/store/postgres/drizzle/0023_artifact_pages.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +42 -0
- package/dist/store/postgres/reads.js +117 -17
- package/dist/store/postgres/record-writer.d.ts +4 -4
- package/dist/store/postgres/record-writer.js +9 -9
- package/dist/store/postgres/record.d.ts +7 -0
- package/dist/store/postgres/record.js +23 -2
- package/dist/store/postgres/schema.d.ts +1213 -779
- package/dist/store/postgres/schema.js +136 -76
- package/dist/store/postgres/store.js +283 -117
- package/dist/store/types.d.ts +206 -88
- package/dist/streams/relay/core.js +1 -13
- package/dist/tenant/advance.d.ts +7 -6
- package/dist/tenant/advance.js +121 -49
- package/dist/tenant/artifact-tool.js +3 -4
- package/dist/tenant/commands.d.ts +15 -2
- package/dist/tenant/commands.js +94 -14
- package/dist/tenant/context.d.ts +67 -33
- package/dist/tenant/context.js +4 -15
- package/dist/tenant/control.d.ts +8 -1
- package/dist/tenant/control.js +2 -0
- package/dist/tenant/effects.d.ts +1 -1
- package/dist/tenant/effects.js +37 -20
- package/dist/tenant/environments.d.ts +167 -0
- package/dist/tenant/environments.js +823 -0
- package/dist/tenant/harness-tokens.d.ts +21 -0
- package/dist/tenant/harness-tokens.js +61 -0
- package/dist/tenant/host-token.d.ts +1 -1
- package/dist/tenant/host-token.js +2 -4
- package/dist/tenant/identity-file.d.ts +4 -4
- package/dist/tenant/identity-file.js +17 -8
- package/dist/tenant/issuers.js +3 -3
- package/dist/tenant/principals.js +1 -1
- package/dist/tenant/reset.js +3 -3
- package/dist/tenant/runtime.d.ts +6 -1
- package/dist/tenant/runtime.js +30 -4
- package/dist/tenant/scheduler.d.ts +7 -0
- package/dist/tenant/scheduler.js +38 -7
- package/dist/tenant/session-streams.d.ts +0 -1
- package/dist/tenant/session-streams.js +1 -13
- package/dist/tenant/session.d.ts +3 -3
- package/dist/tenant/session.js +0 -14
- package/dist/tenant/sessions.d.ts +13 -13
- package/dist/tenant/sessions.js +61 -138
- package/dist/tenant/signing-keys.d.ts +5 -1
- package/dist/tenant/status.js +3 -3
- package/dist/tenant/streams.js +14 -29
- package/dist/tenant/sweep.d.ts +39 -12
- package/dist/tenant/sweep.js +168 -63
- package/dist/tenant/transcript.d.ts +210 -20
- package/dist/tenant/transcript.js +62 -11
- package/dist/tenant/turn-timing.d.ts +43 -0
- package/dist/tenant/turn-timing.js +39 -0
- package/dist/tenant/types.d.ts +2 -0
- package/dist/traces/ids.d.ts +42 -0
- package/dist/traces/ids.js +85 -0
- package/dist/traces/key.d.ts +6 -0
- package/dist/traces/key.js +37 -0
- package/dist/traces/map.d.ts +113 -0
- package/dist/traces/map.js +317 -0
- package/dist/traces/options.d.ts +7 -0
- package/dist/traces/options.js +1 -0
- package/dist/traces/otlp.d.ts +42 -0
- package/dist/traces/otlp.js +84 -0
- package/dist/traces/projector.d.ts +58 -0
- package/dist/traces/projector.js +200 -0
- package/dist/traces/read.d.ts +23 -0
- package/dist/traces/read.js +16 -0
- package/dist/vault/coverage.d.ts +26 -0
- package/dist/vault/coverage.js +64 -0
- package/dist/vault/error.d.ts +11 -1
- package/dist/vault/error.js +5 -1
- package/dist/vault/service.d.ts +47 -48
- package/dist/vault/service.js +321 -382
- package/dist/vault/sources.d.ts +5 -16
- package/dist/vault/sources.js +6 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +2 -2
- package/package.json +14 -13
- package/dist/api/http/routes/sandboxes.d.ts +0 -9
- package/dist/api/http/routes/sandboxes.js +0 -153
- package/dist/api/http/routes/vaults.d.ts +0 -10
- package/dist/api/http/routes/vaults.js +0 -139
- package/dist/record/sandbox.d.ts +0 -25
- package/dist/record/sandbox.js +0 -43
- package/dist/tenant/sandboxes.d.ts +0 -122
- package/dist/tenant/sandboxes.js +0 -562
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mcpToolOf } from "../mcp/snapshot.js";
|
|
2
|
+
import { canonical } from "../store/canonical.js";
|
|
3
|
+
import { httpToolRefOf } from "./http-tool.js";
|
|
4
|
+
export function journalCheck(vaults) {
|
|
5
|
+
return {
|
|
6
|
+
async check(tenantId, claims, key, call) {
|
|
7
|
+
const store = (await vaults.open(tenantId)).store;
|
|
8
|
+
const refusal = await store.tx(async (t) => {
|
|
9
|
+
const row = await t.get("effects", key);
|
|
10
|
+
if (!row)
|
|
11
|
+
return `No journaled call has the effect id '${key}'`;
|
|
12
|
+
const request = row.request;
|
|
13
|
+
if (request.kind !== "tool" ||
|
|
14
|
+
request.sessionId !== claims.sessionId ||
|
|
15
|
+
request.turnId !== claims.turnId)
|
|
16
|
+
return `Effect '${key}' is not a tool call of this run's turn`;
|
|
17
|
+
if (row.status !== "invoking")
|
|
18
|
+
return `Effect '${key}' is not running: it is ${row.status}`;
|
|
19
|
+
if (call.kind === "http") {
|
|
20
|
+
const { sessionId: _journaled, ...expected } = httpToolRefOf(request);
|
|
21
|
+
const { sessionId: _named, ...named } = call.tool;
|
|
22
|
+
if (canonical(expected) !== canonical(named))
|
|
23
|
+
return `Effect '${key}' calls another tool`;
|
|
24
|
+
if (canonical(request.input ?? null) !== canonical(call.input ?? null))
|
|
25
|
+
return `Effect '${key}' was journaled with other input`;
|
|
26
|
+
return held(row);
|
|
27
|
+
}
|
|
28
|
+
const session = await t.get("sessions", request.sessionId);
|
|
29
|
+
const tool = mcpToolOf(session?.mcpSnapshot, request);
|
|
30
|
+
if (!tool ||
|
|
31
|
+
tool.capabilityId !== call.server.capabilityId ||
|
|
32
|
+
tool.serverName !== call.server.serverName ||
|
|
33
|
+
(tool.agentId ?? null) !== (call.server.agentId ?? null) ||
|
|
34
|
+
tool.serverToolName !== call.name)
|
|
35
|
+
return `Effect '${key}' calls another tool`;
|
|
36
|
+
if (canonical(argumentsOf(request.input)) !== canonical(call.arguments))
|
|
37
|
+
return `Effect '${key}' was journaled with other input`;
|
|
38
|
+
return held(row);
|
|
39
|
+
});
|
|
40
|
+
return typeof refusal === "string" ? { refused: refusal } : refusal;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** A journaled call: its span context, when core recorded one (R7.4). */
|
|
45
|
+
function held(row) {
|
|
46
|
+
const traceparent = row.traceparent;
|
|
47
|
+
return typeof traceparent === "string" ? { traceparent } : {};
|
|
48
|
+
}
|
|
49
|
+
/** A tool call's input as `tools/call` arguments (`callMcpTool`): an object, else none. */
|
|
50
|
+
function argumentsOf(input) {
|
|
51
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
52
|
+
}
|
|
@@ -29,6 +29,10 @@ export interface McpHandler {
|
|
|
29
29
|
effectId: string;
|
|
30
30
|
name: string;
|
|
31
31
|
arguments: Record<string, unknown>;
|
|
32
|
+
/** The call's `Idempotency-Key`, sent to the server in `_meta` (R5.5). */
|
|
33
|
+
key?: string;
|
|
34
|
+
/** The call's span context from its journal row (R7.4), sent as `traceparent`. */
|
|
35
|
+
traceparent?: string;
|
|
32
36
|
}, signal: AbortSignal): Promise<McpAnswer<Record<string, unknown>>>;
|
|
33
37
|
close(server: McpServerRef): Promise<void>;
|
|
34
38
|
/** Closes connections idle past the timeout. */
|
|
@@ -114,7 +114,11 @@ export function createMcpHandler(options) {
|
|
|
114
114
|
async function callOnce(tenantId, request, signal) {
|
|
115
115
|
for (let attempt = 0;; attempt += 1) {
|
|
116
116
|
try {
|
|
117
|
-
return await use(tenantId, request.server, (entry) => entry.connection.client.callTool({ name: request.name, arguments: request.arguments }, {
|
|
117
|
+
return await use(tenantId, request.server, (entry) => entry.connection.client.callTool({ name: request.name, arguments: request.arguments }, {
|
|
118
|
+
signal,
|
|
119
|
+
...(request.key === undefined ? {} : { key: request.key }),
|
|
120
|
+
...(request.traceparent === undefined ? {} : { traceparent: request.traceparent }),
|
|
121
|
+
}), true);
|
|
118
122
|
}
|
|
119
123
|
catch (error) {
|
|
120
124
|
if (!(error instanceof McpConnectionStale) || attempt > 0)
|
package/dist/gates/meter.d.ts
CHANGED
|
@@ -13,8 +13,16 @@ export interface MeterOptions {
|
|
|
13
13
|
readonly now?: () => Date;
|
|
14
14
|
}
|
|
15
15
|
export declare function createMeter(options: MeterOptions): Meter;
|
|
16
|
-
/**
|
|
17
|
-
export
|
|
16
|
+
/** When the gate sent a call and when its answer was complete (R7.1). */
|
|
17
|
+
export interface CallTiming {
|
|
18
|
+
readonly startedAt: Date;
|
|
19
|
+
readonly endedAt: Date;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The ledger row of an outcome that carries usage; undefined for a failure. With `timing`, the
|
|
23
|
+
* row keeps the gate's start and end of the call, and the adapter's `firstTokenMs`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function usageRow(request: ModelGateRequest, outcome: ModelGateOutcome, at: Date, timing?: CallTiming): ModelUsageWrite | undefined;
|
|
18
26
|
/**
|
|
19
27
|
* Why the budget refuses another call, or undefined. Calls in flight count at the scope's
|
|
20
28
|
* average cost per recorded call.
|
package/dist/gates/meter.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
* the scope at the scope's average cost per call, reaches its limit. Calls in a turn run one
|
|
9
9
|
* at a time, so a scope overspends by at most one call per concurrent call. The refusal is a
|
|
10
10
|
* `budget_exhausted` failure, which the turn does not retry.
|
|
11
|
+
*
|
|
12
|
+
* The row also keeps the gate's own timing of the call (R7.1): `now()` before the call and
|
|
13
|
+
* after it, and the adapter's `firstTokenMs` (`evidence.extras`), so a model call's span is
|
|
14
|
+
* what the gate measured.
|
|
11
15
|
*/
|
|
12
16
|
import { randomUUID } from "node:crypto";
|
|
13
17
|
import { failure } from "../model/classify.js";
|
|
@@ -60,8 +64,9 @@ export function createMeter(options) {
|
|
|
60
64
|
for (const key of keys)
|
|
61
65
|
inflight.set(key, (inflight.get(key) ?? 0) + 1);
|
|
62
66
|
try {
|
|
67
|
+
const startedAt = now();
|
|
63
68
|
const outcome = await call();
|
|
64
|
-
await record(store, request, outcome);
|
|
69
|
+
await record(store, request, outcome, { startedAt, endedAt: now() });
|
|
65
70
|
return outcome;
|
|
66
71
|
}
|
|
67
72
|
finally {
|
|
@@ -75,8 +80,8 @@ export function createMeter(options) {
|
|
|
75
80
|
}
|
|
76
81
|
},
|
|
77
82
|
};
|
|
78
|
-
async function record(store, request, outcome) {
|
|
79
|
-
const row = usageRow(request, outcome,
|
|
83
|
+
async function record(store, request, outcome, timing) {
|
|
84
|
+
const row = usageRow(request, outcome, timing.endedAt, timing);
|
|
80
85
|
if (!row)
|
|
81
86
|
return;
|
|
82
87
|
try {
|
|
@@ -96,11 +101,15 @@ export function createMeter(options) {
|
|
|
96
101
|
}
|
|
97
102
|
}
|
|
98
103
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
104
|
+
/**
|
|
105
|
+
* The ledger row of an outcome that carries usage; undefined for a failure. With `timing`, the
|
|
106
|
+
* row keeps the gate's start and end of the call, and the adapter's `firstTokenMs`.
|
|
107
|
+
*/
|
|
108
|
+
export function usageRow(request, outcome, at, timing) {
|
|
101
109
|
if (typeof outcome !== "object" || "kind" in outcome || !outcome.usage)
|
|
102
110
|
return undefined;
|
|
103
111
|
const usage = outcome.usage;
|
|
112
|
+
const firstTokenMs = outcome.evidence?.extras?.firstTokenMs;
|
|
104
113
|
const producer = producerOf(outcome);
|
|
105
114
|
const inputTokens = count(usage.inputTokens);
|
|
106
115
|
const outputTokens = count(usage.outputTokens);
|
|
@@ -126,6 +135,12 @@ export function usageRow(request, outcome, at) {
|
|
|
126
135
|
reasoningTokens: count(usage.reasoningTokens),
|
|
127
136
|
costUsd: Number.isFinite(usage.costUsd) && usage.costUsd > 0 ? usage.costUsd : 0,
|
|
128
137
|
createdAt: at.toISOString(),
|
|
138
|
+
...(timing
|
|
139
|
+
? { startedAt: timing.startedAt.toISOString(), endedAt: timing.endedAt.toISOString() }
|
|
140
|
+
: {}),
|
|
141
|
+
...(typeof firstTokenMs === "number" && Number.isFinite(firstTokenMs) && firstTokenMs >= 0
|
|
142
|
+
? { firstTokenMs: Math.round(firstTokenMs) }
|
|
143
|
+
: {}),
|
|
129
144
|
};
|
|
130
145
|
}
|
|
131
146
|
/** The ledger rows a budget counts for `request`; undefined when it doesn't apply. */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool policy (R5 step 2). Every MCP and HTTP tool call of every harness is decided as core
|
|
3
|
+
* journals it (`harness-api/record.ts`), which each harness must do before it sends a call:
|
|
4
|
+
* `deny` fails it to the model as `tool.denied`, unsent; `ask` holds the row until a person
|
|
5
|
+
* approves it on the session; `allow` runs it. The Tool Gate sends only a call the journal holds
|
|
6
|
+
* running (`journal.ts`), so a held or refused call cannot run, whichever harness asks.
|
|
7
|
+
*
|
|
8
|
+
* The rules are the Tenant's (`PUT /v1/tenant/tool-policy`, the `tool-policy` setting). A rule
|
|
9
|
+
* matches a call when every selector it sets matches; the strictest matching rule decides
|
|
10
|
+
* (`deny`, then `ask`, then `allow`), and a call no rule matches runs. A session pinned to a
|
|
11
|
+
* manifest saved before R5 step 2 keeps its `approval: "always"` as an ask: no session loses
|
|
12
|
+
* the approval it was opened with. A policy that cannot be read refuses every call.
|
|
13
|
+
*/
|
|
14
|
+
import { type ToolPolicy, type ToolRule } from "@nylorun/core/contracts";
|
|
15
|
+
import type { AgentManifest } from "@nylorun/core/define";
|
|
16
|
+
import type { HostEffect } from "@nylorun/harness/run";
|
|
17
|
+
import { type McpSnapshot } from "../mcp/snapshot.js";
|
|
18
|
+
import type { Tx } from "../store/types.js";
|
|
19
|
+
import type { Session } from "../tenant/context.js";
|
|
20
|
+
/** The `tenant_settings` key the policy is kept under. */
|
|
21
|
+
export declare const TOOL_POLICY_SETTING = "tool-policy";
|
|
22
|
+
/** A call as the policy reads it. */
|
|
23
|
+
export interface PolicyCall {
|
|
24
|
+
readonly kind: "mcp" | "http";
|
|
25
|
+
/** The name the model calls the tool by; a flow stage's key. */
|
|
26
|
+
readonly tool: string;
|
|
27
|
+
/** The MCP server's name. */
|
|
28
|
+
readonly server?: string;
|
|
29
|
+
/** The agent that calls it. */
|
|
30
|
+
readonly agent: string;
|
|
31
|
+
readonly input: unknown;
|
|
32
|
+
}
|
|
33
|
+
export type PolicyDecision = {
|
|
34
|
+
readonly effect: "allow";
|
|
35
|
+
} | {
|
|
36
|
+
readonly effect: "ask" | "deny";
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
};
|
|
39
|
+
/** The Tenant's policy; an empty one when none was set. Throws when it cannot be read. */
|
|
40
|
+
export declare function readToolPolicy(t: Pick<Tx, "getSetting">): Promise<ToolPolicy>;
|
|
41
|
+
/**
|
|
42
|
+
* The decision for `request`, a session's MCP or HTTP tool call: the Tenant's rules and the
|
|
43
|
+
* pinned manifest's `approval`. A policy that cannot be read refuses the call.
|
|
44
|
+
*/
|
|
45
|
+
export declare function toolCallDecision(t: Pick<Tx, "getSetting">, s: Session, request: HostEffect, kind: "mcp" | "http"): Promise<PolicyDecision>;
|
|
46
|
+
/** Decides `call` by `rules`; `pinnedAsk` adds the ask of a manifest saved before R5 step 2. */
|
|
47
|
+
export declare function decide(rules: readonly ToolRule[], call: PolicyCall, pinnedAsk?: boolean): PolicyDecision;
|
|
48
|
+
/** The pinned `approval: "always"` of the call's tool, in a manifest saved before R5 step 2. */
|
|
49
|
+
export declare function pinnedApproval(manifest: AgentManifest, snapshot: McpSnapshot | undefined, request: HostEffect): boolean;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool policy (R5 step 2). Every MCP and HTTP tool call of every harness is decided as core
|
|
3
|
+
* journals it (`harness-api/record.ts`), which each harness must do before it sends a call:
|
|
4
|
+
* `deny` fails it to the model as `tool.denied`, unsent; `ask` holds the row until a person
|
|
5
|
+
* approves it on the session; `allow` runs it. The Tool Gate sends only a call the journal holds
|
|
6
|
+
* running (`journal.ts`), so a held or refused call cannot run, whichever harness asks.
|
|
7
|
+
*
|
|
8
|
+
* The rules are the Tenant's (`PUT /v1/tenant/tool-policy`, the `tool-policy` setting). A rule
|
|
9
|
+
* matches a call when every selector it sets matches; the strictest matching rule decides
|
|
10
|
+
* (`deny`, then `ask`, then `allow`), and a call no rule matches runs. A session pinned to a
|
|
11
|
+
* manifest saved before R5 step 2 keeps its `approval: "always"` as an ask: no session loses
|
|
12
|
+
* the approval it was opened with. A policy that cannot be read refuses every call.
|
|
13
|
+
*/
|
|
14
|
+
import { ToolPolicySchema } from "@nylorun/core/contracts";
|
|
15
|
+
import { mcpToolOf, mcpToolSettingsIn } from "../mcp/snapshot.js";
|
|
16
|
+
import { canonical } from "../store/canonical.js";
|
|
17
|
+
import { turnManifestOf } from "../tenant/session.js";
|
|
18
|
+
import { declaredHttpTool } from "./http-tool.js";
|
|
19
|
+
/** The `tenant_settings` key the policy is kept under. */
|
|
20
|
+
export const TOOL_POLICY_SETTING = "tool-policy";
|
|
21
|
+
const STRICTNESS = { allow: 0, ask: 1, deny: 2 };
|
|
22
|
+
/** The Tenant's policy; an empty one when none was set. Throws when it cannot be read. */
|
|
23
|
+
export async function readToolPolicy(t) {
|
|
24
|
+
const raw = await t.getSetting(TOOL_POLICY_SETTING);
|
|
25
|
+
if (raw === undefined)
|
|
26
|
+
return { rules: [], updatedAt: null };
|
|
27
|
+
return ToolPolicySchema.parse(JSON.parse(raw));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The decision for `request`, a session's MCP or HTTP tool call: the Tenant's rules and the
|
|
31
|
+
* pinned manifest's `approval`. A policy that cannot be read refuses the call.
|
|
32
|
+
*/
|
|
33
|
+
export async function toolCallDecision(t, s, request, kind) {
|
|
34
|
+
let rules;
|
|
35
|
+
try {
|
|
36
|
+
rules = (await readToolPolicy(t)).rules;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { effect: "deny", reason: "The Tenant's tool policy could not be read, so no tool call runs" };
|
|
40
|
+
}
|
|
41
|
+
const mcp = mcpToolOf(s.mcpSnapshot, request);
|
|
42
|
+
return decide(rules, {
|
|
43
|
+
kind,
|
|
44
|
+
tool: request.toolName ?? request.key ?? request.effectId,
|
|
45
|
+
...(mcp ? { server: mcp.serverName } : {}),
|
|
46
|
+
agent: request.agent?.id ?? s.agentId,
|
|
47
|
+
input: request.input,
|
|
48
|
+
}, pinnedApproval(turnManifestOf(s), s.mcpSnapshot, request));
|
|
49
|
+
}
|
|
50
|
+
/** Decides `call` by `rules`; `pinnedAsk` adds the ask of a manifest saved before R5 step 2. */
|
|
51
|
+
export function decide(rules, call, pinnedAsk = false) {
|
|
52
|
+
let strictest = pinnedAsk
|
|
53
|
+
? { effect: "ask", reason: `The agent's manifest asks for approval of '${call.tool}'` }
|
|
54
|
+
: undefined;
|
|
55
|
+
for (const rule of rules) {
|
|
56
|
+
if (!matches(rule, call))
|
|
57
|
+
continue;
|
|
58
|
+
if (strictest && STRICTNESS[rule.effect] <= STRICTNESS[strictest.effect])
|
|
59
|
+
continue;
|
|
60
|
+
strictest = { effect: rule.effect, reason: rule.reason ?? describe(rule) };
|
|
61
|
+
}
|
|
62
|
+
if (!strictest || strictest.effect === "allow")
|
|
63
|
+
return { effect: "allow" };
|
|
64
|
+
return { effect: strictest.effect, reason: strictest.reason };
|
|
65
|
+
}
|
|
66
|
+
/** The pinned `approval: "always"` of the call's tool, in a manifest saved before R5 step 2. */
|
|
67
|
+
export function pinnedApproval(manifest, snapshot, request) {
|
|
68
|
+
const mcp = mcpToolOf(snapshot, request);
|
|
69
|
+
if (mcp)
|
|
70
|
+
return mcpToolSettingsIn(manifest, mcp).approval === "always";
|
|
71
|
+
if (request.capabilityId === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
return declaredHttpTool(manifest, request.agent?.id, request.capabilityId, request.toolName)?.approval === "always";
|
|
74
|
+
}
|
|
75
|
+
function matches(rule, call) {
|
|
76
|
+
if (rule.kind !== undefined && rule.kind !== call.kind)
|
|
77
|
+
return false;
|
|
78
|
+
if (rule.tool !== undefined && !glob(rule.tool).test(call.tool))
|
|
79
|
+
return false;
|
|
80
|
+
if (rule.server !== undefined && (call.server === undefined || !glob(rule.server).test(call.server)))
|
|
81
|
+
return false;
|
|
82
|
+
if (rule.agent !== undefined && !glob(rule.agent).test(call.agent))
|
|
83
|
+
return false;
|
|
84
|
+
for (const [path, condition] of Object.entries(rule.when ?? {}))
|
|
85
|
+
if (!holds(condition, fieldOf(call.input, path)))
|
|
86
|
+
return false;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
/** A pattern where `*` is any run of characters, matched whole. */
|
|
90
|
+
function glob(pattern) {
|
|
91
|
+
return new RegExp(`^${pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
|
|
92
|
+
}
|
|
93
|
+
/** The value at a dotted `path` of `input`, or undefined. */
|
|
94
|
+
function fieldOf(input, path) {
|
|
95
|
+
let value = input;
|
|
96
|
+
for (const key of path.split(".")) {
|
|
97
|
+
if (!value || typeof value !== "object")
|
|
98
|
+
return undefined;
|
|
99
|
+
value = value[key];
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function holds(condition, value) {
|
|
104
|
+
if (value === undefined)
|
|
105
|
+
return false;
|
|
106
|
+
if (condition.equals !== undefined && canonical(value) !== canonical(condition.equals))
|
|
107
|
+
return false;
|
|
108
|
+
if (condition.in !== undefined && !condition.in.some((item) => canonical(item) === canonical(value)))
|
|
109
|
+
return false;
|
|
110
|
+
const number = typeof value === "number" ? value : undefined;
|
|
111
|
+
if (condition.gt !== undefined && !(number !== undefined && number > condition.gt))
|
|
112
|
+
return false;
|
|
113
|
+
if (condition.gte !== undefined && !(number !== undefined && number >= condition.gte))
|
|
114
|
+
return false;
|
|
115
|
+
if (condition.lt !== undefined && !(number !== undefined && number < condition.lt))
|
|
116
|
+
return false;
|
|
117
|
+
if (condition.lte !== undefined && !(number !== undefined && number <= condition.lte))
|
|
118
|
+
return false;
|
|
119
|
+
if (condition.matches !== undefined && !(typeof value === "string" && new RegExp(condition.matches).test(value)))
|
|
120
|
+
return false;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
/** What a rule with no `reason` says. */
|
|
124
|
+
function describe(rule) {
|
|
125
|
+
const what = [
|
|
126
|
+
rule.tool === undefined ? undefined : `tool '${rule.tool}'`,
|
|
127
|
+
rule.server === undefined ? undefined : `server '${rule.server}'`,
|
|
128
|
+
rule.kind === undefined ? undefined : `${rule.kind} tools`,
|
|
129
|
+
rule.agent === undefined ? undefined : `agent '${rule.agent}'`,
|
|
130
|
+
rule.when === undefined ? undefined : `input ${Object.keys(rule.when).join(", ")}`,
|
|
131
|
+
].filter((part) => part !== undefined);
|
|
132
|
+
const verb = rule.effect === "deny" ? "refuses" : "asks a person about";
|
|
133
|
+
return `The Tenant's tool policy ${verb} ${what.length === 0 ? "every tool call" : `calls of ${what.join(", ")}`}`;
|
|
134
|
+
}
|
|
@@ -29,15 +29,15 @@ export interface TenantVault {
|
|
|
29
29
|
session(sessionId: string): Promise<Session | undefined>;
|
|
30
30
|
/**
|
|
31
31
|
* The authorization of one request to a session's remote MCP server or HTTP tool: its
|
|
32
|
-
* credential from the session's
|
|
32
|
+
* credential from the vault of the session's Environment.
|
|
33
33
|
*/
|
|
34
34
|
authorizeMcp(sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
35
35
|
/** Vault writes and token signing with the Tenant's vault key (the keys service, F4.2). */
|
|
36
36
|
keys(): Keys;
|
|
37
|
-
/** Whether
|
|
38
|
-
environmentSecretBound(
|
|
37
|
+
/** Whether the Environment's vault has an `environment_secret` bound to `host` (R2c). */
|
|
38
|
+
environmentSecretBound(environmentId: string, host: string): Promise<boolean>;
|
|
39
39
|
/** The header an `environment_secret` sets on one request to `host` (R2c). */
|
|
40
|
-
releaseEnvironmentSecret(
|
|
40
|
+
releaseEnvironmentSecret(environmentId: string, host: string): ReturnType<VaultService["releaseEnvironmentSecret"]>;
|
|
41
41
|
}
|
|
42
42
|
export interface TenantVaults {
|
|
43
43
|
/**
|
|
@@ -60,8 +60,8 @@ export interface TenantVaultsOptions {
|
|
|
60
60
|
}
|
|
61
61
|
export declare function createTenantVaults(options: TenantVaultsOptions): TenantVaults;
|
|
62
62
|
/**
|
|
63
|
-
* A request to a session's remote MCP server, authorized from
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* A request to a session's remote MCP server, authorized from its Environment's vault: what the
|
|
64
|
+
* loop does in its own process (`tenant/effects.ts` `authorize`). The Environment comes from the
|
|
65
|
+
* session row.
|
|
66
66
|
*/
|
|
67
67
|
export declare function authorizeSessionMcp(vault: Pick<VaultService, "authorize">, session: (sessionId: string) => Promise<Session | undefined>, sessionId: string, request: McpCredentialRequest): Promise<AuthorizeResult>;
|
|
@@ -52,8 +52,8 @@ export function createTenantVaults(options) {
|
|
|
52
52
|
session,
|
|
53
53
|
authorizeMcp: (sessionId, request) => authorizeSessionMcp(credentials, session, sessionId, request),
|
|
54
54
|
keys: () => keys,
|
|
55
|
-
environmentSecretBound: (
|
|
56
|
-
releaseEnvironmentSecret: (
|
|
55
|
+
environmentSecretBound: (environmentId, host) => credentials.environmentSecretBound(environmentId, host),
|
|
56
|
+
releaseEnvironmentSecret: (environmentId, host) => credentials.releaseEnvironmentSecret({ environmentId, host }),
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
/** The database's Tenant, once its schema is at this build's version. */
|
|
@@ -81,9 +81,9 @@ export function createTenantVaults(options) {
|
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* A request to a session's remote MCP server, authorized from
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* A request to a session's remote MCP server, authorized from its Environment's vault: what the
|
|
85
|
+
* loop does in its own process (`tenant/effects.ts` `authorize`). The Environment comes from the
|
|
86
|
+
* session row.
|
|
87
87
|
*/
|
|
88
88
|
export async function authorizeSessionMcp(vault, session, sessionId, request) {
|
|
89
89
|
const found = await session(sessionId);
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* - The gate also writes a `tool_crossings` row before it calls the server, and the answer
|
|
9
9
|
* after. A re-send that finds the answer gets it. One that finds a row without an answer,
|
|
10
10
|
* for a call this gateway is not running, was lost with an earlier gateway (a restart mid
|
|
11
|
-
* call): it
|
|
11
|
+
* call): it is started again under the same key (R5.3), so the tool sees the key twice and
|
|
12
|
+
* a tool that honors it acts once. Core counts the sends and stops at its cap (R5.4).
|
|
13
|
+
* - A re-send that another gateway process started or settled between this one's read and its
|
|
14
|
+
* write answers `{uncertain: true}`: not answered here, so the caller sends it again later.
|
|
12
15
|
*
|
|
13
|
-
* A cancel aborts the call and
|
|
14
|
-
*
|
|
16
|
+
* A cancel aborts the call and settles its row as cancelled: it never runs again under its key.
|
|
17
|
+
* A shutdown leaves the row open, so the next gateway starts it again. Settled rows are deleted
|
|
18
|
+
* a day later (`prune`); a re-send after that runs the call again, which at least once allows.
|
|
15
19
|
*/
|
|
16
20
|
import type { Logger } from "../tenant/types.js";
|
|
17
21
|
import { type InflightCallsOptions, type InflightOwner } from "./inflight.js";
|
|
@@ -29,8 +33,9 @@ export interface ToolCalls {
|
|
|
29
33
|
*/
|
|
30
34
|
run(tenantId: string | undefined, key: string, hash: string, call: (signal: AbortSignal) => Promise<ToolCallAnswer>, owner?: InflightOwner): Promise<ToolCallAnswer>;
|
|
31
35
|
/**
|
|
32
|
-
* Aborts the call under `key
|
|
33
|
-
* when the running call is another session's (nothing
|
|
36
|
+
* Aborts the call under `key` and settles its crossing as cancelled: it is never run again
|
|
37
|
+
* under that key. With `sessionId`, false when the running call is another session's (nothing
|
|
38
|
+
* is aborted).
|
|
34
39
|
*/
|
|
35
40
|
cancel(key: string, sessionId?: string): boolean;
|
|
36
41
|
/** Aborts every running call (gateway shutdown). */
|
package/dist/gates/tool-calls.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { createInflightCalls, InflightConflict, } from "./inflight.js";
|
|
2
2
|
/** How long a settled crossing is kept. */
|
|
3
3
|
export const TOOL_CROSSING_TTL_MS = 24 * 60 * 60_000;
|
|
4
|
-
const
|
|
4
|
+
const CANCELLED = {
|
|
5
|
+
ok: false,
|
|
6
|
+
error: { message: "The call was cancelled while it ran; it may have reached its server" },
|
|
7
|
+
};
|
|
8
|
+
const ELSEWHERE = {
|
|
5
9
|
ok: false,
|
|
6
10
|
error: {
|
|
7
11
|
uncertain: true,
|
|
8
|
-
message: "
|
|
12
|
+
message: "Another gateway process is running this tool call; send it again",
|
|
9
13
|
},
|
|
10
14
|
};
|
|
11
15
|
export function createToolCalls(options) {
|
|
@@ -16,7 +20,7 @@ export function createToolCalls(options) {
|
|
|
16
20
|
return {
|
|
17
21
|
async run(tenantId, key, hash, call, owner) {
|
|
18
22
|
if (inflight.has(key))
|
|
19
|
-
return inflight.run(key, hash, () => Promise.resolve(
|
|
23
|
+
return inflight.run(key, hash, () => Promise.resolve(ELSEWHERE), owner);
|
|
20
24
|
const vault = await vaults.open(tenantId);
|
|
21
25
|
const store = vault.store;
|
|
22
26
|
const row = await store.tx((t) => t.toolCrossing(key));
|
|
@@ -25,16 +29,19 @@ export function createToolCalls(options) {
|
|
|
25
29
|
throw new InflightConflict(`A different request already ran under ${key}`);
|
|
26
30
|
if (row.answer !== null && row.answer !== undefined)
|
|
27
31
|
return row.answer;
|
|
28
|
-
// Started here a moment ago
|
|
32
|
+
// Started here a moment ago: join it.
|
|
29
33
|
if (inflight.has(key))
|
|
30
|
-
return inflight.run(key, hash, () => Promise.resolve(
|
|
31
|
-
|
|
34
|
+
return inflight.run(key, hash, () => Promise.resolve(ELSEWHERE), owner);
|
|
35
|
+
// Otherwise it was lost with an earlier gateway: it starts again below (R5.3).
|
|
32
36
|
}
|
|
33
37
|
return inflight.run(key, hash, async (signal) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
38
|
+
const startedAt = iso();
|
|
39
|
+
const started = await store.tx((t) => row
|
|
40
|
+
? t.restartToolCrossing({ key, hash, startedAt })
|
|
41
|
+
: t.startToolCrossing({ key, hash, startedAt }));
|
|
42
|
+
// Another gateway process started or settled it between the read and now.
|
|
36
43
|
if (!started)
|
|
37
|
-
return
|
|
44
|
+
return ELSEWHERE;
|
|
38
45
|
const answer = await call(signal);
|
|
39
46
|
// A cancel or a shutdown: the call may have reached the server; the row stays open.
|
|
40
47
|
if (signal.aborted)
|
|
@@ -48,7 +55,18 @@ export function createToolCalls(options) {
|
|
|
48
55
|
return answer;
|
|
49
56
|
}, owner);
|
|
50
57
|
},
|
|
51
|
-
cancel
|
|
58
|
+
cancel(key, sessionId) {
|
|
59
|
+
if (!inflight.cancel(key, sessionId))
|
|
60
|
+
return false;
|
|
61
|
+
void vaults
|
|
62
|
+
.open(undefined)
|
|
63
|
+
.then((vault) => vault.store.tx((t) => t.settleToolCrossing(key, CANCELLED, iso())))
|
|
64
|
+
.catch((error) => logger.warn("tool_crossing_unsettled", {
|
|
65
|
+
effect: key,
|
|
66
|
+
message: error instanceof Error ? error.message : String(error),
|
|
67
|
+
}));
|
|
68
|
+
return true;
|
|
69
|
+
},
|
|
52
70
|
close: () => inflight.close(),
|
|
53
71
|
async prune() {
|
|
54
72
|
try {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { ToolGate } from "./tool-gate.js";
|
|
2
2
|
import type { RunTokens } from "../tenant/run-grants.js";
|
|
3
|
+
/**
|
|
4
|
+
* A keyed call whose answer the hop lost (R5.3): the gate never got it, the connection broke,
|
|
5
|
+
* the gate answered `5xx`, or another gateway process holds the call. Sending it again with the
|
|
6
|
+
* same key is safe: the gate joins the call, answers it from its crossing, or runs it again.
|
|
7
|
+
*/
|
|
8
|
+
export declare class GateCallLost extends Error {
|
|
9
|
+
readonly name = "GateCallLost";
|
|
10
|
+
}
|
|
3
11
|
export interface HttpToolGateOptions {
|
|
4
12
|
/** The gates service, e.g. `http://gateway:4100` (`NYLORUN_GATES_URL`). */
|
|
5
13
|
readonly url: string;
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* Failures keep the meaning they had in the loop's own process:
|
|
9
9
|
* - An MCP request that fails throws, so the pool reports a diagnostic. A tool call's coded
|
|
10
10
|
* failure (R2b C7) throws the `McpCallFailed` the loop's own call would have, which becomes a
|
|
11
|
-
* failed outcome the model sees.
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
11
|
+
* failed outcome the model sees.
|
|
12
|
+
* - An HTTP tool call throws only for the hop: its own failures are outcomes.
|
|
13
|
+
* - A hop that lost the answer (the gate unreachable, the connection lost, a `5xx`, or a call
|
|
14
|
+
* another gateway process holds) throws `GateCallLost`: sending the call again with the same
|
|
15
|
+
* key joins it, collects its answer, or runs it again (R5.3). A gate's refusal (`4xx`) throws
|
|
16
|
+
* a plain error.
|
|
15
17
|
*
|
|
16
18
|
* Credentials (F5): a session's MCP requests and keyed cancels carry its run token, read per
|
|
17
19
|
* request from the advance's grant, and leave the session out of the body: the gate takes it
|
|
@@ -26,6 +28,14 @@ import { McpCallFailed, } from "../mcp/connect.js";
|
|
|
26
28
|
import { TENANT_HEADER } from "./contract.js";
|
|
27
29
|
import { GATE_CLIENT_TIMEOUT_MS } from "./http-client.js";
|
|
28
30
|
import { HTTP_CALLS_PATH, MCP_CLOSE_PATH, MCP_CONNECT_PATH, MCP_LIST_PATH, TOOL_CALLS_PATH, } from "./tool-contract.js";
|
|
31
|
+
/**
|
|
32
|
+
* A keyed call whose answer the hop lost (R5.3): the gate never got it, the connection broke,
|
|
33
|
+
* the gate answered `5xx`, or another gateway process holds the call. Sending it again with the
|
|
34
|
+
* same key is safe: the gate joins the call, answers it from its crossing, or runs it again.
|
|
35
|
+
*/
|
|
36
|
+
export class GateCallLost extends Error {
|
|
37
|
+
name = "GateCallLost";
|
|
38
|
+
}
|
|
29
39
|
/** How long a close or a cancel may take; neither changes an outcome. */
|
|
30
40
|
const SHORT_TIMEOUT_MS = 2_000;
|
|
31
41
|
export function httpToolGate(options) {
|
|
@@ -111,8 +121,7 @@ export function httpToolGate(options) {
|
|
|
111
121
|
}
|
|
112
122
|
/**
|
|
113
123
|
* The answer of an MCP or HTTP tool request, or a throw: a coded MCP failure (`McpCallFailed`),
|
|
114
|
-
*
|
|
115
|
-
* `uncertain`.
|
|
124
|
+
* a lost hop (`GateCallLost`, sent again), or a gate's refusal.
|
|
116
125
|
*/
|
|
117
126
|
async function answer(path, scope, request) {
|
|
118
127
|
const result = await exchange(path, scope.body, {
|
|
@@ -123,8 +132,10 @@ export function httpToolGate(options) {
|
|
|
123
132
|
if (result.kind === "aborted")
|
|
124
133
|
throw result.reason ?? new Error("aborted");
|
|
125
134
|
if (result.kind !== "answer")
|
|
126
|
-
throw new
|
|
135
|
+
throw new GateCallLost(result.message);
|
|
127
136
|
const parsed = parse(result.text);
|
|
137
|
+
if (result.status >= 500)
|
|
138
|
+
throw new GateCallLost(gateRefusal(result.status, parsed));
|
|
128
139
|
if (result.status !== 200 || !parsed || !("ok" in parsed))
|
|
129
140
|
throw new Error(gateRefusal(result.status, parsed));
|
|
130
141
|
if (parsed.ok)
|
|
@@ -176,7 +187,9 @@ export function httpToolGate(options) {
|
|
|
176
187
|
async callHttp(call, signal) {
|
|
177
188
|
const token = options.runTokens?.token(call.tool.sessionId);
|
|
178
189
|
const { sessionId: _, ...unnamed } = call.tool;
|
|
179
|
-
|
|
190
|
+
// The gate reads the call's span context from its journal row (R7.4), not from the caller.
|
|
191
|
+
const { traceparent: _traceparent, ...sent } = call;
|
|
192
|
+
const body = { ...sent, tool: token ? unnamed : { ...call.tool } };
|
|
180
193
|
return answered(HTTP_CALLS_PATH, { body, ...(token ? { bearer: token } : {}) }, { signal, idempotencyKey: call.effectId });
|
|
181
194
|
},
|
|
182
195
|
async cancel(request) {
|
|
@@ -190,10 +203,13 @@ export function httpToolGate(options) {
|
|
|
190
203
|
}
|
|
191
204
|
/** An MCP failure the gate reported, as the loop's own MCP code would have thrown it. */
|
|
192
205
|
function mcpError(error) {
|
|
193
|
-
//
|
|
194
|
-
if (error.
|
|
206
|
+
// Held by another gateway process: send it again (R5.3).
|
|
207
|
+
if (error.uncertain)
|
|
208
|
+
return new GateCallLost(error.message);
|
|
209
|
+
// Coded (R2b C7): the model sees it.
|
|
210
|
+
if (error.failure)
|
|
195
211
|
return McpCallFailed.from(error.failure);
|
|
196
|
-
const thrown = new Error(error.
|
|
212
|
+
const thrown = new Error(error.message);
|
|
197
213
|
if (error.code !== undefined)
|
|
198
214
|
thrown.code = error.code;
|
|
199
215
|
if (error.credentialIds !== undefined)
|