@sema-agent/server 7.140.0 → 7.142.0
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/MIGRATION.md +505 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +142 -25
- package/dist/approval-ask-audit-store.d.ts +54 -38
- package/dist/approval-ask-audit-store.js +58 -39
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +39 -24
- package/dist/approval-card.js +6 -3
- package/dist/approval-crash-convergence.d.ts +17 -7
- package/dist/approval-crash-convergence.js +94 -22
- package/dist/approval-deny-reasons.d.ts +34 -30
- package/dist/approval-deny-reasons.js +21 -6
- package/dist/approval-policy-settlement.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +33 -1
- package/dist/approval-reconciler.js +43 -40
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/auth-bridge.d.ts +9 -6
- package/dist/auth-bridge.js +9 -3
- package/dist/boot/adoption.js +2 -1
- package/dist/boot/budget-tracing.d.ts +16 -0
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +34 -27
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +3 -2
- package/dist/boot/parked-revive-gate.js +2 -1
- package/dist/boot/permission-rules-audit.d.ts +0 -28
- package/dist/boot/permission-rules-audit.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +4 -3
- package/dist/boot/resolve-spec.js +18 -6
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.js +16 -13
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +8 -6
- package/dist/boot/shutdown.js +8 -2
- package/dist/boot/stage-04-stores.js +3 -2
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.js +11 -7
- package/dist/boot/stage-06-runners.d.ts +1 -3
- package/dist/boot/stage-06-runners.js +3 -2
- package/dist/boot/stage-07-capability-layer.d.ts +1 -2
- package/dist/boot/stage-07-capability-layer.js +10 -11
- package/dist/boot/stage-08-reapers.d.ts +1 -2
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +1 -2
- package/dist/boot/stage-10-http-server.d.ts +1 -2
- package/dist/boot/stage-10-http-server.js +6 -4
- package/dist/boot/stores.js +13 -6
- package/dist/boot/web-admin.js +18 -17
- package/dist/boot/webfetch-summarize-lane.js +2 -1
- package/dist/boot/workflow-orchestration.js +3 -2
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/builtin-tools.js +2 -4
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +33 -0
- package/dist/capability-facts.js +32 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +12 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +39 -10
- package/dist/config.d.ts +6 -0
- package/dist/config.js +72 -49
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +50 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/fleet/fleet-bus.d.ts +6 -3
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +30 -1
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +8 -2
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.js +42 -29
- package/dist/http/route-table.js +4 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +10 -6
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +27 -7
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/runs.js +3 -3
- package/dist/http/routes/session-memory-sync.d.ts +63 -0
- package/dist/http/routes/session-memory-sync.js +157 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +70 -71
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +10 -4
- package/dist/http/send.d.ts +6 -1
- package/dist/http/send.js +10 -3
- package/dist/http/server.d.ts +20 -7
- package/dist/http/server.js +2 -4
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +2 -2
- package/dist/http/sync-submit-body.d.ts +21 -4
- package/dist/http/sync-submit-body.js +13 -0
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +35 -23
- package/dist/memory-sync-client.js +122 -52
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +7 -1
- package/dist/memory-sync.js +6 -3
- package/dist/observability/fail-open.d.ts +57 -12
- package/dist/observability/fail-open.js +50 -9
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +93 -31
- package/dist/observability/run-terminal.js +175 -171
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +48 -7
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +9 -8
- package/dist/plugins/approval-ask-store-sql.d.ts +42 -10
- package/dist/plugins/approval-ask-store-sql.js +49 -19
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +7 -2
- package/dist/plugins/file-run-store.js +12 -2
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +260 -39
- package/dist/plugins/local-session-store.js +582 -57
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +7 -2
- package/dist/plugins/memory-run-store.js +12 -2
- package/dist/plugins/one-time-migrations.d.ts +68 -2
- package/dist/plugins/one-time-migrations.js +120 -4
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +47 -7
- package/dist/plugins/remote-env-e2b.js +242 -46
- package/dist/plugins/remote-env-k8s.d.ts +51 -6
- package/dist/plugins/remote-env-k8s.js +234 -44
- package/dist/plugins/remote-env-local-docker.d.ts +40 -14
- package/dist/plugins/remote-env-local-docker.js +127 -33
- package/dist/plugins/run-store-sql.d.ts +11 -3
- package/dist/plugins/run-store-sql.js +13 -4
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +41 -7
- package/dist/plugins/store-backend.js +22 -4
- package/dist/plugins/store-contracts.d.ts +35 -3
- package/dist/plugins/store-contracts.js +3 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +221 -28
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/plugins/workspace-fill.d.ts +60 -0
- package/dist/plugins/workspace-fill.js +106 -0
- package/dist/project-bundle.d.ts +38 -0
- package/dist/project-bundle.js +609 -0
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/run-local.js +2 -8
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +69 -55
- package/dist/sandbox-egress-policy.d.ts +4 -1
- package/dist/sandbox-egress-policy.js +3 -1
- package/dist/sandbox-git-plane.d.ts +31 -12
- package/dist/sandbox-git-plane.js +24 -4
- package/dist/security.d.ts +11 -2
- package/dist/security.js +8 -0
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +46 -4
- package/dist/session-error-wire.js +23 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +62 -27
- package/dist/task-settings.js +105 -29
- package/dist/terminal.d.ts +97 -20
- package/dist/terminal.js +107 -18
- package/dist/tool-approval.d.ts +4 -3
- package/dist/tool-approval.js +12 -14
- package/dist/trace/core-keyset-guard.d.ts +196 -2
- package/dist/trace/engine-notice-wire.d.ts +8 -2
- package/dist/trace/engine-notice-wire.js +4 -2
- package/dist/trace/exit-seat.d.ts +9 -1
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +8 -4
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/project.d.ts +18 -1
- package/dist/trace/project.js +16 -0
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +419 -4
- package/dist/trace/self-minted-wire.js +145 -0
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +22 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/dist/web-admin-client/types.d.ts +4 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- package/wire-changes/server@7.141.0.json +367 -0
- package/wire-changes/server@7.142.0.json +639 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { ExecutionError } from "@sema-agent/core";
|
|
4
|
+
import { shellQuote } from "./remote-shell.js";
|
|
5
|
+
import { echoCallerText } from "../caller-text-echo.js";
|
|
6
|
+
import { ADAPTER_VOLUME_NAMES } from "./k8s-exec-protocol.js";
|
|
7
|
+
import { SANDBOX_READ_FLOOR_ENV, sandboxReadFloorState } from "../execution-lane-caps.js";
|
|
8
|
+
export const READ_FLOOR_UNAVAILABLE_REASONS = ["arch", "user_notif", "addfd_send", "uid_zero", "writable_system_path", "binary_digest", "relay_silent", "relay_identity", "provenance"];
|
|
9
|
+
const REASON_SET = new Set(READ_FLOOR_UNAVAILABLE_REASONS);
|
|
10
|
+
function reasonSentence(reason) {
|
|
11
|
+
switch (reason) {
|
|
12
|
+
case "arch":
|
|
13
|
+
return "the sandbox's machine architecture has no supervisor build (x86_64 and aarch64 only)";
|
|
14
|
+
case "user_notif":
|
|
15
|
+
return "the sandbox kernel does not offer seccomp user notification";
|
|
16
|
+
case "addfd_send":
|
|
17
|
+
return "the sandbox kernel cannot hand a judged file to the command (SECCOMP_IOCTL_NOTIF_ADDFD with SEND needs Linux 5.14 or later)";
|
|
18
|
+
case "uid_zero":
|
|
19
|
+
return "the command would run as root (a root command can read the raw block device below any path floor) — run the image as a non-root user";
|
|
20
|
+
case "writable_system_path":
|
|
21
|
+
return "a system path the supervisor starts from (PATH directories, the dynamic loader, /etc/ld.so.preload) is writable by the command's user";
|
|
22
|
+
case "binary_digest":
|
|
23
|
+
return "the supervisor binary in the sandbox is not the one this server pins";
|
|
24
|
+
case "relay_silent":
|
|
25
|
+
return "the relay that carries the floor's questions to this server did not answer";
|
|
26
|
+
case "relay_identity":
|
|
27
|
+
return "the supervisor could not authenticate its relay";
|
|
28
|
+
case "provenance":
|
|
29
|
+
return "the sandbox was not under the floor since it was created (a resumed sandbox without the floor's marker)";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function buildReadFloorUnavailableMessage(reason, detail, floor) {
|
|
33
|
+
const first = floor?.rows[0];
|
|
34
|
+
return (`read floor unavailable on this sandbox (${reason}): ${reasonSentence(reason)}${detail ? ` — ${detail}` : ""}. ` +
|
|
35
|
+
`This command carries a read-deny table${first !== undefined ? ` (first row "${first}")` : ""} this sandbox cannot keep, so it is refused rather than run with fewer rows. ` +
|
|
36
|
+
`To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off (the lane then declares keepsReadFloor=false: each run with read-deny rows is announced, or refused when READ_DENY_FLOOR_REQUIRED=true).`);
|
|
37
|
+
}
|
|
38
|
+
export function buildReadFloorMissingMessage(lane) {
|
|
39
|
+
return (`read floor: this ${lane} sandbox keeps the read-deny byte floor, and this command start carries no floor — it is refused rather than run a process the floor does not judge ` +
|
|
40
|
+
`(every start on a lane that keeps the floor must carry the run's read-deny table). ` +
|
|
41
|
+
`To run this lane without the byte floor, set ${SANDBOX_READ_FLOOR_ENV}=off.`);
|
|
42
|
+
}
|
|
43
|
+
export const ADAPTER_START = Object.freeze({ kind: "adapter" });
|
|
44
|
+
export const BOOTSTRAP_START = Object.freeze({ kind: "bootstrap" });
|
|
45
|
+
export function engineStart(readFloor, hold = false) {
|
|
46
|
+
return hold ? { kind: "engine", readFloor, hold } : { kind: "engine", readFloor };
|
|
47
|
+
}
|
|
48
|
+
const BARE = Object.freeze({ mode: "bare" });
|
|
49
|
+
const ARCH_OF_UNAME = Object.freeze({ x86_64: "x86_64", amd64: "x86_64", aarch64: "aarch64", arm64: "aarch64" });
|
|
50
|
+
export const FLOOR_DIR = "/opt/sema/floor";
|
|
51
|
+
export function floorBinaryPath(arch) {
|
|
52
|
+
return `${FLOOR_DIR}/${arch}/sema-floor`;
|
|
53
|
+
}
|
|
54
|
+
function laneLaunchPlan(lane) {
|
|
55
|
+
switch (lane) {
|
|
56
|
+
case "e2b":
|
|
57
|
+
return { start: "root", control: "root", dropToWorkspaceOwner: true, stateDir: `${FLOOR_DIR}/state` };
|
|
58
|
+
case "local-docker":
|
|
59
|
+
return { start: "exec-user", control: "root", dropToWorkspaceOwner: false, stateDir: `${FLOOR_DIR}/state` };
|
|
60
|
+
case "k8s":
|
|
61
|
+
return { start: "exec-user", control: "exec-user", dropToWorkspaceOwner: false, stateDir: "/tmp/.sema-floor-state" };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export const FLOOR_ARGV = {
|
|
65
|
+
run: (bin, startId, as) => [bin, "run", "--start", startId, ...(as !== undefined ? ["--as", as] : [])],
|
|
66
|
+
plain: (bin, as) => [bin, "plain", ...(as !== undefined ? ["--as", as] : [])],
|
|
67
|
+
serve: (bin, stateDir, fresh) => [bin, "serve", "--state", stateDir, ...(fresh ? ["--fresh"] : [])],
|
|
68
|
+
selfcheck: (bin, digest, as) => [bin, "selfcheck", "--digest", digest, ...(as !== undefined ? ["--as", as] : [])],
|
|
69
|
+
};
|
|
70
|
+
function shellLine(argv) {
|
|
71
|
+
return argv.map(shellQuote).join(" ");
|
|
72
|
+
}
|
|
73
|
+
export function parseSelfcheck(stdout, exitCode) {
|
|
74
|
+
const line = stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "").at(-1) ?? "";
|
|
75
|
+
const okm = /^sema-floor selfcheck ok (\S+) (\S+)$/.exec(line);
|
|
76
|
+
if (okm && exitCode === 0)
|
|
77
|
+
return { ok: true, version: okm[1], arch: okm[2] };
|
|
78
|
+
const no = /^sema-floor selfcheck refused ([a-z_]+): ?(.*)$/.exec(line);
|
|
79
|
+
if (no && exitCode === 125 && REASON_SET.has(no[1]))
|
|
80
|
+
return { ok: false, reason: no[1], ...(no[2] ? { detail: echoCallerText(no[2], { maxChars: 300 }) } : {}) };
|
|
81
|
+
return { ok: false, reason: "binary_digest", detail: `the selfcheck answer is not one this server reads (exit ${exitCode}${line !== "" ? `, ${JSON.stringify(echoCallerText(line, { maxChars: 120 }))}` : ", no output"})` };
|
|
82
|
+
}
|
|
83
|
+
const START_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
84
|
+
export const MAX_PATHS_PER_QUESTION = 256;
|
|
85
|
+
export const MAX_RELAY_LINE_BYTES = 1024 * 1024;
|
|
86
|
+
const isInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
87
|
+
const exactKeys = (o, keys) => {
|
|
88
|
+
const ks = Object.keys(o);
|
|
89
|
+
return ks.length === keys.length && keys.every((k) => Object.hasOwn(o, k));
|
|
90
|
+
};
|
|
91
|
+
export function parseRelayFrame(line) {
|
|
92
|
+
let v;
|
|
93
|
+
try {
|
|
94
|
+
v = JSON.parse(line);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return { t: "broken", why: `not JSON: ${echoCallerText(e instanceof Error ? e.message : String(e), { maxChars: 120 })}` };
|
|
98
|
+
}
|
|
99
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
100
|
+
return { t: "broken", why: "not a JSON object" };
|
|
101
|
+
const o = v;
|
|
102
|
+
const id = isInt(o.id) ? o.id : undefined;
|
|
103
|
+
const count = Array.isArray(o.paths) && o.paths.length >= 1 && o.paths.length <= MAX_PATHS_PER_QUESTION ? o.paths.length : undefined;
|
|
104
|
+
const broken = (why) => ({ t: "broken", why, ...(id !== undefined ? { id } : {}), ...(id !== undefined && count !== undefined ? { count } : {}) });
|
|
105
|
+
switch (o.t) {
|
|
106
|
+
case "ready":
|
|
107
|
+
return exactKeys(o, ["t", "v", "pid"]) && o.v === 1 && isInt(o.pid) ? { t: "ready", pid: o.pid } : broken("ready: fields");
|
|
108
|
+
case "refused":
|
|
109
|
+
return exactKeys(o, ["t", "why", "detail"]) && typeof o.why === "string" && typeof o.detail === "string" ? { t: "refused", why: o.why, detail: o.detail } : broken("refused: fields");
|
|
110
|
+
case "gone":
|
|
111
|
+
return exactKeys(o, ["t", "start"]) && typeof o.start === "string" && START_ID.test(o.start) ? { t: "gone", start: o.start } : broken("gone: fields");
|
|
112
|
+
case "q":
|
|
113
|
+
if (!exactKeys(o, ["t", "id", "start", "paths"]) || id === undefined || typeof o.start !== "string" || !START_ID.test(o.start) || count === undefined)
|
|
114
|
+
return broken("q: fields");
|
|
115
|
+
if (!o.paths.every((p) => typeof p === "string" && p.startsWith("/")))
|
|
116
|
+
return broken("q: a path that is not an absolute string");
|
|
117
|
+
return { t: "q", id, start: o.start, paths: o.paths };
|
|
118
|
+
default:
|
|
119
|
+
return broken("unknown frame type");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export const UNJUDGED_VERDICT = "read floor: no verdict for this start";
|
|
123
|
+
function judgeOne(floor, path) {
|
|
124
|
+
let v;
|
|
125
|
+
try {
|
|
126
|
+
v = floor.denies(path);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
v = undefined;
|
|
130
|
+
}
|
|
131
|
+
if (v === null)
|
|
132
|
+
return null;
|
|
133
|
+
if (typeof v === "string" && v !== "")
|
|
134
|
+
return v;
|
|
135
|
+
return floor.rows[0] ?? UNJUDGED_VERDICT;
|
|
136
|
+
}
|
|
137
|
+
export function createFloorJudge() {
|
|
138
|
+
const starts = new Map();
|
|
139
|
+
const unjudged = (id, n) => ({ t: "a", id, verdicts: Array.from({ length: n }, () => UNJUDGED_VERDICT) });
|
|
140
|
+
return {
|
|
141
|
+
register(floor, held) {
|
|
142
|
+
const id = randomBytes(12).toString("hex");
|
|
143
|
+
starts.set(id, { floor, open: held, connected: true });
|
|
144
|
+
return id;
|
|
145
|
+
},
|
|
146
|
+
launch(startId) {
|
|
147
|
+
const e = starts.get(startId);
|
|
148
|
+
if (e === undefined || !e.open)
|
|
149
|
+
return false;
|
|
150
|
+
e.connected = true;
|
|
151
|
+
return true;
|
|
152
|
+
},
|
|
153
|
+
release(startId) {
|
|
154
|
+
const e = starts.get(startId);
|
|
155
|
+
if (e === undefined)
|
|
156
|
+
return;
|
|
157
|
+
e.open = false;
|
|
158
|
+
if (!e.connected)
|
|
159
|
+
starts.delete(startId);
|
|
160
|
+
},
|
|
161
|
+
answer(frame) {
|
|
162
|
+
switch (frame.t) {
|
|
163
|
+
case "ready":
|
|
164
|
+
case "refused":
|
|
165
|
+
return undefined;
|
|
166
|
+
case "gone": {
|
|
167
|
+
const e = starts.get(frame.start);
|
|
168
|
+
if (e === undefined)
|
|
169
|
+
return undefined;
|
|
170
|
+
e.connected = false;
|
|
171
|
+
if (!e.open)
|
|
172
|
+
starts.delete(frame.start);
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
case "broken":
|
|
176
|
+
return frame.id !== undefined && frame.count !== undefined ? unjudged(frame.id, frame.count) : undefined;
|
|
177
|
+
case "q": {
|
|
178
|
+
const e = starts.get(frame.start);
|
|
179
|
+
if (e === undefined)
|
|
180
|
+
return unjudged(frame.id, frame.paths.length);
|
|
181
|
+
return { t: "a", id: frame.id, verdicts: frame.paths.map((p) => judgeOne(e.floor, p)) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
get size() {
|
|
186
|
+
return starts.size;
|
|
187
|
+
},
|
|
188
|
+
clear() {
|
|
189
|
+
starts.clear();
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function fsFailure(e) {
|
|
194
|
+
const code = e instanceof Error ? Reflect.get(e, "code") : undefined;
|
|
195
|
+
return typeof code === "string" ? code : "unreadable";
|
|
196
|
+
}
|
|
197
|
+
export function createPackagedFloorBinaries(root = new URL("../../native/sandbox-read-floor/", import.meta.url)) {
|
|
198
|
+
let pinned;
|
|
199
|
+
const pins = () => {
|
|
200
|
+
if (pinned !== undefined)
|
|
201
|
+
return pinned;
|
|
202
|
+
let text;
|
|
203
|
+
try {
|
|
204
|
+
text = readFileSync(new URL("DIGESTS", root), "utf8");
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
return (pinned = { why: `this package carries no supervisor digests (${fsFailure(e)})` });
|
|
208
|
+
}
|
|
209
|
+
const m = new Map();
|
|
210
|
+
for (const line of text.split("\n")) {
|
|
211
|
+
const hit = /^([0-9a-f]{64})\s+\*?(x86_64|aarch64)\/sema-floor\s*$/.exec(line.trim());
|
|
212
|
+
if (hit)
|
|
213
|
+
m.set(hit[2], hit[1]);
|
|
214
|
+
}
|
|
215
|
+
return (pinned = { pins: m });
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
build(arch) {
|
|
219
|
+
const p = pins();
|
|
220
|
+
if ("why" in p)
|
|
221
|
+
return { ok: false, why: p.why };
|
|
222
|
+
const want = p.pins.get(arch);
|
|
223
|
+
if (want === undefined)
|
|
224
|
+
return { ok: false, why: `no supervisor build is pinned for ${arch}` };
|
|
225
|
+
let b;
|
|
226
|
+
try {
|
|
227
|
+
b = readFileSync(new URL(`${arch}/sema-floor`, root));
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return { ok: false, why: `the pinned ${arch} build is unreadable (${fsFailure(e)})` };
|
|
231
|
+
}
|
|
232
|
+
return createHash("sha256").update(b).digest("hex") === want ? { ok: true, digest: want, bytes: new Uint8Array(b) } : { ok: false, why: `the ${arch} build does not match its pinned digest` };
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function createBareFloor(lane, state) {
|
|
237
|
+
return {
|
|
238
|
+
lane,
|
|
239
|
+
state,
|
|
240
|
+
binaries: undefined,
|
|
241
|
+
wrap: () => ({ ok: true, value: BARE }),
|
|
242
|
+
release: () => { },
|
|
243
|
+
attach: async () => { },
|
|
244
|
+
refusePlacement: () => { },
|
|
245
|
+
dispose: () => { },
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const okLaunch = (value) => ({ ok: true, value });
|
|
249
|
+
const spawnError = (message) => ({ ok: false, error: new ExecutionError("spawn_error", message) });
|
|
250
|
+
export function createKeptSandboxReadFloor(opts) {
|
|
251
|
+
const lane = opts.lane;
|
|
252
|
+
const plan = laneLaunchPlan(lane);
|
|
253
|
+
const judge = createFloorJudge();
|
|
254
|
+
const readyMs = opts.relayReadyMs ?? 15_000;
|
|
255
|
+
const maxRestarts = opts.relayRestarts ?? 3;
|
|
256
|
+
const backoffMs = opts.relayRestartBackoffMs ?? 1_000;
|
|
257
|
+
let phase = { kind: "idle" };
|
|
258
|
+
let generation = 0;
|
|
259
|
+
let attachBegun = false;
|
|
260
|
+
let verified;
|
|
261
|
+
let bin;
|
|
262
|
+
let as;
|
|
263
|
+
let transport;
|
|
264
|
+
let link;
|
|
265
|
+
let relayUp = false;
|
|
266
|
+
let restarting;
|
|
267
|
+
let placementRefused;
|
|
268
|
+
let disposed = false;
|
|
269
|
+
let broken = 0;
|
|
270
|
+
const say = (level, event, fields) => {
|
|
271
|
+
opts.logger?.[level]?.(event, { lane, ...fields });
|
|
272
|
+
};
|
|
273
|
+
const linkCall = (what, fn) => {
|
|
274
|
+
try {
|
|
275
|
+
fn();
|
|
276
|
+
}
|
|
277
|
+
catch (e) {
|
|
278
|
+
say("warn", "sandbox_read_floor_link_error", { what, error: echoCallerText(String(e), { maxChars: 200 }) });
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const closeLink = () => {
|
|
282
|
+
const l = link;
|
|
283
|
+
link = undefined;
|
|
284
|
+
relayUp = false;
|
|
285
|
+
if (l !== undefined)
|
|
286
|
+
linkCall("close", () => l.close());
|
|
287
|
+
};
|
|
288
|
+
const openRelay = (gen, fresh) => new Promise((resolve) => {
|
|
289
|
+
if (transport === undefined || bin === undefined)
|
|
290
|
+
return resolve({ ok: false, reason: "relay_silent", detail: "the floor is not placed" });
|
|
291
|
+
let buffered = "";
|
|
292
|
+
let first = true;
|
|
293
|
+
let settled = false;
|
|
294
|
+
let dead = false;
|
|
295
|
+
let opened;
|
|
296
|
+
const pendingOut = [];
|
|
297
|
+
const settle = (v) => {
|
|
298
|
+
if (settled)
|
|
299
|
+
return;
|
|
300
|
+
settled = true;
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
resolve(v);
|
|
303
|
+
};
|
|
304
|
+
const send = (line) => {
|
|
305
|
+
const o = opened;
|
|
306
|
+
if (o === undefined)
|
|
307
|
+
pendingOut.push(line);
|
|
308
|
+
else
|
|
309
|
+
linkCall("send", () => o.send(line));
|
|
310
|
+
};
|
|
311
|
+
const lost = (why) => {
|
|
312
|
+
if (dead)
|
|
313
|
+
return;
|
|
314
|
+
dead = true;
|
|
315
|
+
settle({ ok: false, reason: "relay_silent", detail: why });
|
|
316
|
+
const o = opened;
|
|
317
|
+
if (o !== undefined)
|
|
318
|
+
linkCall("close", () => o.close());
|
|
319
|
+
if (gen !== generation || o === undefined || link !== o)
|
|
320
|
+
return;
|
|
321
|
+
link = undefined;
|
|
322
|
+
const wasUp = relayUp;
|
|
323
|
+
relayUp = false;
|
|
324
|
+
if (!wasUp || disposed)
|
|
325
|
+
return;
|
|
326
|
+
say("warn", "sandbox_read_floor_relay_lost", { why });
|
|
327
|
+
void restartRelay(gen);
|
|
328
|
+
};
|
|
329
|
+
const timer = setTimeout(() => lost(`the relay sent no first frame within ${readyMs}ms`), readyMs);
|
|
330
|
+
timer.unref?.();
|
|
331
|
+
const events = {
|
|
332
|
+
data(chunk) {
|
|
333
|
+
if (dead || gen !== generation)
|
|
334
|
+
return;
|
|
335
|
+
buffered += chunk;
|
|
336
|
+
let nl;
|
|
337
|
+
while ((nl = buffered.indexOf("\n")) >= 0) {
|
|
338
|
+
const raw = buffered.slice(0, nl);
|
|
339
|
+
buffered = buffered.slice(nl + 1);
|
|
340
|
+
if (Buffer.byteLength(raw, "utf8") > MAX_RELAY_LINE_BYTES)
|
|
341
|
+
return lost(`the relay sent a frame over ${MAX_RELAY_LINE_BYTES} bytes`);
|
|
342
|
+
const line = raw.trim();
|
|
343
|
+
if (line === "")
|
|
344
|
+
continue;
|
|
345
|
+
const frame = parseRelayFrame(line);
|
|
346
|
+
if (first) {
|
|
347
|
+
first = false;
|
|
348
|
+
if (frame.t === "ready") {
|
|
349
|
+
relayUp = true;
|
|
350
|
+
settle({ ok: true });
|
|
351
|
+
}
|
|
352
|
+
else if (frame.t === "refused") {
|
|
353
|
+
settle({ ok: false, reason: REASON_SET.has(frame.why) ? frame.why : "binary_digest", detail: `the relay refused to serve: ${echoCallerText(frame.detail, { maxChars: 200 })}` });
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
settle({ ok: false, reason: "binary_digest", detail: "the relay's first frame is neither ready nor refused" });
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (frame.t === "broken") {
|
|
361
|
+
broken++;
|
|
362
|
+
say("warn", "sandbox_read_floor_frame_broken", { broken, refused: frame.count !== undefined, why: frame.why });
|
|
363
|
+
}
|
|
364
|
+
const out = judge.answer(frame);
|
|
365
|
+
if (out !== undefined)
|
|
366
|
+
send(`${JSON.stringify(out)}\n`);
|
|
367
|
+
}
|
|
368
|
+
if (Buffer.byteLength(buffered, "utf8") > MAX_RELAY_LINE_BYTES)
|
|
369
|
+
lost(`the relay sent more than ${MAX_RELAY_LINE_BYTES} bytes without a line end`);
|
|
370
|
+
},
|
|
371
|
+
closed(why) {
|
|
372
|
+
lost(`the relay closed (${echoCallerText(why, { maxChars: 200 })})`);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
const o = transport.openRelay(plan.control, `exec ${shellLine(FLOOR_ARGV.serve(bin, plan.stateDir, fresh))}`, events);
|
|
376
|
+
opened = o;
|
|
377
|
+
if (dead)
|
|
378
|
+
return linkCall("close", () => o.close());
|
|
379
|
+
link = o;
|
|
380
|
+
for (const line of pendingOut.splice(0))
|
|
381
|
+
send(line);
|
|
382
|
+
});
|
|
383
|
+
const restartRelay = (gen) => (restarting ??= (async () => {
|
|
384
|
+
let last = { ok: false, reason: "relay_silent", detail: "no restart was attempted" };
|
|
385
|
+
try {
|
|
386
|
+
for (let i = 0; i < maxRestarts; i++) {
|
|
387
|
+
if (i > 0)
|
|
388
|
+
await new Promise((r) => setTimeout(r, backoffMs * 2 ** (i - 1)));
|
|
389
|
+
if (disposed || gen !== generation)
|
|
390
|
+
return;
|
|
391
|
+
last = await openRelay(gen, false);
|
|
392
|
+
if (last.ok) {
|
|
393
|
+
say("info", "sandbox_read_floor_relay_restarted", { attempt: i + 1 });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (!last.ok)
|
|
398
|
+
fail(gen, last.reason, `${last.detail} (after ${maxRestarts} restart(s))`);
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
restarting = undefined;
|
|
402
|
+
}
|
|
403
|
+
})());
|
|
404
|
+
const fail = (gen, reason, detail) => {
|
|
405
|
+
if (gen !== generation || disposed)
|
|
406
|
+
return;
|
|
407
|
+
phase = { kind: "unavailable", reason, ...(detail !== undefined ? { detail } : {}) };
|
|
408
|
+
say("warn", "sandbox_read_floor_unavailable", { reason, ...(detail !== undefined ? { detail: detail.slice(0, 300) } : {}) });
|
|
409
|
+
};
|
|
410
|
+
const runLaunch = (startId) => ({ mode: "run", head: FLOOR_ARGV.run(bin, startId, as), identity: plan.start, startId });
|
|
411
|
+
return {
|
|
412
|
+
lane,
|
|
413
|
+
state: "kept",
|
|
414
|
+
binaries: opts.binaries,
|
|
415
|
+
wrap(start) {
|
|
416
|
+
switch (start.kind) {
|
|
417
|
+
case "bootstrap":
|
|
418
|
+
return phase.kind === "attaching" ? okLaunch({ mode: "control", identity: plan.control }) : spawnError(`read floor: a bootstrap start outside the floor's attach on this ${lane} sandbox is refused (an adapter defect)`);
|
|
419
|
+
case "adapter":
|
|
420
|
+
if (verified !== undefined)
|
|
421
|
+
return okLaunch({ mode: "plain", head: FLOOR_ARGV.plain(verified.bin, verified.as), identity: plan.start });
|
|
422
|
+
if (!attachBegun)
|
|
423
|
+
return okLaunch(BARE);
|
|
424
|
+
return spawnError(phase.kind === "unavailable"
|
|
425
|
+
? buildReadFloorUnavailableMessage(phase.reason, phase.detail, undefined)
|
|
426
|
+
: `read floor: an adapter command on this ${lane} sandbox while the floor's attach has not verified a supervisor — refused rather than run it outside the floor`);
|
|
427
|
+
case "under": {
|
|
428
|
+
if (phase.kind === "unavailable")
|
|
429
|
+
return spawnError(buildReadFloorUnavailableMessage(phase.reason, phase.detail, undefined));
|
|
430
|
+
if (bin === undefined || start.startId === undefined || !judge.launch(start.startId))
|
|
431
|
+
return spawnError(`read floor: the start this command reads on behalf of is no longer registered on this ${lane} sandbox — refused rather than read its output outside its floor`);
|
|
432
|
+
return okLaunch(runLaunch(start.startId));
|
|
433
|
+
}
|
|
434
|
+
case "engine": {
|
|
435
|
+
const floor = start.readFloor;
|
|
436
|
+
if (floor === undefined)
|
|
437
|
+
return spawnError(buildReadFloorMissingMessage(lane));
|
|
438
|
+
if (phase.kind === "unavailable")
|
|
439
|
+
return spawnError(buildReadFloorUnavailableMessage(phase.reason, phase.detail, floor));
|
|
440
|
+
if (phase.kind !== "ready" || bin === undefined)
|
|
441
|
+
return spawnError(buildReadFloorUnavailableMessage("relay_silent", "the floor is not attached to this sandbox yet", floor));
|
|
442
|
+
if (!relayUp && transport !== undefined)
|
|
443
|
+
void restartRelay(generation);
|
|
444
|
+
const id = judge.register(floor, start.hold === true);
|
|
445
|
+
return okLaunch(runLaunch(id));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
release(startId) {
|
|
450
|
+
if (startId !== undefined)
|
|
451
|
+
judge.release(startId);
|
|
452
|
+
},
|
|
453
|
+
async attach(t, how) {
|
|
454
|
+
if (disposed)
|
|
455
|
+
return;
|
|
456
|
+
attachBegun = true;
|
|
457
|
+
const gen = ++generation;
|
|
458
|
+
closeLink();
|
|
459
|
+
judge.clear();
|
|
460
|
+
transport = t;
|
|
461
|
+
bin = undefined;
|
|
462
|
+
as = undefined;
|
|
463
|
+
phase = { kind: "attaching" };
|
|
464
|
+
if (placementRefused !== undefined)
|
|
465
|
+
return fail(gen, "binary_digest", placementRefused);
|
|
466
|
+
const u = await t.bootstrap("uname -m");
|
|
467
|
+
if (gen !== generation)
|
|
468
|
+
return;
|
|
469
|
+
if (!u.ok || u.exitCode !== 0)
|
|
470
|
+
return fail(gen, "relay_silent", `the sandbox did not answer the architecture probe (${u.ok ? `exit ${u.exitCode}` : echoCallerText(u.error, { maxChars: 200 })})`);
|
|
471
|
+
const arch = ARCH_OF_UNAME[u.stdout.trim()];
|
|
472
|
+
if (arch === undefined)
|
|
473
|
+
return fail(gen, "arch", `uname -m answered ${JSON.stringify(echoCallerText(u.stdout.trim(), { maxChars: 40 }))}`);
|
|
474
|
+
if (plan.dropToWorkspaceOwner) {
|
|
475
|
+
const o = await t.bootstrap(`stat -c %u -- ${shellQuote(how.workspaceRoot)}`);
|
|
476
|
+
if (gen !== generation)
|
|
477
|
+
return;
|
|
478
|
+
const uid = o.ok && o.exitCode === 0 ? o.stdout.trim() : "";
|
|
479
|
+
if (!/^\d{1,10}$/.test(uid))
|
|
480
|
+
return fail(gen, "relay_silent", `the sandbox did not answer the workspace owner probe for ${how.workspaceRoot}`);
|
|
481
|
+
if (uid === "0")
|
|
482
|
+
return fail(gen, "uid_zero", `${how.workspaceRoot} is owned by root`);
|
|
483
|
+
as = uid;
|
|
484
|
+
}
|
|
485
|
+
const build = opts.binaries.build(arch);
|
|
486
|
+
if (!build.ok)
|
|
487
|
+
return fail(gen, "binary_digest", `this server carries no usable supervisor build for ${arch} (${build.why})`);
|
|
488
|
+
const path = floorBinaryPath(arch);
|
|
489
|
+
const placed = await t.place(path, build.bytes);
|
|
490
|
+
if (gen !== generation)
|
|
491
|
+
return;
|
|
492
|
+
if (!placed.ok)
|
|
493
|
+
return fail(gen, "binary_digest", `placing the supervisor failed: ${echoCallerText(placed.error, { maxChars: 200 })}`);
|
|
494
|
+
bin = path;
|
|
495
|
+
const relay = await openRelay(gen, how.origin === "created");
|
|
496
|
+
if (gen !== generation)
|
|
497
|
+
return;
|
|
498
|
+
if (!relay.ok) {
|
|
499
|
+
bin = undefined;
|
|
500
|
+
return fail(gen, relay.reason, relay.detail);
|
|
501
|
+
}
|
|
502
|
+
const sc = await t.bootstrap(shellLine(FLOOR_ARGV.selfcheck(path, build.digest, as)));
|
|
503
|
+
if (gen !== generation)
|
|
504
|
+
return;
|
|
505
|
+
if (!sc.ok) {
|
|
506
|
+
bin = undefined;
|
|
507
|
+
return fail(gen, "relay_silent", `the selfcheck did not run (${echoCallerText(sc.error, { maxChars: 200 })})`);
|
|
508
|
+
}
|
|
509
|
+
const verdict = parseSelfcheck(sc.stdout, sc.exitCode);
|
|
510
|
+
if (!verdict.ok || verdict.arch !== arch) {
|
|
511
|
+
bin = undefined;
|
|
512
|
+
closeLink();
|
|
513
|
+
return verdict.ok ? fail(gen, "arch", `the supervisor reports ${verdict.arch}, the sandbox answered ${arch}`) : fail(gen, verdict.reason, verdict.detail);
|
|
514
|
+
}
|
|
515
|
+
phase = { kind: "ready" };
|
|
516
|
+
verified = { bin: path, as };
|
|
517
|
+
say("info", "sandbox_read_floor_attached", { arch, origin: how.origin, version: verdict.version });
|
|
518
|
+
},
|
|
519
|
+
refusePlacement(detail) {
|
|
520
|
+
placementRefused = detail;
|
|
521
|
+
},
|
|
522
|
+
dispose() {
|
|
523
|
+
disposed = true;
|
|
524
|
+
generation++;
|
|
525
|
+
closeLink();
|
|
526
|
+
judge.clear();
|
|
527
|
+
phase = { kind: "idle" };
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
export function createSandboxReadFloor(opts) {
|
|
532
|
+
const state = sandboxReadFloorState(opts.lane, opts.knob);
|
|
533
|
+
switch (state) {
|
|
534
|
+
case "kept":
|
|
535
|
+
return createKeptSandboxReadFloor({ lane: opts.lane, binaries: createPackagedFloorBinaries(), ...(opts.logger !== undefined ? { logger: opts.logger } : {}) });
|
|
536
|
+
case "off":
|
|
537
|
+
case "unserved":
|
|
538
|
+
return createBareFloor(opts.lane, state);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export function floorArgv(launch, shellArgv) {
|
|
542
|
+
switch (launch.mode) {
|
|
543
|
+
case "bare":
|
|
544
|
+
case "control":
|
|
545
|
+
return shellArgv;
|
|
546
|
+
case "run":
|
|
547
|
+
case "plain":
|
|
548
|
+
return [...launch.head, "--", ...shellArgv];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export function e2bFloorCommand(launch, command, envs) {
|
|
552
|
+
switch (launch.mode) {
|
|
553
|
+
case "bare":
|
|
554
|
+
return { cmd: command, ...(envs !== undefined ? { envs: { ...envs } } : {}) };
|
|
555
|
+
case "control":
|
|
556
|
+
return { cmd: command, ...e2bUser(launch.identity) };
|
|
557
|
+
case "run":
|
|
558
|
+
return { cmd: `exec ${shellLine([...launch.head, ...e2bEnvFlags(envs), "--", "/bin/bash", "-l", "-c", command])}`, ...e2bUser(launch.identity) };
|
|
559
|
+
case "plain":
|
|
560
|
+
return { cmd: `exec ${shellLine([...launch.head, ...e2bEnvFlags(envs), "--", "/bin/bash", "-c", command])}`, ...e2bUser(launch.identity) };
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const e2bUser = (identity) => {
|
|
564
|
+
switch (identity) {
|
|
565
|
+
case "root":
|
|
566
|
+
return { user: "root" };
|
|
567
|
+
case "exec-user":
|
|
568
|
+
return {};
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
const e2bEnvFlags = (envs) => Object.entries(envs ?? {}).flatMap(([k, v]) => ["--env", `${k}=${v}`]);
|
|
572
|
+
export function dockerIdentityFlags(launch) {
|
|
573
|
+
switch (launch.mode) {
|
|
574
|
+
case "bare":
|
|
575
|
+
return [];
|
|
576
|
+
case "control":
|
|
577
|
+
case "run":
|
|
578
|
+
case "plain":
|
|
579
|
+
switch (launch.identity) {
|
|
580
|
+
case "root":
|
|
581
|
+
return ["-u", "0"];
|
|
582
|
+
case "exec-user":
|
|
583
|
+
return [];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const ARCHES = ["x86_64", "aarch64"];
|
|
588
|
+
export const FLOOR_K8S_VOLUME_NAMES = Object.freeze({ x86_64: ADAPTER_VOLUME_NAMES.floorX86_64, aarch64: ADAPTER_VOLUME_NAMES.floorAarch64 });
|
|
589
|
+
export const FLOOR_K8S_CONFIGMAP_KEY = "sema-floor";
|
|
590
|
+
export function buildFloorPodSeat(floor) {
|
|
591
|
+
const binaries = floor.binaries;
|
|
592
|
+
if (floor.state !== "kept" || floor.lane !== "k8s" || binaries === undefined)
|
|
593
|
+
return undefined;
|
|
594
|
+
const configMaps = [];
|
|
595
|
+
const volumes = [];
|
|
596
|
+
const mounts = [];
|
|
597
|
+
for (const arch of ARCHES) {
|
|
598
|
+
const build = binaries.build(arch);
|
|
599
|
+
if (!build.ok)
|
|
600
|
+
continue;
|
|
601
|
+
const name = `sema-floor-${arch === "x86_64" ? "x86-64" : arch}-${build.digest.slice(0, 16)}`;
|
|
602
|
+
configMaps.push({ name, arch, binaryDataB64: Buffer.from(build.bytes).toString("base64") });
|
|
603
|
+
volumes.push({ name: FLOOR_K8S_VOLUME_NAMES[arch], configMap: { name, items: [{ key: FLOOR_K8S_CONFIGMAP_KEY, path: "sema-floor" }], defaultMode: 0o555 } });
|
|
604
|
+
mounts.push({ name: FLOOR_K8S_VOLUME_NAMES[arch], mountPath: `${FLOOR_DIR}/${arch}`, readOnly: true });
|
|
605
|
+
}
|
|
606
|
+
return { configMaps, volumes, mounts };
|
|
607
|
+
}
|
|
608
|
+
//# sourceMappingURL=sandbox-read-floor.js.map
|
|
@@ -25,7 +25,8 @@ export declare function assertFirstOwnerWritable(sessionId: string, owner: unkno
|
|
|
25
25
|
/**
|
|
26
26
|
* 导入者的属主 —— 换装的写腿(公开 `importEntries` / `replaceEntries` 与暂存提交)把调用方给的属主重戳进会话行之前的唯一读法:
|
|
27
27
|
* 主体名或 `null`(匿名)⇒ 原样交回;别的一切(空串、空白串、带控制 / 分隔 / 非连接符格式字符的串、非串)⇒ `TypeError` 响亮拒 ——
|
|
28
|
-
* 与引擎导入口拒同一类值时同一类错、同一句开头(
|
|
28
|
+
* 与引擎导入口拒同一类值时同一类错、同一句开头(引擎那一判只作用于交给它的属主,而 local 店交给引擎仓的属主恒缺席 —— 引擎仓是匿名内容仓,属主只在本店的行上 ——
|
|
29
|
+
* 所以这一判在本仓)。
|
|
29
30
|
* 调用方在读写任何东西之前判,拒了就一个字节没写。判官 = {@link isSessionOwnerValue}(core `principalNameOf`),不在本仓另写规则。
|
|
30
31
|
*/
|
|
31
32
|
export declare function importerOwnerOf(owner: unknown): string | null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref] C18(core 5.48.0 [ref]「不造新店」提货批,黑板 [ref] 点名义务)——**subagent 转录持久化**
|
|
3
|
-
* 的 placement
|
|
3
|
+
* 的 placement 语义,三只会话店(SQL 两孪生 + local)共用的一份声明与判据。
|
|
4
4
|
*
|
|
5
5
|
* ## 这是什么
|
|
6
6
|
*
|
|
@@ -35,16 +35,29 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord, type SessionStore } from "@sema-agent/core";
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
38
|
+
* 三只持久会话店(SQL 两孪生 + local 后端)的**会话耐久声明**(core `SessionStore.durability`:未 placed 的会话活不活过进程重启)——
|
|
39
|
+
* **单源**:三处声明(两孪生类上的顶层 `durability`、`LocalBackend` 交给店的顶层 `durability`)与委派分区的 {@link SESSION_PLACEMENTS}
|
|
40
|
+
* 引用的都是这一只常量,源码里不再有第二份 `"durable"` 字面(`test/session-store-durability-declaration.test.ts` 的 AST 钉)。
|
|
41
|
+
*
|
|
42
|
+
* 为什么必须声明:core 的静态装配对**缺席**折 `"process-local"`(它不能替没表态的店猜),接线清单 `session.store` 与起服自检读的就是它 ——
|
|
43
|
+
* 不声明 ⇒ 一个真持久的 SQL 部署读成「重启即丢」,没有任何一处报错。依据同下面分区声明那一段:两孪生的会话史落在 `session_meta` + `session_event` 两张真表,
|
|
44
|
+
* local 的会话史落在数据根下的会话存档,进程重启后都能把已有会话取回来。包装层(缓存层)逐字转发;fork 路由层不声明(它自己的未 placed 会话住进程内短 TTL 店,
|
|
45
|
+
* 顶层耐久只说未 placed 的会话——见 `fork-routing-session-store.ts` 头注)。
|
|
46
|
+
*/
|
|
47
|
+
export declare const SESSION_DURABILITY: "durable";
|
|
48
|
+
/**
|
|
49
|
+
* 三只持久会话店的 placement **声明**(core `SessionStore.placements`):两支 SQL 孪生(类上的字段)与 local 后端
|
|
50
|
+
* (`LocalBackend` 装配 `LocalSessionStore` 时交进去 —— 值与 core `FileStorageBackend` 对同一种文件 repo 的声明同)。
|
|
39
51
|
*
|
|
40
52
|
* 🔴 声明制,不是鸭子类型:core 的铸造门判「row store ∧ session 店声明」,不声明的店**不收** placed
|
|
41
53
|
* 语义(合法降级:placement 参数被忽略,会话就是普通会话)。所以这一格只有在上面六条义务**全部**在
|
|
42
54
|
* 本店实装之后才允许存在——它是一句关于本店行为的承诺,不是一个功能开关。
|
|
43
55
|
* `durable` 的依据:两支孪生的会话史落在 `session_meta`+`session_event` 两张真表上,进程重启后
|
|
44
56
|
* `acquire(id,{requireExisting:true})` 从事件日志重放(义务 1 的机器证在
|
|
45
|
-
* `test/session-placement-db-integration.test.ts` 的跨进程复活格)
|
|
57
|
+
* `test/session-placement-db-integration.test.ts` 的跨进程复活格);local 的会话史落在数据根下的会话存档
|
|
58
|
+
* (元组在存档行头),属主与化身在同一数据根的会话元数据边档(机器证在 `test/local-session-placement.test.ts` 的重启格)。
|
|
46
59
|
*/
|
|
47
|
-
export declare const
|
|
60
|
+
export declare const SESSION_PLACEMENTS: {
|
|
48
61
|
readonly subagent: {
|
|
49
62
|
readonly durability: "durable";
|
|
50
63
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SessionError } from "@sema-agent/core";
|
|
2
|
-
export const
|
|
2
|
+
export const SESSION_DURABILITY = "durable";
|
|
3
|
+
export const SESSION_PLACEMENTS = { subagent: { durability: SESSION_DURABILITY } };
|
|
3
4
|
export const PLACEMENT_SELECT_COLUMNS = "placement_kind, placement_scope, placement_parent_session_id, placement_root_session_id, placement_handle, placed_at";
|
|
4
5
|
function toEpochMs(v) {
|
|
5
6
|
if (v == null)
|