@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A environment's lifecycle stream (blueprint D39, F7.1): `environment.created`, `environment.attached`,
|
|
3
3
|
* `environment.detached` and `environment.deleted` (and the rest of `ENVIRONMENT_EVENT_TYPES`, egress-gate's
|
|
4
|
-
* `environment.egress`
|
|
4
|
+
* `environment.egress` for every connection among them, D67), on the `nylorun.environment-event/1` envelope, numbered
|
|
5
5
|
* from 0 per environment id in `nylorun_streams.environment_events`. The record module is its only
|
|
6
6
|
* writer, as for session events; the store runs the two statements through
|
|
7
7
|
* `EnvironmentRecordWriter` in the caller's transaction, under the environment row's lock (or, for a
|
package/dist/sandbox/join.js
CHANGED
|
@@ -159,6 +159,7 @@ export function hostAuthority(ctx) {
|
|
|
159
159
|
epoch,
|
|
160
160
|
setup: row ? await setupPlanOf(ctx, t, row) : undefined,
|
|
161
161
|
seeded: row?.diskSeed !== undefined,
|
|
162
|
+
afterSeed: row?.diskSeed?.setup === true,
|
|
162
163
|
};
|
|
163
164
|
});
|
|
164
165
|
if ("lost" in joined) {
|
|
@@ -171,6 +172,9 @@ export function hostAuthority(ctx) {
|
|
|
171
172
|
if (joined.seeded) {
|
|
172
173
|
// The engine connects next; the seed waits for it, then writes the version.
|
|
173
174
|
void runPendingSeed(ctx, sandboxId);
|
|
175
|
+
// A fork onto other setup (Q17): the engine runs it once the version is written.
|
|
176
|
+
if (joined.afterSeed)
|
|
177
|
+
return { ...answer, seeded: true, afterSeed: true, ...(joined.setup ? { setup: joined.setup } : {}) };
|
|
174
178
|
return { ...answer, seeded: true };
|
|
175
179
|
}
|
|
176
180
|
return joined.setup ? { ...answer, setup: joined.setup } : answer;
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Sandbox Manager: owns sandbox lifecycle for the Runtime. One workspace per sandbox resource
|
|
3
3
|
* (`sandbox: { id }`, keyed by the sandbox id), or per owning session for a sandbox chosen
|
|
4
4
|
* inline or by the Tenant default (keyed by the session id); created on the first sandbox tool
|
|
5
|
-
* call, stopped after its idle timeout, reattached on the next call.
|
|
6
|
-
* workspace run
|
|
5
|
+
* call, stopped after its idle timeout, reattached on the next call. Each session's calls on a
|
|
6
|
+
* workspace run in the order it sends them, and different sessions' calls run at the same time
|
|
7
|
+
* (D66): the sessions of one Environment run their turns at once. Opening, stopping and removing
|
|
8
|
+
* a workspace run alone: a stop or removal waits for the calls running, and calls that come after
|
|
9
|
+
* it wait for it.
|
|
7
10
|
*
|
|
8
11
|
* The manager keeps no timers and reaches no store: it keeps its compute records through a
|
|
9
12
|
* `SandboxRecords` port (the Tenant's `sandboxes` table in core's process, a file in a harness's)
|
|
@@ -12,18 +15,18 @@
|
|
|
12
15
|
* (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
|
|
13
16
|
* reconcile once per process, as the Tenant sweep does in core's process.
|
|
14
17
|
*
|
|
15
|
-
* A session's skills (track R2 M4) are mounted on its workspace, read-only
|
|
16
|
-
* `/skills/<name>/`, before the first call that
|
|
17
|
-
* through the `definitionFile` port.
|
|
18
|
+
* A session's skills (track R2 M4) are mounted on its workspace, read-only, one folder per
|
|
19
|
+
* version (`/skills/<name>@<hash>/`, D66), before the first call that needs them (`skills.ts`);
|
|
20
|
+
* their bytes come through the `definitionFile` port.
|
|
18
21
|
*/
|
|
19
22
|
import type { EventPayload } from "@nylorun/core/contracts";
|
|
20
|
-
import { type AgentManifest, type
|
|
23
|
+
import { type AgentManifest, type SandboxManifest, type SandboxToolName } from "@nylorun/core/define";
|
|
21
24
|
import { type SandboxRecord, type SandboxRecords } from "./records.js";
|
|
22
25
|
import { type SandboxSelection, type SandboxSelectionReport } from "./select.js";
|
|
23
26
|
import { type DiskAnswer, type DiskOp } from "./disk-fs.js";
|
|
24
27
|
import { type SandboxToolOutcome } from "./tools.js";
|
|
25
28
|
import { type SandboxBackend, type SandboxListing } from "./types.js";
|
|
26
|
-
export {
|
|
29
|
+
export { sandboxToolSpec } from "./runtime-tools.js";
|
|
27
30
|
export type { SandboxRecord, SandboxRecords, SandboxState } from "./records.js";
|
|
28
31
|
export interface SandboxSessionRef {
|
|
29
32
|
/** The session whose log records the workspace's events: the owning session. */
|
|
@@ -35,6 +38,11 @@ export interface SandboxSessionRef {
|
|
|
35
38
|
* so every session attached to it shares the files; absent, the workspace is the session's.
|
|
36
39
|
*/
|
|
37
40
|
readonly environmentId?: string;
|
|
41
|
+
/**
|
|
42
|
+
* For a pod Environment: the host epoch it is at, so the call reaches a host that joined at
|
|
43
|
+
* that epoch or later, never one a restore or a lost volume retired.
|
|
44
|
+
*/
|
|
45
|
+
readonly hostEpoch?: number;
|
|
38
46
|
/** Variables every command of the call gets (R2c): secret names set to the sentinel, plain values. */
|
|
39
47
|
readonly environment?: Readonly<Record<string, string>>;
|
|
40
48
|
/**
|
|
@@ -111,13 +119,22 @@ export declare class SandboxManager {
|
|
|
111
119
|
report(): Promise<SandboxSelectionReport & {
|
|
112
120
|
readonly defaultImage: string;
|
|
113
121
|
}>;
|
|
114
|
-
run(session: SandboxSessionRef,
|
|
122
|
+
run(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, toolName: SandboxToolName, input: unknown, signal: AbortSignal): Promise<SandboxToolOutcome>;
|
|
123
|
+
/**
|
|
124
|
+
* Runs `call` in session `sessionId`'s queue on `live`: after its earlier calls, and after any
|
|
125
|
+
* stop or removal pending, beside other sessions' calls (D66).
|
|
126
|
+
*/
|
|
127
|
+
private inLane;
|
|
128
|
+
/** Runs `step` on `live` alone: after the calls running, before the calls that come after it. */
|
|
129
|
+
private alone;
|
|
130
|
+
/** The workspace's handle, opened once for the calls that need it at the same time. */
|
|
131
|
+
private handleOf;
|
|
115
132
|
/**
|
|
116
133
|
* The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
|
|
117
134
|
* as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
118
135
|
* cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
|
|
119
136
|
*/
|
|
120
|
-
readBytes(session: SandboxSessionRef,
|
|
137
|
+
readBytes(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, path: string, maxBytes: number, signal: AbortSignal): Promise<{
|
|
121
138
|
kind: "read";
|
|
122
139
|
path: string;
|
|
123
140
|
bytes: Uint8Array;
|
|
@@ -132,7 +149,7 @@ export declare class SandboxManager {
|
|
|
132
149
|
* the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
|
|
133
150
|
* started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
|
|
134
151
|
*/
|
|
135
|
-
listFiles(session: SandboxSessionRef,
|
|
152
|
+
listFiles(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, dir: string, maxEntries: number, signal: AbortSignal): Promise<{
|
|
136
153
|
kind: "listed";
|
|
137
154
|
path: string;
|
|
138
155
|
listing: SandboxListing;
|
|
@@ -146,7 +163,7 @@ export declare class SandboxManager {
|
|
|
146
163
|
* One operation on the disk of the session's sandbox (R2d step 3, `sandbox/disk-fs.ts`), in the
|
|
147
164
|
* same queue as its tool calls, starting it first when it is stopped.
|
|
148
165
|
*/
|
|
149
|
-
disk(session: SandboxSessionRef,
|
|
166
|
+
disk(session: SandboxSessionRef, sandbox: SandboxManifest | undefined, op: DiskOp, signal: AbortSignal): Promise<DiskAnswer | Extract<SandboxToolOutcome, {
|
|
150
167
|
kind: "failed";
|
|
151
168
|
}>>;
|
|
152
169
|
/** Runs `use` on the session's sandbox in its queue, starting it first when it is stopped. */
|
|
@@ -156,9 +173,10 @@ export declare class SandboxManager {
|
|
|
156
173
|
private execute;
|
|
157
174
|
private executeOpen;
|
|
158
175
|
/**
|
|
159
|
-
* Mounts the session
|
|
160
|
-
* all of them, later only
|
|
161
|
-
* one
|
|
176
|
+
* Mounts the skill folders the session needs that the workspace does not hold (D66): on a
|
|
177
|
+
* handle just opened all of them, later only versions another session has not mounted. Calls
|
|
178
|
+
* that need one folder at once share its mount. A failed outcome when one cannot be mounted;
|
|
179
|
+
* the folder is not marked mounted then, so the next call tries again.
|
|
162
180
|
*/
|
|
163
181
|
private mountSkills;
|
|
164
182
|
private open;
|
package/dist/sandbox/manager.js
CHANGED
|
@@ -2,11 +2,11 @@ import { SANDBOX_WORKSPACE, } from "@nylorun/core/define";
|
|
|
2
2
|
import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
|
|
3
3
|
import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
|
|
4
4
|
import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
|
|
5
|
-
import { mountSkill, sandboxSkillsOf
|
|
5
|
+
import { mountSkill, sandboxSkillsOf } from "./skills.js";
|
|
6
6
|
import { runDiskOp } from "./disk-fs.js";
|
|
7
7
|
import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
8
8
|
import { SandboxFileTooLargeError, } from "./types.js";
|
|
9
|
-
export {
|
|
9
|
+
export { sandboxToolSpec } from "./runtime-tools.js";
|
|
10
10
|
const READ_TOOLS = new Set(["read", "grep", "glob"]);
|
|
11
11
|
export class SandboxManager {
|
|
12
12
|
options;
|
|
@@ -44,23 +44,20 @@ export class SandboxManager {
|
|
|
44
44
|
if (this.closing)
|
|
45
45
|
return;
|
|
46
46
|
const now = at ?? Date.now();
|
|
47
|
-
const idle = (live) => !!live.handle && live.
|
|
47
|
+
const idle = (live) => !!live.handle && live.running.size === 0 && now - live.lastUsedAt >= live.idleMs;
|
|
48
48
|
const stops = [];
|
|
49
49
|
for (const [key, live] of this.live) {
|
|
50
50
|
if (!idle(live))
|
|
51
51
|
continue;
|
|
52
|
-
//
|
|
53
|
-
live
|
|
54
|
-
.then(() => (idle(live) ? this.stop(key, live) : undefined))
|
|
55
|
-
.catch(() => undefined);
|
|
56
|
-
stops.push(live.tail);
|
|
52
|
+
// After the calls running; checked again when its turn comes.
|
|
53
|
+
stops.push(this.alone(live, () => (idle(live) ? this.stop(key, live) : undefined)).catch(() => undefined));
|
|
57
54
|
}
|
|
58
55
|
await Promise.all(stops);
|
|
59
56
|
for (const record of await this.options.records.list()) {
|
|
60
57
|
if (record.state === "stopped")
|
|
61
58
|
continue;
|
|
62
59
|
const live = this.live.get(record.key);
|
|
63
|
-
if (live && (live.handle || live.
|
|
60
|
+
if (live && (live.handle || live.running.size > 0))
|
|
64
61
|
continue;
|
|
65
62
|
await this.options.records.put({ ...record, state: "stopped" });
|
|
66
63
|
}
|
|
@@ -102,22 +99,72 @@ export class SandboxManager {
|
|
|
102
99
|
async report() {
|
|
103
100
|
return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
|
|
104
101
|
}
|
|
105
|
-
async run(session,
|
|
106
|
-
const prepared = await this.prepare(session,
|
|
102
|
+
async run(session, sandbox, toolName, input, signal) {
|
|
103
|
+
const prepared = await this.prepare(session, sandbox);
|
|
107
104
|
if ("kind" in prepared)
|
|
108
105
|
return prepared;
|
|
109
106
|
const { live: entry, spec } = prepared;
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Runs `call` in session `sessionId`'s queue on `live`: after its earlier calls, and after any
|
|
111
|
+
* stop or removal pending, beside other sessions' calls (D66).
|
|
112
|
+
*/
|
|
113
|
+
inLane(live, sessionId, call) {
|
|
114
|
+
const task = (live.lanes.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
115
|
+
// A stop queued while this waited goes first.
|
|
116
|
+
for (let gate = live.gate;; gate = live.gate) {
|
|
117
|
+
await gate;
|
|
118
|
+
if (gate === live.gate)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const running = call();
|
|
122
|
+
live.running.add(running);
|
|
123
|
+
try {
|
|
124
|
+
return await running;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
live.running.delete(running);
|
|
128
|
+
live.lastUsedAt = Date.now();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
const queued = task.catch(() => undefined);
|
|
132
|
+
live.lanes.set(sessionId, queued);
|
|
133
|
+
void queued.then(() => {
|
|
134
|
+
if (live.lanes.get(sessionId) === queued)
|
|
135
|
+
live.lanes.delete(sessionId);
|
|
136
|
+
});
|
|
137
|
+
return task;
|
|
138
|
+
}
|
|
139
|
+
/** Runs `step` on `live` alone: after the calls running, before the calls that come after it. */
|
|
140
|
+
alone(live, step) {
|
|
141
|
+
const task = live.gate.then(async () => {
|
|
142
|
+
while (live.running.size > 0)
|
|
143
|
+
await Promise.allSettled([...live.running]);
|
|
144
|
+
return step();
|
|
145
|
+
});
|
|
146
|
+
live.gate = task.catch(() => undefined);
|
|
112
147
|
return task;
|
|
113
148
|
}
|
|
149
|
+
/** The workspace's handle, opened once for the calls that need it at the same time. */
|
|
150
|
+
async handleOf(live, session, spec) {
|
|
151
|
+
if (live.handle)
|
|
152
|
+
return live.handle;
|
|
153
|
+
const opening = (live.opening ??= this.open(live, session, spec).finally(() => {
|
|
154
|
+
if (live.opening === opening)
|
|
155
|
+
live.opening = undefined;
|
|
156
|
+
}));
|
|
157
|
+
const handle = await opening;
|
|
158
|
+
live.handle ??= handle;
|
|
159
|
+
return handle;
|
|
160
|
+
}
|
|
114
161
|
/**
|
|
115
162
|
* The bytes of one file in the session's sandbox, for `save_artifact` (F8.1), in the same queue
|
|
116
163
|
* as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
117
164
|
* cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
|
|
118
165
|
*/
|
|
119
|
-
async readBytes(session,
|
|
120
|
-
return this.withHandle(session,
|
|
166
|
+
async readBytes(session, sandbox, path, maxBytes, signal) {
|
|
167
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
121
168
|
const resolved = resolveSandboxPath(path, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
122
169
|
if (!handle.readBytes)
|
|
123
170
|
return {
|
|
@@ -147,14 +194,14 @@ export class SandboxManager {
|
|
|
147
194
|
* the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
|
|
148
195
|
* started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
|
|
149
196
|
*/
|
|
150
|
-
async listFiles(session,
|
|
197
|
+
async listFiles(session, sandbox, dir, maxEntries, signal) {
|
|
151
198
|
const key = this.keyFor(session);
|
|
152
199
|
if (!this.live.get(key)?.handle) {
|
|
153
200
|
const record = await this.options.records.get(key);
|
|
154
201
|
if (!record)
|
|
155
202
|
return { kind: "missing", path: dir };
|
|
156
203
|
}
|
|
157
|
-
return this.withHandle(session,
|
|
204
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
158
205
|
const resolved = resolveSandboxPath(dir, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
159
206
|
if (!handle.listFiles)
|
|
160
207
|
return {
|
|
@@ -172,8 +219,8 @@ export class SandboxManager {
|
|
|
172
219
|
* One operation on the disk of the session's sandbox (R2d step 3, `sandbox/disk-fs.ts`), in the
|
|
173
220
|
* same queue as its tool calls, starting it first when it is stopped.
|
|
174
221
|
*/
|
|
175
|
-
async disk(session,
|
|
176
|
-
return this.withHandle(session,
|
|
222
|
+
async disk(session, sandbox, op, signal) {
|
|
223
|
+
return this.withHandle(session, sandbox, signal, async (handle, live) => {
|
|
177
224
|
if (!handle.diskRoots)
|
|
178
225
|
return {
|
|
179
226
|
kind: "failed",
|
|
@@ -184,45 +231,35 @@ export class SandboxManager {
|
|
|
184
231
|
});
|
|
185
232
|
}
|
|
186
233
|
/** Runs `use` on the session's sandbox in its queue, starting it first when it is stopped. */
|
|
187
|
-
async withHandle(session,
|
|
188
|
-
const prepared = await this.prepare(session,
|
|
234
|
+
async withHandle(session, sandbox, signal, use) {
|
|
235
|
+
const prepared = await this.prepare(session, sandbox);
|
|
189
236
|
if ("kind" in prepared)
|
|
190
237
|
return prepared;
|
|
191
238
|
const { live, spec } = prepared;
|
|
192
|
-
|
|
239
|
+
return this.inLane(live, session.id, async () => {
|
|
193
240
|
if (signal.aborted)
|
|
194
241
|
throw new Error("Turn cancelled");
|
|
195
242
|
if (this.closing)
|
|
196
243
|
throw new Error("Runtime is shutting down");
|
|
197
|
-
|
|
244
|
+
let handle;
|
|
198
245
|
try {
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
live.handle = await this.open(live, session, spec);
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
return {
|
|
205
|
-
kind: "failed",
|
|
206
|
-
code: "sandbox.start_failed",
|
|
207
|
-
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const unmounted = await this.mountSkills(live, live.handle, session, signal);
|
|
212
|
-
if (unmounted)
|
|
213
|
-
return unmounted;
|
|
214
|
-
return await use(live.handle, live);
|
|
246
|
+
handle = await this.handleOf(live, session, spec);
|
|
215
247
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
248
|
+
catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
kind: "failed",
|
|
251
|
+
code: "sandbox.start_failed",
|
|
252
|
+
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
253
|
+
};
|
|
219
254
|
}
|
|
255
|
+
const unmounted = await this.mountSkills(live, handle, session, signal);
|
|
256
|
+
if (unmounted)
|
|
257
|
+
return unmounted;
|
|
258
|
+
return await use(handle, live);
|
|
220
259
|
});
|
|
221
|
-
live.tail = task.catch(() => undefined);
|
|
222
|
-
return task;
|
|
223
260
|
}
|
|
224
261
|
/** The session's live sandbox entry and spec, or why it cannot run here. */
|
|
225
|
-
async prepare(session,
|
|
262
|
+
async prepare(session, sandbox) {
|
|
226
263
|
const selection = await this.ready;
|
|
227
264
|
const backend = selection.backend;
|
|
228
265
|
if (!backend)
|
|
@@ -234,10 +271,10 @@ export class SandboxManager {
|
|
|
234
271
|
const key = this.keyFor(session);
|
|
235
272
|
const spec = {
|
|
236
273
|
key,
|
|
237
|
-
image:
|
|
238
|
-
cpus:
|
|
239
|
-
memoryMiB: memoryMiBOf(
|
|
240
|
-
network: resolveNetwork(
|
|
274
|
+
image: sandbox?.image ?? DEFAULT_SANDBOX_IMAGE,
|
|
275
|
+
cpus: sandbox?.resources?.cpus ?? DEFAULT_SANDBOX_CPUS,
|
|
276
|
+
memoryMiB: memoryMiBOf(sandbox),
|
|
277
|
+
network: resolveNetwork(sandbox),
|
|
241
278
|
};
|
|
242
279
|
const unmet = backend.unmet(spec);
|
|
243
280
|
if (unmet)
|
|
@@ -252,10 +289,11 @@ export class SandboxManager {
|
|
|
252
289
|
sessionId: session.id,
|
|
253
290
|
...(session.environmentId === undefined ? {} : { environmentId: session.environmentId }),
|
|
254
291
|
backend,
|
|
255
|
-
idleMs: idleMsOf(
|
|
256
|
-
|
|
292
|
+
idleMs: idleMsOf(sandbox),
|
|
293
|
+
running: new Set(),
|
|
257
294
|
lastUsedAt: Date.now(),
|
|
258
|
-
|
|
295
|
+
lanes: new Map(),
|
|
296
|
+
gate: Promise.resolve(),
|
|
259
297
|
skills: new Map(),
|
|
260
298
|
};
|
|
261
299
|
this.live.set(key, live);
|
|
@@ -267,39 +305,31 @@ export class SandboxManager {
|
|
|
267
305
|
throw new Error("Turn cancelled");
|
|
268
306
|
if (this.closing)
|
|
269
307
|
throw new Error("Runtime is shutting down");
|
|
270
|
-
live
|
|
271
|
-
try {
|
|
272
|
-
return await this.executeOpen(live, session, spec, toolName, input, signal);
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
live.active -= 1;
|
|
276
|
-
live.lastUsedAt = Date.now();
|
|
277
|
-
}
|
|
308
|
+
return this.executeOpen(live, session, spec, toolName, input, signal);
|
|
278
309
|
}
|
|
279
310
|
async executeOpen(live, session, spec, toolName, input, signal) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
311
|
+
let handle;
|
|
312
|
+
try {
|
|
313
|
+
handle = await this.handleOf(live, session, spec);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
317
|
+
const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
|
|
318
|
+
await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "stopped", backend: live.backend.name, error: message }, { record, ...claimOf(session) });
|
|
319
|
+
// Nothing ran yet, so a failed start is a definite outcome, not an uncertain one.
|
|
320
|
+
return {
|
|
321
|
+
kind: "failed",
|
|
322
|
+
code: "sandbox.start_failed",
|
|
323
|
+
message: `The sandbox could not start on ${live.backend.name} (image ${spec.image ?? "the backend's default"}): ${message}`,
|
|
324
|
+
};
|
|
295
325
|
}
|
|
296
|
-
const unmounted = await this.mountSkills(live,
|
|
326
|
+
const unmounted = await this.mountSkills(live, handle, session, signal);
|
|
297
327
|
if (unmounted)
|
|
298
328
|
return unmounted;
|
|
299
329
|
const started = Date.now();
|
|
300
330
|
let report = {};
|
|
301
331
|
try {
|
|
302
|
-
const outcome = await runSandboxTool(
|
|
332
|
+
const outcome = await runSandboxTool(handle, toolName, (input ?? {}), signal, (value) => {
|
|
303
333
|
report = value;
|
|
304
334
|
}, session.environment);
|
|
305
335
|
await this.options.emit(session.id, session.activeTurnId, "environment.exec", {
|
|
@@ -313,8 +343,9 @@ export class SandboxManager {
|
|
|
313
343
|
return outcome;
|
|
314
344
|
}
|
|
315
345
|
catch (error) {
|
|
316
|
-
// The backend itself failed; reopen on the next call.
|
|
317
|
-
live.handle
|
|
346
|
+
// The backend itself failed; reopen on the next call, unless another call already did.
|
|
347
|
+
if (live.handle === handle)
|
|
348
|
+
live.handle = undefined;
|
|
318
349
|
if (signal.aborted || !READ_TOOLS.has(toolName))
|
|
319
350
|
throw error;
|
|
320
351
|
return {
|
|
@@ -325,32 +356,38 @@ export class SandboxManager {
|
|
|
325
356
|
}
|
|
326
357
|
}
|
|
327
358
|
/**
|
|
328
|
-
* Mounts the session
|
|
329
|
-
* all of them, later only
|
|
330
|
-
* one
|
|
359
|
+
* Mounts the skill folders the session needs that the workspace does not hold (D66): on a
|
|
360
|
+
* handle just opened all of them, later only versions another session has not mounted. Calls
|
|
361
|
+
* that need one folder at once share its mount. A failed outcome when one cannot be mounted;
|
|
362
|
+
* the folder is not marked mounted then, so the next call tries again.
|
|
331
363
|
*/
|
|
332
364
|
async mountSkills(live, handle, session, signal) {
|
|
333
365
|
const read = this.options.definitionFile;
|
|
334
366
|
if (!read)
|
|
335
367
|
return undefined;
|
|
336
|
-
for (const [
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
368
|
+
for (const [folder, files] of sandboxSkillsOf(session.manifest)) {
|
|
369
|
+
let mounting = live.skills.get(folder);
|
|
370
|
+
if (!mounting) {
|
|
371
|
+
const mount = mountSkill(handle, folder, files, (sha256) => read(sha256, session), signal);
|
|
372
|
+
live.skills.set(folder, mount);
|
|
373
|
+
void mount.catch(() => {
|
|
374
|
+
if (live.skills.get(folder) === mount)
|
|
375
|
+
live.skills.delete(folder);
|
|
376
|
+
});
|
|
377
|
+
mounting = mount;
|
|
378
|
+
}
|
|
340
379
|
try {
|
|
341
|
-
await
|
|
380
|
+
await mounting;
|
|
342
381
|
}
|
|
343
382
|
catch (error) {
|
|
344
383
|
if (signal.aborted)
|
|
345
384
|
throw error;
|
|
346
|
-
live.skills.delete(name);
|
|
347
385
|
return {
|
|
348
386
|
kind: "failed",
|
|
349
387
|
code: "sandbox.skills_unavailable",
|
|
350
|
-
message: `The files of skill
|
|
388
|
+
message: `The files of skill folder ${folder} could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
|
|
351
389
|
};
|
|
352
390
|
}
|
|
353
|
-
live.skills.set(name, signature);
|
|
354
391
|
}
|
|
355
392
|
return undefined;
|
|
356
393
|
}
|
|
@@ -421,8 +458,9 @@ export class SandboxManager {
|
|
|
421
458
|
async remove(key) {
|
|
422
459
|
const live = this.live.get(key);
|
|
423
460
|
if (live) {
|
|
424
|
-
await
|
|
425
|
-
|
|
461
|
+
await this.alone(live, () => {
|
|
462
|
+
live.handle = undefined;
|
|
463
|
+
}).catch(() => undefined);
|
|
426
464
|
this.live.delete(key);
|
|
427
465
|
}
|
|
428
466
|
const { backend } = await this.ready;
|
|
@@ -455,7 +493,8 @@ export class SandboxManager {
|
|
|
455
493
|
if (!this.selection)
|
|
456
494
|
return;
|
|
457
495
|
const tasks = [...this.live.entries()].map(async ([key, live]) => {
|
|
458
|
-
await live.
|
|
496
|
+
await Promise.allSettled([...live.lanes.values(), live.gate]);
|
|
497
|
+
await Promise.allSettled([...live.running]);
|
|
459
498
|
if (this.options.ephemeral) {
|
|
460
499
|
live.handle = undefined;
|
|
461
500
|
await live.backend.remove(key);
|
|
@@ -19,7 +19,7 @@ export declare const DEFAULT_PLACEMENT: readonly SandboxPlacementHost[];
|
|
|
19
19
|
/** The hosts harness `harnessId` may run on. */
|
|
20
20
|
export declare function placementOf(config: TenantSandboxConfig, harnessId?: string): readonly SandboxPlacementHost[];
|
|
21
21
|
/** How to enable sandbox pods, for the messages that need them. */
|
|
22
|
-
export declare const ENABLE_PODS = "run `nylorun sandbox enable --context <name>` to give this Tenant a cluster";
|
|
22
|
+
export declare const ENABLE_PODS = "run `nylorun local sandbox enable --context <name>` to give this Tenant a cluster";
|
|
23
23
|
/** Refuses a session whose harness, on the host its Environment needs, the Tenant does not allow. */
|
|
24
24
|
export declare function checkPlacement(config: TenantSandboxConfig, kind: EnvironmentKind | undefined, harness?: string): void;
|
|
25
25
|
/** Whether the sandboxes service is up, cached for a few seconds. */
|
|
@@ -21,7 +21,7 @@ export function placementOf(config, harnessId = HARNESS_ID) {
|
|
|
21
21
|
return placement?.[harnessId]?.hosts ?? placement?.["*"]?.hosts ?? DEFAULT_PLACEMENT;
|
|
22
22
|
}
|
|
23
23
|
/** How to enable sandbox pods, for the messages that need them. */
|
|
24
|
-
export const ENABLE_PODS = "run `nylorun sandbox enable --context <name>` to give this Tenant a cluster";
|
|
24
|
+
export const ENABLE_PODS = "run `nylorun local sandbox enable --context <name>` to give this Tenant a cluster";
|
|
25
25
|
/** Refuses a session whose harness, on the host its Environment needs, the Tenant does not allow. */
|
|
26
26
|
export function checkPlacement(config, kind, harness = HARNESS_ID) {
|
|
27
27
|
const host = kind === "pod" ? "sandbox" : "harness-container";
|
|
@@ -53,7 +53,7 @@ export async function requirePods(pods, what) {
|
|
|
53
53
|
code: "environment_unavailable",
|
|
54
54
|
});
|
|
55
55
|
if (!(await podsReady(pods)))
|
|
56
|
-
fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun sandbox status\`.`, {
|
|
56
|
+
fail(409, `${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun local sandbox status\`.`, {
|
|
57
57
|
code: "environment_unavailable",
|
|
58
58
|
details: { retryAfterSeconds: 5 },
|
|
59
59
|
}, { "retry-after": "5" });
|
|
@@ -44,6 +44,8 @@ export interface PodStatus {
|
|
|
44
44
|
readonly expired: boolean;
|
|
45
45
|
readonly podUID?: string;
|
|
46
46
|
readonly podPhase?: string;
|
|
47
|
+
/** The workload container's image as the node pulled it (`repo@sha256:…`), once it runs. */
|
|
48
|
+
readonly imageID?: string;
|
|
47
49
|
readonly volume: "present" | "missing";
|
|
48
50
|
readonly opId?: string;
|
|
49
51
|
readonly shutdownTime?: string;
|
|
@@ -52,7 +54,7 @@ export interface PodStatus {
|
|
|
52
54
|
/** With `?wait=`: whether the state was reached before the timeout. */
|
|
53
55
|
readonly met?: boolean;
|
|
54
56
|
}
|
|
55
|
-
/** `GET /v1/info`: the cluster `nylorun sandbox enable` recorded. */
|
|
57
|
+
/** `GET /v1/info`: the cluster `nylorun local sandbox enable` recorded. */
|
|
56
58
|
export interface ClusterInfo {
|
|
57
59
|
readonly namespace: string;
|
|
58
60
|
readonly context: string;
|
|
@@ -27,12 +27,20 @@
|
|
|
27
27
|
*
|
|
28
28
|
* - **Setup** (R2d step 2): a failed setup (`sandbox/setup.ts`) leaves the pod `failed` with a
|
|
29
29
|
* reason that starts `setup:`, even while the pod is ready, until a turn start asks it to run.
|
|
30
|
+
* - **Image** (Q14): the first ready pod's workload image is recorded by digest (`imageDigest`),
|
|
31
|
+
* and every later apply runs that digest, so a stop, relaunch or reset never pulls another
|
|
32
|
+
* image under the same tag. An image id with no repository (a locally loaded image) pins nothing.
|
|
30
33
|
*
|
|
31
34
|
* An unused Environment is deleted by the Tenant sweep (`lifecycle.deleteAfter`), not here.
|
|
32
35
|
*/
|
|
33
36
|
import type { EnvironmentEventPayload, EnvironmentEventType } from "@nylorun/core/contracts";
|
|
34
37
|
import type { Environment, SandboxPodPatch, SandboxPodState } from "../../store/types.js";
|
|
35
38
|
import type { PodStatus } from "./client.js";
|
|
39
|
+
/**
|
|
40
|
+
* The pullable reference of the image id a container status names (`repo@sha256:…`), or
|
|
41
|
+
* undefined for one without a repository, such as an image loaded onto the node.
|
|
42
|
+
*/
|
|
43
|
+
export declare function imageDigestOf(imageID: string | undefined): string | undefined;
|
|
36
44
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
37
45
|
export declare const SETUP_FAILED = "setup:";
|
|
38
46
|
/** A pod not ready this long after it was asked to run has failed. */
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pullable reference of the image id a container status names (`repo@sha256:…`), or
|
|
3
|
+
* undefined for one without a repository, such as an image loaded onto the node.
|
|
4
|
+
*/
|
|
5
|
+
export function imageDigestOf(imageID) {
|
|
6
|
+
const id = imageID?.replace(/^docker-pullable:\/\//, "");
|
|
7
|
+
if (!id)
|
|
8
|
+
return undefined;
|
|
9
|
+
const at = id.indexOf("@sha256:");
|
|
10
|
+
return at > 0 && /^[0-9a-f]{64}$/.test(id.slice(at + 8)) ? id : undefined;
|
|
11
|
+
}
|
|
1
12
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
2
13
|
export const SETUP_FAILED = "setup:";
|
|
3
14
|
/** A pod not ready this long after it was asked to run has failed. */
|
|
@@ -137,6 +148,10 @@ export function decide(facts) {
|
|
|
137
148
|
patch.observed = "running";
|
|
138
149
|
patch.reason = null;
|
|
139
150
|
}
|
|
151
|
+
// The image it runs, pinned by digest from now on (Q14).
|
|
152
|
+
const digest = pod.imageDigest === undefined ? imageDigestOf(status.imageID) : undefined;
|
|
153
|
+
if (digest !== undefined)
|
|
154
|
+
patch.imageDigest = digest;
|
|
140
155
|
return done();
|
|
141
156
|
}
|
|
142
157
|
const startedAt = Date.parse(pod.startedAt ?? row.updatedAt);
|
|
@@ -36,7 +36,8 @@ export function storageGiBOf(spec) {
|
|
|
36
36
|
export function podSpecOf(input) {
|
|
37
37
|
const { spec, pod, config } = input;
|
|
38
38
|
const lifecycle = podLifecycleConfig(config);
|
|
39
|
-
|
|
39
|
+
// Pinned by digest once a pod ran it (Q14).
|
|
40
|
+
const image = pod.imageDigest ?? spec.image;
|
|
40
41
|
const ours = (input.harness ?? DEFAULT_HARNESS) === DEFAULT_HARNESS;
|
|
41
42
|
return {
|
|
42
43
|
opId: opIdOf(pod),
|