@sema-agent/server 7.43.0 → 7.44.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/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
|
@@ -1,28 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bake-runner core (IMAGE-API-DESIGN.md §P2.0/§P2.12) — the privileged build-host daemon that actually runs `build.sh`.
|
|
3
|
-
*
|
|
4
|
-
* image-api (the unprivileged controller) holds ZERO build credentials; this runner is the ONE privileged,
|
|
5
|
-
* non-customer-facing tier (node-pinned build-host, replicas=1, docker.sock + ctr + /root/.docker mounted). It:
|
|
6
|
-
* 1. long-polls image-api for a queued bake and CLAIMS it (single-flight via the DB lease — image-api's job);
|
|
7
|
-
* 2. checks out the recipe at the deployed RECIPE_REF (so build.sh's git rev-parse is REAL — §P2.12);
|
|
8
|
-
* 3. spawns `build.sh <VETTED argv> --json` as a CHILD IN ITS OWN PROCESS GROUP (setsid/detached) so a cancel
|
|
9
|
-
* can SIGTERM/SIGKILL the whole group — channel-close does NOT kill a build (§P2.11);
|
|
10
|
-
* 4. forwards each STDOUT line to `…/ingest` with an incrementing lineOrd (at-least-once dedupe — §P2.10);
|
|
11
|
-
* 5. heartbeats the lease (+ polls cancel_requested off the ingest/heartbeat response) every ~30s;
|
|
12
|
-
* 6. on cancel/deadline kills the process GROUP and synthesizes the terminal `done` (build.sh emits none — §P2.11);
|
|
13
|
-
* 7. on the `done` line reads the manifest BODY off disk and forwards it so image-api can auto-register (§P2.10).
|
|
14
|
-
*
|
|
15
|
-
* This file is the I/O-injected core: the HTTP client, the child spawner, the fs reader, the clock and the logger
|
|
16
|
-
* are all dependencies, so the orchestration is structurally testable here while `main.ts` wires the real ones (the
|
|
17
|
-
* runner can only be exercised live on build-host). The line→frame mapping itself lives PURE in ./protocol.ts.
|
|
18
|
-
*/
|
|
19
1
|
import { resolve as resolvePath } from "node:path";
|
|
20
2
|
import { lineToFrame, dockerBuildTickFrame, synthDoneFrame, exitDoneFrame, deadlineMsForProfile, parseDfFreeGb, parseBuildShLine, manifestPathFromLine, } from "./protocol.js";
|
|
21
3
|
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
22
4
|
const DEFAULT_IDLE_POLL_MS = 5_000;
|
|
23
5
|
const DEFAULT_MIN_FREE_GB = 20;
|
|
24
|
-
/** A high, fixed lineOrd for the loop-level catch terminal (runOne owns the per-bake counter, so its current value
|
|
25
|
-
* is out of scope here) — far above any realistic in-bake line count so it never collides on uq_line. */
|
|
26
6
|
const TERMINAL_FALLBACK_ORD = 2_000_000_000;
|
|
27
7
|
export class BakeRunner {
|
|
28
8
|
o;
|
|
@@ -36,12 +16,9 @@ export class BakeRunner {
|
|
|
36
16
|
this.idlePollMs = opts.idlePollMs ?? DEFAULT_IDLE_POLL_MS;
|
|
37
17
|
this.minFreeGb = opts.minFreeGb ?? DEFAULT_MIN_FREE_GB;
|
|
38
18
|
}
|
|
39
|
-
/** Cooperative shutdown — finishes the in-flight bake's current frame, then exits the claim loop. */
|
|
40
19
|
stop() {
|
|
41
20
|
this.stopped = true;
|
|
42
21
|
}
|
|
43
|
-
/** The long-poll loop: claim → run → release → poll next. Single-flight is enforced DB-side (the claim CAS), so a
|
|
44
|
-
* second runner replica simply never wins a claim. Runs until `stop()`. */
|
|
45
22
|
async loop() {
|
|
46
23
|
while (!this.stopped) {
|
|
47
24
|
let claimed = null;
|
|
@@ -59,28 +36,18 @@ export class BakeRunner {
|
|
|
59
36
|
await this.runOne(claimed);
|
|
60
37
|
}
|
|
61
38
|
catch (err) {
|
|
62
|
-
// A bake that threw out of runOne (an unexpected host/API failure) — best-effort terminal so readers settle.
|
|
63
|
-
// The lineOrd here is unknown (runOne owns the counter), so use a NULL-equivalent high ordinal that won't
|
|
64
|
-
// collide with an in-flight frame's uq_line (image-api still ++seq's it; the reaper backstops a true miss).
|
|
65
39
|
this.o.log.error("bake_run_error", { bakeId: claimed.bakeId, err: String(err) });
|
|
66
40
|
await this.safeIngest(claimed, exitDoneFrame(TERMINAL_FALLBACK_ORD, 1, String(err)));
|
|
67
41
|
}
|
|
68
42
|
}
|
|
69
43
|
}
|
|
70
|
-
/** Run a single CLAIMED bake end-to-end. Owns the lineOrd counter, the heartbeat/cancel/deadline timers, the
|
|
71
|
-
* child process group, the stdout→ingest pump, and the terminal done synthesis. */
|
|
72
44
|
async runOne(bake) {
|
|
73
|
-
const ord = { n: 0 };
|
|
45
|
+
const ord = { n: 0 };
|
|
74
46
|
this.o.log.info("bake_claimed", { bakeId: bake.bakeId, push: bake.push, dryRun: bake.dryRun, logs: bake.logs });
|
|
75
|
-
// M2: serialize the stdout→ingest pump. A burst chunk (bufferLines calls onStdoutLine per newline synchronously)
|
|
76
|
-
// + the progress tick were fired as un-awaited `void safeIngest`, so N POSTs raced concurrently — image-api's
|
|
77
|
-
// non-atomic seq alloc then lost a frame on a (bake_id,seq) PK collision. Chain each non-terminal ingest so they
|
|
78
|
-
// POST one-at-a-time in lineOrd order, and AWAIT the chain before runOne returns (alongside the done forward).
|
|
79
47
|
let pump = Promise.resolve();
|
|
80
48
|
const enqueueIngest = (frame) => {
|
|
81
49
|
pump = pump.then(() => this.safeIngest(bake, frame));
|
|
82
50
|
};
|
|
83
|
-
// ── Pre-flight disk guard (§P2.7): below the floor, fail FAST rather than dying mid-build with a half-layer.
|
|
84
51
|
if (!bake.dryRun) {
|
|
85
52
|
const freeGb = await this.freeGbOrNull();
|
|
86
53
|
if (freeGb !== null && freeGb < this.minFreeGb) {
|
|
@@ -96,7 +63,6 @@ export class BakeRunner {
|
|
|
96
63
|
return;
|
|
97
64
|
}
|
|
98
65
|
}
|
|
99
|
-
// ── Recipe checkout at the pinned ref so build.sh's git rev-parse gitsha is REAL (§P2.12).
|
|
100
66
|
let cwd;
|
|
101
67
|
try {
|
|
102
68
|
cwd = await this.o.host.checkoutRecipe(this.o.recipeRef);
|
|
@@ -105,24 +71,16 @@ export class BakeRunner {
|
|
|
105
71
|
await this.safeIngest(bake, exitDoneFrame(nextOrd(ord), 1, `recipe checkout failed: ${String(err)}`));
|
|
106
72
|
return;
|
|
107
73
|
}
|
|
108
|
-
// ── Spawn build.sh in its OWN PROCESS GROUP (so a cancel/deadline kill signals the whole detached tree).
|
|
109
74
|
const stderrTail = [];
|
|
110
75
|
let lastManifestPath = null;
|
|
111
|
-
let lastImageTag = null;
|
|
76
|
+
let lastImageTag = null;
|
|
112
77
|
let sawDone = false;
|
|
113
|
-
// build.sh's `done` triggers an ASYNC tail (read the manifest body / the push retry, then ingest) — the child
|
|
114
|
-
// can exit the instant after printing it. It is chained ONTO `pump` (below), so `await pump` before runOne
|
|
115
|
-
// returns also awaits this pending forward (the lease isn't released before the auto-register input lands).
|
|
116
|
-
// 🔴 Resolve buildShPath against the recipe CHECKOUT cwd, NOT via PATH: node/bun's spawn resolves a BARE or
|
|
117
|
-
// RELATIVE argv[0] through $PATH (a bare `build.sh` silently ENOENT→exit-127 footgun — cost an hour live), so
|
|
118
|
-
// path.resolve(cwd, buildShPath) pins it to the checkout. An absolute buildShPath is returned unchanged.
|
|
119
78
|
const buildShAbs = resolvePath(cwd, this.o.buildShPath);
|
|
120
79
|
const buildArgv = [buildShAbs, ...bake.argv, "--json"];
|
|
121
80
|
const child = this.o.spawner.spawn({
|
|
122
81
|
argv: buildArgv,
|
|
123
82
|
cwd,
|
|
124
83
|
onStdoutLine: (raw) => {
|
|
125
|
-
// Forward EVERY stdout line through the pure mapper; remember the manifest path for the auto-register read.
|
|
126
84
|
const ev = parseBuildShLine(raw);
|
|
127
85
|
if (ev && ev.event === "manifest" && typeof ev.path === "string") {
|
|
128
86
|
lastManifestPath = ev.path;
|
|
@@ -132,47 +90,39 @@ export class BakeRunner {
|
|
|
132
90
|
}
|
|
133
91
|
if (ev && ev.event === "done") {
|
|
134
92
|
sawDone = true;
|
|
135
|
-
// The done line needs the manifest BODY attached before ingest (auto-register reads it off disk — §P2.10).
|
|
136
|
-
// M2: chain it ON the serialized pump so the terminal `done` is forwarded STRICTLY AFTER every queued
|
|
137
|
-
// non-terminal frame (the SSE log's last event), in lineOrd order — never racing ahead of them.
|
|
138
93
|
pump = pump.then(() => this.forwardDone(bake, ord, ev, lastManifestPath, lastImageTag));
|
|
139
94
|
return;
|
|
140
95
|
}
|
|
141
96
|
enqueueIngest(lineToFrame(raw, nextOrd(ord)));
|
|
142
97
|
},
|
|
143
|
-
// stderr is the failure tail (`tail -40 build.log >&2`), NOT JSON — buffer it, surface on a non-zero exit.
|
|
144
98
|
onStderrLine: (raw) => {
|
|
145
99
|
if (stderrTail.length < 80)
|
|
146
100
|
stderrTail.push(raw);
|
|
147
101
|
},
|
|
148
102
|
});
|
|
149
|
-
// ── The heartbeat / cancel-poll / docker-build-progress / deadline supervisor, racing the child exit.
|
|
150
103
|
const deadlineMs = deadlineMsForProfile(bake.profile ?? "custom", bake.bands ?? null);
|
|
151
104
|
const startedAt = this.o.clock.now();
|
|
152
105
|
let killedFor = null;
|
|
153
|
-
let childExited = false;
|
|
106
|
+
let childExited = false;
|
|
154
107
|
const supervisor = (async () => {
|
|
155
|
-
// eslint-disable-next-line no-constant-condition
|
|
156
108
|
while (true) {
|
|
157
109
|
await this.o.clock.sleep(this.heartbeatMs);
|
|
158
110
|
if (childExited || killedFor)
|
|
159
|
-
return;
|
|
111
|
+
return;
|
|
160
112
|
const elapsed = this.o.clock.now() - startedAt;
|
|
161
|
-
// Deadline first (a hung build past its profile budget — kill the group, synthesize FAILED 124).
|
|
162
113
|
if (elapsed >= deadlineMs) {
|
|
163
114
|
killedFor = "timeout";
|
|
164
115
|
this.o.log.warn("bake_deadline_kill", { bakeId: bake.bakeId, elapsedMs: elapsed, deadlineMs });
|
|
165
116
|
this.killGroup(child);
|
|
166
117
|
return;
|
|
167
118
|
}
|
|
168
|
-
// Heartbeat the lease + read back the cancel flag (the durable cross-replica cancel — §P2.11).
|
|
169
119
|
let ack;
|
|
170
120
|
try {
|
|
171
121
|
ack = await this.o.api.heartbeat(bake.bakeId, bake.ingestSecret);
|
|
172
122
|
}
|
|
173
123
|
catch (err) {
|
|
174
124
|
this.o.log.warn("bake_heartbeat_error", { bakeId: bake.bakeId, err: String(err) });
|
|
175
|
-
continue;
|
|
125
|
+
continue;
|
|
176
126
|
}
|
|
177
127
|
if (ack.cancelRequested) {
|
|
178
128
|
killedFor = "cancelled";
|
|
@@ -181,61 +131,37 @@ export class BakeRunner {
|
|
|
181
131
|
return;
|
|
182
132
|
}
|
|
183
133
|
if (ack.leaseValid === false) {
|
|
184
|
-
// The lease was stolen (image-api reaped us as stale) — abort; we no longer own this build.
|
|
185
134
|
killedFor = "timeout";
|
|
186
135
|
this.o.log.warn("bake_lease_lost", { bakeId: bake.bakeId });
|
|
187
136
|
this.killGroup(child);
|
|
188
137
|
return;
|
|
189
138
|
}
|
|
190
|
-
// No cancel, no deadline: emit the docker-build progress tick so a resuming reader sees motion (logs off).
|
|
191
139
|
enqueueIngest(dockerBuildTickFrame(nextOrd(ord), Math.floor(elapsed / 1000)));
|
|
192
140
|
}
|
|
193
141
|
})();
|
|
194
|
-
// #131-6:创建即挂接——rejection 若等到 stopSupervisor(child 退出后)才有人接,会在整个构建
|
|
195
|
-
// 时长内悬置,Node 默认 unhandled-rejections=throw 直接终结 runner 进程。挂接后 supervisor 腿
|
|
196
|
-
// 静默死掉的后果有界:lease 心跳停 → image-api reaper 兜底(与 heartbeat error continue 同口径)。
|
|
197
142
|
void supervisor.catch((err) => {
|
|
198
143
|
try {
|
|
199
144
|
this.o.log.warn("bake_supervisor_error", { bakeId: bake.bakeId, err: String(err) });
|
|
200
145
|
}
|
|
201
146
|
catch {
|
|
202
|
-
/* 日志面故障不再外抛 */
|
|
203
147
|
}
|
|
204
148
|
});
|
|
205
149
|
const exit = await child.exited;
|
|
206
150
|
childExited = true;
|
|
207
151
|
this.stopSupervisor(supervisor);
|
|
208
|
-
// M2: drain the serialized pump BEFORE synthesizing a terminal. The pump carries every non-terminal frame AND
|
|
209
|
-
// (when build.sh printed its own done) the chained `forwardDone` — so by here the real `done` has already been
|
|
210
|
-
// forwarded LAST in lineOrd order. Draining first guarantees the lease isn't released before the auto-register
|
|
211
|
-
// input (manifest body) is in image-api's hands.
|
|
212
152
|
await pump.catch(() => undefined);
|
|
213
|
-
// ── Terminal done synthesis. build.sh has NO SIGTERM trap, so a killed child emits no done — we synthesize it.
|
|
214
153
|
if (killedFor) {
|
|
215
154
|
await this.safeIngest(bake, synthDoneFrame(nextOrd(ord), killedFor));
|
|
216
155
|
return;
|
|
217
156
|
}
|
|
218
157
|
if (sawDone) {
|
|
219
|
-
// build.sh printed its own done — already forwarded (manifest-body read / push retry) via the drained pump.
|
|
220
158
|
return;
|
|
221
159
|
}
|
|
222
|
-
// The child exited WITHOUT a done line (a crash/SIGKILL before build.sh's die() trap) — backstop a FAILED done
|
|
223
|
-
// carrying the exit code + the stderr failure tail (image-api redacts it on store).
|
|
224
160
|
const code = exit.code ?? (exit.signal ? 137 : 1);
|
|
225
161
|
await this.safeIngest(bake, exitDoneFrame(nextOrd(ord), code, stderrTail.join("\n")));
|
|
226
162
|
}
|
|
227
|
-
/** Forward build.sh's own `done` line — but FIRST read the manifest BODY off disk and attach it so image-api can
|
|
228
|
-
* auto-register (build.sh's done carries only a build-host path, NOT the body — §P2.10). A manifest read failure is
|
|
229
|
-
* non-fatal: the done still forwards (the digest is on the line); image-api then registers indexId:null partial.
|
|
230
|
-
*
|
|
231
|
-
* BIG-BLOB PUSH RETRY (§P2.12): build.sh dies exit 13 on a flaky `docker push`. The retry is a THIN `docker push`
|
|
232
|
-
* wrapper on the host (NOT build.sh --replay — that re-bakes + hits the dup-check exit 10). On a push-fail done
|
|
233
|
-
* for a `--push` bake with a known tag, the runner retries the push; on success it rewrites the done to COMPLETE
|
|
234
|
-
* with the recovered post-push digest (no manifest on disk — build.sh dies before the manifest write — so image-
|
|
235
|
-
* api registers the built-and-pushed-but-unregistered partial, which the P3 reconciliation tick finalizes). */
|
|
236
163
|
async forwardDone(bake, ord, doneLine, lastManifestPath, lastImageTag) {
|
|
237
164
|
let line = doneLine;
|
|
238
|
-
// Push-fail (exit 13) recovery: retry the big-blob push out-of-band before declaring the bake failed.
|
|
239
165
|
if (line.status === "FAILED" &&
|
|
240
166
|
line.exitCode === 13 &&
|
|
241
167
|
bake.push &&
|
|
@@ -245,7 +171,6 @@ export class BakeRunner {
|
|
|
245
171
|
try {
|
|
246
172
|
const r = await this.o.host.dockerPushRetry(lastImageTag, 3);
|
|
247
173
|
if (r.ok) {
|
|
248
|
-
// Recovered: rewrite the terminal to COMPLETE with the post-push RepoDigest (design §6 runtime ref).
|
|
249
174
|
line = { ...line, status: "COMPLETE", exitCode: 0, error: null, digest: r.digest ?? line.digest, pushed: true };
|
|
250
175
|
this.o.log.info("bake_push_recovered", { bakeId: bake.bakeId, tag: lastImageTag });
|
|
251
176
|
}
|
|
@@ -263,36 +188,28 @@ export class BakeRunner {
|
|
|
263
188
|
}
|
|
264
189
|
catch (err) {
|
|
265
190
|
this.o.log.warn("bake_manifest_read_failed", { bakeId: bake.bakeId, path, err: String(err) });
|
|
266
|
-
// leave manifestBody unset → image-api registers the built-but-unregistered partial (digest, indexId:null).
|
|
267
191
|
}
|
|
268
192
|
}
|
|
269
193
|
}
|
|
270
194
|
await this.safeIngest(bake, frame);
|
|
271
195
|
}
|
|
272
|
-
/** Kill the process GROUP: SIGTERM, then SIGKILL shortly after (a SIGKILL mid-docker-build can leave dangling
|
|
273
|
-
* layers; the dup-check + lockhash prevent a poisoned re-bake, and a startup workdir sweep reclaims them — §P2.11). */
|
|
274
196
|
killGroup(child) {
|
|
275
197
|
try {
|
|
276
198
|
child.killGroup("SIGTERM");
|
|
277
199
|
}
|
|
278
200
|
catch {
|
|
279
|
-
/* the group may already be gone */
|
|
280
201
|
}
|
|
281
202
|
void this.o.clock.sleep(5_000).then(() => {
|
|
282
203
|
try {
|
|
283
204
|
child.killGroup("SIGKILL");
|
|
284
205
|
}
|
|
285
206
|
catch {
|
|
286
|
-
/* already dead */
|
|
287
207
|
}
|
|
288
208
|
});
|
|
289
209
|
}
|
|
290
210
|
stopSupervisor(p) {
|
|
291
|
-
// The supervisor loop exits on its own once killedFor is set or the child exits; this just swallows its result.
|
|
292
211
|
void p.catch(() => undefined);
|
|
293
212
|
}
|
|
294
|
-
/** Pre-flight free-GiB read; null (fail-open) on a df parse/exec miss — a df hiccup must not block a bake (the
|
|
295
|
-
* reaper + a mid-build fill guard still protect the host). */
|
|
296
213
|
async freeGbOrNull() {
|
|
297
214
|
try {
|
|
298
215
|
return parseDfFreeGb(await this.o.host.dfData());
|
|
@@ -301,8 +218,6 @@ export class BakeRunner {
|
|
|
301
218
|
return null;
|
|
302
219
|
}
|
|
303
220
|
}
|
|
304
|
-
/** Ingest one frame, swallowing transient errors (the buffered-retry of unsent lines is the real-runner's job;
|
|
305
|
-
* image-api's uq_line makes a re-send a no-op, so a dropped frame is recoverable, never a crash). */
|
|
306
221
|
async safeIngest(bake, frame) {
|
|
307
222
|
try {
|
|
308
223
|
return await this.o.api.ingest(bake.bakeId, bake.ingestSecret, frame);
|
|
@@ -313,7 +228,6 @@ export class BakeRunner {
|
|
|
313
228
|
}
|
|
314
229
|
}
|
|
315
230
|
}
|
|
316
|
-
/** Allocate the next per-bake lineOrd (monotonic from 1) — the at-least-once ingest dedupe key (§P2.10). */
|
|
317
231
|
export function nextOrd(ord) {
|
|
318
232
|
ord.n += 1;
|
|
319
233
|
return ord.n;
|
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L8 G-c artifact — the signed deploy-authenticity evidence the design/93 §5.2 G-c aggregator requires
|
|
3
|
-
* (SPEC-RSI-L7-L8 §A.3). The artifact is canonical-JSON serialized and ed25519-signed so the GO aggregator can
|
|
4
|
-
* verify it was produced by a real L8 run and link it to a commit + adapter build + boundary.
|
|
5
|
-
*
|
|
6
|
-
* 🔴 SIGNING IS PROVENANCE, NOT AUTHENTICITY-OF-RESULT (SPEC §A.3 RISK-4): a valid signature only proves "this
|
|
7
|
-
* run, with this key, produced these probe VALUES"; it does NOT prove the probes hit a real boundary — that is
|
|
8
|
-
* the P0 fail-loud + the external collector's job. The aggregator MUST check `gC.satisfied` AND
|
|
9
|
-
* `boundary.isolationClassVerified` AND `p1Mode !== "adapter-RO-seam-absent"`, never just "signature valid".
|
|
10
|
-
*
|
|
11
|
-
* 🔴 ED25519, NOT HMAC: a shared HMAC secret is forgeable by anyone who holds it; ed25519 is asymmetric
|
|
12
|
-
* — the private key signs (control-plane held, env L8_SIGNER_KEY_B64), the PUBLIC key (shipped to the aggregator)
|
|
13
|
-
* only verifies. node:crypto's ed25519 is built-in (no new dep).
|
|
14
|
-
*
|
|
15
|
-
* 🔴 TIMESTAMPS ARE INJECTED, NOT `Date.now()` (deterministic-test requirement): `buildArtifact` takes
|
|
16
|
-
* `producedAt` from the caller (the live runner passes a real ISO string; the unit test passes a fixed one), so
|
|
17
|
-
* the canonical JSON is byte-stable in tests.
|
|
18
|
-
*/
|
|
19
1
|
import { createPrivateKey, createPublicKey, sign as cryptoSign, verify as cryptoVerify } from "node:crypto";
|
|
20
2
|
import { L8_SCHEMA_VERSION } from "./escape.js";
|
|
21
|
-
// ───────────────────────────── canonical JSON ─────────────────────────────
|
|
22
|
-
/**
|
|
23
|
-
* Canonical JSON: object keys sorted recursively, no insignificant whitespace, arrays preserve order. Stable
|
|
24
|
-
* across re-serialization so the signature/contentHash are deterministic. Rejects non-finite numbers (NaN/Inf
|
|
25
|
-
* would serialize as `null` and silently corrupt the canonical form).
|
|
26
|
-
*/
|
|
27
3
|
export function canonicalJson(value) {
|
|
28
4
|
return serialize(value);
|
|
29
5
|
}
|
|
@@ -45,7 +21,7 @@ function serialize(v) {
|
|
|
45
21
|
if (t === "object") {
|
|
46
22
|
const obj = v;
|
|
47
23
|
const keys = Object.keys(obj)
|
|
48
|
-
.filter((k) => obj[k] !== undefined)
|
|
24
|
+
.filter((k) => obj[k] !== undefined)
|
|
49
25
|
.sort();
|
|
50
26
|
return `{${keys.map((k) => `${JSON.stringify(k)}:${serialize(obj[k])}`).join(",")}}`;
|
|
51
27
|
}
|
|
@@ -53,26 +29,14 @@ function serialize(v) {
|
|
|
53
29
|
throw new Error("canonicalJson: bigint is not serializable");
|
|
54
30
|
throw new Error(`canonicalJson: unsupported type ${t}`);
|
|
55
31
|
}
|
|
56
|
-
// ───────────────────────────── content hash ─────────────────────────────
|
|
57
32
|
import { createHash } from "node:crypto";
|
|
58
|
-
/** The body that the signature + contentHash cover = the artifact WITHOUT `signature` and WITHOUT `contentHash`. */
|
|
59
33
|
function signableBody(a) {
|
|
60
34
|
const { signature: _sig, contentHash: _ch, ...rest } = a;
|
|
61
35
|
return rest;
|
|
62
36
|
}
|
|
63
|
-
/** sha256 of the canonical body — links the signature to the exact body bytes. */
|
|
64
37
|
export function computeContentHash(a) {
|
|
65
38
|
return createHash("sha256").update(canonicalJson(signableBody(a)), "utf8").digest("hex");
|
|
66
39
|
}
|
|
67
|
-
// ───────────────────────────── gC fold (pure) ─────────────────────────────
|
|
68
|
-
/**
|
|
69
|
-
* Fold G-c (SPEC §A.3): satisfied iff
|
|
70
|
-
* - no escapes, AND
|
|
71
|
-
* - boundary.provisioned AND boundary.isolationClassVerified, AND
|
|
72
|
-
* - every probe passed (NONE pending, NONE failed), AND
|
|
73
|
-
* - the immutable-mount probe's p1Mode is NOT "adapter-RO-seam-absent".
|
|
74
|
-
* G-c is necessary-non-sufficient: any pending/failed probe ⇒ NO-GO. This is what keeps tier-ON NO-GO honest.
|
|
75
|
-
*/
|
|
76
40
|
export function foldGc(boundary, probes, escapes) {
|
|
77
41
|
const reasons = [];
|
|
78
42
|
if (escapes.length > 0)
|
|
@@ -81,9 +45,6 @@ export function foldGc(boundary, probes, escapes) {
|
|
|
81
45
|
reasons.push("boundary not provisioned");
|
|
82
46
|
if (!boundary.isolationClassVerified)
|
|
83
47
|
reasons.push("isolationClass not verified (P0/P2 — possible runc downgrade)");
|
|
84
|
-
// 🔴 (review HIGH) require the FULL canonical probe set PRESENT before satisfied can be true — else an empty or
|
|
85
|
-
// partial probe array silently satisfies G-c (a GO with NO evidence; foldGc(boundary,[],[]) used to return
|
|
86
|
-
// satisfied:true). Each required id must appear exactly once. Mirrors PROBE_MATRIX ids (P0..P4).
|
|
87
48
|
const REQUIRED_PROBE_IDS = ["P0", "P1", "P2", "P3", "P4"];
|
|
88
49
|
const idCounts = new Map();
|
|
89
50
|
for (const p of probes)
|
|
@@ -100,8 +61,6 @@ export function foldGc(boundary, probes, escapes) {
|
|
|
100
61
|
const failedProbes = probes.filter((p) => !p.pending && p.passed !== true);
|
|
101
62
|
if (failedProbes.length > 0)
|
|
102
63
|
reasons.push(`failed/unproven probe(s): ${failedProbes.map((p) => p.id).join(", ")}`);
|
|
103
|
-
// wq64gmm5e: key on the constrained `id` (every other gate above keys on id) — a free-form `name` drift must
|
|
104
|
-
// never fail this seam-absent guard OPEN into a fake-green GO.
|
|
105
64
|
const p1 = probes.find((p) => p.id === "P1");
|
|
106
65
|
if (p1 && p1.p1Mode === "adapter-RO-seam-absent") {
|
|
107
66
|
reasons.push("P1 ran in adapter-RO-seam-absent mode (necessary-non-sufficient for the grader contract)");
|
|
@@ -114,8 +73,6 @@ export function foldGc(boundary, probes, escapes) {
|
|
|
114
73
|
: `G-c NOT satisfied: ${reasons.join(" | ")}`,
|
|
115
74
|
};
|
|
116
75
|
}
|
|
117
|
-
// ───────────────────────────── build (pure) ─────────────────────────────
|
|
118
|
-
/** Build the artifact body + contentHash. Pure: timestamps injected, gC folded. No signature yet. */
|
|
119
76
|
export function buildArtifact(meta, probes, escapes) {
|
|
120
77
|
const gC = foldGc(meta.boundary, probes, escapes);
|
|
121
78
|
const draft = {
|
|
@@ -129,25 +86,17 @@ export function buildArtifact(meta, probes, escapes) {
|
|
|
129
86
|
probes,
|
|
130
87
|
escapes,
|
|
131
88
|
gC,
|
|
132
|
-
contentHash: "",
|
|
89
|
+
contentHash: "",
|
|
133
90
|
};
|
|
134
91
|
draft.contentHash = computeContentHash(draft);
|
|
135
92
|
return draft;
|
|
136
93
|
}
|
|
137
|
-
// ───────────────────────────── ed25519 sign / verify ─────────────────────────────
|
|
138
|
-
/** Load an ed25519 private key from a base64 PKCS8 DER (the L8_SIGNER_KEY_B64 control-plane secret). */
|
|
139
94
|
export function loadPrivateKeyB64(b64) {
|
|
140
95
|
return createPrivateKey({ key: Buffer.from(b64, "base64"), format: "der", type: "pkcs8" });
|
|
141
96
|
}
|
|
142
|
-
/** Load an ed25519 public key from a base64 SPKI DER (the key shipped to the aggregator). */
|
|
143
97
|
export function loadPublicKeyB64(b64) {
|
|
144
98
|
return createPublicKey({ key: Buffer.from(b64, "base64"), format: "der", type: "spki" });
|
|
145
99
|
}
|
|
146
|
-
/**
|
|
147
|
-
* Sign the artifact: recompute the contentHash (defends against a body mutated after build), sign the canonical
|
|
148
|
-
* body with ed25519 (`sign(null, msg, key)` — ed25519 takes no separate digest algorithm), attach the detached
|
|
149
|
-
* base64 signature. Returns a NEW artifact (does not mutate the input).
|
|
150
|
-
*/
|
|
151
100
|
export function signArtifact(artifact, privateKey, keyId) {
|
|
152
101
|
const withHash = { ...artifact, signature: undefined, contentHash: "" };
|
|
153
102
|
withHash.contentHash = computeContentHash(withHash);
|
|
@@ -155,14 +104,6 @@ export function signArtifact(artifact, privateKey, keyId) {
|
|
|
155
104
|
const sig = cryptoSign(null, msg, privateKey);
|
|
156
105
|
return { ...withHash, signature: { alg: "ed25519", keyId, sig: sig.toString("base64") } };
|
|
157
106
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Verify an artifact: recompute the contentHash over the body and check it matches the stored one (tamper of any
|
|
160
|
-
* body field flips the hash), then ed25519-verify the detached signature over the canonical body. Returns false
|
|
161
|
-
* (never throws) on a missing/garbage signature, a content-hash mismatch, or a bad signature.
|
|
162
|
-
*
|
|
163
|
-
* 🔴 A `true` here means INTEGRITY/PROVENANCE only — the caller MUST still gate on gC.satisfied AND
|
|
164
|
-
* boundary.isolationClassVerified AND p1Mode (see file header). Verification alone is NOT a GO.
|
|
165
|
-
*/
|
|
166
107
|
export function verifyArtifact(artifact, publicKey) {
|
|
167
108
|
const sig = artifact.signature;
|
|
168
109
|
if (!sig || sig.alg !== "ed25519" || typeof sig.sig !== "string")
|
package/dist/bench/l8/escape.js
CHANGED
|
@@ -1,29 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L8 escape vocabulary — the `l8.v1` schema (design/93 §2, SPEC-RSI-L7-L8 §A.3).
|
|
3
|
-
*
|
|
4
|
-
* 🔴 WHY A SEPARATE SCHEMA (do NOT silently reuse core's L6 union):
|
|
5
|
-
* core's L6 bench (`sema-core/test/rsi-l6-bench.test.ts:92-94`, VERIFIED) defines
|
|
6
|
-
* EscapeKind = "E1" | "E2-readable" | "E2-callable"
|
|
7
|
-
* Classification = "gate" | "assembly"
|
|
8
|
-
* GateId = "G1" | "G2" | "G3" | "G4" | "tripwire" | "fold"
|
|
9
|
-
* It has **NO `E3` kind and NO `"deploy"` classification.** L8's escapes are E3 / deploy-class — they do NOT
|
|
10
|
-
* fit the L6 union as-is. So the "one aggregator composes L6+L8" claim is an OPEN QUESTION (SPEC §A.3 RISK-3),
|
|
11
|
-
* not a free reuse: either (i) core extends `EscapeKind` with `E3` + a deploy lane (a CORE change — must be
|
|
12
|
-
* coordinated, never unilaterally invented here), or (ii) L8 ships its OWN self-describing `l8.v1` schema and
|
|
13
|
-
* the GO aggregator maps both. This file is (ii): the artifact is `schemaVersion:"l8.v1"`, self-describing.
|
|
14
|
-
*
|
|
15
|
-
* We deliberately make `L8EscapeKind` a SUPERSET of the L6 kinds (so a future aggregator can map L6 escapes
|
|
16
|
-
* straight in) and ADD `"E3"`; and `L8Classification` adds `"deploy"`. Nothing in this file claims the
|
|
17
|
-
* vocabularies are already aligned — that alignment is the cross-team decision (`SCHEMA_ALIGNMENT`).
|
|
18
|
-
*/
|
|
19
|
-
/** Schema version stamped into every artifact — the self-describing handshake for the GO aggregator. */
|
|
20
1
|
export const L8_SCHEMA_VERSION = "l8.v1";
|
|
21
|
-
/**
|
|
22
|
-
* 🔴 OPEN QUESTION (cross-team, do NOT resolve unilaterally — SPEC §A.3 / design/93 open-questions):
|
|
23
|
-
* whether the GO aggregator accepts `l8.v1` + ed25519 verification + an E3/deploy lane, OR core extends its
|
|
24
|
-
* L6 `EscapeKind`/`Classification` instead. Until confirmed, this constant records the unaligned state so a
|
|
25
|
-
* reader (and a test) cannot mistake "L8 ships its own schema" for "the vocabularies are already merged".
|
|
26
|
-
*/
|
|
27
2
|
export const SCHEMA_ALIGNMENT = {
|
|
28
3
|
l6KindsCovered: ["E1", "E2-readable", "E2-callable"],
|
|
29
4
|
l8AddedKinds: ["E3"],
|
package/dist/bench/l8/index.js
CHANGED
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L8 deploy-contract authenticity bench (RSI L8, design/93 §5.2 G-c, SPEC-RSI-L7-L8 §A).
|
|
3
|
-
*
|
|
4
|
-
* NO-DEPENDENCY scaffold: the probe matrix + the l8.v1 escape schema + the signed G-c artifact (build/sign/verify)
|
|
5
|
-
* + the probe HARNESS that runs the BUILDABLE probes (P0 runtime-verification, P2 isolation) and emits HONEST
|
|
6
|
-
* PENDING records for the probes blocked on a cross-team dep:
|
|
7
|
-
* - P1 immutable-mount → the adapter podSpecPatch RO-mount seam is now MERGED (remote-env-k8s, service-side);
|
|
8
|
-
* pending only the live-runner wiring (provision WITH the RO mount) + a real Kata run.
|
|
9
|
-
* - P3 bash-egress → pending an egress-enforcement layer + the cluster CNI decision (center).
|
|
10
|
-
*
|
|
11
|
-
* The REAL boundary execution (live Kata/E2B + the external collector) is the live runner (scripts/, gated,
|
|
12
|
-
* outside this scaffold). This module is pure/deterministic-testable; it NEVER asserts a probe passes against a
|
|
13
|
-
* fake env (the A.6 anti-fake-green meta-lesson).
|
|
14
|
-
*/
|
|
15
1
|
export * from "./escape.js";
|
|
16
2
|
export * from "./probes.js";
|
|
17
3
|
export * from "./artifact.js";
|
package/dist/bench/l8/probes.js
CHANGED
|
@@ -1,26 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* L8 probe matrix — the deploy-contract authenticity probes (SPEC-RSI-L7-L8 §A.2, design/93 §5.2 G-c, §2 escape).
|
|
3
|
-
*
|
|
4
|
-
* Each probe asserts a REAL kernel/VM/network boundary and is designed to distinguish:
|
|
5
|
-
* (a) a REAL enforcement (the kernel/VM/CNI actually blocked it) from
|
|
6
|
-
* (b) a typed-tool refusal (the ToolPolicy layer said no — NOT a boundary), and from
|
|
7
|
-
* (c) a self-declared boolean (`capabilities.isolation:true`, which a lying/downgraded adapter still reports —
|
|
8
|
-
* `remote-env-k8s.ts:241` hardcodes it; a `runtimeClass:""` runc pod ALSO reports it).
|
|
9
|
-
*
|
|
10
|
-
* 🔴 DISCIPLINE: nothing here imports the typed-tool / ToolPolicy layer. Probes are pure DEFINITIONS +
|
|
11
|
-
* classification logic + an INDEPENDENT grader; the actual cluster read / sandbox exec is an injected seam
|
|
12
|
-
* ({@link ProbeExec}) so the unit test mocks it deterministically and NEVER asserts a probe "passes" against a
|
|
13
|
-
* fake env (that would be the fake-green this whole thread exists to prevent — A.6 meta-lesson). The REAL
|
|
14
|
-
* verdict comes only from the live runner against a real Kata/E2B boundary + the external collector.
|
|
15
|
-
*
|
|
16
|
-
* Buildability per probe (SPEC §A.2):
|
|
17
|
-
* P0/P2 runtime-verification ....... BUILDABLE NOW (read the real runtimeClass + kernel + host-unreachability)
|
|
18
|
-
* P1 immutable-mount ............... STRUCTURE NOW; real-RO execution PENDING the adapter podSpecPatch seam
|
|
19
|
-
* P3 bash-egress ................... STRUCTURE NOW; HONESTLY reports egressEnforced:false (no CNI / E2B default)
|
|
20
|
-
* P4 artifact ...................... see artifact.ts
|
|
21
|
-
*/
|
|
22
1
|
import { shellQuote } from "../../plugins/remote-shell.js";
|
|
23
|
-
/** What a real-isolation runtime looks like — kata-qemu / kata-fc are VM boundaries; runc / "" are NOT. */
|
|
24
2
|
export const REAL_ISOLATION_RUNTIME_CLASSES = ["kata-qemu", "kata-fc", "kata"];
|
|
25
3
|
export const PROBE_MATRIX = [
|
|
26
4
|
{
|
|
@@ -67,16 +45,6 @@ export function probeDef(id) {
|
|
|
67
45
|
throw new Error(`unknown probe id: ${id}`);
|
|
68
46
|
return d;
|
|
69
47
|
}
|
|
70
|
-
/**
|
|
71
|
-
* Classify the REAL runtime from observations — the lying-adapter detector. A runc downgrade still reports
|
|
72
|
-
* `declaredIsolation:true`; this function IGNORES that and decides from `actualRuntimeClass` + kernel-inequality.
|
|
73
|
-
*
|
|
74
|
-
* - kata: VM-isolated iff the runtimeClass is a real isolation class AND guestKernel != hostKernel. If the
|
|
75
|
-
* runtimeClass was dropped to "" / "runc" the kernel MATCHES the host → runc-downgrade → NOT verified.
|
|
76
|
-
* - e2b: the host is not ours, so kernel-vs-host is meaningless (RISK-7). VM-isolation evidence for e2b is the
|
|
77
|
-
* Firecracker kernel string being present (a non-empty distinct guest kernel) — host-unreachability (P2) is
|
|
78
|
-
* the corroborating half. We never assert a kata-only kernel-inequality invariant on e2b.
|
|
79
|
-
*/
|
|
80
48
|
export function classifyRuntime(o) {
|
|
81
49
|
const isRealClass = REAL_ISOLATION_RUNTIME_CLASSES.includes(o.actualRuntimeClass);
|
|
82
50
|
if (o.backend === "kata") {
|
|
@@ -104,7 +72,6 @@ export function classifyRuntime(o) {
|
|
|
104
72
|
reason: `runtimeClass=${o.actualRuntimeClass} and guest kernel ${o.guestKernel} != host ${o.hostKernel}`,
|
|
105
73
|
};
|
|
106
74
|
}
|
|
107
|
-
// e2b: no host-kernel comparison (RISK-7). Require a present guest kernel; host-unreachability (P2) corroborates.
|
|
108
75
|
if (o.guestKernel.trim().length === 0) {
|
|
109
76
|
return {
|
|
110
77
|
isolationClassVerified: false,
|
|
@@ -120,10 +87,6 @@ export function classifyRuntime(o) {
|
|
|
120
87
|
reason: `e2b: Firecracker guest kernel ${o.guestKernel} present (kernel-vs-host N/A on e2b — RISK-7); host-unreachability is the corroborating half (P2)`,
|
|
121
88
|
};
|
|
122
89
|
}
|
|
123
|
-
/**
|
|
124
|
-
* The probe COMMANDS for the runtime read (used by the live runner; shown here so they live with the probe).
|
|
125
|
-
* Every interpolated value is shellQuoted (RISK-9). These are read-only — no side effects.
|
|
126
|
-
*/
|
|
127
90
|
export function runtimeReadCommands() {
|
|
128
91
|
return {
|
|
129
92
|
uname: "uname -r",
|
|
@@ -131,10 +94,7 @@ export function runtimeReadCommands() {
|
|
|
131
94
|
dmi: "cat /sys/devices/virtual/dmi/id/product_name 2>/dev/null || echo unknown",
|
|
132
95
|
};
|
|
133
96
|
}
|
|
134
|
-
// ───────────────────────────── P1 — immutable-mount INDEPENDENT grader (structure now) ─────────────────────────────
|
|
135
|
-
/** The escalations P1 attempts after the initial write (the real test, beyond core's point-in-time probe). */
|
|
136
97
|
export const P1_ESCALATIONS = ["remount-rw", "chattr-i", "unshare-m", "busybox-mount"];
|
|
137
|
-
/** The escalation shell commands for an oracle path — every path shellQuoted (RISK-9). */
|
|
138
98
|
export function p1EscalationCommand(escalation, oraclePath) {
|
|
139
99
|
const q = shellQuote(oraclePath);
|
|
140
100
|
switch (escalation) {
|
|
@@ -148,12 +108,6 @@ export function p1EscalationCommand(escalation, oraclePath) {
|
|
|
148
108
|
return `cp /bin/busybox /tmp/l8bb 2>/dev/null && /tmp/l8bb mount -o remount,rw ${q}`;
|
|
149
109
|
}
|
|
150
110
|
}
|
|
151
|
-
/**
|
|
152
|
-
* P1 verdict — PASS iff the initial write failed AND EVERY escalation failed AND the independent grader confirms
|
|
153
|
-
* no probe file was written and the inode is unchanged AND CAP_SYS_ADMIN was confirmed absent. The pass is only
|
|
154
|
-
* valid for the grader contract when the adapter RO seam is present (mode); on a proxy path it is honest but
|
|
155
|
-
* necessary-non-sufficient.
|
|
156
|
-
*/
|
|
157
111
|
export function gradeP1(o) {
|
|
158
112
|
const allBlocked = o.initialWriteBlocked && P1_ESCALATIONS.every((e) => o.escalationsBlocked[e] === true);
|
|
159
113
|
const independentlyClean = !o.oracleProbeFileExistsAfter && o.oracleInodeUnchanged;
|
|
@@ -178,13 +132,7 @@ export function gradeP1(o) {
|
|
|
178
132
|
}
|
|
179
133
|
return { passed, validForGraderContract, allBlocked, reason };
|
|
180
134
|
}
|
|
181
|
-
// ───────────────────────────── P3 — bash-egress attempt matrix + honest classification (structure now) ─────────────────────────────
|
|
182
|
-
/**
|
|
183
|
-
* The egress vectors P3 attempts — NOT just one curl. Each is a distinct exfil channel a real CNI must block.
|
|
184
|
-
* The verdict comes from the EXTERNAL collector (per-run nonce), never the in-sandbox exit code.
|
|
185
|
-
*/
|
|
186
135
|
export const EGRESS_VECTORS = ["https-tcp", "dns", "udp", "ipv6", "metadata-169254", "unix-socket"];
|
|
187
|
-
/** Build the attempt command for a vector. `host`/`url` carry the per-run nonce; everything shellQuoted (RISK-9). */
|
|
188
136
|
export function egressAttemptCommand(vector, target, nonce) {
|
|
189
137
|
const q = shellQuote(target);
|
|
190
138
|
const n = shellQuote(nonce);
|
|
@@ -192,7 +140,6 @@ export function egressAttemptCommand(vector, target, nonce) {
|
|
|
192
140
|
case "https-tcp":
|
|
193
141
|
return `curl -sS --max-time 5 ${q}/${n} >/dev/null 2>&1; echo rc=$?`;
|
|
194
142
|
case "dns":
|
|
195
|
-
// a DNS lookup of a nonce-labelled subdomain — exfil-via-DNS channel.
|
|
196
143
|
return `nslookup ${shellQuote(`${nonce}.${target}`)} >/dev/null 2>&1; echo rc=$?`;
|
|
197
144
|
case "udp":
|
|
198
145
|
return `printf %s ${n} | nc -u -w2 ${q} 53 >/dev/null 2>&1; echo rc=$?`;
|
|
@@ -204,15 +151,8 @@ export function egressAttemptCommand(vector, target, nonce) {
|
|
|
204
151
|
return `printf %s ${n} | nc -U ${q} >/dev/null 2>&1; echo rc=$?`;
|
|
205
152
|
}
|
|
206
153
|
}
|
|
207
|
-
/**
|
|
208
|
-
* P3 verdict — PASS iff egress is actually enforced AND the collector saw NO nonce AND the allowlisted control
|
|
209
|
-
* works AND (if applicable) the host resource is untouched. Against the CURRENT boundary `egressEnforcementPresent`
|
|
210
|
-
* is FALSE → `egressEnforced:false` → P3 cannot pass. That is a real finding (the G-c blocker), not fake-green.
|
|
211
|
-
*/
|
|
212
154
|
export function gradeP3(o) {
|
|
213
155
|
const egressEnforced = o.egressEnforcementPresent && !o.collectorObservedNonce;
|
|
214
|
-
// 🔴 (review LOW) a block is only real when SOMETHING enforces it: with no enforcement present, a curl that
|
|
215
|
-
// merely failed (network blip / unresolved host) is NOT "blocked" — reads false when nothing is enforcing.
|
|
216
156
|
const nonAllowlistedBlocked = o.egressEnforcementPresent && !o.collectorObservedNonce;
|
|
217
157
|
const a4Ok = !o.a4Applicable || o.a4HostUntouched;
|
|
218
158
|
const passed = egressEnforced && nonAllowlistedBlocked && o.allowlistedWorks && a4Ok;
|