@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
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Rows store the request's hash (`requestHash`) and, for a model call, the request without its
|
|
13
13
|
* prompt: drift is a hash compare. Rows written before carry their full request, hashed on read.
|
|
14
|
+
*
|
|
15
|
+
* Every row a run journals keeps when it was journaled (`startedAt`, R7.1), and a rejoin keeps
|
|
16
|
+
* it: an outcome's `durationMs` is the journal's window, from the intent to the outcome. A model
|
|
17
|
+
* call's outcome event also carries what the meter recorded for it (`modelOutcomeFacts`). A tool
|
|
18
|
+
* row keeps its span's context (`traceparent`, R7.4), which the `execute` answer carries to the
|
|
19
|
+
* harness and the Tool Gate reads with the row.
|
|
14
20
|
*/
|
|
15
|
-
import { HarnessApiError, effectRequestHash, } from "@nylorun/core/harness-api";
|
|
21
|
+
import { HARNESS_TOOLS_CAPABILITY, HarnessApiError, effectRequestHash, isEffectIdOf, } from "@nylorun/core/harness-api";
|
|
16
22
|
import { isFlowEffect, isFlowToolEffect, recordVerdict, settleAgentEffect, } from "../core/flow-host.js";
|
|
17
23
|
import { sandboxCapabilityOf } from "../sandbox/capability.js";
|
|
18
24
|
import { abortOn, isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
|
|
@@ -26,8 +32,13 @@ import { manifestFor, mcpToolOf } from "../mcp/snapshot.js";
|
|
|
26
32
|
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
27
33
|
import { ownedSession } from "../tenant/context.js";
|
|
28
34
|
import { isGateToolEffect, linkedOutcome, recoversModelCalls, recoversToolCalls, resolveNewFlowEffect, } from "../tenant/effects.js";
|
|
29
|
-
import { assistantMessage, contextCompacted, modelFailed,
|
|
35
|
+
import { assistantMessage, contextCompacted, modelFailed, modelOutcomeFacts, } from "../tenant/transcript.js";
|
|
30
36
|
import { abortKind } from "../tenant/worker.js";
|
|
37
|
+
import { redactTokens } from "./redact.js";
|
|
38
|
+
import { toolCallDecision } from "../gates/policy.js";
|
|
39
|
+
import { traceKey } from "../traces/key.js";
|
|
40
|
+
import { effectSpanOf, traceparentOf, turnTraceIdOf } from "../traces/ids.js";
|
|
41
|
+
import { MAX_TOOL_ATTEMPTS, firstSend, interrupted, settleToolRow, toolKindOf, } from "./tool-journal.js";
|
|
31
42
|
const TURN_CANCELLED = () => new HarnessApiError("turn_cancelled", "Turn cancelled");
|
|
32
43
|
/** The hash a journal row's request was recorded with. */
|
|
33
44
|
export function hashOf(row) {
|
|
@@ -35,10 +46,9 @@ export function hashOf(row) {
|
|
|
35
46
|
}
|
|
36
47
|
/** The completed outcomes of a segment, which a run replays without asking. */
|
|
37
48
|
export async function bulkOutcomes(t, session, checkpoint) {
|
|
38
|
-
const prefix = `${checkpoint.turnId}:${checkpoint.segment}:`;
|
|
39
49
|
const rows = await t.effectsForTurn(session.id, checkpoint.turnId, ["completed"]);
|
|
40
50
|
return rows
|
|
41
|
-
.filter((row) => row.request.effectId.
|
|
51
|
+
.filter((row) => isEffectIdOf(row.request.effectId, checkpoint.turnId, checkpoint.segment) && row.outcome)
|
|
42
52
|
.map((row) => ({ effectId: row.request.effectId, requestHash: hashOf(row), outcome: row.outcome }));
|
|
43
53
|
}
|
|
44
54
|
/**
|
|
@@ -48,10 +58,14 @@ export async function bulkOutcomes(t, session, checkpoint) {
|
|
|
48
58
|
export async function recordIntent(scope, effect, requestHash) {
|
|
49
59
|
const { ctx, lease, signal } = scope;
|
|
50
60
|
const request = effect;
|
|
61
|
+
// Read before the transaction: the first read of the Tenant may create the key.
|
|
62
|
+
const key = await traceKey(ctx.store);
|
|
51
63
|
const answer = await ctx.store.tx(async (t) => {
|
|
52
64
|
const s = await ownedSession(t, lease, request.sessionId);
|
|
53
65
|
if (s.status === "cancelled" || s.activeTurnId !== request.turnId)
|
|
54
66
|
throw TURN_CANCELLED();
|
|
67
|
+
// A tool call's span context (R7.4), kept on its row.
|
|
68
|
+
const traced = () => ({ traceparent: toolTraceparent(key, ctx.store.tenantId, s, request) });
|
|
55
69
|
// An aborted advance starts no effect; the advance decides what the abort means.
|
|
56
70
|
signal.throwIfAborted();
|
|
57
71
|
const existing = await t.get("effects", request.effectId);
|
|
@@ -61,12 +75,12 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
61
75
|
if (existing.status === "completed")
|
|
62
76
|
return { status: "completed", outcome: existing.outcome };
|
|
63
77
|
// A call its previous owner left running at the gate: re-send it, same key and request,
|
|
64
|
-
// to join it or collect its outcome (P1.2
|
|
65
|
-
// tool calls).
|
|
78
|
+
// to join it or collect its outcome (P1.2).
|
|
66
79
|
if (existing.status === "invoking" && request.kind === "model" && recoversModelCalls(ctx))
|
|
67
80
|
return { status: "execute", rejoin: true };
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
// A tool call a crash, a shutdown or a lost gate hop interrupted (R5.3, R5.4).
|
|
82
|
+
if (existing.status === "invoking" && existing.request.kind === "tool")
|
|
83
|
+
return resend(t, s, existing, recoversToolCalls(ctx));
|
|
70
84
|
if (request.kind === "agent" && existing.status === "pending" && existing.agentSessionId) {
|
|
71
85
|
const agent = await t.get("sessions", existing.agentSessionId);
|
|
72
86
|
const outcome = await linkedOutcome(t, existing, agent);
|
|
@@ -86,26 +100,38 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
86
100
|
if (request.kind !== "model" && effectRequestHash(request) !== requestHash)
|
|
87
101
|
throw new HarnessApiError("invalid", "The request hash does not match the request");
|
|
88
102
|
if (flowHttp) {
|
|
89
|
-
|
|
103
|
+
const row = { request, requestHash, status: "invoking", ...firstSend({ toolKind: "http" }), ...traced() };
|
|
90
104
|
await t.event(s.id, s.activeTurnId, "node.started", {
|
|
91
105
|
path: request.path,
|
|
92
106
|
kind: "http",
|
|
93
107
|
key: request.key,
|
|
94
108
|
...(request.iterations !== undefined ? { iterations: request.iterations } : {}),
|
|
95
|
-
});
|
|
96
|
-
|
|
109
|
+
}, scope.writer);
|
|
110
|
+
const decided = await bypolicy(t, s, row, "http");
|
|
111
|
+
if (decided)
|
|
112
|
+
return decided;
|
|
113
|
+
await t.put("effects", request.effectId, row);
|
|
114
|
+
return { status: "execute", traceparent: row.traceparent };
|
|
97
115
|
}
|
|
98
116
|
if (request.kind === "delegation") {
|
|
99
117
|
// Lifecycle points of an agent used as a tool: journaled once, so replays never re-emit.
|
|
100
118
|
const outcome = { value: null };
|
|
101
|
-
|
|
119
|
+
const at = new Date().toISOString();
|
|
120
|
+
await t.put("effects", request.effectId, {
|
|
121
|
+
request,
|
|
122
|
+
requestHash,
|
|
123
|
+
status: "completed",
|
|
124
|
+
outcome,
|
|
125
|
+
startedAt: at,
|
|
126
|
+
settledAt: at,
|
|
127
|
+
});
|
|
102
128
|
const settled = request.effectId.endsWith(":settled");
|
|
103
129
|
const callId = request.context?.callId;
|
|
104
130
|
await t.event(s.id, s.activeTurnId, settled ? "delegation.completed" : "delegation.started", {
|
|
105
131
|
agent: request.agent,
|
|
106
132
|
...(typeof callId === "string" ? { callId } : {}),
|
|
107
133
|
...request.input,
|
|
108
|
-
});
|
|
134
|
+
}, scope.writer);
|
|
109
135
|
return { status: "completed", outcome };
|
|
110
136
|
}
|
|
111
137
|
const agentManifest = manifestFor(s.manifest, request.agent);
|
|
@@ -124,24 +150,41 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
124
150
|
isReadArtifactCall(agentManifest, request) ||
|
|
125
151
|
isSkillToolCall(agentManifest, request) ||
|
|
126
152
|
// `tool_search` (R2b C10) reads the session's pinned MCP snapshot: core serves it.
|
|
127
|
-
isToolSearchCall(agentManifest, request)
|
|
153
|
+
isToolSearchCall(agentManifest, request) ||
|
|
154
|
+
// A tool the run's harness declared and runs itself (R4.2, Q3).
|
|
155
|
+
(request.capabilityId === HARNESS_TOOLS_CAPABILITY &&
|
|
156
|
+
scope.nativeTools?.has(request.toolName ?? "") === true)));
|
|
128
157
|
if (!executed) {
|
|
129
158
|
// A tool that would run the developer's code: refused at save (a turn's manifest only
|
|
130
159
|
// removes tools), so this is a backstop, as is an MCP tool the turn disabled, which the
|
|
131
160
|
// model was never offered. The model sees the failure; nothing runs.
|
|
132
161
|
const disabled = request.kind === "tool" && mcpToolOf(s.mcpSnapshot, request) !== undefined;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
162
|
+
const row = {
|
|
163
|
+
request,
|
|
164
|
+
requestHash,
|
|
165
|
+
status: "invoking",
|
|
166
|
+
...toolKindOf(s, agentManifest, request),
|
|
167
|
+
attempts: 0,
|
|
168
|
+
};
|
|
169
|
+
const outcome = await settleToolRow(t, s, row, disabled ? disabledTool(request) : unrunnableTool(request), { writer: coreIn(scope) });
|
|
138
170
|
return { status: "completed", outcome };
|
|
139
171
|
}
|
|
140
|
-
|
|
172
|
+
const row = {
|
|
173
|
+
// A model row keeps no prompt; a tool row its whole request.
|
|
141
174
|
request: storedRequest(request),
|
|
142
175
|
requestHash,
|
|
143
176
|
status: "invoking",
|
|
144
|
-
|
|
177
|
+
...(request.kind === "tool"
|
|
178
|
+
? { ...firstSend(toolKindOf(s, agentManifest, request)), ...traced() }
|
|
179
|
+
: { startedAt: new Date().toISOString() }),
|
|
180
|
+
};
|
|
181
|
+
// The Tenant's tool policy decides an MCP or HTTP tool call as it is journaled (R5 step 2).
|
|
182
|
+
if (row.toolKind === "mcp" || row.toolKind === "http") {
|
|
183
|
+
const decided = await bypolicy(t, s, row, row.toolKind);
|
|
184
|
+
if (decided)
|
|
185
|
+
return decided;
|
|
186
|
+
}
|
|
187
|
+
await t.put("effects", request.effectId, row);
|
|
145
188
|
// `save_artifact` writes the Tenant's artifacts: core runs it, reading the file through the
|
|
146
189
|
// workspace capability, wherever the sandbox is.
|
|
147
190
|
if (request.kind === "tool" && isSaveArtifactCall(agentManifest, request))
|
|
@@ -154,7 +197,7 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
154
197
|
return { skill: agentManifest };
|
|
155
198
|
if (isToolSearchCall(agentManifest, request))
|
|
156
199
|
return "search";
|
|
157
|
-
return { status: "execute" };
|
|
200
|
+
return { status: "execute", ...(row.traceparent ? { traceparent: row.traceparent } : {}) };
|
|
158
201
|
});
|
|
159
202
|
if (answer === "save")
|
|
160
203
|
return runInCore(scope, request, () => callSaveArtifact(scope.ctx, request, scope.signal));
|
|
@@ -177,6 +220,9 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
177
220
|
* the outcome as the intent's answer.
|
|
178
221
|
*/
|
|
179
222
|
async function runInCore(scope, request, call) {
|
|
223
|
+
// Core ran the call: its outcome is what core saw, not the harness's claim (R4.9).
|
|
224
|
+
const writer = coreIn(scope);
|
|
225
|
+
const core = { ctx: scope.ctx, lease: scope.lease, signal: scope.signal, ...(writer ? { writer } : {}) };
|
|
180
226
|
let value;
|
|
181
227
|
try {
|
|
182
228
|
value = await call();
|
|
@@ -184,11 +230,70 @@ async function runInCore(scope, request, call) {
|
|
|
184
230
|
catch (error) {
|
|
185
231
|
if (isOwnershipLost(error))
|
|
186
232
|
throw error;
|
|
187
|
-
return recordOutcome(
|
|
233
|
+
return recordOutcome(core, request.effectId, {
|
|
188
234
|
error: error instanceof Error ? error.message : String(error),
|
|
189
235
|
});
|
|
190
236
|
}
|
|
191
|
-
return recordOutcome(
|
|
237
|
+
return recordOutcome(core, request.effectId, { value });
|
|
238
|
+
}
|
|
239
|
+
/** Core's own write during the scope's run (R4.9): core saw it, in that run. */
|
|
240
|
+
function coreIn(scope) {
|
|
241
|
+
const runId = scope.writer?.runId;
|
|
242
|
+
return runId === undefined ? undefined : { source: { kind: "core", id: "runtime" }, runId };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The answer to a tool row still `invoking` (R5.3, R5.4): a crash, a shutdown or a lost gate
|
|
246
|
+
* hop interrupted its send. An MCP or HTTP call (it crosses the Tool Gate, which hands the tool
|
|
247
|
+
* its key) is sent again, one more attempt, until `MAX_TOOL_ATTEMPTS`; then it fails to the
|
|
248
|
+
* model. Any other call (sandbox, core-served) has no key to dedupe on: it fails to the model
|
|
249
|
+
* now, and is not run again (Q2).
|
|
250
|
+
*/
|
|
251
|
+
async function resend(t, s, row, rejoin) {
|
|
252
|
+
if (!isGateToolEffect(s, row.request)) {
|
|
253
|
+
const outcome = await settleToolRow(t, s, row, interrupted("A crash interrupted this call before its outcome was recorded. It may have run; it is not run again."));
|
|
254
|
+
return { status: "completed", outcome };
|
|
255
|
+
}
|
|
256
|
+
const attempts = (row.attempts ?? 1) + 1;
|
|
257
|
+
if (attempts > MAX_TOOL_ATTEMPTS) {
|
|
258
|
+
const outcome = await settleToolRow(t, s, row, interrupted(`The call was interrupted ${MAX_TOOL_ATTEMPTS} times and may have run.`));
|
|
259
|
+
return { status: "completed", outcome };
|
|
260
|
+
}
|
|
261
|
+
row.attempts = attempts;
|
|
262
|
+
await t.put("effects", row.request.effectId, row);
|
|
263
|
+
const traced = row.traceparent ? { traceparent: row.traceparent } : {};
|
|
264
|
+
return rejoin ? { status: "execute", rejoin: true, ...traced } : { status: "execute", ...traced };
|
|
265
|
+
}
|
|
266
|
+
/** The span context of tool call `request` (R7.4): its turn's trace, and its own span. */
|
|
267
|
+
function toolTraceparent(key, tenantId, s, request) {
|
|
268
|
+
const traceId = turnTraceIdOf(key, { tenantId, sessionId: s.id, turnId: request.turnId }, s.turnTrace);
|
|
269
|
+
return traceparentOf(traceId, effectSpanOf(key, request.effectId));
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* The Tenant's tool policy on a tool call being journaled (R5 step 2, `gates/policy.ts`), or
|
|
273
|
+
* undefined when it runs. `deny`: the call fails to the model as `tool.denied`, unsent. `ask`:
|
|
274
|
+
* the row waits as `held` and the effect is pending; the segment ends waiting, the turn pauses,
|
|
275
|
+
* and `approve` answers it (`tenant/commands.ts`). The Tool Gate refuses a row not `invoking`, so
|
|
276
|
+
* neither runs whichever harness sends it. Writes `row`.
|
|
277
|
+
*/
|
|
278
|
+
async function bypolicy(t, s, row, kind) {
|
|
279
|
+
const decision = await toolCallDecision(t, s, row.request, kind);
|
|
280
|
+
if (decision.effect === "allow")
|
|
281
|
+
return undefined;
|
|
282
|
+
// Nothing was sent.
|
|
283
|
+
row.attempts = 0;
|
|
284
|
+
if (decision.effect === "deny") {
|
|
285
|
+
const outcome = await settleToolRow(t, s, row, {
|
|
286
|
+
kind: "failed",
|
|
287
|
+
code: "tool.denied",
|
|
288
|
+
message: decision.reason,
|
|
289
|
+
retryable: false,
|
|
290
|
+
});
|
|
291
|
+
return { status: "completed", outcome };
|
|
292
|
+
}
|
|
293
|
+
row.status = "held";
|
|
294
|
+
row.hold = { reason: decision.reason, at: new Date().toISOString() };
|
|
295
|
+
await t.put("effects", row.request.effectId, row);
|
|
296
|
+
return { status: "pending" };
|
|
192
297
|
}
|
|
193
298
|
/** The failed outcome of an MCP tool the turn's manifest disables (R2b C9). */
|
|
194
299
|
function disabledTool(request) {
|
|
@@ -203,7 +308,7 @@ function unrunnableTool(request) {
|
|
|
203
308
|
return {
|
|
204
309
|
kind: "failed",
|
|
205
310
|
code: "tool.unavailable",
|
|
206
|
-
message: `Tool '${request.toolName ?? ""}' runs your code,
|
|
311
|
+
message: `Tool '${request.toolName ?? ""}' runs your code, but the Runtime runs agents from their manifests alone. Make it an http() tool or serve it from a remote MCP server.`,
|
|
207
312
|
};
|
|
208
313
|
}
|
|
209
314
|
/**
|
|
@@ -211,24 +316,38 @@ function unrunnableTool(request) {
|
|
|
211
316
|
* `error` that left it uncertain. Only a cancel discards an outcome in hand (§10.7): after any
|
|
212
317
|
* other abort (shutdown, deadline) it is recorded, so the next advance replays it.
|
|
213
318
|
*/
|
|
214
|
-
export async function recordOutcome(scope, effectId,
|
|
319
|
+
export async function recordOutcome(scope, effectId, reported) {
|
|
215
320
|
const { ctx, lease, signal } = scope;
|
|
321
|
+
const result = "error" in reported ? { error: redactTokens(reported.error) } : reported;
|
|
216
322
|
if ("error" in result) {
|
|
217
|
-
|
|
323
|
+
return ctx.store.tx(async (t) => {
|
|
218
324
|
const s = await t.assertEpoch(lease.sessionId, lease.epoch);
|
|
219
325
|
const effect = await t.get("effects", effectId);
|
|
220
326
|
if (!effect)
|
|
221
|
-
return;
|
|
327
|
+
return { status: "uncertain" };
|
|
222
328
|
const { request } = effect;
|
|
329
|
+
const active = s && s.status !== "cancelled" && s.activeTurnId === request.turnId;
|
|
330
|
+
// A tool call whose fate the harness could not learn fails to the model (R5.3): it may
|
|
331
|
+
// have run, and nothing sends it again. A cancel already settled it.
|
|
332
|
+
if (request.kind === "tool") {
|
|
333
|
+
if (effect.status === "completed")
|
|
334
|
+
return { status: "completed", outcome: effect.outcome };
|
|
335
|
+
if (effect.status !== "invoking" || !active)
|
|
336
|
+
return { status: "uncertain" };
|
|
337
|
+
effect.error = result.error;
|
|
338
|
+
const outcome = await settleToolRow(t, s, effect, interrupted(`${result.error} (the call may have run; it is not sent again)`), { writer: coreIn(scope) });
|
|
339
|
+
return { status: "completed", outcome };
|
|
340
|
+
}
|
|
223
341
|
effect.status = "uncertain";
|
|
224
342
|
effect.error = result.error;
|
|
225
343
|
await t.put("effects", effectId, effect);
|
|
226
|
-
if (
|
|
227
|
-
await t.event(s.id, request.turnId, "effect.uncertain", { effectId, message: result.error });
|
|
344
|
+
if (active)
|
|
345
|
+
await t.event(s.id, request.turnId, "effect.uncertain", { effectId, message: result.error }, scope.writer);
|
|
346
|
+
return { status: "uncertain" };
|
|
228
347
|
});
|
|
229
|
-
return { status: "uncertain" };
|
|
230
348
|
}
|
|
231
|
-
|
|
349
|
+
// What a harness reports never carries the Runtime's own tokens into the record (R4).
|
|
350
|
+
const value = await shaped(scope, effectId, redactTokens(result.value));
|
|
232
351
|
return ctx.store.tx(async (t) => {
|
|
233
352
|
const s = await t.assertEpoch(lease.sessionId, lease.epoch);
|
|
234
353
|
const effect = await t.get("effects", effectId);
|
|
@@ -237,29 +356,40 @@ export async function recordOutcome(scope, effectId, result) {
|
|
|
237
356
|
const { request } = effect;
|
|
238
357
|
if (s.status === "cancelled" || s.activeTurnId !== request.turnId || abortKind(signal) === "cancel")
|
|
239
358
|
throw TURN_CANCELLED();
|
|
359
|
+
if (request.kind === "tool") {
|
|
360
|
+
// Settled already: a re-send after a lost hop collected what the first send recorded.
|
|
361
|
+
if (effect.status === "completed")
|
|
362
|
+
return { status: "completed", outcome: effect.outcome };
|
|
363
|
+
// What the call returned is the harness's claim, or, for a tool core ran, core's (R4.9).
|
|
364
|
+
const outcome = await settleToolRow(t, s, effect, value, { writer: scope.writer });
|
|
365
|
+
if (request.context?.role === "verify-http")
|
|
366
|
+
await recordVerdict(t, request, value);
|
|
367
|
+
return { status: "completed", outcome };
|
|
368
|
+
}
|
|
240
369
|
effect.status = "completed";
|
|
241
370
|
effect.outcome = { value };
|
|
371
|
+
effect.settledAt = new Date().toISOString();
|
|
242
372
|
await t.put("effects", effectId, effect);
|
|
243
373
|
const model = request.kind === "model";
|
|
244
374
|
const failed = model ? modelFailed(request, value) : undefined;
|
|
245
|
-
// A summary call is never an assistant message;
|
|
375
|
+
// A summary call is never an assistant message; it publishes context.compacted, `partial`
|
|
376
|
+
// unless it is the last.
|
|
246
377
|
const summarizing = model && Boolean(request.context.compaction);
|
|
247
378
|
const compacted = summarizing && !failed ? contextCompacted(request, value) : undefined;
|
|
248
|
-
const transcript = failed || summarizing
|
|
249
|
-
? undefined
|
|
250
|
-
: model
|
|
251
|
-
? assistantMessage(request, value)
|
|
252
|
-
: toolCompleted(request, value);
|
|
379
|
+
const transcript = failed || summarizing || !model ? undefined : assistantMessage(request, value);
|
|
253
380
|
if (request.context?.role === "verify-http")
|
|
254
381
|
await recordVerdict(t, request, value);
|
|
382
|
+
const writer = scope.writer;
|
|
383
|
+
// The call's span (R7.1): the journal's window, and the meter's row for it when a gate ran it.
|
|
384
|
+
const facts = failed || compacted || transcript
|
|
385
|
+
? modelOutcomeFacts(effectId, effect, await t.modelUsageOf(effectId))
|
|
386
|
+
: undefined;
|
|
255
387
|
if (failed)
|
|
256
|
-
await t.event(s.id, request.turnId, "model.failed", failed);
|
|
388
|
+
await t.event(s.id, request.turnId, "model.failed", { ...failed, ...facts }, writer);
|
|
257
389
|
if (compacted)
|
|
258
|
-
await t.event(s.id, request.turnId, "context.compacted", compacted);
|
|
259
|
-
if (transcript
|
|
260
|
-
await t.event(s.id, request.turnId, "message.assistant", transcript);
|
|
261
|
-
else if (transcript)
|
|
262
|
-
await t.event(s.id, request.turnId, "tool.completed", transcript);
|
|
390
|
+
await t.event(s.id, request.turnId, "context.compacted", { ...compacted, ...facts }, writer);
|
|
391
|
+
if (transcript)
|
|
392
|
+
await t.event(s.id, request.turnId, "message.assistant", { ...transcript, ...facts }, writer);
|
|
263
393
|
return { status: "completed", outcome: effect.outcome };
|
|
264
394
|
});
|
|
265
395
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the Runtime's own tokens out of the record (track R4). A harness runs
|
|
3
|
+
* model-written commands with the egress token in their proxy variables, and holds its run and
|
|
4
|
+
* host tokens; a command that prints its environment, or a model that repeats it, would put
|
|
5
|
+
* them into a tool result, an assistant message or a transcript edit. Core replaces every
|
|
6
|
+
* Nylorun-signed token (a compact JWT whose `typ` is `nylorun-…`) in what a harness reports with
|
|
7
|
+
* `[nylorun token redacted]` before recording it, whichever harness reported it. Other values
|
|
8
|
+
* pass through unchanged, and the walk keeps the value's shape.
|
|
9
|
+
*/
|
|
10
|
+
import { tokenType } from "../tenant/jwt.js";
|
|
11
|
+
export const REDACTED_TOKEN = "[nylorun token redacted]";
|
|
12
|
+
/** A compact JWT: three base64url parts, the first two JSON objects. */
|
|
13
|
+
const COMPACT_JWT = /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}/g;
|
|
14
|
+
function redactString(text) {
|
|
15
|
+
if (!text.includes("eyJ"))
|
|
16
|
+
return text;
|
|
17
|
+
return text.replace(COMPACT_JWT, (token) => (tokenType(token)?.startsWith("nylorun-") ? REDACTED_TOKEN : token));
|
|
18
|
+
}
|
|
19
|
+
/** `value` with every Nylorun-signed token in its strings replaced; the same value when there is none. */
|
|
20
|
+
export function redactTokens(value) {
|
|
21
|
+
if (typeof value === "string")
|
|
22
|
+
return redactString(value);
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
let changed = false;
|
|
25
|
+
const next = value.map((item) => {
|
|
26
|
+
const redacted = redactTokens(item);
|
|
27
|
+
if (redacted !== item)
|
|
28
|
+
changed = true;
|
|
29
|
+
return redacted;
|
|
30
|
+
});
|
|
31
|
+
return (changed ? next : value);
|
|
32
|
+
}
|
|
33
|
+
if (value !== null && typeof value === "object") {
|
|
34
|
+
let changed = false;
|
|
35
|
+
const next = {};
|
|
36
|
+
for (const [key, item] of Object.entries(value)) {
|
|
37
|
+
const redacted = redactTokens(item);
|
|
38
|
+
if (redacted !== item)
|
|
39
|
+
changed = true;
|
|
40
|
+
next[key] = redacted;
|
|
41
|
+
}
|
|
42
|
+
return (changed ? next : value);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
@@ -5,6 +5,12 @@ import type { RunOf } from "../tenant/run-grants.js";
|
|
|
5
5
|
export interface HarnessPeer {
|
|
6
6
|
/** For logs. */
|
|
7
7
|
readonly name: string;
|
|
8
|
+
/**
|
|
9
|
+
* The harness id its credential is for (R4.1): `nylorun` for the harness token, a custom
|
|
10
|
+
* harness's id for its own token. Its `hello` must name it. Absent for an in-process
|
|
11
|
+
* harness, which may name any id; a pod's host is checked against its Environment's harness.
|
|
12
|
+
*/
|
|
13
|
+
readonly harness?: string;
|
|
8
14
|
/** A pod sandbox's engine (F7.2): the sandbox it hosts, at the host epoch of its token. */
|
|
9
15
|
readonly host?: {
|
|
10
16
|
readonly sandboxId: string;
|
|
@@ -28,6 +34,8 @@ export interface RunOffer {
|
|
|
28
34
|
* only a harness that hosts no sandbox does.
|
|
29
35
|
*/
|
|
30
36
|
readonly pod?: string;
|
|
37
|
+
/** The session's harness (R4.1): only a connection that said this id takes it. Default ours. */
|
|
38
|
+
readonly harness?: string;
|
|
31
39
|
/** How long to wait for a harness to take it. Default the server's `offerWaitMs`. */
|
|
32
40
|
readonly waitMs?: number;
|
|
33
41
|
}
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
* `connection.lost`.
|
|
26
26
|
*/
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
|
-
import { HARNESS_API_VERSION, HARNESS_CLAIMS, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
28
|
+
import { DEFAULT_HARNESS, HARNESS_API_VERSION, HARNESS_CLAIMS, MAX_HARNESS_STATE_BYTES, isHarnessId, HarnessApiError, } from "@nylorun/core/harness-api";
|
|
29
29
|
import { definitionFilesOf } from "@nylorun/core/contracts";
|
|
30
30
|
import { isOwnershipLost } from "../store/ownership.js";
|
|
31
|
-
import { ownedSession } from "../tenant/context.js";
|
|
31
|
+
import { harnessOf, ownedSession } from "../tenant/context.js";
|
|
32
32
|
import { mcpDiscovered, pinDeferral, sessionToolsOf, } from "../mcp/snapshot.js";
|
|
33
33
|
import { contextWindowOf } from "../model/catalog.js";
|
|
34
34
|
import { turnManifestOf } from "../tenant/session.js";
|
|
@@ -36,6 +36,7 @@ import { sandboxWorkspaceKey, workspacePrefix } from "../sandbox/records.js";
|
|
|
36
36
|
import { abortKind } from "../tenant/worker.js";
|
|
37
37
|
import { readDefinitionFile } from "../tenant/definition-files.js";
|
|
38
38
|
import { recordIntent, recordOutcome } from "./record.js";
|
|
39
|
+
import { redactTokens } from "./redact.js";
|
|
39
40
|
import { beat, renewEveryMs, tokenOf } from "./renew.js";
|
|
40
41
|
/** No attached harness serves workspaces (or, with `pod`, that pod sandbox's). */
|
|
41
42
|
export class NoWorkspaceHarness extends Error {
|
|
@@ -48,6 +49,8 @@ export class NoWorkspaceHarness extends Error {
|
|
|
48
49
|
this.pod = pod;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
/** What our harness said in `hello` before harness ids (R4.1): refused, with the new id. */
|
|
53
|
+
const LEGACY_NAME = "@nylorun/harness";
|
|
51
54
|
const OUTPUTS = [
|
|
52
55
|
"turn.completed",
|
|
53
56
|
"turn.paused",
|
|
@@ -62,8 +65,11 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
62
65
|
const queued = [];
|
|
63
66
|
const runs = new Map();
|
|
64
67
|
let closed = false;
|
|
65
|
-
/**
|
|
66
|
-
|
|
68
|
+
/**
|
|
69
|
+
* A harness may take a run of its own harness id (R4.1): a pod sandbox's host its sandbox's
|
|
70
|
+
* runs, any other the rest.
|
|
71
|
+
*/
|
|
72
|
+
const takes = (connection, run) => connection.harness === (run.offer.harness ?? DEFAULT_HARNESS) && connection.host?.sandboxId === run.offer.pod;
|
|
67
73
|
/** Hands queued runs to waiting harnesses that may take them, in order. */
|
|
68
74
|
const match = () => {
|
|
69
75
|
for (let i = 0; i < queued.length;) {
|
|
@@ -89,16 +95,30 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
89
95
|
throw new HarnessApiError("run_not_held", `Run ${runId} is not held by this harness`);
|
|
90
96
|
return run;
|
|
91
97
|
};
|
|
98
|
+
/** What a harness reports is its claim (R4.9): its harness id and, in a run, the run. */
|
|
99
|
+
const writerOf = (connection, runId) => ({
|
|
100
|
+
source: { kind: "harness", id: connection.harness ?? connection.peer.name },
|
|
101
|
+
evidence: "claimed",
|
|
102
|
+
...(runId === undefined ? {} : { runId }),
|
|
103
|
+
});
|
|
92
104
|
const scope = (run) => ({
|
|
93
105
|
ctx,
|
|
94
106
|
lease: run.offer.lease,
|
|
95
107
|
signal: run.offer.controller.signal,
|
|
108
|
+
...(run.connection?.tools ? { nativeTools: run.connection.tools } : {}),
|
|
109
|
+
...(run.connection ? { writer: writerOf(run.connection, run.grant.runId) } : {}),
|
|
96
110
|
});
|
|
97
111
|
const serve = (connection) => async (method, params, signal) => {
|
|
98
112
|
const p = params;
|
|
99
113
|
if (method === "hello") {
|
|
100
|
-
const { capabilities } = p;
|
|
114
|
+
const { api, name, capabilities } = p;
|
|
115
|
+
// R3's next version adds a window of supported versions; until then there is one.
|
|
116
|
+
if (api !== HARNESS_API_VERSION)
|
|
117
|
+
throw new HarnessApiError("invalid", `unsupported Harness API version ${api}: this Runtime speaks ${HARNESS_API_VERSION}`);
|
|
118
|
+
connection.harness = await harnessIdOf(connection, name);
|
|
101
119
|
connection.workspace = capabilities.workspace !== undefined;
|
|
120
|
+
if (capabilities.tools?.length)
|
|
121
|
+
connection.tools = new Set(capabilities.tools.map((tool) => tool.name));
|
|
102
122
|
return {
|
|
103
123
|
api: HARNESS_API_VERSION,
|
|
104
124
|
tenantId: ctx.config.tenantId,
|
|
@@ -109,6 +129,8 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
109
129
|
}
|
|
110
130
|
if (method === "event")
|
|
111
131
|
return claim(connection, p);
|
|
132
|
+
if (method === "lease" && connection.harness === undefined)
|
|
133
|
+
throw new HarnessApiError("invalid", "Say hello first: a harness leases runs of the id it names");
|
|
112
134
|
if (method === "lease")
|
|
113
135
|
return new Promise((resolve, reject) => {
|
|
114
136
|
const entry = { connection, resolve };
|
|
@@ -121,8 +143,12 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
121
143
|
});
|
|
122
144
|
match();
|
|
123
145
|
});
|
|
124
|
-
if (OUTPUTS.includes(method))
|
|
125
|
-
|
|
146
|
+
if (OUTPUTS.includes(method)) {
|
|
147
|
+
const run = held(connection, String(p.runId), method);
|
|
148
|
+
if (connection.harness !== DEFAULT_HARNESS)
|
|
149
|
+
checkCustomOutput(run, method, p);
|
|
150
|
+
return output(run, method, p);
|
|
151
|
+
}
|
|
126
152
|
const run = held(connection, String(p.runId));
|
|
127
153
|
switch (method) {
|
|
128
154
|
case "lease.renew":
|
|
@@ -150,6 +176,27 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
150
176
|
throw new HarnessApiError("invalid", `Unknown request ${method}`);
|
|
151
177
|
}
|
|
152
178
|
};
|
|
179
|
+
/**
|
|
180
|
+
* The harness id a `hello` names (R4.1): a harness id, and the one the connection's credential
|
|
181
|
+
* is for. A pod's host must be its Environment's harness, so an image that is not the harness
|
|
182
|
+
* its recipe names never takes the Environment's runs.
|
|
183
|
+
*/
|
|
184
|
+
const harnessIdOf = async (connection, name) => {
|
|
185
|
+
if (!isHarnessId(name))
|
|
186
|
+
throw new HarnessApiError("invalid", name === LEGACY_NAME
|
|
187
|
+
? `A harness names its harness id in hello: ours is ${DEFAULT_HARNESS}. Update the harness image (it said ${LEGACY_NAME}).`
|
|
188
|
+
: `hello.name ${JSON.stringify(name)} is not a harness id: lowercase letters, digits and dashes, starting with a letter`);
|
|
189
|
+
const bound = connection.peer.harness;
|
|
190
|
+
if (bound !== undefined && bound !== name)
|
|
191
|
+
throw new HarnessApiError("invalid", `This credential is for the ${bound} harness, not ${name}`);
|
|
192
|
+
if (connection.host) {
|
|
193
|
+
const environment = await ctx.store.tx((t) => t.environment(connection.host.sandboxId));
|
|
194
|
+
const expected = environment?.applied.harness ?? DEFAULT_HARNESS;
|
|
195
|
+
if (expected !== name)
|
|
196
|
+
throw new HarnessApiError("invalid", `Environment ${connection.host.sandboxId} runs the ${expected} harness, and this pod says ${name}`);
|
|
197
|
+
}
|
|
198
|
+
return name;
|
|
199
|
+
};
|
|
153
200
|
/**
|
|
154
201
|
* A `sandbox.*` event the harness claims: for a run it holds (the run's session, or the
|
|
155
202
|
* session owning its sandbox), or a session one of its workspace requests acts for. A
|
|
@@ -169,7 +216,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
169
216
|
throw new HarnessApiError("run_not_held", `This harness may not claim events of ${event.sessionId}`);
|
|
170
217
|
const record = event.record;
|
|
171
218
|
if (record &&
|
|
172
|
-
(event.type !== "
|
|
219
|
+
(event.type !== "environment.state" ||
|
|
173
220
|
record.sessionId !== event.sessionId ||
|
|
174
221
|
!record.key.startsWith(workspacePrefix(ctx.config.tenantId)) ||
|
|
175
222
|
(connection.host !== undefined &&
|
|
@@ -179,7 +226,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
179
226
|
await ctx.store.tx(async (t) => {
|
|
180
227
|
if (record)
|
|
181
228
|
await t.put("sandboxes", record.key, record);
|
|
182
|
-
await t.event(event.sessionId, event.turnId, event.type, event.payload);
|
|
229
|
+
await t.event(event.sessionId, event.turnId, event.type, redactTokens(event.payload), writerOf(connection, event.runId));
|
|
183
230
|
});
|
|
184
231
|
}
|
|
185
232
|
catch (error) {
|
|
@@ -211,7 +258,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
211
258
|
current.mcpSnapshot = pinDeferral(snapshot, current.manifest, contextWindow);
|
|
212
259
|
current.mcpDiagnostics = diagnostics;
|
|
213
260
|
await t.put("sessions", current.id, current);
|
|
214
|
-
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics));
|
|
261
|
+
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics), run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
|
|
215
262
|
}
|
|
216
263
|
else if (diagnostics.length > 0) {
|
|
217
264
|
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
@@ -241,6 +288,27 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
241
288
|
throw new HarnessApiError("unavailable", `The Object store has no bytes for ${sha256}`);
|
|
242
289
|
return { base64: Buffer.from(bytes).toString("base64") };
|
|
243
290
|
};
|
|
291
|
+
/**
|
|
292
|
+
* A custom harness's output (R4.2): its state is its own, stored as sent and never read, so it
|
|
293
|
+
* must be JSON and at most `MAX_HARNESS_STATE_BYTES`; a larger one fails the turn, as an engine
|
|
294
|
+
* that threw would. It cannot pause for approvals in v2 (Q14): approvals read our engine's plan
|
|
295
|
+
* until R5 makes them core holds.
|
|
296
|
+
*/
|
|
297
|
+
const checkCustomOutput = (run, method, params) => {
|
|
298
|
+
if (method === "turn.paused")
|
|
299
|
+
throw new HarnessApiError("invalid", "A custom harness cannot pause for approvals in Harness API 2");
|
|
300
|
+
if (params.state === undefined || run.outputs.has(method))
|
|
301
|
+
return;
|
|
302
|
+
const bytes = Buffer.byteLength(JSON.stringify(params.state) ?? "");
|
|
303
|
+
if (bytes <= MAX_HARNESS_STATE_BYTES)
|
|
304
|
+
return;
|
|
305
|
+
output(run, "turn.failed", {
|
|
306
|
+
runId: run.grant.runId,
|
|
307
|
+
status: "failed",
|
|
308
|
+
thrown: { code: "invalid", message: `The harness's state is ${bytes} bytes, over ${MAX_HARNESS_STATE_BYTES}` },
|
|
309
|
+
}).catch(() => undefined);
|
|
310
|
+
throw new HarnessApiError("invalid", `state over 256 KiB (${bytes} bytes): the turn failed`);
|
|
311
|
+
};
|
|
244
312
|
const output = (run, method, params) => {
|
|
245
313
|
const known = run.outputs.get(method);
|
|
246
314
|
if (known)
|
|
@@ -262,7 +330,10 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
262
330
|
status() {
|
|
263
331
|
return {
|
|
264
332
|
connected: connections.size,
|
|
265
|
-
workspace: [...connections].some((connection) => connection.workspace &&
|
|
333
|
+
workspace: [...connections].some((connection) => connection.workspace &&
|
|
334
|
+
!connection.host &&
|
|
335
|
+
connection.harness === DEFAULT_HARNESS &&
|
|
336
|
+
!connection.channel.closed),
|
|
266
337
|
};
|
|
267
338
|
},
|
|
268
339
|
hosting(sandboxId) {
|
|
@@ -275,10 +346,19 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
275
346
|
},
|
|
276
347
|
async workspace(method, params, signal, target) {
|
|
277
348
|
const pod = target?.pod;
|
|
278
|
-
const
|
|
349
|
+
const session = params.session?.ownerId;
|
|
350
|
+
// A pod's workspace is its host's, whatever harness it is. Any other session's is served
|
|
351
|
+
// by its own harness, if that harness serves workspaces: our harness's workspaces are not
|
|
352
|
+
// a custom harness's files (R4.1). Sweeps and removes go to ours.
|
|
353
|
+
const harness = pod !== undefined || session === undefined
|
|
354
|
+
? DEFAULT_HARNESS
|
|
355
|
+
: harnessOf((await ctx.store.tx((t) => t.get("sessions", session))) ?? {});
|
|
356
|
+
const connection = [...connections].find((item) => item.workspace &&
|
|
357
|
+
!item.channel.closed &&
|
|
358
|
+
item.host?.sandboxId === pod &&
|
|
359
|
+
(pod !== undefined || item.harness === harness));
|
|
279
360
|
if (!connection)
|
|
280
361
|
throw new NoWorkspaceHarness(pod);
|
|
281
|
-
const session = params.session?.ownerId;
|
|
282
362
|
if (session)
|
|
283
363
|
connection.claims.set(session, (connection.claims.get(session) ?? 0) + 1);
|
|
284
364
|
else
|