@nylorun/runtime 0.24.0-beta → 0.26.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 +143 -0
- package/README.md +18 -4
- package/dist/api/a2a/card.d.ts +11 -1
- package/dist/api/a2a/card.js +26 -2
- package/dist/api/a2a/endpoint.js +4 -2
- package/dist/api/a2a/protocol.d.ts +25 -2
- package/dist/api/a2a/protocol.js +20 -3
- package/dist/api/a2a/routes.d.ts +4 -1
- package/dist/api/a2a/routes.js +65 -7
- package/dist/api/ag-ui/endpoint.js +5 -2
- package/dist/api/ag-ui/history.d.ts +6 -1
- package/dist/api/ag-ui/history.js +35 -2
- package/dist/api/ag-ui/routes.js +53 -11
- package/dist/api/components.d.ts +1165 -678
- package/dist/api/components.js +16 -1
- package/dist/api/gate/keys.d.ts +2 -1
- package/dist/api/gate/keys.js +7 -0
- package/dist/api/gate/routes.js +13 -0
- package/dist/api/http/app.d.ts +2 -2
- package/dist/api/http/app.js +15 -4
- package/dist/api/http/body.d.ts +3 -3
- package/dist/api/http/body.js +7 -7
- package/dist/api/http/define.d.ts +1 -1
- package/dist/api/http/respond.js +1 -1
- package/dist/api/http/routes/environment-manifests.d.ts +9 -0
- package/dist/api/http/routes/environment-manifests.js +118 -0
- package/dist/api/http/routes/environments.js +61 -12
- package/dist/api/http/routes/reads.d.ts +52 -3
- package/dist/api/http/routes/sessions.js +14 -4
- package/dist/api/http/routes/tenant.js +35 -7
- package/dist/api/inbound-files.d.ts +65 -0
- package/dist/api/inbound-files.js +115 -0
- package/dist/api/openapi.js +18 -3
- package/dist/artifacts/disk.js +20 -14
- package/dist/artifacts/media-types.d.ts +15 -0
- package/dist/artifacts/media-types.js +56 -4
- package/dist/artifacts/parts.d.ts +12 -3
- package/dist/artifacts/parts.js +15 -4
- package/dist/artifacts/workspace.d.ts +4 -4
- package/dist/artifacts/workspace.js +6 -18
- package/dist/core/flow-host.d.ts +1 -1
- package/dist/core/flow-host.js +2 -2
- package/dist/core/sandbox-routes.js +2 -2
- package/dist/core/turn-manifest.d.ts +3 -2
- package/dist/core/turn-manifest.js +29 -5
- package/dist/gates/egress-credentials.js +1 -1
- package/dist/gates/egress.d.ts +13 -19
- package/dist/gates/egress.js +32 -29
- package/dist/gates/http-tool.d.ts +3 -4
- package/dist/gates/http-tool.js +23 -19
- package/dist/gates/journal.js +0 -1
- package/dist/gates/mcp-handler.js +2 -3
- package/dist/gates/policy.js +1 -3
- package/dist/gates/tool-contract.d.ts +1 -1
- package/dist/harness/calls.d.ts +2 -2
- package/dist/harness/calls.js +9 -12
- package/dist/harness/main.js +7 -1
- package/dist/harness/pod.d.ts +6 -1
- package/dist/harness/pod.js +5 -0
- package/dist/harness/service.js +5 -5
- package/dist/harness/setup.d.ts +2 -0
- package/dist/harness/setup.js +25 -1
- package/dist/harness-api/record.d.ts +1 -1
- package/dist/harness-api/record.js +13 -13
- package/dist/harness-api/server.d.ts +27 -4
- package/dist/harness-api/server.js +41 -8
- package/dist/harness-api/tool-journal.d.ts +17 -8
- package/dist/harness-api/tool-journal.js +41 -11
- package/dist/harness-api/workspace.d.ts +7 -5
- package/dist/harness-api/workspace.js +25 -19
- package/dist/host/gates.js +2 -2
- package/dist/host/stack-config.js +1 -1
- package/dist/keys/contract.d.ts +1 -1
- package/dist/keys/contract.js +1 -1
- package/dist/keys/keys.d.ts +9 -2
- package/dist/keys/keys.js +4 -1
- package/dist/management-openapi.json +366 -61
- package/dist/mcp/connect.d.ts +1 -2
- package/dist/mcp/connect.js +1 -4
- package/dist/mcp/pool.d.ts +1 -4
- package/dist/mcp/pool.js +13 -27
- package/dist/mcp/preview.d.ts +1 -1
- package/dist/mcp/preview.js +1 -2
- package/dist/mcp/snapshot.d.ts +11 -13
- package/dist/mcp/snapshot.js +27 -38
- package/dist/model/capabilities.d.ts +26 -0
- package/dist/model/capabilities.js +44 -0
- package/dist/model/catalog.d.ts +34 -3
- package/dist/model/catalog.js +38 -2
- package/dist/model/models.d.ts +2 -0
- package/dist/model/models.js +1 -1
- package/dist/model/native-blocks.d.ts +35 -0
- package/dist/model/native-blocks.js +170 -0
- package/dist/model/pi-model.js +106 -94
- package/dist/openapi.json +9554 -5076
- package/dist/record/environment.js +1 -1
- package/dist/sandbox/join.js +4 -0
- package/dist/sandbox/manager.d.ts +32 -14
- package/dist/sandbox/manager.js +132 -93
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +2 -2
- package/dist/sandbox/pods/client.d.ts +3 -1
- package/dist/sandbox/pods/lifecycle.d.ts +8 -0
- package/dist/sandbox/pods/lifecycle.js +15 -0
- package/dist/sandbox/pods/spec.js +2 -1
- package/dist/sandbox/resolve.d.ts +5 -3
- package/dist/sandbox/resolve.js +9 -2
- package/dist/sandbox/runtime-tools.d.ts +9 -0
- package/dist/sandbox/runtime-tools.js +16 -0
- package/dist/sandbox/session-sandbox.d.ts +4 -6
- package/dist/sandbox/session-sandbox.js +47 -94
- package/dist/sandbox/setup.d.ts +4 -1
- package/dist/sandbox/setup.js +46 -9
- package/dist/sandbox/share.d.ts +12 -7
- package/dist/sandbox/share.js +27 -25
- package/dist/sandbox/skills.d.ts +8 -6
- package/dist/sandbox/skills.js +24 -23
- package/dist/store/postgres/drizzle/0024_flat_manifest.sql +7 -0
- package/dist/store/postgres/drizzle/0025_environment_manifests.sql +14 -0
- package/dist/store/postgres/drizzle/0026_environment_forks.sql +1 -0
- package/dist/store/postgres/drizzle/0027_environment_image_digest.sql +1 -0
- package/dist/store/postgres/drizzle/meta/_journal.json +28 -0
- package/dist/store/postgres/migrate.js +1 -1
- package/dist/store/postgres/reads.js +8 -2
- package/dist/store/postgres/schema.d.ts +217 -0
- package/dist/store/postgres/schema.js +36 -2
- package/dist/store/postgres/store.js +79 -4
- package/dist/store/types.d.ts +68 -14
- package/dist/tenant/advance.js +3 -0
- package/dist/tenant/artifact-tool.d.ts +1 -1
- package/dist/tenant/artifact-tool.js +43 -22
- package/dist/tenant/commands.js +24 -4
- package/dist/tenant/context.d.ts +10 -4
- package/dist/tenant/effects.js +7 -9
- package/dist/tenant/environment-manifests.d.ts +57 -0
- package/dist/tenant/environment-manifests.js +200 -0
- package/dist/tenant/environments.d.ts +54 -47
- package/dist/tenant/environments.js +309 -85
- package/dist/tenant/ephemeral.d.ts +1 -1
- package/dist/tenant/host-token.js +1 -1
- package/dist/tenant/identity-file.d.ts +1 -1
- package/dist/tenant/identity-file.js +1 -1
- package/dist/tenant/model-setting.d.ts +14 -0
- package/dist/tenant/model-setting.js +51 -0
- package/dist/tenant/outbound.d.ts +6 -5
- package/dist/tenant/outbound.js +8 -6
- package/dist/tenant/sessions.d.ts +5 -1
- package/dist/tenant/sessions.js +45 -12
- package/dist/tenant/skill-tool.d.ts +3 -3
- package/dist/tenant/skill-tool.js +6 -11
- package/dist/tenant/sweep.js +1 -1
- package/dist/tenant/tool-results.js +1 -1
- package/dist/tenant/tool-search.d.ts +7 -11
- package/dist/tenant/tool-search.js +8 -5
- package/dist/tenant/transcript.d.ts +51 -32
- package/dist/tenant/transcript.js +2 -1
- package/dist/tenant/types.d.ts +2 -2
- package/dist/traces/ids.d.ts +0 -1
- package/dist/traces/ids.js +0 -4
- package/dist/traces/map.d.ts +2 -2
- package/dist/traces/map.js +1 -29
- package/dist/vault/coverage.js +11 -13
- package/dist/vault/host-model.d.ts +4 -1
- package/dist/vault/host-model.js +10 -2
- package/dist/vault/service.d.ts +49 -4
- package/dist/vault/service.js +153 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/sandbox/capability.d.ts +0 -3
- package/dist/sandbox/capability.js +0 -10
package/dist/harness/pod.js
CHANGED
|
@@ -33,6 +33,7 @@ export function podHost(config, logger, options = {}) {
|
|
|
33
33
|
let held;
|
|
34
34
|
let setup;
|
|
35
35
|
let seeded = false;
|
|
36
|
+
let afterSeed = false;
|
|
36
37
|
/** The CA in the bundle on disk, once one is written. */
|
|
37
38
|
let bundled;
|
|
38
39
|
const post = async (path, init) => {
|
|
@@ -56,6 +57,7 @@ export function podHost(config, logger, options = {}) {
|
|
|
56
57
|
if (path === "join") {
|
|
57
58
|
setup = body.setup;
|
|
58
59
|
seeded = body.seeded === true;
|
|
60
|
+
afterSeed = body.afterSeed === true;
|
|
59
61
|
}
|
|
60
62
|
return {
|
|
61
63
|
hostToken: body.hostToken,
|
|
@@ -168,6 +170,9 @@ export function podHost(config, logger, options = {}) {
|
|
|
168
170
|
get seeded() {
|
|
169
171
|
return seeded;
|
|
170
172
|
},
|
|
173
|
+
get afterSeed() {
|
|
174
|
+
return afterSeed;
|
|
175
|
+
},
|
|
171
176
|
async setupState() {
|
|
172
177
|
const response = await authorized("setup", { method: "GET" });
|
|
173
178
|
if (!response.ok)
|
package/dist/harness/service.js
CHANGED
|
@@ -82,16 +82,16 @@ export function startHarnessService(options) {
|
|
|
82
82
|
if ("disk" in params) {
|
|
83
83
|
// A disk version's read or write (R2d step 3).
|
|
84
84
|
const call = params;
|
|
85
|
-
const answer = await sandbox().disk(refOf(call.session),
|
|
85
|
+
const answer = await sandbox().disk(refOf(call.session), call.spec, fromWireOp(call.disk), signal);
|
|
86
86
|
return "code" in answer ? answer : toWireAnswer(answer);
|
|
87
87
|
}
|
|
88
88
|
if (method === "workspace.read" && "list" in params) {
|
|
89
89
|
const call = params;
|
|
90
|
-
return sandbox().listFiles(refOf(call.session),
|
|
90
|
+
return sandbox().listFiles(refOf(call.session), call.spec, call.list.dir, call.list.maxEntries, signal);
|
|
91
91
|
}
|
|
92
92
|
if (method === "workspace.read" && "bytes" in params) {
|
|
93
93
|
const call = params;
|
|
94
|
-
const read = await sandbox().readBytes(refOf(call.session),
|
|
94
|
+
const read = await sandbox().readBytes(refOf(call.session), call.spec, call.bytes.path, call.bytes.maxBytes, signal);
|
|
95
95
|
return read.kind === "read"
|
|
96
96
|
? { kind: "read", path: read.path, base64: Buffer.from(read.bytes).toString("base64") }
|
|
97
97
|
: read;
|
|
@@ -99,7 +99,7 @@ export function startHarnessService(options) {
|
|
|
99
99
|
const call = params;
|
|
100
100
|
if (METHODS[call.tool] !== method)
|
|
101
101
|
throw new HarnessApiError("invalid", `${method} does not run ${call.tool}`);
|
|
102
|
-
return sandbox().run(refOf(call.session),
|
|
102
|
+
return sandbox().run(refOf(call.session), call.spec, call.tool, call.input, signal);
|
|
103
103
|
}
|
|
104
104
|
case "workspace.report":
|
|
105
105
|
return sandbox().report();
|
|
@@ -138,7 +138,7 @@ export function startHarnessService(options) {
|
|
|
138
138
|
// Our harness: its hello names our harness id (R4.1).
|
|
139
139
|
name: DEFAULT_HARNESS,
|
|
140
140
|
...(options.version ? { version: options.version } : {}),
|
|
141
|
-
|
|
141
|
+
features: { workspace: { backends: backends.map((backend) => backend.name) } },
|
|
142
142
|
onHello(answer) {
|
|
143
143
|
tenantId = answer.tenantId;
|
|
144
144
|
manager ??= new SandboxManager({
|
package/dist/harness/setup.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface DiskSetupOptions {
|
|
|
6
6
|
readonly host: Pick<PodHost, "setup" | "setupState" | "reportSetup" | "setupFile"> & {
|
|
7
7
|
/** The volume starts from a disk version (R2d step 3): it holds what setup made. */
|
|
8
8
|
readonly seeded?: boolean;
|
|
9
|
+
/** A fork onto other setup (Q17): run it once core has written the version. */
|
|
10
|
+
readonly afterSeed?: boolean;
|
|
9
11
|
};
|
|
10
12
|
/** Runs one command as the sandbox tools do (`localExec`). */
|
|
11
13
|
readonly exec: (request: ExecRequest, signal: AbortSignal) => Promise<ExecResult>;
|
package/dist/harness/setup.js
CHANGED
|
@@ -8,12 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* A failure is reported to core, which fails the pod and the turn waiting for it; the engine then
|
|
10
10
|
* polls until a turn start asks the pod to run again, and runs the setup again from the start.
|
|
11
|
+
*
|
|
12
|
+
* A volume seeded from a disk version skips it (the version holds what setup made), except on a
|
|
13
|
+
* fork onto other setup (Q17): the engine connects first, waits until core has written the
|
|
14
|
+
* version, then runs the setup on it as above. Turns wait until it reports the setup done.
|
|
11
15
|
*/
|
|
12
16
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
13
17
|
import { randomUUID } from "node:crypto";
|
|
14
18
|
import { dirname, posix } from "node:path";
|
|
15
19
|
/** How often a failed setup asks whether to run again. */
|
|
16
20
|
export const SETUP_RETRY_POLL_MS = 5_000;
|
|
21
|
+
/** How often a fork's setup asks whether its disk version is written (Q17). */
|
|
22
|
+
const AFTER_SEED_POLL_MS = 1_000;
|
|
17
23
|
/** The end of a failed command's output kept in its report. */
|
|
18
24
|
const REPORT_TAIL = 4 * 1024;
|
|
19
25
|
async function readText(path) {
|
|
@@ -89,7 +95,25 @@ async function runOnce(options, setup) {
|
|
|
89
95
|
export async function prepareDisk(options) {
|
|
90
96
|
let setup = options.host.setup;
|
|
91
97
|
const volumeId = await readText(options.volumeFile);
|
|
92
|
-
if (options.host.
|
|
98
|
+
if (options.host.afterSeed) {
|
|
99
|
+
// A fork onto other setup (Q17): wait until core has written the disk version, then run it.
|
|
100
|
+
for (;;) {
|
|
101
|
+
if (options.signal?.aborted)
|
|
102
|
+
return;
|
|
103
|
+
const state = await options.host.setupState().catch(() => undefined);
|
|
104
|
+
if (state?.retry) {
|
|
105
|
+
setup = state.setup;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollMs ?? AFTER_SEED_POLL_MS));
|
|
109
|
+
}
|
|
110
|
+
if (!setup) {
|
|
111
|
+
// Nothing to run: say so, so the fork's turns may start.
|
|
112
|
+
await options.host.reportSetup({ status: "ok" }).catch(() => undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (options.host.seeded) {
|
|
93
117
|
// A volume seeded from a disk version: the version holds what setup made, so mark it done.
|
|
94
118
|
if (volumeId) {
|
|
95
119
|
const done = { volumeId, recipeVersion: setup?.recipeVersion ?? 0, status: "ok" };
|
|
@@ -27,7 +27,7 @@ export interface RecordScope {
|
|
|
27
27
|
readonly ctx: TenantContext;
|
|
28
28
|
readonly lease: Lease;
|
|
29
29
|
readonly signal: AbortSignal;
|
|
30
|
-
/** The tools the run's harness declared it runs itself (`hello` `
|
|
30
|
+
/** The tools the run's harness declared it runs itself (`hello` `features.tools`, R4.2), by name. */
|
|
31
31
|
readonly nativeTools?: ReadonlySet<string>;
|
|
32
32
|
/**
|
|
33
33
|
* Who reported what this scope records (R4.9): the run's harness, so the events derived from
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
* row keeps its span's context (`traceparent`, R7.4), which the `execute` answer carries to the
|
|
19
19
|
* harness and the Tool Gate reads with the row.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import { HarnessApiError, effectRequestHash, isEffectIdOf, } from "@nylorun/core/harness-api";
|
|
22
22
|
import { isFlowEffect, isFlowToolEffect, recordVerdict, settleAgentEffect, } from "../core/flow-host.js";
|
|
23
|
-
import {
|
|
23
|
+
import { sandboxToolSpec } from "../sandbox/runtime-tools.js";
|
|
24
24
|
import { abortOn, isReadArtifactCall, isSaveArtifactCall } from "../harness/calls.js";
|
|
25
25
|
import { callReadArtifact, callSaveArtifact } from "../tenant/artifact-tool.js";
|
|
26
26
|
import { mayNeedShaping, resultBytes, resultLimit, shapeToolOutcome, } from "../tenant/tool-results.js";
|
|
@@ -143,17 +143,17 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
143
143
|
(mcpToolOf(s.mcpSnapshot, request, turnManifestOf(s)) !== undefined ||
|
|
144
144
|
// HTTP tools (R2 M3) cross the Tool Gate.
|
|
145
145
|
isHttpToolCall(s.manifest, request) ||
|
|
146
|
-
|
|
146
|
+
sandboxToolSpec(agentManifest, request.toolName) !== undefined ||
|
|
147
147
|
// `save_artifact` (F8.1) runs beside the sandbox tools, as do `read_artifact` (R2b
|
|
148
148
|
// C11) and the skill tools (R2 M4).
|
|
149
149
|
isSaveArtifactCall(agentManifest, request) ||
|
|
150
150
|
isReadArtifactCall(agentManifest, request) ||
|
|
151
151
|
isSkillToolCall(agentManifest, request) ||
|
|
152
152
|
// `tool_search` (R2b C10) reads the session's pinned MCP snapshot: core serves it.
|
|
153
|
-
isToolSearchCall(
|
|
154
|
-
// A tool the run's harness declared and runs itself (R4.2, Q3)
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
isToolSearchCall(s.mcpSnapshot, request) ||
|
|
154
|
+
// A tool the run's harness declared and runs itself (R4.2, Q3), by its name: the turn
|
|
155
|
+
// refuses a native tool whose name another tool of the agent's has (`harness-api/server.ts`).
|
|
156
|
+
(request.toolName !== undefined && scope.nativeTools?.has(request.toolName) === true)));
|
|
157
157
|
if (!executed) {
|
|
158
158
|
// A tool that would run the developer's code: refused at save (a turn's manifest only
|
|
159
159
|
// removes tools), so this is a backstop, as is an MCP tool the turn disabled, which the
|
|
@@ -163,7 +163,7 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
163
163
|
request,
|
|
164
164
|
requestHash,
|
|
165
165
|
status: "invoking",
|
|
166
|
-
...toolKindOf(s, agentManifest, request),
|
|
166
|
+
...toolKindOf(s, agentManifest, request, scope.nativeTools),
|
|
167
167
|
attempts: 0,
|
|
168
168
|
};
|
|
169
169
|
const outcome = await settleToolRow(t, s, row, disabled ? disabledTool(request) : unrunnableTool(request), { writer: coreIn(scope) });
|
|
@@ -175,7 +175,7 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
175
175
|
requestHash,
|
|
176
176
|
status: "invoking",
|
|
177
177
|
...(request.kind === "tool"
|
|
178
|
-
? { ...firstSend(toolKindOf(s, agentManifest, request)), ...traced() }
|
|
178
|
+
? { ...firstSend(toolKindOf(s, agentManifest, request, scope.nativeTools)), ...traced() }
|
|
179
179
|
: { startedAt: new Date().toISOString() }),
|
|
180
180
|
};
|
|
181
181
|
// The Tenant's tool policy decides an MCP or HTTP tool call as it is journaled (R5 step 2).
|
|
@@ -186,7 +186,7 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
186
186
|
}
|
|
187
187
|
await t.put("effects", request.effectId, row);
|
|
188
188
|
// `save_artifact` writes the Tenant's artifacts: core runs it, reading the file through the
|
|
189
|
-
// workspace
|
|
189
|
+
// workspace feature, wherever the sandbox is.
|
|
190
190
|
if (request.kind === "tool" && isSaveArtifactCall(agentManifest, request))
|
|
191
191
|
return "save";
|
|
192
192
|
// `read_artifact` reads the session's artifacts: core serves it, with or without a sandbox.
|
|
@@ -195,7 +195,7 @@ export async function recordIntent(scope, effect, requestHash) {
|
|
|
195
195
|
// The skill tools read the agent's definition files: core serves them.
|
|
196
196
|
if (isSkillToolCall(agentManifest, request))
|
|
197
197
|
return { skill: agentManifest };
|
|
198
|
-
if (isToolSearchCall(
|
|
198
|
+
if (isToolSearchCall(s.mcpSnapshot, request))
|
|
199
199
|
return "search";
|
|
200
200
|
return { status: "execute", ...(row.traceparent ? { traceparent: row.traceparent } : {}) };
|
|
201
201
|
});
|
|
@@ -413,8 +413,8 @@ async function shaped(scope, effectId, value) {
|
|
|
413
413
|
if (!s || !effect || effect.status === "completed")
|
|
414
414
|
return undefined;
|
|
415
415
|
const request = effect.request;
|
|
416
|
-
// An agent's tool; a flow's HTTP stage or verifier names no
|
|
417
|
-
if (request.kind !== "tool" || request.
|
|
416
|
+
// An agent's tool; a flow's HTTP stage or verifier names no tool, and its output is data.
|
|
417
|
+
if (request.kind !== "tool" || request.toolName === undefined)
|
|
418
418
|
return undefined;
|
|
419
419
|
const mcp = mcpToolOf(s.mcpSnapshot, request) !== undefined;
|
|
420
420
|
if (!mcp && !isHttpToolCall(s.manifest, request))
|
|
@@ -59,6 +59,14 @@ export type RunEnd = {
|
|
|
59
59
|
/** No harness took the run within `offerWaitMs`. */
|
|
60
60
|
| {
|
|
61
61
|
readonly kind: "unavailable";
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The harness that would take the run cannot: a tool it runs itself (`features.tools`) has the
|
|
65
|
+
* name of a tool of the turn's agents. The turn fails with `message`.
|
|
66
|
+
*/
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: "refused";
|
|
69
|
+
readonly message: string;
|
|
62
70
|
};
|
|
63
71
|
export interface HarnessApiServer {
|
|
64
72
|
/** Serves a harness's channel until it closes. Returns a function that detaches it. */
|
|
@@ -66,19 +74,26 @@ export interface HarnessApiServer {
|
|
|
66
74
|
offer(offer: RunOffer): Promise<RunEnd>;
|
|
67
75
|
/**
|
|
68
76
|
* Sends a `workspace.*` request to a harness that serves workspaces: with `pod`, the host of
|
|
69
|
-
* that pod sandbox
|
|
70
|
-
*
|
|
77
|
+
* that pod sandbox (with `minEpoch`, one that joined at that host epoch or later, so a pod's
|
|
78
|
+
* new volume is never written through its old host); otherwise one that hosts no sandbox.
|
|
79
|
+
* Throws `NoWorkspaceHarness` when none is attached.
|
|
71
80
|
*/
|
|
72
81
|
workspace<M extends CoreMethod>(method: M, params: ParamsOf<M>, signal?: AbortSignal, target?: {
|
|
73
82
|
readonly pod?: string;
|
|
83
|
+
readonly minEpoch?: number;
|
|
74
84
|
}): Promise<ResultOf<M>>;
|
|
75
85
|
/**
|
|
76
86
|
* Pod sandbox `sandboxId`'s host epoch moved to `epoch` (F7.2): connections hosting it at an
|
|
77
87
|
* older epoch are closed.
|
|
78
88
|
*/
|
|
79
89
|
revokeHost(sandboxId: string, epoch: number): void;
|
|
80
|
-
/**
|
|
81
|
-
|
|
90
|
+
/**
|
|
91
|
+
* Whether a host of pod sandbox `sandboxId` is connected: with `minEpoch`, one that joined at
|
|
92
|
+
* that host epoch or later. A restore or a lost volume moves the epoch before the old host's
|
|
93
|
+
* connection is closed (`host.revoked` is delivered after the commit), so a caller that writes
|
|
94
|
+
* the new volume passes the Environment's epoch.
|
|
95
|
+
*/
|
|
96
|
+
hosting(sandboxId: string, minEpoch?: number): boolean;
|
|
82
97
|
/** Harnesses attached now, and whether one serves workspaces. */
|
|
83
98
|
status(): HarnessStatus;
|
|
84
99
|
/** Harnesses attached now. */
|
|
@@ -102,3 +117,11 @@ export interface HarnessApiServerOptions {
|
|
|
102
117
|
sandboxPreference?: string;
|
|
103
118
|
}
|
|
104
119
|
export declare function createHarnessApiServer(ctx: TenantContext, options?: HarnessApiServerOptions): HarnessApiServer;
|
|
120
|
+
/**
|
|
121
|
+
* The tools a harness runs itself (`features.tools`) whose names the turn already uses: a tool of
|
|
122
|
+
* the turn's agents (their own and the Runtime's `runtime` tools), a session tool, `tool_search` or
|
|
123
|
+
* `tool_call` (session tools a later step may get). Tools are known by name alone, so a native
|
|
124
|
+
* tool must not take one (Harness API 3); `hello` already refuses a name with `__`, which names an
|
|
125
|
+
* MCP server's tools. A flow turn runs no native tools.
|
|
126
|
+
*/
|
|
127
|
+
export declare function nativeToolClashes(tools: ReadonlySet<string> | undefined, start: TurnStart): string[];
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { randomUUID } from "node:crypto";
|
|
28
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
|
+
import { TOOL_CALL_TOOL, TOOL_SEARCH_TOOL, delegatesOf, } from "@nylorun/core/define";
|
|
30
31
|
import { isOwnershipLost } from "../store/ownership.js";
|
|
31
32
|
import { harnessOf, ownedSession } from "../tenant/context.js";
|
|
32
33
|
import { mcpDiscovered, pinDeferral, sessionToolsOf, } from "../mcp/snapshot.js";
|
|
@@ -80,6 +81,15 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
80
81
|
continue;
|
|
81
82
|
}
|
|
82
83
|
queued.splice(i, 1);
|
|
84
|
+
const clash = nativeToolClashes(waiting[at].connection.tools, run.offer.start);
|
|
85
|
+
if (clash.length > 0) {
|
|
86
|
+
// The harness keeps waiting for a run its tools fit; this turn fails (Harness API 3).
|
|
87
|
+
run.end({
|
|
88
|
+
kind: "refused",
|
|
89
|
+
message: `Harness '${waiting[at].connection.harness}' runs tools named like tools of this turn's agents: ${clash.join(", ")}. A tool the harness runs itself needs a name no other tool of the session has.`,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
83
93
|
const lease = waiting.splice(at, 1)[0];
|
|
84
94
|
run.connection = lease.connection;
|
|
85
95
|
// The run token the advance holds now (F5): the gate calls of this run present it.
|
|
@@ -111,14 +121,14 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
111
121
|
const serve = (connection) => async (method, params, signal) => {
|
|
112
122
|
const p = params;
|
|
113
123
|
if (method === "hello") {
|
|
114
|
-
const { api, name,
|
|
124
|
+
const { api, name, features } = p;
|
|
115
125
|
// R3's next version adds a window of supported versions; until then there is one.
|
|
116
126
|
if (api !== HARNESS_API_VERSION)
|
|
117
127
|
throw new HarnessApiError("invalid", `unsupported Harness API version ${api}: this Runtime speaks ${HARNESS_API_VERSION}`);
|
|
118
128
|
connection.harness = await harnessIdOf(connection, name);
|
|
119
|
-
connection.workspace =
|
|
120
|
-
if (
|
|
121
|
-
connection.tools = new Set(
|
|
129
|
+
connection.workspace = features.workspace !== undefined;
|
|
130
|
+
if (features.tools?.length)
|
|
131
|
+
connection.tools = new Set(features.tools.map((tool) => tool.name));
|
|
122
132
|
return {
|
|
123
133
|
api: HARNESS_API_VERSION,
|
|
124
134
|
tenantId: ctx.config.tenantId,
|
|
@@ -258,12 +268,12 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
258
268
|
current.mcpSnapshot = pinDeferral(snapshot, current.manifest, contextWindow);
|
|
259
269
|
current.mcpDiagnostics = diagnostics;
|
|
260
270
|
await t.put("sessions", current.id, current);
|
|
261
|
-
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics), run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
|
|
271
|
+
await t.event(current.id, run.grant.turnId, "mcp.discovered", mcpDiscovered(current.mcpSnapshot, diagnostics, current.manifest), run.connection ? writerOf(run.connection, run.grant.runId) : undefined);
|
|
262
272
|
}
|
|
263
273
|
else if (diagnostics.length > 0) {
|
|
264
274
|
const prior = [...(current.mcpDiagnostics ?? [])];
|
|
265
275
|
for (const item of diagnostics) {
|
|
266
|
-
const index = prior.findIndex((existing) => existing.
|
|
276
|
+
const index = prior.findIndex((existing) => (existing.agentId ?? null) === (item.agentId ?? null) && existing.serverName === item.serverName);
|
|
267
277
|
if (index >= 0)
|
|
268
278
|
prior[index] = item;
|
|
269
279
|
else
|
|
@@ -336,8 +346,10 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
336
346
|
!connection.channel.closed),
|
|
337
347
|
};
|
|
338
348
|
},
|
|
339
|
-
hosting(sandboxId) {
|
|
340
|
-
return [...connections].some((connection) => connection.host?.sandboxId === sandboxId &&
|
|
349
|
+
hosting(sandboxId, minEpoch) {
|
|
350
|
+
return [...connections].some((connection) => connection.host?.sandboxId === sandboxId &&
|
|
351
|
+
connection.host.epoch >= (minEpoch ?? 0) &&
|
|
352
|
+
!connection.channel.closed);
|
|
341
353
|
},
|
|
342
354
|
revokeHost(sandboxId, epoch) {
|
|
343
355
|
for (const connection of [...connections])
|
|
@@ -356,6 +368,7 @@ export function createHarnessApiServer(ctx, options = {}) {
|
|
|
356
368
|
const connection = [...connections].find((item) => item.workspace &&
|
|
357
369
|
!item.channel.closed &&
|
|
358
370
|
item.host?.sandboxId === pod &&
|
|
371
|
+
(item.host === undefined || item.host.epoch >= (target?.minEpoch ?? 0)) &&
|
|
359
372
|
(pod !== undefined || item.harness === harness));
|
|
360
373
|
if (!connection)
|
|
361
374
|
throw new NoWorkspaceHarness(pod);
|
|
@@ -496,3 +509,23 @@ function isSnapshot(value) {
|
|
|
496
509
|
typeof snapshot.manifestHash === "string" &&
|
|
497
510
|
Array.isArray(snapshot.mcpTools));
|
|
498
511
|
}
|
|
512
|
+
/**
|
|
513
|
+
* The tools a harness runs itself (`features.tools`) whose names the turn already uses: a tool of
|
|
514
|
+
* the turn's agents (their own and the Runtime's `runtime` tools), a session tool, `tool_search` or
|
|
515
|
+
* `tool_call` (session tools a later step may get). Tools are known by name alone, so a native
|
|
516
|
+
* tool must not take one (Harness API 3); `hello` already refuses a name with `__`, which names an
|
|
517
|
+
* MCP server's tools. A flow turn runs no native tools.
|
|
518
|
+
*/
|
|
519
|
+
export function nativeToolClashes(tools, start) {
|
|
520
|
+
if (!tools?.size || start.engine !== "agent")
|
|
521
|
+
return [];
|
|
522
|
+
const manifest = start.manifest;
|
|
523
|
+
const taken = new Set([TOOL_SEARCH_TOOL, TOOL_CALL_TOOL]);
|
|
524
|
+
for (const agent of [manifest, ...delegatesOf(manifest).map((delegate) => delegate.manifest)])
|
|
525
|
+
for (const tool of [...(agent.tools ?? []), ...(agent.runtime?.tools ?? [])])
|
|
526
|
+
taken.add(tool.name);
|
|
527
|
+
for (const tool of (start.sessionTools ?? []))
|
|
528
|
+
if (typeof tool?.name === "string")
|
|
529
|
+
taken.add(tool.name);
|
|
530
|
+
return [...tools].filter((name) => taken.has(name)).sort();
|
|
531
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* (`policy`). A flow's HTTP stage, which names no tool call, ends in `node.completed` instead.
|
|
19
19
|
*/
|
|
20
20
|
import type { EventWriter } from "@nylorun/core/contracts";
|
|
21
|
-
import type
|
|
21
|
+
import { type AgentManifest, type PluginLabel } from "@nylorun/core/define";
|
|
22
22
|
import type { HostEffect } from "@nylorun/harness/run";
|
|
23
23
|
import type { Session } from "../tenant/context.js";
|
|
24
24
|
import { type GateTimes, type ToolCallFacts, type ToolCallKind, type ToolCallStatus, type ToolPolicyFact } from "../tenant/transcript.js";
|
|
@@ -40,6 +40,9 @@ export interface ToolRowFacts {
|
|
|
40
40
|
};
|
|
41
41
|
toolKind?: ToolCallKind;
|
|
42
42
|
server?: string;
|
|
43
|
+
/** The skill the call ran, and the plugin that brought its skill or MCP server. */
|
|
44
|
+
skill?: string;
|
|
45
|
+
plugin?: PluginLabel;
|
|
43
46
|
attempts?: number;
|
|
44
47
|
startedAt?: string;
|
|
45
48
|
settledAt?: string;
|
|
@@ -57,16 +60,22 @@ export type ToolRow = EffectDoc & ToolRowFacts & {
|
|
|
57
60
|
};
|
|
58
61
|
error?: string;
|
|
59
62
|
};
|
|
60
|
-
/** Where a tool call runs, and
|
|
61
|
-
export
|
|
63
|
+
/** Where a tool call runs, and what it traces to (Q7, Q11). */
|
|
64
|
+
export interface ToolCallPlace {
|
|
62
65
|
toolKind: ToolCallKind;
|
|
66
|
+
/** Its MCP server or, for an agent's HTTP tool, its URL's host. */
|
|
63
67
|
server?: string;
|
|
64
|
-
|
|
68
|
+
skill?: string;
|
|
69
|
+
plugin?: PluginLabel;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Where a tool call runs, and what it traces to: an MCP call names its server and the plugin
|
|
73
|
+
* that brought it; a skill tool, or a sandbox tool whose input names `/skills/<name>@<hash>/`, names the
|
|
74
|
+
* skill and its plugin. A tool the turn's harness declared (`nativeTools`) runs in the harness.
|
|
75
|
+
*/
|
|
76
|
+
export declare function toolKindOf(s: Pick<Session, "manifest" | "mcpSnapshot">, agentManifest: AgentManifest | undefined, request: HostEffect, nativeTools?: ReadonlySet<string>): ToolCallPlace;
|
|
65
77
|
/** The facts a new tool row starts with: its first send, now. */
|
|
66
|
-
export declare function firstSend(kind:
|
|
67
|
-
toolKind: ToolCallKind;
|
|
68
|
-
server?: string;
|
|
69
|
-
}, now?: Date): ToolRowFacts;
|
|
78
|
+
export declare function firstSend(kind: ToolCallPlace, now?: Date): ToolRowFacts;
|
|
70
79
|
/** A failed outcome the Runtime gives a call a crash interrupted: it may have run. */
|
|
71
80
|
export declare function interrupted(message: string): {
|
|
72
81
|
kind: string;
|
|
@@ -1,21 +1,49 @@
|
|
|
1
|
+
import { pluginOf, skillOfToolCall } from "@nylorun/core/define";
|
|
1
2
|
import { isFlowToolEffect } from "../core/flow-host.js";
|
|
2
|
-
import { isHttpToolCall } from "../gates/http-tool.js";
|
|
3
|
+
import { declaredHttpTool, isHttpToolCall } from "../gates/http-tool.js";
|
|
3
4
|
import { mcpToolOf } from "../mcp/snapshot.js";
|
|
4
|
-
import {
|
|
5
|
+
import { sandboxToolSpec } from "../sandbox/runtime-tools.js";
|
|
5
6
|
import { toolCompleted, } from "../tenant/transcript.js";
|
|
6
7
|
import { toolCallDuration, toolCallStatus } from "./tool-status.js";
|
|
7
8
|
/** How many times a tool call is sent before it fails to the model as `tool.interrupted`. */
|
|
8
9
|
export const MAX_TOOL_ATTEMPTS = 3;
|
|
9
|
-
/**
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Where a tool call runs, and what it traces to: an MCP call names its server and the plugin
|
|
12
|
+
* that brought it; a skill tool, or a sandbox tool whose input names `/skills/<name>@<hash>/`, names the
|
|
13
|
+
* skill and its plugin. A tool the turn's harness declared (`nativeTools`) runs in the harness.
|
|
14
|
+
*/
|
|
15
|
+
export function toolKindOf(s, agentManifest, request, nativeTools) {
|
|
11
16
|
const mcp = mcpToolOf(s.mcpSnapshot, request);
|
|
12
|
-
if (mcp)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (mcp) {
|
|
18
|
+
const plugin = pluginOf(agentManifest, { server: mcp.serverName });
|
|
19
|
+
return { toolKind: "mcp", server: mcp.serverName, ...(plugin === undefined ? {} : { plugin }) };
|
|
20
|
+
}
|
|
21
|
+
if (isHttpToolCall(s.manifest, request)) {
|
|
22
|
+
const host = httpHostOf(declaredHttpTool(s.manifest, request.agent?.id, request.toolName)?.http.url);
|
|
23
|
+
return { toolKind: "http", ...(host === undefined ? {} : { server: host }) };
|
|
24
|
+
}
|
|
25
|
+
if (request.toolName !== undefined && nativeTools?.has(request.toolName))
|
|
26
|
+
return { toolKind: "harness" };
|
|
27
|
+
const skill = skillOfToolCall(agentManifest, request.toolName, request.input);
|
|
28
|
+
const plugin = skill === undefined ? undefined : pluginOf(agentManifest, { skill });
|
|
29
|
+
const traced = {
|
|
30
|
+
...(skill === undefined ? {} : { skill }),
|
|
31
|
+
...(plugin === undefined ? {} : { plugin }),
|
|
32
|
+
};
|
|
33
|
+
if (sandboxToolSpec(agentManifest, request.toolName))
|
|
34
|
+
return { toolKind: "sandbox", ...traced };
|
|
35
|
+
return { toolKind: "platform", ...traced };
|
|
36
|
+
}
|
|
37
|
+
/** The host (and port, when it names one) of an HTTP tool's URL. */
|
|
38
|
+
function httpHostOf(url) {
|
|
39
|
+
if (url === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
return new URL(url).host || undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
19
47
|
}
|
|
20
48
|
/** The facts a new tool row starts with: its first send, now. */
|
|
21
49
|
export function firstSend(kind, now = new Date()) {
|
|
@@ -33,6 +61,8 @@ export function factsOf(row, status, gate) {
|
|
|
33
61
|
status,
|
|
34
62
|
...(row.toolKind === undefined ? {} : { kind: row.toolKind }),
|
|
35
63
|
...(row.server === undefined ? {} : { server: row.server }),
|
|
64
|
+
...(row.skill === undefined ? {} : { skill: row.skill }),
|
|
65
|
+
...(row.plugin === undefined ? {} : { plugin: row.plugin }),
|
|
36
66
|
...(row.attempts === undefined ? {} : { attempts: row.attempts }),
|
|
37
67
|
...(durationMs === undefined ? {} : { durationMs }),
|
|
38
68
|
effectId: row.request.effectId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SandboxManifest, SandboxToolName } from "@nylorun/core/define";
|
|
2
2
|
import type { SandboxManager, SandboxSessionRef } from "../sandbox/manager.js";
|
|
3
3
|
import { type DiskOp } from "../sandbox/disk-fs.js";
|
|
4
4
|
import type { SandboxSelectionReport } from "../sandbox/select.js";
|
|
@@ -13,13 +13,13 @@ export type WorkspaceReport = SandboxSelectionReport & {
|
|
|
13
13
|
type Exists = (id: string) => boolean | Promise<boolean>;
|
|
14
14
|
export interface WorkspacePort {
|
|
15
15
|
/** Runs one sandbox tool in the workspace of `session`. */
|
|
16
|
-
run(session: SandboxSessionRef,
|
|
16
|
+
run(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
|
|
17
17
|
/** Lists the files under `dir` of the workspace of `session` (the turn-end export, F8.2). */
|
|
18
|
-
listFiles(session: SandboxSessionRef,
|
|
18
|
+
listFiles(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, dir: string, maxEntries: number, signal: AbortSignal): ReturnType<SandboxManager["listFiles"]>;
|
|
19
19
|
/** Reads a file of the workspace of `session` as bytes, at most `maxBytes` (`save_artifact`). */
|
|
20
|
-
readBytes(session: SandboxSessionRef,
|
|
20
|
+
readBytes(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, path: string, maxBytes: number, signal: AbortSignal): ReturnType<SandboxManager["readBytes"]>;
|
|
21
21
|
/** One operation on the disk of the workspace of `session` (R2d step 3). */
|
|
22
|
-
disk(session: SandboxSessionRef,
|
|
22
|
+
disk(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, op: DiskOp, signal: AbortSignal): ReturnType<SandboxManager["disk"]>;
|
|
23
23
|
report(): Promise<WorkspaceReport>;
|
|
24
24
|
/** One Tenant sweep pass: stop idle workspaces, remove those whose owner is gone. */
|
|
25
25
|
sweep(options: {
|
|
@@ -43,6 +43,8 @@ export interface RemoteWorkspaceOptions {
|
|
|
43
43
|
readonly preference: string;
|
|
44
44
|
/** The pod sandbox whose host serves these workspaces (F7.2). */
|
|
45
45
|
readonly pod?: string;
|
|
46
|
+
/** With `pod`: the oldest host epoch that may serve them (`SandboxSessionRef.hostEpoch`). */
|
|
47
|
+
readonly minEpoch?: number;
|
|
46
48
|
}
|
|
47
49
|
/** The workspaces of the harness that serves them. */
|
|
48
50
|
export declare function remoteWorkspace(options: RemoteWorkspaceOptions): WorkspacePort;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The workspace
|
|
2
|
+
* The workspace feature (F6.2): what core does with the Tenant's sandbox workspaces outside a
|
|
3
3
|
* run. `ctx.sandbox` is a `WorkspacePort`: the sandbox tool route (`POST
|
|
4
4
|
* /v1/sessions/:id/sandbox/:tool`), Tenant status, the Tenant sweep, a sandbox resource's deletion and a sandboxes reset.
|
|
5
5
|
*
|
|
@@ -25,10 +25,10 @@ import { NoWorkspaceHarness } from "./server.js";
|
|
|
25
25
|
/** The Tenant's own SandboxManager. */
|
|
26
26
|
export function localWorkspace(manager) {
|
|
27
27
|
return {
|
|
28
|
-
run: (session,
|
|
29
|
-
listFiles: (session,
|
|
30
|
-
readBytes: (session,
|
|
31
|
-
disk: (session,
|
|
28
|
+
run: (session, sandbox, toolName, input, signal) => manager.run(session, sandbox, toolName, input, signal),
|
|
29
|
+
listFiles: (session, sandbox, dir, maxEntries, signal) => manager.listFiles(session, sandbox, dir, maxEntries, signal),
|
|
30
|
+
readBytes: (session, sandbox, path, maxBytes, signal) => manager.readBytes(session, sandbox, path, maxBytes, signal),
|
|
31
|
+
disk: (session, sandbox, op, signal) => manager.disk(session, sandbox, op, signal),
|
|
32
32
|
report: () => manager.report(),
|
|
33
33
|
sweep: (options) => manager.sweep(options),
|
|
34
34
|
removeSandbox: (environmentId) => manager.removeSandbox(environmentId),
|
|
@@ -54,7 +54,9 @@ function sessionOf(session) {
|
|
|
54
54
|
/** The workspaces of the harness that serves them. */
|
|
55
55
|
export function remoteWorkspace(options) {
|
|
56
56
|
const { store } = options;
|
|
57
|
-
const target = options.pod === undefined
|
|
57
|
+
const target = options.pod === undefined
|
|
58
|
+
? undefined
|
|
59
|
+
: { pod: options.pod, ...(options.minEpoch === undefined ? {} : { minEpoch: options.minEpoch }) };
|
|
58
60
|
const unavailable = (error) => error.pod === undefined
|
|
59
61
|
? fail(503, error.message, { code: "request_rejected" })
|
|
60
62
|
: fail(409, error.message, { code: "environment_unavailable" });
|
|
@@ -98,11 +100,11 @@ export function remoteWorkspace(options) {
|
|
|
98
100
|
});
|
|
99
101
|
};
|
|
100
102
|
return {
|
|
101
|
-
async run(session,
|
|
103
|
+
async run(session, sandbox, toolName, input, signal) {
|
|
102
104
|
try {
|
|
103
105
|
return (await options.server.workspace(METHODS[toolName], {
|
|
104
106
|
session: sessionOf(session),
|
|
105
|
-
spec:
|
|
107
|
+
spec: sandbox ?? {},
|
|
106
108
|
tool: toolName,
|
|
107
109
|
input: input ?? {},
|
|
108
110
|
}, signal, target));
|
|
@@ -117,9 +119,9 @@ export function remoteWorkspace(options) {
|
|
|
117
119
|
throw error;
|
|
118
120
|
}
|
|
119
121
|
},
|
|
120
|
-
async listFiles(session,
|
|
122
|
+
async listFiles(session, sandbox, dir, maxEntries, signal) {
|
|
121
123
|
try {
|
|
122
|
-
const answer = (await options.server.workspace("workspace.read", { session: sessionOf(session), spec:
|
|
124
|
+
const answer = (await options.server.workspace("workspace.read", { session: sessionOf(session), spec: sandbox ?? {}, list: { dir, maxEntries } }, signal, target));
|
|
123
125
|
if (answer.kind === "listed")
|
|
124
126
|
return {
|
|
125
127
|
kind: "listed",
|
|
@@ -137,11 +139,11 @@ export function remoteWorkspace(options) {
|
|
|
137
139
|
throw error;
|
|
138
140
|
}
|
|
139
141
|
},
|
|
140
|
-
async readBytes(session,
|
|
142
|
+
async readBytes(session, sandbox, path, maxBytes, signal) {
|
|
141
143
|
try {
|
|
142
144
|
const answer = (await options.server.workspace("workspace.read", {
|
|
143
145
|
session: sessionOf(session),
|
|
144
|
-
spec:
|
|
146
|
+
spec: sandbox ?? {},
|
|
145
147
|
bytes: { path, maxBytes },
|
|
146
148
|
}, signal, target));
|
|
147
149
|
if (answer.kind === "read")
|
|
@@ -156,9 +158,9 @@ export function remoteWorkspace(options) {
|
|
|
156
158
|
throw error;
|
|
157
159
|
}
|
|
158
160
|
},
|
|
159
|
-
async disk(session,
|
|
161
|
+
async disk(session, sandbox, op, signal) {
|
|
160
162
|
try {
|
|
161
|
-
const answer = (await options.server.workspace("list" in op || "read" in op ? "workspace.read" : "workspace.write", { session: sessionOf(session), spec:
|
|
163
|
+
const answer = (await options.server.workspace("list" in op || "read" in op ? "workspace.read" : "workspace.write", { session: sessionOf(session), spec: sandbox ?? {}, disk: toWireOp(op) }, signal, target));
|
|
162
164
|
return answer.kind === "failed"
|
|
163
165
|
? { kind: "failed", code: String(answer.code), message: String(answer.message) }
|
|
164
166
|
: fromWireAnswer(answer);
|
|
@@ -204,13 +206,17 @@ export function remoteWorkspace(options) {
|
|
|
204
206
|
*/
|
|
205
207
|
export function withPodWorkspaces(base, options) {
|
|
206
208
|
const routed = async (session) => session.environmentId !== undefined && (await options.isPod(session.environmentId))
|
|
207
|
-
? remoteWorkspace({
|
|
209
|
+
? remoteWorkspace({
|
|
210
|
+
...options,
|
|
211
|
+
pod: session.environmentId,
|
|
212
|
+
...(session.hostEpoch === undefined ? {} : { minEpoch: session.hostEpoch }),
|
|
213
|
+
})
|
|
208
214
|
: base;
|
|
209
215
|
return {
|
|
210
216
|
...base,
|
|
211
|
-
run: async (session,
|
|
212
|
-
listFiles: async (session,
|
|
213
|
-
readBytes: async (session,
|
|
214
|
-
disk: async (session,
|
|
217
|
+
run: async (session, sandbox, toolName, input, signal) => (await routed(session)).run(session, sandbox, toolName, input, signal),
|
|
218
|
+
listFiles: async (session, sandbox, dir, maxEntries, signal) => (await routed(session)).listFiles(session, sandbox, dir, maxEntries, signal),
|
|
219
|
+
readBytes: async (session, sandbox, path, maxBytes, signal) => (await routed(session)).readBytes(session, sandbox, path, maxBytes, signal),
|
|
220
|
+
disk: async (session, sandbox, op, signal) => (await routed(session)).disk(session, sandbox, op, signal),
|
|
215
221
|
};
|
|
216
222
|
}
|