@nylorun/runtime 0.25.0-beta → 0.27.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 +169 -0
- package/README.md +26 -16
- package/dist/adapters/execution/restate.d.ts +15 -18
- package/dist/adapters/execution/restate.js +31 -27
- package/dist/adapters/replication/pgoutput.d.ts +1 -1
- package/dist/adapters/replication/pgoutput.js +3 -2
- package/dist/adapters/sandbox/local.js +8 -5
- package/dist/adapters/sandbox/virtual.js +2 -2
- package/dist/adapters/streams/s2.d.ts +1 -1
- package/dist/adapters/streams/s2.js +18 -14
- package/dist/api/a2a/card.js +1 -1
- package/dist/api/a2a/endpoint.js +3 -4
- package/dist/api/a2a/protocol.js +4 -0
- package/dist/api/a2a/routes.d.ts +2 -6
- package/dist/api/a2a/routes.js +12 -18
- package/dist/api/ag-ui/endpoint.js +2 -3
- package/dist/api/ag-ui/routes.js +4 -4
- package/dist/api/components.d.ts +326 -45
- package/dist/api/components.js +5 -1
- package/dist/api/gate/keys.js +7 -4
- package/dist/api/gate/routes.d.ts +6 -2
- package/dist/api/gate/routes.js +15 -11
- package/dist/api/http/app.d.ts +2 -2
- package/dist/api/http/app.js +2 -2
- package/dist/api/http/body.d.ts +15 -3
- package/dist/api/http/body.js +15 -7
- package/dist/api/http/define.d.ts +1 -1
- package/dist/api/http/respond.js +7 -3
- package/dist/api/http/routes/access.js +5 -4
- package/dist/api/http/routes/artifacts.js +19 -7
- package/dist/api/http/routes/coverage.js +1 -1
- package/dist/api/http/routes/environment-manifests.js +1 -1
- package/dist/api/http/routes/environments.js +6 -6
- package/dist/api/http/routes/files.d.ts +2 -1
- package/dist/api/http/routes/files.js +31 -5
- package/dist/api/http/routes/mcp.js +1 -1
- package/dist/api/http/routes/reads.d.ts +35 -0
- package/dist/api/http/routes/reads.js +50 -22
- package/dist/api/http/routes/sessions.js +12 -11
- package/dist/api/http/routes/tenant.d.ts +2 -2
- package/dist/api/http/routes/tenant.js +69 -16
- package/dist/api/inbound-files.d.ts +3 -9
- package/dist/api/inbound-files.js +18 -24
- package/dist/api/openapi.js +6 -2
- package/dist/artifacts/disk.d.ts +0 -4
- package/dist/artifacts/disk.js +77 -73
- package/dist/artifacts/files.d.ts +13 -4
- package/dist/artifacts/files.js +27 -9
- package/dist/artifacts/folder-upload.d.ts +75 -0
- package/dist/artifacts/folder-upload.js +263 -0
- package/dist/artifacts/folders.d.ts +14 -8
- package/dist/artifacts/folders.js +19 -11
- package/dist/artifacts/links.js +7 -6
- package/dist/artifacts/parts.d.ts +9 -14
- package/dist/artifacts/parts.js +11 -9
- package/dist/artifacts/pipe.d.ts +15 -0
- package/dist/artifacts/pipe.js +71 -0
- package/dist/artifacts/quota.d.ts +36 -0
- package/dist/artifacts/quota.js +44 -0
- package/dist/artifacts/service.d.ts +25 -1
- package/dist/artifacts/service.js +70 -25
- package/dist/blob/fs.js +2 -1
- package/dist/blob/s3.d.ts +6 -2
- package/dist/blob/s3.js +9 -7
- package/dist/blob/types.d.ts +1 -1
- package/dist/blob/types.js +1 -1
- package/dist/core/limits.d.ts +11 -35
- package/dist/core/limits.js +5 -73
- package/dist/core/sandbox-routes.js +0 -1
- package/dist/core/turn-manifest.d.ts +1 -2
- package/dist/core/turn-manifest.js +4 -28
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +8 -6
- package/dist/execution/types.d.ts +1 -6
- package/dist/execution/types.js +0 -5
- package/dist/gates/contract.d.ts +3 -2
- package/dist/gates/contract.js +1 -1
- package/dist/gates/egress-credentials.js +4 -3
- package/dist/gates/egress.d.ts +9 -9
- package/dist/gates/egress.js +19 -19
- package/dist/gates/fixture-model.js +13 -2
- package/dist/gates/handler.d.ts +1 -3
- package/dist/gates/handler.js +4 -3
- package/dist/gates/http-client.d.ts +4 -3
- package/dist/gates/http-client.js +5 -7
- package/dist/gates/http-tool.d.ts +24 -4
- package/dist/gates/http-tool.js +53 -17
- package/dist/gates/in-process.d.ts +2 -1
- package/dist/gates/in-process.js +17 -9
- package/dist/gates/inflight.d.ts +2 -2
- package/dist/gates/inflight.js +3 -2
- package/dist/gates/journal.js +5 -7
- package/dist/gates/mcp-handler.d.ts +4 -3
- package/dist/gates/mcp-handler.js +15 -5
- package/dist/gates/model-gate.d.ts +11 -2
- package/dist/gates/policy.d.ts +28 -5
- package/dist/gates/policy.js +60 -17
- package/dist/gates/provider-gate.d.ts +22 -0
- package/dist/gates/provider-gate.js +30 -0
- package/dist/gates/tool-calls.d.ts +1 -3
- package/dist/gates/tool-calls.js +2 -3
- package/dist/gates/tool-client.d.ts +1 -1
- package/dist/gates/tool-client.js +5 -6
- package/dist/gates/tool-contract.d.ts +8 -5
- package/dist/gates/tool-contract.js +1 -1
- package/dist/gates/tool-gate.d.ts +18 -7
- package/dist/gates/tool-gate.js +49 -7
- package/dist/harness/calls.d.ts +27 -25
- package/dist/harness/calls.js +40 -46
- package/dist/harness/executors.d.ts +10 -3
- package/dist/harness/executors.js +53 -23
- package/dist/harness/main.js +2 -2
- package/dist/harness/pod.d.ts +0 -2
- package/dist/harness/pod.js +7 -6
- package/dist/harness/run-tokens.d.ts +4 -1
- package/dist/harness/run-tokens.js +6 -2
- package/dist/harness/service.d.ts +17 -14
- package/dist/harness/service.js +21 -35
- package/dist/harness/setup.d.ts +0 -2
- package/dist/harness/setup.js +4 -9
- package/dist/harness/ws-client.d.ts +2 -2
- package/dist/harness/ws-client.js +7 -5
- package/dist/harness/ws-port.d.ts +3 -4
- package/dist/harness/ws-port.js +3 -4
- package/dist/harness-api/artifacts.d.ts +20 -0
- package/dist/harness-api/artifacts.js +478 -0
- package/dist/harness-api/in-process.d.ts +2 -2
- package/dist/harness-api/in-process.js +18 -16
- package/dist/harness-api/record.d.ts +10 -9
- package/dist/harness-api/record.js +102 -157
- package/dist/harness-api/renew.d.ts +1 -1
- package/dist/harness-api/renew.js +2 -2
- package/dist/harness-api/server.d.ts +1 -16
- package/dist/harness-api/server.js +82 -73
- package/dist/harness-api/start.d.ts +20 -2
- package/dist/harness-api/start.js +57 -18
- package/dist/harness-api/tool-journal.d.ts +15 -10
- package/dist/harness-api/tool-journal.js +26 -24
- package/dist/harness-api/workspace.d.ts +0 -4
- package/dist/harness-api/workspace.js +2 -45
- package/dist/harness-api/ws-server.js +10 -10
- package/dist/host/create-host.js +2 -3
- package/dist/host/execution.js +2 -3
- package/dist/host/gates.d.ts +2 -9
- package/dist/host/gates.js +15 -12
- package/dist/host/http.d.ts +0 -2
- package/dist/host/http.js +0 -27
- package/dist/host/main.js +10 -3
- package/dist/host/operate.js +12 -3
- package/dist/host/stack-config.d.ts +11 -0
- package/dist/host/stack-config.js +21 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/index.js +2 -1
- package/dist/infra/readiness.d.ts +0 -2
- package/dist/infra/readiness.js +4 -3
- package/dist/keys/client.d.ts +1 -0
- package/dist/keys/client.js +2 -3
- package/dist/keys/contract.d.ts +1 -3
- package/dist/keys/contract.js +1 -3
- package/dist/keys/egress-ca.d.ts +0 -2
- package/dist/keys/egress-ca.js +2 -3
- package/dist/keys/keys.js +6 -1
- package/dist/keys/x509.d.ts +8 -4
- package/dist/keys/x509.js +14 -5
- package/dist/management-openapi.json +1961 -994
- package/dist/mcp/connect.d.ts +35 -26
- package/dist/mcp/connect.js +112 -141
- package/dist/mcp/pool.d.ts +27 -22
- package/dist/mcp/pool.js +34 -48
- package/dist/mcp/preview.d.ts +13 -6
- package/dist/mcp/preview.js +37 -20
- package/dist/mcp/snapshot.d.ts +35 -86
- package/dist/mcp/snapshot.js +93 -135
- package/dist/model/auth-store.js +5 -3
- package/dist/model/capabilities.d.ts +1 -1
- package/dist/model/catalog.d.ts +9 -36
- package/dist/model/catalog.js +6 -33
- package/dist/model/descriptor.d.ts +51 -0
- package/dist/model/descriptor.js +83 -0
- package/dist/model/models.d.ts +0 -3
- package/dist/model/models.js +3 -5
- package/dist/model/native-blocks.js +1 -1
- package/dist/model/pi-model.d.ts +16 -5
- package/dist/model/pi-model.js +86 -105
- package/dist/openapi.json +866 -1218
- package/dist/reads/cursor.js +3 -1
- package/dist/record/append.d.ts +34 -2
- package/dist/record/append.js +50 -1
- package/dist/record/environment.d.ts +2 -0
- package/dist/record/environment.js +2 -0
- package/dist/record/index.d.ts +1 -1
- package/dist/record/index.js +1 -1
- package/dist/redact.d.ts +1 -1
- package/dist/redact.js +3 -2
- package/dist/sandbox/egress-token.d.ts +3 -3
- package/dist/sandbox/egress-token.js +3 -3
- package/dist/sandbox/join.js +3 -2
- package/dist/sandbox/manager.d.ts +41 -41
- package/dist/sandbox/manager.js +112 -44
- package/dist/sandbox/placement.d.ts +1 -1
- package/dist/sandbox/placement.js +5 -8
- package/dist/sandbox/pods/client.d.ts +1 -10
- package/dist/sandbox/pods/client.js +8 -4
- package/dist/sandbox/pods/lifecycle.d.ts +19 -11
- package/dist/sandbox/pods/lifecycle.js +45 -23
- package/dist/sandbox/pods/network-gate.d.ts +4 -5
- package/dist/sandbox/pods/network-gate.js +7 -7
- package/dist/sandbox/pods/reconcile.js +4 -1
- package/dist/sandbox/pods/spec.js +8 -5
- package/dist/sandbox/policy.d.ts +7 -1
- package/dist/sandbox/policy.js +10 -3
- package/dist/sandbox/resolve.d.ts +8 -3
- package/dist/sandbox/resolve.js +12 -2
- package/dist/sandbox/setup.d.ts +3 -1
- package/dist/sandbox/setup.js +34 -11
- package/dist/sandbox/share.d.ts +5 -14
- package/dist/sandbox/share.js +5 -34
- package/dist/sandbox/skills.d.ts +7 -13
- package/dist/sandbox/skills.js +16 -31
- package/dist/sandbox/tenant-config.d.ts +2 -2
- package/dist/sandbox/tenant-config.js +4 -3
- package/dist/sandbox/tools.d.ts +7 -1
- package/dist/sandbox/tools.js +26 -26
- package/dist/sandbox/types.d.ts +10 -3
- package/dist/sandbox/types.js +9 -0
- package/dist/store/postgres/connect.d.ts +6 -5
- package/dist/store/postgres/connect.js +14 -11
- package/dist/store/postgres/control.js +13 -13
- package/dist/store/postgres/drizzle/0028_artifact_write_keys.sql +2 -0
- package/dist/store/postgres/drizzle/0029_protocol_15.sql +11 -0
- package/dist/store/postgres/drizzle/0030_protocol_15_runtime_block.sql +11 -0
- package/dist/store/postgres/drizzle/0031_protocol_15_mcp_snapshot.sql +8 -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 +10 -2
- package/dist/store/postgres/schema.d.ts +19 -0
- package/dist/store/postgres/schema.js +7 -0
- package/dist/store/postgres/store.js +55 -26
- package/dist/store/types.d.ts +17 -7
- package/dist/streams/relay/core.d.ts +3 -3
- package/dist/streams/relay/core.js +9 -10
- package/dist/tenant/advance.js +21 -31
- package/dist/tenant/commands.js +9 -16
- package/dist/tenant/context.d.ts +14 -17
- package/dist/tenant/definition-files.d.ts +25 -4
- package/dist/tenant/definition-files.js +85 -47
- package/dist/tenant/effects.js +9 -36
- package/dist/tenant/environment-manifests.js +9 -1
- package/dist/tenant/environments.d.ts +0 -2
- package/dist/tenant/environments.js +54 -30
- package/dist/tenant/ephemeral.d.ts +1 -1
- package/dist/tenant/ephemeral.js +2 -1
- package/dist/tenant/history.d.ts +1 -1
- package/dist/tenant/history.js +1 -1
- package/dist/tenant/host-token.d.ts +3 -3
- package/dist/tenant/host-token.js +12 -11
- package/dist/tenant/http.d.ts +5 -8
- package/dist/tenant/http.js +9 -0
- package/dist/tenant/identity-file.d.ts +24 -1
- package/dist/tenant/identity-file.js +35 -7
- package/dist/tenant/issuers.d.ts +5 -5
- package/dist/tenant/issuers.js +31 -23
- package/dist/tenant/jwt.d.ts +8 -2
- package/dist/tenant/jwt.js +10 -2
- package/dist/tenant/limits.d.ts +90 -0
- package/dist/tenant/limits.js +175 -0
- package/dist/tenant/module.d.ts +4 -1
- package/dist/tenant/module.js +9 -8
- package/dist/tenant/outbound.d.ts +17 -19
- package/dist/tenant/outbound.js +18 -21
- package/dist/tenant/run-grants.js +3 -2
- package/dist/tenant/run-token.d.ts +0 -4
- package/dist/tenant/run-token.js +6 -11
- package/dist/tenant/runtime-limits.d.ts +24 -0
- package/dist/tenant/runtime-limits.js +27 -0
- package/dist/tenant/runtime.js +30 -43
- package/dist/tenant/session-streams.js +7 -6
- package/dist/tenant/session.d.ts +1 -1
- package/dist/tenant/session.js +1 -1
- package/dist/tenant/sessions.js +32 -29
- package/dist/tenant/signing-keys.d.ts +1 -1
- package/dist/tenant/signing-keys.js +5 -6
- package/dist/tenant/status.js +4 -3
- package/dist/tenant/streams.js +18 -20
- package/dist/tenant/sweep.d.ts +1 -21
- package/dist/tenant/sweep.js +39 -48
- package/dist/tenant/transcript.d.ts +70 -23
- package/dist/tenant/transcript.js +28 -22
- package/dist/tenant/types.d.ts +11 -16
- package/dist/tenant/worker.d.ts +2 -10
- package/dist/tenant/worker.js +5 -14
- package/dist/traces/ids.js +5 -1
- package/dist/traces/map.d.ts +9 -6
- package/dist/traces/map.js +9 -11
- package/dist/traces/otlp.d.ts +3 -3
- package/dist/traces/otlp.js +5 -3
- package/dist/traces/projector.d.ts +9 -4
- package/dist/traces/projector.js +13 -7
- package/dist/traces/read.js +6 -1
- package/dist/tuning.d.ts +383 -0
- package/dist/tuning.js +658 -0
- package/dist/vault/headers.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/artifacts/export.d.ts +0 -8
- package/dist/artifacts/export.js +0 -204
- package/dist/artifacts/media-types.d.ts +0 -30
- package/dist/artifacts/media-types.js +0 -137
- package/dist/artifacts/workspace.d.ts +0 -60
- package/dist/artifacts/workspace.js +0 -45
- package/dist/media.d.ts +0 -10
- package/dist/media.js +0 -58
- package/dist/sandbox/runtime-tools.d.ts +0 -9
- package/dist/sandbox/runtime-tools.js +0 -16
- package/dist/sandbox/session-sandbox.d.ts +0 -16
- package/dist/sandbox/session-sandbox.js +0 -77
- package/dist/tenant/artifact-tool.d.ts +0 -13
- package/dist/tenant/artifact-tool.js +0 -243
- package/dist/tenant/skill-tool.d.ts +0 -20
- package/dist/tenant/skill-tool.js +0 -67
- package/dist/tenant/tool-results.d.ts +0 -57
- package/dist/tenant/tool-results.js +0 -298
- package/dist/tenant/tool-search.d.ts +0 -39
- package/dist/tenant/tool-search.js +0 -103
package/dist/sandbox/manager.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox Manager: owns sandbox lifecycle for the Runtime. One workspace per sandbox resource
|
|
3
|
+
* (`sandbox: { id }`, keyed by the sandbox id), or per owning session for a sandbox chosen
|
|
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. 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.
|
|
10
|
+
*
|
|
11
|
+
* The manager keeps no timers and reaches no store: it keeps its compute records through a
|
|
12
|
+
* `SandboxRecords` port (the Tenant's `sandboxes` table in core's process, a file in a harness's)
|
|
13
|
+
* and writes its events through `emit`. Its owner drives it: `stopIdle` stops sandboxes idle
|
|
14
|
+
* past their timeout and marks records of compute this process no longer holds as stopped
|
|
15
|
+
* (after a restart); `reconcile` removes workspaces whose owner is gone. `sweep` does both, the
|
|
16
|
+
* reconcile once per process, as the Tenant sweep does in core's process.
|
|
17
|
+
*
|
|
18
|
+
* A folder the harness asks for (`mountFolder`: our engine's skill folders, `/skills/<name>@<hash>/`,
|
|
19
|
+
* D66, protocol 15) is written read-only, once per open handle: the harness decides which folders
|
|
20
|
+
* and when, and gives their bytes; the manager only writes them (`skills.ts`).
|
|
21
|
+
*/
|
|
22
|
+
import { posix } from "node:path";
|
|
23
|
+
import { SANDBOX_WORKSPACE } from "@nylorun/core/define";
|
|
2
24
|
import { sandboxWorkspaceKey, sessionWorkspaceKey, workspacePrefix, } from "./records.js";
|
|
3
25
|
import { DEFAULT_SANDBOX_CPUS, DEFAULT_SANDBOX_IMAGE, describeNetwork, idleMsOf, memoryMiBOf, resolveNetwork, } from "./policy.js";
|
|
4
26
|
import { parseSandboxPreference, reportSelection, selectSandboxBackend, } from "./select.js";
|
|
5
|
-
import {
|
|
27
|
+
import { mountFolderFiles } from "./skills.js";
|
|
28
|
+
import { DEFAULT_SANDBOX_TOOL_LIMITS } from "@nylorun/harness";
|
|
6
29
|
import { runDiskOp } from "./disk-fs.js";
|
|
7
|
-
import { resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
30
|
+
import { helperCommandTimeoutMs, quote, resolveSandboxPath, runSandboxTool, } from "./tools.js";
|
|
8
31
|
import { SandboxFileTooLargeError, } from "./types.js";
|
|
9
|
-
|
|
32
|
+
import { hostTuning } from "../tuning.js";
|
|
10
33
|
const READ_TOOLS = new Set(["read", "grep", "glob"]);
|
|
11
34
|
export class SandboxManager {
|
|
12
35
|
options;
|
|
@@ -99,12 +122,16 @@ export class SandboxManager {
|
|
|
99
122
|
async report() {
|
|
100
123
|
return { ...reportSelection(await this.ready), defaultImage: DEFAULT_SANDBOX_IMAGE };
|
|
101
124
|
}
|
|
102
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Runs sandbox operation `toolName` in the session's sandbox, held to `limits`: the harness's
|
|
127
|
+
* sandbox tool limits (`DEFAULT_SANDBOX_TOOL_LIMITS` when it names none).
|
|
128
|
+
*/
|
|
129
|
+
async run(session, sandbox, toolName, input, signal, limits = DEFAULT_SANDBOX_TOOL_LIMITS) {
|
|
103
130
|
const prepared = await this.prepare(session, sandbox);
|
|
104
131
|
if ("kind" in prepared)
|
|
105
132
|
return prepared;
|
|
106
133
|
const { live: entry, spec } = prepared;
|
|
107
|
-
return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal));
|
|
134
|
+
return this.inLane(entry, session.id, () => this.execute(entry, session, spec, toolName, input, signal, limits));
|
|
108
135
|
}
|
|
109
136
|
/**
|
|
110
137
|
* Runs `call` in session `sessionId`'s queue on `live`: after its earlier calls, and after any
|
|
@@ -159,8 +186,8 @@ export class SandboxManager {
|
|
|
159
186
|
return handle;
|
|
160
187
|
}
|
|
161
188
|
/**
|
|
162
|
-
* The bytes of one file in the session's sandbox, for `save_artifact`
|
|
163
|
-
* as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
189
|
+
* The bytes of one file in the session's sandbox, for our engine's `save_artifact` and its
|
|
190
|
+
* outputs folder (`files.read`), in the same queue as its tool calls: `missing` when there is no such file, a failed outcome when the sandbox
|
|
164
191
|
* cannot open or the file is larger than `maxBytes`. Relative paths resolve in the workspace.
|
|
165
192
|
*/
|
|
166
193
|
async readBytes(session, sandbox, path, maxBytes, signal) {
|
|
@@ -190,8 +217,37 @@ export class SandboxManager {
|
|
|
190
217
|
});
|
|
191
218
|
}
|
|
192
219
|
/**
|
|
193
|
-
*
|
|
194
|
-
* the same queue as its tool calls
|
|
220
|
+
* Writes `body` to one file of the session's sandbox, as it comes, for our engine's
|
|
221
|
+
* `read_artifact` with `path` (protocol 15), in the same queue as its tool calls: its folders
|
|
222
|
+
* are made first, and a file there is replaced. Relative paths resolve in the workspace.
|
|
223
|
+
*/
|
|
224
|
+
async writeBytes(session, sandbox, path, body, signal) {
|
|
225
|
+
return this.withHandle(session, sandbox, signal, async (handle) => {
|
|
226
|
+
const resolved = resolveSandboxPath(path, handle.workspace ?? SANDBOX_WORKSPACE);
|
|
227
|
+
const parent = posix.dirname(resolved);
|
|
228
|
+
const made = await handle.exec({ command: `mkdir -p ${quote(parent)}`, cwd: "/", timeoutMs: helperCommandTimeoutMs() }, signal);
|
|
229
|
+
if (made.exitCode !== 0)
|
|
230
|
+
return {
|
|
231
|
+
kind: "failed",
|
|
232
|
+
code: "sandbox.write_failed",
|
|
233
|
+
message: made.stderr.trim() || `Could not create ${parent}`,
|
|
234
|
+
};
|
|
235
|
+
let size = 0;
|
|
236
|
+
const counted = body instanceof Uint8Array
|
|
237
|
+
? ((size = body.byteLength), body)
|
|
238
|
+
: (async function* () {
|
|
239
|
+
for await (const chunk of body) {
|
|
240
|
+
size += chunk.byteLength;
|
|
241
|
+
yield chunk;
|
|
242
|
+
}
|
|
243
|
+
})();
|
|
244
|
+
await handle.writeFile(resolved, counted);
|
|
245
|
+
return { kind: "written", path: resolved, size };
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The files under directory `dir` of the session's sandbox, for our engine's outputs folder
|
|
250
|
+
* (`files.list`, protocol 15), in the same queue as its tool calls. `missing` when the sandbox was never created (nothing is
|
|
195
251
|
* started to look) or `dir` is not a directory. Relative paths resolve in the workspace.
|
|
196
252
|
*/
|
|
197
253
|
async listFiles(session, sandbox, dir, maxEntries, signal) {
|
|
@@ -252,9 +308,6 @@ export class SandboxManager {
|
|
|
252
308
|
message: `The sandbox could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
253
309
|
};
|
|
254
310
|
}
|
|
255
|
-
const unmounted = await this.mountSkills(live, handle, session, signal);
|
|
256
|
-
if (unmounted)
|
|
257
|
-
return unmounted;
|
|
258
311
|
return await use(handle, live);
|
|
259
312
|
});
|
|
260
313
|
}
|
|
@@ -294,48 +347,52 @@ export class SandboxManager {
|
|
|
294
347
|
lastUsedAt: Date.now(),
|
|
295
348
|
lanes: new Map(),
|
|
296
349
|
gate: Promise.resolve(),
|
|
297
|
-
|
|
350
|
+
mounts: new Map(),
|
|
298
351
|
};
|
|
299
352
|
this.live.set(key, live);
|
|
300
353
|
}
|
|
301
354
|
return { live, spec };
|
|
302
355
|
}
|
|
303
|
-
async execute(live, session, spec, toolName, input, signal) {
|
|
356
|
+
async execute(live, session, spec, toolName, input, signal, limits) {
|
|
304
357
|
if (signal.aborted)
|
|
305
358
|
throw new Error("Turn cancelled");
|
|
306
359
|
if (this.closing)
|
|
307
360
|
throw new Error("Runtime is shutting down");
|
|
308
|
-
return this.executeOpen(live, session, spec, toolName, input, signal);
|
|
361
|
+
return this.executeOpen(live, session, spec, toolName, input, signal, limits);
|
|
309
362
|
}
|
|
310
|
-
|
|
311
|
-
|
|
363
|
+
/**
|
|
364
|
+
* The workspace's handle, opened when there is none. A start that fails is recorded as stopped
|
|
365
|
+
* and answered as a failed outcome: nothing ran yet, so it is definite, not uncertain.
|
|
366
|
+
*/
|
|
367
|
+
async started(live, session, spec) {
|
|
312
368
|
try {
|
|
313
|
-
|
|
369
|
+
return await this.handleOf(live, session, spec);
|
|
314
370
|
}
|
|
315
371
|
catch (error) {
|
|
316
372
|
const message = error instanceof Error ? error.message : String(error);
|
|
317
373
|
const record = await this.record(spec.key, live, session.id, spec.image, "stopped");
|
|
318
374
|
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
375
|
return {
|
|
321
376
|
kind: "failed",
|
|
322
377
|
code: "sandbox.start_failed",
|
|
323
378
|
message: `The sandbox could not start on ${live.backend.name} (image ${spec.image ?? "the backend's default"}): ${message}`,
|
|
324
379
|
};
|
|
325
380
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
381
|
+
}
|
|
382
|
+
async executeOpen(live, session, spec, toolName, input, signal, limits) {
|
|
383
|
+
const handle = await this.started(live, session, spec);
|
|
384
|
+
if ("kind" in handle)
|
|
385
|
+
return handle;
|
|
329
386
|
const started = Date.now();
|
|
330
387
|
let report = {};
|
|
331
388
|
try {
|
|
332
389
|
const outcome = await runSandboxTool(handle, toolName, (input ?? {}), signal, (value) => {
|
|
333
390
|
report = value;
|
|
334
|
-
}, session.environment);
|
|
391
|
+
}, limits, session.environment);
|
|
335
392
|
await this.options.emit(session.id, session.activeTurnId, "environment.exec", {
|
|
336
393
|
tool: toolName,
|
|
337
394
|
...report,
|
|
338
|
-
...(report.command ? { command: report.command.slice(0,
|
|
395
|
+
...(report.command ? { command: report.command.slice(0, hostTuning().harness.execCommandChars) } : {}),
|
|
339
396
|
outcome: outcome.kind,
|
|
340
397
|
...(outcome.kind === "failed" ? { code: outcome.code } : {}),
|
|
341
398
|
durationMs: Date.now() - started,
|
|
@@ -356,23 +413,34 @@ export class SandboxManager {
|
|
|
356
413
|
}
|
|
357
414
|
}
|
|
358
415
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
416
|
+
* Writes `files` (path in `folder` to `sha256:<hex>`) to `folder` of the session's sandbox,
|
|
417
|
+
* read-only, each one's bytes from `read`, in the session's queue (protocol 15: the harness
|
|
418
|
+
* decides which folders, and when). A folder the open handle already holds is `present` without
|
|
419
|
+
* an exec; calls that need one folder at once share its mount. A failed outcome when the sandbox
|
|
420
|
+
* cannot start or the folder cannot be written; it is not marked mounted then, so the next call
|
|
421
|
+
* tries again. A reopened handle holds none.
|
|
363
422
|
*/
|
|
364
|
-
async
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
423
|
+
async mountFolder(session, sandbox, folder, files, read, signal) {
|
|
424
|
+
const prepared = await this.prepare(session, sandbox);
|
|
425
|
+
if ("kind" in prepared)
|
|
426
|
+
return prepared;
|
|
427
|
+
const { live, spec } = prepared;
|
|
428
|
+
return this.inLane(live, session.id, async () => {
|
|
429
|
+
if (signal.aborted)
|
|
430
|
+
throw new Error("Turn cancelled");
|
|
431
|
+
if (this.closing)
|
|
432
|
+
throw new Error("Runtime is shutting down");
|
|
433
|
+
const handle = await this.started(live, session, spec);
|
|
434
|
+
if ("kind" in handle)
|
|
435
|
+
return handle;
|
|
436
|
+
let mounting = live.mounts.get(folder);
|
|
437
|
+
const present = mounting !== undefined;
|
|
370
438
|
if (!mounting) {
|
|
371
|
-
const mount =
|
|
372
|
-
live.
|
|
439
|
+
const mount = mountFolderFiles(handle, folder, files, async (sha256) => read(sha256), signal);
|
|
440
|
+
live.mounts.set(folder, mount);
|
|
373
441
|
void mount.catch(() => {
|
|
374
|
-
if (live.
|
|
375
|
-
live.
|
|
442
|
+
if (live.mounts.get(folder) === mount)
|
|
443
|
+
live.mounts.delete(folder);
|
|
376
444
|
});
|
|
377
445
|
mounting = mount;
|
|
378
446
|
}
|
|
@@ -385,11 +453,11 @@ export class SandboxManager {
|
|
|
385
453
|
return {
|
|
386
454
|
kind: "failed",
|
|
387
455
|
code: "sandbox.skills_unavailable",
|
|
388
|
-
message: `The files of
|
|
456
|
+
message: `The files of folder ${folder} could not be put in the sandbox: ${error instanceof Error ? error.message : String(error)}`,
|
|
389
457
|
};
|
|
390
458
|
}
|
|
391
|
-
|
|
392
|
-
|
|
459
|
+
return { kind: present ? "present" : "mounted" };
|
|
460
|
+
});
|
|
393
461
|
}
|
|
394
462
|
async open(live, session, spec) {
|
|
395
463
|
const existing = await this.options.records.get(spec.key);
|
|
@@ -402,8 +470,8 @@ export class SandboxManager {
|
|
|
402
470
|
const creating = await this.record(spec.key, live, session.id, spec.image, "creating", existing);
|
|
403
471
|
await this.options.emit(session.id, session.activeTurnId, "environment.state", { state: "creating", ...payload, ...(existing ? { reattach: true } : {}) }, { record: creating, ...claimOf(session) });
|
|
404
472
|
const handle = await live.backend.open(spec);
|
|
405
|
-
// A handle opened afresh holds no
|
|
406
|
-
live.
|
|
473
|
+
// A handle opened afresh holds no folder this manager knows of.
|
|
474
|
+
live.mounts.clear();
|
|
407
475
|
const running = await this.record(spec.key, live, session.id, spec.image, "running", existing);
|
|
408
476
|
await this.options.emit(session.id, session.activeTurnId, "environment.state", {
|
|
409
477
|
state: "running",
|
|
@@ -22,7 +22,7 @@ export declare function placementOf(config: TenantSandboxConfig, harnessId?: str
|
|
|
22
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
|
-
/** Whether the sandboxes service is up,
|
|
25
|
+
/** Whether the sandboxes service is up, the answer reused for `pods.readyCacheMs` (Host tuning). */
|
|
26
26
|
export declare function podsReady(pods: TenantPods): Promise<boolean>;
|
|
27
27
|
/** Refuses a pod Environment without sandbox pods, or with the sandboxes service down. */
|
|
28
28
|
export declare function requirePods(pods: TenantPods | undefined, what: string): Promise<TenantPods>;
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* sandbox enable`), or with the sandboxes service not ready, it is `409 environment_unavailable`.
|
|
12
12
|
*/
|
|
13
13
|
import { DEFAULT_HARNESS, } from "@nylorun/core/contracts";
|
|
14
|
-
import { fail } from "../tenant/http.js";
|
|
14
|
+
import { fail, failRetryLater } from "../tenant/http.js";
|
|
15
|
+
import { hostTuning } from "../tuning.js";
|
|
15
16
|
/** Our harness's id (blueprint D38): what a session runs unless its Environment names another. */
|
|
16
17
|
export const HARNESS_ID = DEFAULT_HARNESS;
|
|
17
18
|
export const DEFAULT_PLACEMENT = ["harness-container", "sandbox"];
|
|
@@ -35,12 +36,11 @@ export function checkPlacement(config, kind, harness = HARNESS_ID) {
|
|
|
35
36
|
? `This Tenant does not let the ${harness} harness run in a sandbox pod (${where}).`
|
|
36
37
|
: `This Tenant runs the ${harness} harness only in sandbox pods (${where}): open the session on a pod Environment.`, { code: "placement_refused", details: { harness, host, allowed: [...allowed] } });
|
|
37
38
|
}
|
|
38
|
-
const READY_CACHE_MS = 5_000;
|
|
39
39
|
const ready = new WeakMap();
|
|
40
|
-
/** Whether the sandboxes service is up,
|
|
40
|
+
/** Whether the sandboxes service is up, the answer reused for `pods.readyCacheMs` (Host tuning). */
|
|
41
41
|
export async function podsReady(pods) {
|
|
42
42
|
const cached = ready.get(pods);
|
|
43
|
-
if (cached && Date.now() - cached.at <
|
|
43
|
+
if (cached && Date.now() - cached.at < hostTuning().pods.readyCacheMs)
|
|
44
44
|
return cached.ready;
|
|
45
45
|
const answer = await pods.client.ready();
|
|
46
46
|
ready.set(pods, { at: Date.now(), ready: answer });
|
|
@@ -53,9 +53,6 @@ export async function requirePods(pods, what) {
|
|
|
53
53
|
code: "environment_unavailable",
|
|
54
54
|
});
|
|
55
55
|
if (!(await podsReady(pods)))
|
|
56
|
-
|
|
57
|
-
code: "environment_unavailable",
|
|
58
|
-
details: { retryAfterSeconds: 5 },
|
|
59
|
-
}, { "retry-after": "5" });
|
|
56
|
+
failRetryLater(`${what} needs sandbox pods, and the sandboxes service is not ready. Check \`nylorun local sandbox status\`.`, "environment_unavailable");
|
|
60
57
|
return pods;
|
|
61
58
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The sandboxes service's client (F7.2, D32): the Go service (`sandboxes/`) that alone holds
|
|
3
|
-
* the cluster credentials and drives agent-sandbox Sandboxes in the Tenant's namespace. Core
|
|
4
|
-
* reaches it at `NYLORUN_SANDBOXES_URL` with `NYLORUN_SANDBOXES_TOKEN`; nothing else does.
|
|
5
|
-
*
|
|
6
|
-
* Pull, not push: the service never calls core. Core reads a Sandbox's status when it
|
|
7
|
-
* reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
|
|
8
|
-
* asks for carries an operation id, so a retried `PUT` is a no-op.
|
|
9
|
-
*/
|
|
10
1
|
/** `PUT /v1/pods/{name}`: the Sandbox the service applies (`driver.Spec` in Go). */
|
|
11
2
|
export interface PodSpec {
|
|
12
3
|
readonly opId: string;
|
|
@@ -92,6 +83,6 @@ export declare function httpSandboxesClient(options: {
|
|
|
92
83
|
readonly url: string;
|
|
93
84
|
readonly token: string;
|
|
94
85
|
readonly fetch?: typeof fetch;
|
|
95
|
-
/** Per request, unless the request waits longer. Default 20 s. */
|
|
86
|
+
/** Per request, unless the request waits longer. Default `pods.requestTimeoutMs` (Host tuning, 20 s). */
|
|
96
87
|
readonly timeoutMs?: number;
|
|
97
88
|
}): SandboxesClient;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* reconciles (`GET /v1/pods/{name}`, optionally blocking with `?wait=`), and every change it
|
|
8
8
|
* asks for carries an operation id, so a retried `PUT` is a no-op.
|
|
9
9
|
*/
|
|
10
|
+
import { hostTuning, SANDBOXES_MAX_WAIT_MS } from "../../tuning.js";
|
|
10
11
|
/** The service refused a request, or did not answer. */
|
|
11
12
|
export class SandboxesError extends Error {
|
|
12
13
|
status;
|
|
@@ -23,7 +24,7 @@ export function httpSandboxesClient(options) {
|
|
|
23
24
|
const base = options.url.replace(/\/+$/, "");
|
|
24
25
|
const call = options.fetch ?? fetch;
|
|
25
26
|
const request = async (method, path, init = {}) => {
|
|
26
|
-
const timeout = AbortSignal.timeout(init.timeoutMs ?? options.timeoutMs ??
|
|
27
|
+
const timeout = AbortSignal.timeout(init.timeoutMs ?? options.timeoutMs ?? hostTuning().pods.requestTimeoutMs);
|
|
27
28
|
const signal = init.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
28
29
|
let response;
|
|
29
30
|
try {
|
|
@@ -58,7 +59,7 @@ export function httpSandboxesClient(options) {
|
|
|
58
59
|
return {
|
|
59
60
|
async ready(signal) {
|
|
60
61
|
try {
|
|
61
|
-
await request("GET", "/ready", { auth: false, timeoutMs:
|
|
62
|
+
await request("GET", "/ready", { auth: false, timeoutMs: hostTuning().pods.readyProbeTimeoutMs, ...(signal ? { signal } : {}) });
|
|
62
63
|
return true;
|
|
63
64
|
}
|
|
64
65
|
catch {
|
|
@@ -68,10 +69,13 @@ export function httpSandboxesClient(options) {
|
|
|
68
69
|
info: (signal) => request("GET", "/v1/info", signal ? { signal } : {}),
|
|
69
70
|
put: (name, spec, signal) => request("PUT", pod(name), { body: spec, ...(signal ? { signal } : {}) }),
|
|
70
71
|
status(name, { wait, timeoutMs, signal } = {}) {
|
|
71
|
-
const
|
|
72
|
+
const tuning = hostTuning().pods;
|
|
73
|
+
// The service waits at most `SANDBOXES_MAX_WAIT_MS`; the answer has `waitSlackMs` to arrive.
|
|
74
|
+
const waitMs = Math.min(timeoutMs ?? tuning.waitMs, SANDBOXES_MAX_WAIT_MS);
|
|
75
|
+
const query = wait ? `?wait=${wait}&timeoutMs=${waitMs}` : "";
|
|
72
76
|
return request("GET", `${pod(name)}${query}`, {
|
|
73
77
|
...(signal ? { signal } : {}),
|
|
74
|
-
...(wait ? { timeoutMs:
|
|
78
|
+
...(wait ? { timeoutMs: waitMs + tuning.waitSlackMs } : {}),
|
|
75
79
|
});
|
|
76
80
|
},
|
|
77
81
|
delete: (name, opId, signal) => request("DELETE", `${pod(name)}?opId=${encodeURIComponent(opId)}`, signal ? { signal } : {}),
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* per sandbox and keeps its timers.
|
|
8
8
|
*
|
|
9
9
|
* - **Create** is eager: a `PUT` of a new pod sandbox asks for `Running` at once. One that is
|
|
10
|
-
* not ready within `
|
|
10
|
+
* not ready within `pods.readyDeadlineMs` (Host tuning) is `failed`, with the pod's reason; the next turn
|
|
11
11
|
* start tries again.
|
|
12
12
|
* - **Every change** the Runtime asks for bumps `rev`; the `PUT` carries `opId =
|
|
13
13
|
* <name>.r<rev>`, so a repeated apply is a no-op at the service, and a Sandbox whose `opId`
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
* reason that starts `setup:`, even while the pod is ready, until a turn start asks it to run.
|
|
30
30
|
* - **Image** (Q14): the first ready pod's workload image is recorded by digest (`imageDigest`),
|
|
31
31
|
* and every later apply runs that digest, so a stop, relaunch or reset never pulls another
|
|
32
|
-
* image under the same tag.
|
|
32
|
+
* image under the same tag. Only a digest of the repository the Environment asked for is
|
|
33
|
+
* pinned: an image id with no repository, or one a node gave a name of its own (kind names an
|
|
34
|
+
* image it loads `import-<date>`), pins nothing, since no registry serves it under that name.
|
|
33
35
|
*
|
|
34
36
|
* An unused Environment is deleted by the Tenant sweep (`lifecycle.deleteAfter`), not here.
|
|
35
37
|
*/
|
|
@@ -37,18 +39,19 @@ import type { EnvironmentEventPayload, EnvironmentEventType } from "@nylorun/cor
|
|
|
37
39
|
import type { Environment, SandboxPodPatch, SandboxPodState } from "../../store/types.js";
|
|
38
40
|
import type { PodStatus } from "./client.js";
|
|
39
41
|
/**
|
|
40
|
-
* The
|
|
41
|
-
*
|
|
42
|
+
* The repository an image reference names, as a container runtime writes it: the tag and digest
|
|
43
|
+
* dropped, Docker Hub's host and `library/` filled in (`python:3.13-slim` is
|
|
44
|
+
* `docker.io/library/python`).
|
|
42
45
|
*/
|
|
43
|
-
export declare function
|
|
46
|
+
export declare function repositoryOf(reference: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* The pullable reference of the image id a container status names (`repo@sha256:…`), when it is
|
|
49
|
+
* a digest of `requested`'s repository; undefined for an id without a repository, or with one
|
|
50
|
+
* the node made up for an image loaded onto it, which no registry serves.
|
|
51
|
+
*/
|
|
52
|
+
export declare function imageDigestOf(imageID: string | undefined, requested: string): string | undefined;
|
|
44
53
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
45
54
|
export declare const SETUP_FAILED = "setup:";
|
|
46
|
-
/** A pod not ready this long after it was asked to run has failed. */
|
|
47
|
-
export declare const READY_DEADLINE_MS: number;
|
|
48
|
-
/** How soon to look again while the pod changes. */
|
|
49
|
-
export declare const POLL_MS = 2000;
|
|
50
|
-
/** How soon to look again when the sandboxes service does not answer. */
|
|
51
|
-
export declare const UNREACHABLE_RETRY_MS = 5000;
|
|
52
55
|
/** The lifecycle settings (D36), read each time a decision is made. */
|
|
53
56
|
export interface PodLifecycleConfig {
|
|
54
57
|
readonly idleMs: number;
|
|
@@ -61,6 +64,11 @@ export type PodRow = Environment & {
|
|
|
61
64
|
};
|
|
62
65
|
export interface PodFacts {
|
|
63
66
|
readonly row: PodRow;
|
|
67
|
+
/**
|
|
68
|
+
* The workload image the pod is asked to run: its applied recipe's, else the sandboxes
|
|
69
|
+
* service's default (`DEFAULT_SANDBOX_IMAGE`). Only a digest of its repository is pinned.
|
|
70
|
+
*/
|
|
71
|
+
readonly image: string;
|
|
64
72
|
readonly config: PodLifecycleConfig;
|
|
65
73
|
/** The Sandbox's status; undefined when the sandboxes service did not answer. */
|
|
66
74
|
readonly status: PodStatus | undefined;
|
|
@@ -1,22 +1,41 @@
|
|
|
1
|
+
import { hostTuning } from "../../tuning.js";
|
|
2
|
+
import { DEFAULT_SANDBOX_IMAGE } from "../policy.js";
|
|
1
3
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
4
|
+
* The repository an image reference names, as a container runtime writes it: the tag and digest
|
|
5
|
+
* dropped, Docker Hub's host and `library/` filled in (`python:3.13-slim` is
|
|
6
|
+
* `docker.io/library/python`).
|
|
4
7
|
*/
|
|
5
|
-
export function
|
|
8
|
+
export function repositoryOf(reference) {
|
|
9
|
+
let name = reference.replace(/^docker-pullable:\/\//, "");
|
|
10
|
+
const at = name.indexOf("@");
|
|
11
|
+
if (at >= 0)
|
|
12
|
+
name = name.slice(0, at);
|
|
13
|
+
const colon = name.lastIndexOf(":");
|
|
14
|
+
if (colon > name.lastIndexOf("/"))
|
|
15
|
+
name = name.slice(0, colon);
|
|
16
|
+
const parts = name.split("/");
|
|
17
|
+
const first = parts[0] ?? "";
|
|
18
|
+
const hasHost = parts.length > 1 && (first.includes(".") || first.includes(":") || first === "localhost");
|
|
19
|
+
if (hasHost)
|
|
20
|
+
return name;
|
|
21
|
+
return parts.length === 1 ? `docker.io/library/${name}` : `docker.io/${name}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The pullable reference of the image id a container status names (`repo@sha256:…`), when it is
|
|
25
|
+
* a digest of `requested`'s repository; undefined for an id without a repository, or with one
|
|
26
|
+
* the node made up for an image loaded onto it, which no registry serves.
|
|
27
|
+
*/
|
|
28
|
+
export function imageDigestOf(imageID, requested) {
|
|
6
29
|
const id = imageID?.replace(/^docker-pullable:\/\//, "");
|
|
7
30
|
if (!id)
|
|
8
31
|
return undefined;
|
|
9
32
|
const at = id.indexOf("@sha256:");
|
|
10
|
-
|
|
33
|
+
if (at <= 0 || !/^[0-9a-f]{64}$/.test(id.slice(at + 8)))
|
|
34
|
+
return undefined;
|
|
35
|
+
return repositoryOf(id) === repositoryOf(requested) ? id : undefined;
|
|
11
36
|
}
|
|
12
37
|
/** A setup failure's `reason` starts with this (`sandbox/setup.ts`); it is sticky. */
|
|
13
38
|
export const SETUP_FAILED = "setup:";
|
|
14
|
-
/** A pod not ready this long after it was asked to run has failed. */
|
|
15
|
-
export const READY_DEADLINE_MS = 5 * 60_000;
|
|
16
|
-
/** How soon to look again while the pod changes. */
|
|
17
|
-
export const POLL_MS = 2_000;
|
|
18
|
-
/** How soon to look again when the sandboxes service does not answer. */
|
|
19
|
-
export const UNREACHABLE_RETRY_MS = 5_000;
|
|
20
39
|
/** The service's operation id of the row's current spec. */
|
|
21
40
|
export function opIdOf(pod) {
|
|
22
41
|
return `${pod.k8sName}.r${pod.rev}`;
|
|
@@ -28,6 +47,9 @@ function gone(status) {
|
|
|
28
47
|
const event = (type, payload) => ({ type, payload });
|
|
29
48
|
export function decide(facts) {
|
|
30
49
|
const { row, status, now } = facts;
|
|
50
|
+
// A pod not ready `readyDeadlineMs` after it was asked to run has failed; look again after
|
|
51
|
+
// `pollMs` while it changes, or `unreachableRetryMs` when the sandboxes service does not answer.
|
|
52
|
+
const { readyDeadlineMs, pollMs, unreachableRetryMs } = hostTuning().pods;
|
|
31
53
|
const pod = row.pod;
|
|
32
54
|
const patch = {};
|
|
33
55
|
const events = [];
|
|
@@ -49,17 +71,17 @@ export function decide(facts) {
|
|
|
49
71
|
if (pod.retiring !== undefined) {
|
|
50
72
|
const retiring = facts.retiring;
|
|
51
73
|
if (retiring === undefined)
|
|
52
|
-
again(
|
|
74
|
+
again(unreachableRetryMs);
|
|
53
75
|
else if (gone(retiring))
|
|
54
76
|
patch.retiring = null;
|
|
55
77
|
else {
|
|
56
78
|
if (retiring.exists && !retiring.deleting)
|
|
57
79
|
deletes.push(pod.retiring);
|
|
58
|
-
again(
|
|
80
|
+
again(pollMs);
|
|
59
81
|
}
|
|
60
82
|
}
|
|
61
83
|
if (status === undefined) {
|
|
62
|
-
again(
|
|
84
|
+
again(unreachableRetryMs);
|
|
63
85
|
return done();
|
|
64
86
|
}
|
|
65
87
|
if (pod.desired === "deleted") {
|
|
@@ -69,7 +91,7 @@ export function decide(facts) {
|
|
|
69
91
|
deletes.push(pod.k8sName);
|
|
70
92
|
if (pod.observed !== "deleting")
|
|
71
93
|
patch.observed = "deleting";
|
|
72
|
-
again(
|
|
94
|
+
again(pollMs);
|
|
73
95
|
return done();
|
|
74
96
|
}
|
|
75
97
|
// Lost is terminal: only a reset (a new name) leaves it.
|
|
@@ -77,7 +99,7 @@ export function decide(facts) {
|
|
|
77
99
|
if (status.exists && !status.deleting)
|
|
78
100
|
deletes.push(pod.k8sName);
|
|
79
101
|
if (status.exists)
|
|
80
|
-
again(
|
|
102
|
+
again(pollMs);
|
|
81
103
|
return done();
|
|
82
104
|
}
|
|
83
105
|
let desired = pod.desired;
|
|
@@ -110,12 +132,12 @@ export function decide(facts) {
|
|
|
110
132
|
if (status.exists && !status.deleting)
|
|
111
133
|
deletes.push(pod.k8sName);
|
|
112
134
|
if (status.exists)
|
|
113
|
-
again(
|
|
135
|
+
again(pollMs);
|
|
114
136
|
return done();
|
|
115
137
|
}
|
|
116
138
|
if (status.deleting) {
|
|
117
139
|
// The previous incarnation of this name is still going (an Environment deleted and created again).
|
|
118
|
-
again(
|
|
140
|
+
again(pollMs);
|
|
119
141
|
return done();
|
|
120
142
|
}
|
|
121
143
|
const opId = opIdOf({ k8sName: pod.k8sName, rev });
|
|
@@ -133,7 +155,7 @@ export function decide(facts) {
|
|
|
133
155
|
// A pod started by this apply (a create, a resume) is the Environment's first, not a relaunch.
|
|
134
156
|
if (desired === "running" && pod.podUid !== undefined)
|
|
135
157
|
patch.podUid = null;
|
|
136
|
-
again(
|
|
158
|
+
again(pollMs);
|
|
137
159
|
return done({
|
|
138
160
|
apply: { mode: desired === "running" ? "Running" : "Suspended", rotateJoin: !status.exists },
|
|
139
161
|
});
|
|
@@ -149,7 +171,7 @@ export function decide(facts) {
|
|
|
149
171
|
patch.reason = null;
|
|
150
172
|
}
|
|
151
173
|
// The image it runs, pinned by digest from now on (Q14).
|
|
152
|
-
const digest = pod.imageDigest === undefined ? imageDigestOf(status.imageID) : undefined;
|
|
174
|
+
const digest = pod.imageDigest === undefined ? imageDigestOf(status.imageID, facts.image) : undefined;
|
|
153
175
|
if (digest !== undefined)
|
|
154
176
|
patch.imageDigest = digest;
|
|
155
177
|
return done();
|
|
@@ -157,9 +179,9 @@ export function decide(facts) {
|
|
|
157
179
|
const startedAt = Date.parse(pod.startedAt ?? row.updatedAt);
|
|
158
180
|
if (pod.observed === "failed")
|
|
159
181
|
return done();
|
|
160
|
-
if (now - startedAt >=
|
|
182
|
+
if (now - startedAt >= readyDeadlineMs) {
|
|
161
183
|
const reason = [status.reason, status.message].filter(Boolean).join(": ") ||
|
|
162
|
-
`The pod was not ready within ${
|
|
184
|
+
`The pod was not ready within ${Math.round(readyDeadlineMs / 1000)} s`;
|
|
163
185
|
patch.observed = "failed";
|
|
164
186
|
patch.reason = reason;
|
|
165
187
|
events.push(event("environment.failed", { reason }));
|
|
@@ -167,7 +189,7 @@ export function decide(facts) {
|
|
|
167
189
|
}
|
|
168
190
|
if (pod.observed !== "creating")
|
|
169
191
|
patch.observed = "creating";
|
|
170
|
-
again(
|
|
192
|
+
again(pollMs);
|
|
171
193
|
return done();
|
|
172
194
|
}
|
|
173
195
|
// Suspended, applied: suspended once its pod is gone.
|
|
@@ -176,6 +198,6 @@ export function decide(facts) {
|
|
|
176
198
|
patch.observed = "suspended";
|
|
177
199
|
return done();
|
|
178
200
|
}
|
|
179
|
-
again(
|
|
201
|
+
again(pollMs);
|
|
180
202
|
return done();
|
|
181
203
|
}
|
|
@@ -2,20 +2,19 @@ export interface BlockedAddress {
|
|
|
2
2
|
readonly host: string;
|
|
3
3
|
readonly port: number;
|
|
4
4
|
}
|
|
5
|
-
/** Consecutive failed connections that prove an address blocked. */
|
|
6
|
-
export declare const CONFIRMATIONS = 3;
|
|
7
5
|
/** The addresses a pod's NetworkPolicy must block: the API server's service address. */
|
|
8
6
|
export declare function blockedAddresses(env: Readonly<Record<string, string | undefined>>): BlockedAddress[];
|
|
9
7
|
/** Whether a TCP connection to `address` opens within `timeoutMs`. */
|
|
10
8
|
export declare function opens(address: BlockedAddress, timeoutMs: number): Promise<boolean>;
|
|
11
9
|
export interface NetworkGateOptions {
|
|
12
10
|
readonly addresses: readonly BlockedAddress[];
|
|
13
|
-
/** Give up after this long. Default 120 s. */
|
|
11
|
+
/** Give up after this long. Default `harness.networkPolicyTimeoutMs` (Host tuning, 120 s). */
|
|
14
12
|
readonly timeoutMs?: number;
|
|
15
|
-
/** One connection attempt. Default 1 s. */
|
|
13
|
+
/** One connection attempt. Default `harness.networkProbeMs` (1 s). */
|
|
16
14
|
readonly attemptMs?: number;
|
|
17
|
-
/** Between attempts after a connection opened. Default 250 ms. */
|
|
15
|
+
/** Between attempts after a connection opened. Default `harness.networkPauseMs` (250 ms). */
|
|
18
16
|
readonly pauseMs?: number;
|
|
17
|
+
/** Consecutive failed connections that prove an address blocked. Default `harness.networkConfirmations` (3). */
|
|
19
18
|
readonly confirmations?: number;
|
|
20
19
|
readonly log?: (message: string, fields: Record<string, unknown>) => void;
|
|
21
20
|
}
|