@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,55 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sandbox-image-pool INDEX store (IMAGE-API-DESIGN.md §1, pool design §8) — SINGLE-FILE DUAL-DIALECT
|
|
3
|
-
* (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBImageIndex` /
|
|
4
|
-
* `PgImageIndex` class names survive as thin ctor subclasses so every consumer (store-backend.ts,
|
|
5
|
-
* pg-pool.ts, the unit + real-DB suites) is untouched.
|
|
6
|
-
*
|
|
7
|
-
* One row per published/building image DIGEST — the immutable runtime ref (design §6: refs are @sha256:...).
|
|
8
|
-
* A queryable projection of build.sh's replay-sufficient `sandbox-manifest.json` + the publish lifecycle
|
|
9
|
-
* `status` (building → published → deprecated / failed). This is the SINGLE SOURCE OF TRUTH for "what can a
|
|
10
|
-
* sandbox run"; `POST /v1/images/select` resolves a profile → digest off it (the §7 per-sandbox hot path).
|
|
11
|
-
*
|
|
12
|
-
* Visibility is enforced IN THE QUERY (Codex A1, mirrors the assistant per-user scoping): a tenant principal
|
|
13
|
-
* sees `public` rows + its OWN `tenant` rows only; an operator sees all. mapRow/ImageRow are the row-mapping
|
|
14
|
-
* surface shared by BOTH dialects — they live in the backend-neutral store-contracts.ts (design/158 A3 沉底).
|
|
15
|
-
*
|
|
16
|
-
* ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
|
|
17
|
-
* - `?` placeholders vs positional `$n` (and the placeholder-arity fallout in `list`/`setStatus`, which
|
|
18
|
-
* track a running `$n` index; TiDB's `?` needs no such bookkeeping).
|
|
19
|
-
* - upsert form: `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (id) DO UPDATE SET … EXCLUDED.col`.
|
|
20
|
-
* - the MONOTONIC de-publish guard (IMAGE-API-DESIGN.md §P2.10 / wq64gmm5e): `IF(status='published', old,
|
|
21
|
-
* new)` vs `CASE WHEN … status = 'published' THEN old ELSE new END` — status/visibility/tenant_id/signed
|
|
22
|
-
* are ALL preserved on a live published row in BOTH dialects.
|
|
23
|
-
* - JSON columns: TiDB `JSON.stringify` verbatim vs PG `pgSafeJsonStringify` + `$n::jsonb` casts (lossy —
|
|
24
|
-
* this is an observation-layer blob, not a trust-boundary payload, so `pgSafeJsonStringify`'s U+FFFD
|
|
25
|
-
* replacement is acceptable; codex R15).
|
|
26
|
-
* - PG-only identity-axis validation (codex R17-H1/R19): `repo` (the `idFor` hash input + the `(repo,digest)`
|
|
27
|
-
* unique-key equivalence premise) and `tenantId` reject unstorable bytes UP FRONT (`pgHasUnstorable` →
|
|
28
|
-
* `PgUnstorableError`) rather than silently mangling them — a rewrite would fork the hash id from the
|
|
29
|
-
* stored column (same `(repo,digest)` collides the unique key while `ON CONFLICT (id)` doesn't match,
|
|
30
|
-
* wedging registration permanently). TiDB has no equivalent NUL/lone-surrogate constraint, so it stores
|
|
31
|
-
* verbatim. Same-family scalar sanitize (tag/recipeGitSha/generatorVersion/supersedes — codex R16) is
|
|
32
|
-
* PG-only for the same reason; identity columns (repo/tenantId/digest/manifestSha) are reject-not-rewrite.
|
|
33
|
-
* - null-safe ordering trick: `ORDER BY (build_date IS NULL), build_date DESC` vs `ORDER BY build_date DESC
|
|
34
|
-
* NULLS LAST` (same semantics, dialect-native syntax).
|
|
35
|
-
* - capability-flag predicate: `JSON_EXTRACT(capabilities, '$.key') = true` vs `capabilities -> 'key' =
|
|
36
|
-
* 'true'::jsonb` (NOT a `::boolean` cast, which hard-errors on non-boolean jsonb and aborts the query;
|
|
37
|
-
* the jsonb equality yields false instead, matching MySQL's exclude-don't-throw semantics for a present
|
|
38
|
-
* but non-boolean capability value, e.g. `nestedBuildMode`).
|
|
39
|
-
* - affectedRows vs rowCount, BIGINT (`size_bytes`) read back as a STRING on PG (via SqlDriver / mapImageRow).
|
|
40
|
-
* - schema ownership: TiDB DDL lives in tidb-pool.ts; the PG DDL is exported from HERE
|
|
41
|
-
* (PG_IMAGE_INDEX_SCHEMA + ensureSchema, self-contained for the integration test).
|
|
42
|
-
*/
|
|
43
1
|
import { createHash } from "node:crypto";
|
|
44
2
|
import { pgSafeJsonStringify, pgSanitizeText, pgHasUnstorable, PgUnstorableError } from "./pg-safe-json.js";
|
|
45
|
-
import {
|
|
46
|
-
// mapRow/ImageRow are the row-mapping surface shared by BOTH SQL twins — they live in the
|
|
47
|
-
// backend-neutral store-contracts.ts (design/158 A3 沉底; no adapter imports its twin).
|
|
48
|
-
mapImageRow as mapRow, } from "./store-contracts.js";
|
|
3
|
+
import { mapImageRow as mapRow, } from "./store-contracts.js";
|
|
49
4
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
50
|
-
/** PG translation of the `sandbox_image_index` DDL in tidb-pool.ts (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
|
|
51
|
-
* TINYINT(1)→SMALLINT(布尔列 PG 映射归一,clay 批 2026-07-26 —— 两方言读回同为 number,写入 0/1,
|
|
52
|
-
* 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
|
|
53
5
|
export const PG_IMAGE_INDEX_SCHEMA = [
|
|
54
6
|
`CREATE TABLE IF NOT EXISTS sandbox_image_index (
|
|
55
7
|
id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -80,14 +32,12 @@ export const PG_IMAGE_INDEX_SCHEMA = [
|
|
|
80
32
|
`CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_digest ON sandbox_image_index (digest)`,
|
|
81
33
|
`CREATE INDEX IF NOT EXISTS idx_sandbox_image_index_tenant ON sandbox_image_index (tenant_id, status)`,
|
|
82
34
|
];
|
|
83
|
-
/** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
|
|
84
35
|
export async function ensureSchema(pool) {
|
|
85
36
|
for (const stmt of PG_IMAGE_INDEX_SCHEMA)
|
|
86
37
|
await pool.query(stmt);
|
|
87
38
|
}
|
|
88
39
|
const MAX_LIMIT = 200;
|
|
89
40
|
const DEFAULT_LIMIT = 50;
|
|
90
|
-
/** `(visibility='public' OR tenant_id=?)` unless operator. TiDB form — `?` needs no index bookkeeping. */
|
|
91
41
|
function visibilityClauseTidb(viewer) {
|
|
92
42
|
if (viewer.operator)
|
|
93
43
|
return { sql: "1=1", params: [] };
|
|
@@ -95,7 +45,6 @@ function visibilityClauseTidb(viewer) {
|
|
|
95
45
|
return { sql: "(visibility='public' OR tenant_id=?)", params: [viewer.tenantId] };
|
|
96
46
|
return { sql: "visibility='public'", params: [] };
|
|
97
47
|
}
|
|
98
|
-
/** PG form — positional `$n`, with a running index the caller continues from (PG has no anonymous `?`). */
|
|
99
48
|
function visibilityClausePg(viewer, startIdx) {
|
|
100
49
|
if (viewer.operator)
|
|
101
50
|
return { sql: "1=1", params: [], nextIdx: startIdx };
|
|
@@ -121,7 +70,6 @@ function decodeCursor(c) {
|
|
|
121
70
|
return null;
|
|
122
71
|
}
|
|
123
72
|
}
|
|
124
|
-
/** TiDB upsert/registerBuilding params — the array literal is IDENTICAL text at both TiDB call sites. */
|
|
125
73
|
function tidbUpsertParams(e, id, now) {
|
|
126
74
|
return [
|
|
127
75
|
id, e.profile, JSON.stringify(e.bands ?? []), e.repo, e.tag, e.digest,
|
|
@@ -136,17 +84,12 @@ const PG_UPSERT_COLS = "(id, profile, bands, repo, tag, digest, toolchain_versio
|
|
|
136
84
|
"status, visibility, tenant_id, manifest_sha, recipe_git_sha, generator_version, build_date, supersedes, " +
|
|
137
85
|
"signed, created_at, updated_at)";
|
|
138
86
|
const PG_UPSERT_VALUES = "($1,$2,$3::jsonb,$4,$5,$6,$7::jsonb,$8::jsonb,$9::jsonb,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21)";
|
|
139
|
-
/** PG upsert/registerBuilding params — deep-sanitized JSON (observation-layer, R15) + same-family scalar
|
|
140
|
-
* sanitize (R16); identity columns (repo/tenantId/digest/manifestSha) are reject-not-rewrite (validated by
|
|
141
|
-
* the caller BEFORE this runs — see the `pgHasUnstorable` guards in `upsert`/`registerBuilding`). */
|
|
142
87
|
function pgUpsertParams(e, id, now) {
|
|
143
88
|
return [
|
|
144
89
|
id,
|
|
145
90
|
pgSanitizeText(e.profile),
|
|
146
|
-
pgSafeJsonStringify(e.bands ?? []),
|
|
147
|
-
|
|
148
|
-
// 同族观测标量一并 sanitize(digest/manifestSha 恒 hex 恒等;identity 值 sanitize 恒等=零行为差)。
|
|
149
|
-
e.repo, // R17-H1:repo=身份(idFor 哈希输入)——入口拒绝式,列存原文(见 upsert/registerBuilding)
|
|
91
|
+
pgSafeJsonStringify(e.bands ?? []),
|
|
92
|
+
e.repo,
|
|
150
93
|
pgSanitizeText(e.tag),
|
|
151
94
|
e.digest,
|
|
152
95
|
pgSafeJsonStringify(e.toolchainVersions ?? {}),
|
|
@@ -155,7 +98,7 @@ function pgUpsertParams(e, id, now) {
|
|
|
155
98
|
e.sizeBytes,
|
|
156
99
|
e.status,
|
|
157
100
|
e.visibility,
|
|
158
|
-
e.tenantId ?? null,
|
|
101
|
+
e.tenantId ?? null,
|
|
159
102
|
e.manifestSha ?? null,
|
|
160
103
|
e.recipeGitSha == null ? null : pgSanitizeText(e.recipeGitSha),
|
|
161
104
|
e.generatorVersion == null ? null : pgSanitizeText(e.generatorVersion),
|
|
@@ -166,33 +109,18 @@ function pgUpsertParams(e, id, now) {
|
|
|
166
109
|
now,
|
|
167
110
|
];
|
|
168
111
|
}
|
|
169
|
-
/** Dual-dialect INDEX store. See the file header for the dialect-delta ledger. */
|
|
170
112
|
export class SqlImageIndex {
|
|
171
113
|
db;
|
|
172
114
|
constructor(db) {
|
|
173
115
|
this.db = db;
|
|
174
116
|
}
|
|
175
|
-
/** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
|
|
176
117
|
q(tidb, pg) {
|
|
177
118
|
return this.db.dialect === "tidb" ? tidb : pg;
|
|
178
119
|
}
|
|
179
|
-
/** Stable index id = sha256(repo@digest) — identity is the digest, not the mutable tag. */
|
|
180
120
|
static idFor(repo, digest) {
|
|
181
121
|
return createHash("sha256").update(`${repo}@${digest}`).digest("hex").slice(0, 64);
|
|
182
122
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Register or update an index row (idempotent on repo+digest). Returns the row id.
|
|
185
|
-
*
|
|
186
|
-
* @deprecated #193 车7(staleness-sweep P2-11):**生产零调用**——曾声称的 operator
|
|
187
|
-
* `/v1/images/register` 路径实际走 `registerBuilding`(routes/images.ts),本方法只剩集成测试当
|
|
188
|
-
* seed 口在用。危险语义原样保留着:UNCONDITIONAL upsert 会把 `published` 行打回 `building`
|
|
189
|
-
* (正是 §P2.10 静默 de-publish 病的成因,`registerBuilding` 因此才带 STATUS-MONOTONIC 守卫)。
|
|
190
|
-
* **禁止新增生产调用**;测试 seed 请优先走 `registerBuilding`(不需要覆盖语义时)。留而不删的
|
|
191
|
-
* 唯一理由=16 处 pg 集成测试 seed 依赖,整删随该套件下次翻新一并做。
|
|
192
|
-
*/
|
|
193
123
|
async upsert(e) {
|
|
194
|
-
// 🔴 PG-only identity-axis validation (codex R17-H1/R19) — TiDB has no NUL/lone-surrogate column
|
|
195
|
-
// constraint, so it stores repo/tenantId verbatim; PG rejects up front (see file-header dialect delta).
|
|
196
124
|
if (this.db.dialect === "pg") {
|
|
197
125
|
if (pgHasUnstorable(e.repo))
|
|
198
126
|
throw new PgUnstorableError("image repo (identity)");
|
|
@@ -224,32 +152,7 @@ export class SqlImageIndex {
|
|
|
224
152
|
supersedes = EXCLUDED.supersedes, signed = EXCLUDED.signed, updated_at = EXCLUDED.updated_at`), this.db.dialect === "tidb" ? tidbUpsertParams(e, id, now) : pgUpsertParams(e, id, now));
|
|
225
153
|
return id;
|
|
226
154
|
}
|
|
227
|
-
/**
|
|
228
|
-
* STATUS-MONOTONIC register (IMAGE-API-DESIGN.md §P2.10 MUST-FIX) — the auto-register path the bake runner uses.
|
|
229
|
-
*
|
|
230
|
-
* `upsert` does an UNCONDITIONAL upsert: a re-ingested `done` (at-least-once from the runner) or a P2 bake of
|
|
231
|
-
* a digest P3 already PROMOTED would flip a live `published` row back to `building`, and `latestPublished`
|
|
232
|
-
* (`WHERE status='published'`) then DE-PUBLISHES a live, in-use image → sandbox-create 404s. This guarded path
|
|
233
|
-
* NEVER downgrades a `published` row: on a fresh row it inserts (`status:'building'` by default); on an
|
|
234
|
-
* existing row it updates the manifest projection + bumps `updated_at` but writes `status` only when the row
|
|
235
|
-
* isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
|
|
236
|
-
* lifecycle, never a regression. Returns the row id.
|
|
237
|
-
*
|
|
238
|
-
* Distinct from the unconditional `upsert` (#193 车7 更正:那条 operator `/v1/images/register` 路径
|
|
239
|
-
* **也走本方法**——routes/images.ts:446 `idx.registerBuilding(...)`;`upsert` 已 @deprecated,只剩
|
|
240
|
-
* 测试 seed 在用)。This is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
|
|
241
|
-
* node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
|
|
242
|
-
*
|
|
243
|
-
* The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
|
|
244
|
-
* defense: once a row is `published` and live, a re-register must NOT downgrade `status` NOR flip the
|
|
245
|
-
* columns `latestPublished` filters on. wq64gmm5e: guarding only `status` left `visibility`/`tenant_id`
|
|
246
|
-
* (and `signed`) overwritable, so a re-register with visibility=private/tenant or a different tenant_id
|
|
247
|
-
* silently dropped a live row out of every viewer's `latestPublished` — an effective de-publish. ALL FOUR
|
|
248
|
-
* (status/visibility/tenant_id/signed) are gated identically in BOTH dialects; non-published rows still
|
|
249
|
-
* take the freshest projection.
|
|
250
|
-
*/
|
|
251
155
|
async registerBuilding(e) {
|
|
252
|
-
// 🔴 PG-only identity-axis validation (codex R17-H1/R19) — same rationale as `upsert` above.
|
|
253
156
|
if (this.db.dialect === "pg") {
|
|
254
157
|
if (pgHasUnstorable(e.repo))
|
|
255
158
|
throw new PgUnstorableError("image repo (identity)");
|
|
@@ -295,15 +198,10 @@ export class SqlImageIndex {
|
|
|
295
198
|
const { rows } = await this.db.query(this.q("SELECT * FROM sandbox_image_index WHERE id=? LIMIT 1", "SELECT * FROM sandbox_image_index WHERE id = $1 LIMIT 1"), [id]);
|
|
296
199
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
297
200
|
}
|
|
298
|
-
/** By immutable digest — what a running sandbox was built from. */
|
|
299
201
|
async getByDigest(digest) {
|
|
300
202
|
const { rows } = await this.db.query(this.q("SELECT * FROM sandbox_image_index WHERE digest=? ORDER BY updated_at DESC LIMIT 1", "SELECT * FROM sandbox_image_index WHERE digest = $1 ORDER BY updated_at DESC LIMIT 1"), [digest]);
|
|
301
203
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
302
204
|
}
|
|
303
|
-
/** The newest PUBLISHED, viewer-visible digest for a profile — the `select` resolution.
|
|
304
|
-
* 🔴 genuinely-divergent shape (not just SQL text): TiDB's `visibilityClauseTidb` needs no placeholder
|
|
305
|
-
* index (anonymous `?`); PG's `visibilityClausePg` threads a running `$n` index the caller continues
|
|
306
|
-
* from. Also the null-safe ORDER BY trick differs ((build_date IS NULL) vs NULLS LAST) — same semantics. */
|
|
307
205
|
async latestPublished(profile, viewer) {
|
|
308
206
|
if (this.db.dialect === "tidb") {
|
|
309
207
|
const vis = visibilityClauseTidb(viewer);
|
|
@@ -318,13 +216,6 @@ export class SqlImageIndex {
|
|
|
318
216
|
ORDER BY build_date DESC NULLS LAST, created_at DESC LIMIT 1`, [profile, ...vis.params]);
|
|
319
217
|
return rows[0] ? mapRow(rows[0]) : null;
|
|
320
218
|
}
|
|
321
|
-
/**
|
|
322
|
-
* Paginated, viewer-scoped catalog query (the UI's list). Keyset cursor on (created_at, id) desc.
|
|
323
|
-
* 🔴 genuinely-divergent shape: the PG arm threads an explicit `$n` index through every optional predicate
|
|
324
|
-
* (node-pg has no anonymous placeholder); the TiDB arm just appends `?` in call order. Kept as two full
|
|
325
|
-
* branches rather than a shared placeholder-numbering helper (sql-driver.ts STEP 3/4) — a reviewer sees each
|
|
326
|
-
* dialect's real query shape, not a generated abstraction over it.
|
|
327
|
-
*/
|
|
328
219
|
async list(filter) {
|
|
329
220
|
const limit = Math.min(Math.max(1, filter.limit ?? DEFAULT_LIMIT), MAX_LIMIT);
|
|
330
221
|
let rawRows;
|
|
@@ -341,7 +232,6 @@ export class SqlImageIndex {
|
|
|
341
232
|
params.push(filter.status);
|
|
342
233
|
}
|
|
343
234
|
if (filter.capability) {
|
|
344
|
-
// JSON boolean true for the capability key.
|
|
345
235
|
where.push("JSON_EXTRACT(capabilities, ?) = true");
|
|
346
236
|
params.push(`$.${filter.capability}`);
|
|
347
237
|
}
|
|
@@ -371,11 +261,6 @@ export class SqlImageIndex {
|
|
|
371
261
|
params.push(filter.status);
|
|
372
262
|
}
|
|
373
263
|
if (filter.capability) {
|
|
374
|
-
// JSON boolean true for the capability key. Compare at the jsonb layer (NOT a ::boolean cast): a
|
|
375
|
-
// `::boolean` cast HARD-ERRORS on any non-boolean jsonb value (string/number/JSON null) and aborts the
|
|
376
|
-
// whole query, whereas `= 'true'::jsonb` yields false for those — faithfully reproducing MySQL's
|
|
377
|
-
// `JSON_EXTRACT(...) = true` semantics (a present-but-non-boolean cap, e.g.
|
|
378
|
-
// nestedBuildMode="rootless-buildkit", simply excludes the row).
|
|
379
264
|
where.push(`capabilities -> $${idx++} = 'true'::jsonb`);
|
|
380
265
|
params.push(filter.capability);
|
|
381
266
|
}
|
|
@@ -401,13 +286,11 @@ export class SqlImageIndex {
|
|
|
401
286
|
nextCursor = encodeCursor({ createdAt: last.createdAt, id: last.id });
|
|
402
287
|
}
|
|
403
288
|
if (filter.latestOnly) {
|
|
404
|
-
// collapse to the newest row per profile (rows are already build/created-desc within the page).
|
|
405
289
|
const seen = new Set();
|
|
406
290
|
entries = entries.filter((e) => (seen.has(e.profile) ? false : (seen.add(e.profile), true)));
|
|
407
291
|
}
|
|
408
292
|
return { entries, nextCursor };
|
|
409
293
|
}
|
|
410
|
-
/** Lifecycle transition (atomic-publish: building→published, or →failed/deprecated). */
|
|
411
294
|
async setStatus(id, status, opts) {
|
|
412
295
|
if (this.db.dialect === "tidb") {
|
|
413
296
|
const sets = ["status=?", "updated_at=?"];
|
|
@@ -431,13 +314,11 @@ export class SqlImageIndex {
|
|
|
431
314
|
await this.db.query(`UPDATE sandbox_image_index SET ${sets.join(", ")} WHERE id = $${idx}`, params);
|
|
432
315
|
}
|
|
433
316
|
}
|
|
434
|
-
/** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
|
|
435
317
|
export class TiDBImageIndex extends SqlImageIndex {
|
|
436
318
|
constructor(pool) {
|
|
437
319
|
super(mysqlDriver(pool));
|
|
438
320
|
}
|
|
439
321
|
}
|
|
440
|
-
/** PostgreSQL binding — historical class name + ctor shape preserved. */
|
|
441
322
|
export class PgImageIndex extends SqlImageIndex {
|
|
442
323
|
constructor(pool) {
|
|
443
324
|
super(pgDriver(pool));
|
|
@@ -1,36 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/103 background-shell pod-side scripts (pure, exported for testing) + design/116 detach-capable exec —
|
|
3
|
-
* the k8s adapter's text generation/parsing layer for background shells (`buildBg*Script` ×5 +
|
|
4
|
-
* `buildDetachCapableExec` + `parseBgPollOutput`). Split out of `remote-env-k8s.ts` (design/158 A13,
|
|
5
|
-
* internal-lossless — `remote-env-k8s.ts` re-exports every symbol below unchanged).
|
|
6
|
-
*
|
|
7
|
-
* The pod-side runner is `setsid`'d into its OWN session so the whole job (cmd + its grandchildren) shares one
|
|
8
|
-
* process group we can group-kill (`kill -<pgid>`); the session leader self-reports its pgid (`$$`) so we never
|
|
9
|
-
* depend on `setsid &`'s `$!` (which is the short-lived parent). The command writes DIRECTLY to pod files (out/err),
|
|
10
|
-
* re-read by byte cursor each poll (path ① — survives the consume-once exec seam).
|
|
11
|
-
*
|
|
12
|
-
* 🔴 Why DIRECT redirect (`> out 2> err`), not a FIFO+`head` cap: on-box kata verification proved a FIFO+`head`
|
|
13
|
-
* pipeline block-buffers `head`'s stdout → the `out` file stays EMPTY until head's stdio buffer fills (KBs) or EOF,
|
|
14
|
-
* so `pollBackground` sees nothing for a slow streamer (the whole dev-server use case). Direct redirect makes the
|
|
15
|
-
* shell write each `echo` straight to the file → immediately pollable (the command's OWN buffering still applies,
|
|
16
|
-
* same as TOC/E2B's pipe). DISK bound: the pod has no native ephemeral cap, so a runaway producer is bounded by
|
|
17
|
-
* (a) the BG-timeout hard wall + pod `activeDeadlineSeconds`, and (b) a control-plane size guard in the driver's
|
|
18
|
-
* read() that reaps a job whose output exceeds `K8S_BG_FILE_CAP` (kata VM isolation already contains the blast to
|
|
19
|
-
* this one disposable pod). per-poll TRANSFER stays bounded by the readCap in buildBgPollScript regardless.
|
|
20
|
-
*/
|
|
21
1
|
import { shellQuote } from "./remote-shell.js";
|
|
22
|
-
/** The detached runner (base64'd into the pod, run under `setsid`). Self-reports pgid; streams output to out/err. */
|
|
23
2
|
export function buildBgRunnerScript(dir) {
|
|
24
3
|
const q = (name) => shellQuote(`${dir}/${name}`);
|
|
25
4
|
return [
|
|
26
|
-
// 🔴 self-report the pgid: $$ is this (session-leader) shell's pid == the session/group id. Atomic write.
|
|
27
5
|
`echo $$ > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`,
|
|
28
|
-
`sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`,
|
|
6
|
+
`sh ${q("cmd.sh")} > ${q("out")} 2> ${q("err")} < /dev/null`,
|
|
29
7
|
`__RC=$?`,
|
|
30
|
-
`echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`,
|
|
8
|
+
`echo $__RC > ${q("exit.tmp")} 2>/dev/null && mv ${q("exit.tmp")} ${q("exit")} 2>/dev/null`,
|
|
31
9
|
].join("\n");
|
|
32
10
|
}
|
|
33
|
-
/** Short foreground launcher: writes cmd.sh + runner.sh, then `setsid`s the runner detached and returns immediately. */
|
|
34
11
|
export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
|
|
35
12
|
const d = shellQuote(dir);
|
|
36
13
|
const q = (name) => shellQuote(`${dir}/${name}`);
|
|
@@ -38,28 +15,14 @@ export function buildBgLauncherScript(dir, cmdShB64, runnerShB64) {
|
|
|
38
15
|
`mkdir -p ${d} && chmod 700 ${d}`,
|
|
39
16
|
`printf %s '${cmdShB64}' | base64 -d > ${q("cmd.sh")}`,
|
|
40
17
|
`printf %s '${runnerShB64}' | base64 -d > ${q("runner.sh")}`,
|
|
41
|
-
// detach into a new session (survives the exec socket close — disposable-pod posture / startBackground precedent)
|
|
42
18
|
`setsid sh ${q("runner.sh")} >/dev/null 2>&1 &`,
|
|
43
|
-
// 🔴 Do NOT report launched until the runner has self-reported its pgid — otherwise a killBackground
|
|
44
|
-
// / timeout landing in the gap finds no pgid, no-ops, yet the manager marks the shell killed → a LEAKED process
|
|
45
|
-
// group. Bounded (~2s); the runner writes pgid as its very FIRST action, so this is one iteration in practice.
|
|
46
19
|
`__i=0; while [ ! -s ${q("pgid")} ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
|
|
47
|
-
// 🔴 (workflow MED) Only report launched if the runner actually started (pgid present). If setsid is missing or the
|
|
48
|
-
// runner crashed before writing pgid, report failure → the driver surfaces spawn_failed instead of a phantom
|
|
49
|
-
// "running" shell that never ends until the BG timeout.
|
|
50
20
|
`if [ -s ${q("pgid")} ]; then echo SEMABG_LAUNCHED; else echo SEMABG_NOPGID; fi`,
|
|
51
21
|
].join("\n");
|
|
52
22
|
}
|
|
53
|
-
/** Poll script: emit totals + liveness + exit, then the new bytes per stream (base64, bounded by `readCap`). */
|
|
54
23
|
export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
|
|
55
24
|
const d = shellQuote(dir);
|
|
56
|
-
const slice = (file, cursor) =>
|
|
57
|
-
// 🔴 (workflow MED) Read is bounded to the wc SNAPSHOT (__T), not to the live EOF — `head -c` caps the tail so a
|
|
58
|
-
// producer writing between `wc -c` and `tail` cannot return bytes past __T (the driver advances the cursor to __T,
|
|
59
|
-
// so reading past it would DUPLICATE next poll AND defeat the readCap transfer bound). ≤readCap new bytes → the
|
|
60
|
-
// whole increment [cursor, __T); otherwise the LAST readCap bytes [__T-readCap, __T) (recent tail, like TOC) and
|
|
61
|
-
// the control plane records the gap as bytesDroppedBeforeCursor.
|
|
62
|
-
[
|
|
25
|
+
const slice = (file, cursor) => [
|
|
63
26
|
`__N=$((__T - ${cursor}))`,
|
|
64
27
|
`if [ "$__N" -gt 0 ]; then`,
|
|
65
28
|
` if [ "$__N" -le ${readCap} ]; then tail -c +$(( ${cursor} + 1 )) ${d}/${file} 2>/dev/null | head -c "$__N" | base64;`,
|
|
@@ -68,8 +31,6 @@ export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
|
|
|
68
31
|
].join("\n");
|
|
69
32
|
return [
|
|
70
33
|
`if [ ! -d ${d} ]; then echo SEMABG_GONE; exit 0; fi`,
|
|
71
|
-
// `$(( ... + 0 ))` normalizes the byte count: it both defaults empty→0 AND strips the leading whitespace that
|
|
72
|
-
// BSD/macOS `wc -c < file` emits (GNU/Debian pods don't pad, but keep the META line parseable on either).
|
|
73
34
|
`__OT=$(wc -c < ${d}/out 2>/dev/null); __OT=$(( \${__OT:-0} + 0 ))`,
|
|
74
35
|
`__ET=$(wc -c < ${d}/err 2>/dev/null); __ET=$(( \${__ET:-0} + 0 ))`,
|
|
75
36
|
`__EX=$( [ -f ${d}/exit ] && cat ${d}/exit 2>/dev/null || echo - )`,
|
|
@@ -85,33 +46,15 @@ export function buildBgPollScript(dir, stdoutCursor, stderrCursor, readCap) {
|
|
|
85
46
|
`echo SEMABG_X`,
|
|
86
47
|
].join("\n");
|
|
87
48
|
}
|
|
88
|
-
/** KillShell: graceful SIGTERM to the group, then a forced SIGKILL (idempotent — a dead group just errors out).
|
|
89
|
-
* The `|| kill <pid>` fallback covers an ADOPTED exec whose pgid prelude self-reported `$$` without a `setsid`
|
|
90
|
-
* (whether the k8s-exec'd sh is a group leader is runtime-dependent); a launch-mode runner is always setsid'd,
|
|
91
|
-
* so its group kill succeeds and the fallback never fires there. */
|
|
92
49
|
export function buildBgKillScript(dir) {
|
|
93
50
|
const d = shellQuote(dir);
|
|
94
51
|
return [
|
|
95
|
-
// 🔴 review LOW (Fable-5 #4 tightened): an ADOPTED shell (create-then-abort detach) can be killed in the
|
|
96
|
-
// sub-second window BEFORE its detach-capable exec even ran `mkdir` — let alone forked the job + wrote pgid.
|
|
97
|
-
// A bare read (or a dir-existence gate: the dir does not exist YET in that window, indistinguishable from
|
|
98
|
-
// "already ended + EXIT-trap-removed") would skip the kill yet report SEMABG_KILLED — a silent no-op, the
|
|
99
|
-
// command escapes the wall. So wait UNCONDITIONALLY for the pgid file (bounded ~2s, mirroring the LAUNCH
|
|
100
|
-
// path's pre-report pgid gate). Cost of the removed dir-gate: a kill racing the command's natural end (dir
|
|
101
|
-
// already EXIT-trap-removed) idles the full ~2s before no-op'ing — acceptable, because driver.kill only runs
|
|
102
|
-
// while the manager still believes the shell is running (that overlap window is sub-second), and the pod is
|
|
103
|
-
// disposable. For a launch-mode shell pgid is already present → first iteration exits (no-op).
|
|
104
52
|
`__i=0; while [ ! -s ${d}/pgid ] && [ $__i -lt 40 ]; do __i=$((__i+1)); sleep 0.05; done`,
|
|
105
53
|
`__PG=$(cat ${d}/pgid 2>/dev/null); __PG=\${__PG:--}`,
|
|
106
54
|
`if [ "$__PG" != "-" ]; then kill -TERM -"$__PG" 2>/dev/null || kill -TERM "$__PG" 2>/dev/null; sleep 1; kill -KILL -"$__PG" 2>/dev/null || kill -KILL "$__PG" 2>/dev/null; fi`,
|
|
107
55
|
`echo SEMABG_KILLED`,
|
|
108
56
|
].join("\n");
|
|
109
57
|
}
|
|
110
|
-
/** dispose: forced SIGKILL to the group + remove the job dir (no grace — teardown). Best-effort.
|
|
111
|
-
* ⚠️ Deliberately NO pgid-wait (unlike {@link buildBgKillScript}, review #4): a create-then-abort adopt disposed
|
|
112
|
-
* in the pre-mkdir window escapes THIS script — but every dispose path ends in pod deletion (task-end destroy;
|
|
113
|
-
* suspendVM deletes the pod after the snapshot), so the escape is pod-lifetime-bounded, and a wait here would
|
|
114
|
-
* add 2s × N shells to the suspend/teardown path for a sub-second window. Kill (the model-facing wall) waits. */
|
|
115
58
|
export function buildBgDisposeScript(dir) {
|
|
116
59
|
const d = shellQuote(dir);
|
|
117
60
|
return [
|
|
@@ -121,54 +64,27 @@ export function buildBgDisposeScript(dir) {
|
|
|
121
64
|
`echo SEMABG_DISPOSED`,
|
|
122
65
|
].join("\n");
|
|
123
66
|
}
|
|
124
|
-
/**
|
|
125
|
-
* design/116 detach: wrap a foreground exec so it is ADOPTABLE mid-flight, running the command as a real
|
|
126
|
-
* process-group LEADER so an adopted shell's KillShell / BG hard wall reap the WHOLE subtree.
|
|
127
|
-
*
|
|
128
|
-
* 🔴 review MED (why `setsid`, not bare `$$`): the wrapper `sh` that `kubectl exec` starts has a RUNTIME-DEPENDENT
|
|
129
|
-
* process group — under many container runtimes it is NOT its own group leader (its pgid is some ancestor's), so
|
|
130
|
-
* writing `$$` as the pgid and later `kill -$$` targets a group that does not exist → the fallback `kill $$` reaps
|
|
131
|
-
* only the wrapper, orphaning the command's children PAST the §3.6 hard wall (they keep streaming into the adopted
|
|
132
|
-
* shell's buffer until the pod's `activeDeadlineSeconds`). `setsid` starts the command in a NEW session ⇒ a new
|
|
133
|
-
* process group whose leader pid IS the backgrounded job pid, so `kill -<pid>` reaps the command and every
|
|
134
|
-
* descendant. The command's stdout/stderr fds (the exec socket's channels) are INHERITED across setsid — only the
|
|
135
|
-
* controlling terminal is dropped (there is none: kubectl-exec here is non-tty) — so output/exit still flow on the
|
|
136
|
-
* SAME WebSocket, which IS the adopted shell's feed. `wait` propagates the real exit code.
|
|
137
|
-
*
|
|
138
|
-
* The command is base64'd into a pod-side `cmd.sh` (no quoting hazard, mirrors the launch path). The pgid is written
|
|
139
|
-
* atomically (tmp+mv) AFTER the job forks, so a poll never reads a half-written id. Every step is best-effort: a pod
|
|
140
|
-
* without `setsid` (rare on the kata base image) degrades to the `$$` path (the pre-fix behavior), and a read-only
|
|
141
|
-
* /tmp degrades to a pgid-less adopt (kill no-ops; the disposable pod's teardown reaps) — never a broken exec. The
|
|
142
|
-
* EXIT trap removes the dir when the command ends normally, so non-detached execs don't litter the pod.
|
|
143
|
-
*/
|
|
144
67
|
export function buildDetachCapableExec(dir, command) {
|
|
145
68
|
const d = shellQuote(dir);
|
|
146
69
|
const q = (name) => shellQuote(`${dir}/${name}`);
|
|
147
70
|
const cmdB64 = Buffer.from(command, "utf8").toString("base64");
|
|
148
71
|
const writePgid = (val) => `printf %s ${val} > ${q("pgid.tmp")} 2>/dev/null && mv ${q("pgid.tmp")} ${q("pgid")} 2>/dev/null`;
|
|
149
72
|
return [
|
|
150
|
-
// Decode the command into a VARIABLE (not a pod-side file): a read-only-/tmp degrade must still RUN the command
|
|
151
|
-
// (review's degrade-safety case) — a file target under an un-creatable dir would leave nothing to run. `sh -c
|
|
152
|
-
// "$__cmd"` is byte-equivalent to sourcing a cmd.sh whose content is $__cmd. base64 has no quoting hazard.
|
|
153
73
|
`__cmd=$(printf %s '${cmdB64}' | base64 -d)`,
|
|
154
74
|
`mkdir -p ${d} 2>/dev/null`,
|
|
155
75
|
`trap 'rm -rf ${d} 2>/dev/null' EXIT`,
|
|
156
76
|
`if command -v setsid >/dev/null 2>&1; then`,
|
|
157
|
-
// setsid backgrounds the job as a new-session group leader; $! is that leader's pid == its pgid. Output fds are
|
|
158
|
-
// inherited (the exec socket), only the tty is dropped. `wait` blocks the wrapper until the job ends (or is
|
|
159
|
-
// group-killed) and yields its exit code — so exec/adopt semantics are unchanged, only the reap target is fixed.
|
|
160
77
|
` setsid sh -c "$__cmd" &`,
|
|
161
78
|
` __pg=$!`,
|
|
162
79
|
` ${writePgid('"$__pg"')}`,
|
|
163
80
|
` wait "$__pg"; __rc=$?`,
|
|
164
81
|
`else`,
|
|
165
|
-
` ${writePgid('"$$"')}`,
|
|
82
|
+
` ${writePgid('"$$"')}`,
|
|
166
83
|
` sh -c "$__cmd"; __rc=$?`,
|
|
167
84
|
`fi`,
|
|
168
85
|
`exit $__rc`,
|
|
169
86
|
].join("\n");
|
|
170
87
|
}
|
|
171
|
-
/** Parse {@link buildBgPollScript} stdout. Markers contain `_` (absent from the base64 alphabet) so they never collide. */
|
|
172
88
|
export function parseBgPollOutput(raw) {
|
|
173
89
|
if (raw.includes("SEMABG_GONE")) {
|
|
174
90
|
return { gone: true, outTotal: 0, errTotal: 0, alive: false, stdout: "", stdoutBytes: 0, stderr: "", stderrBytes: 0 };
|
|
@@ -1,26 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The confined, ADDITIVE pod-spec patch ({@link PodSpecPatch}/{@link applyPodSpecPatch}) + the exec-socket TLS
|
|
3
|
-
* shape ({@link execSocketTlsOptions}) + the v4 channel status-frame parse ({@link exitCodeFromStatus}) — the
|
|
4
|
-
* k8s adapter's protocol/patch layer, independent of pod lifecycle. Split out of `remote-env-k8s.ts` (design/158
|
|
5
|
-
* A13, internal-lossless — `remote-env-k8s.ts` re-exports every symbol below unchanged).
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Apply a {@link PodSpecPatch} to a sandbox pod object IN PLACE — purely ADDITIVE. Volumes + volumeMounts are
|
|
9
|
-
* APPENDED (never replaced); securityContext objects are shallow-MERGED onto any existing; labels are merged. It
|
|
10
|
-
* deliberately touches ONLY spec.volumes / spec.securityContext / containers[0].{volumeMounts,securityContext} /
|
|
11
|
-
* metadata.labels — so the hard isolation invariants (runtimeClassName, automountServiceAccountToken,
|
|
12
|
-
* restartPolicy, activeDeadlineSeconds, image, command) are preserved by construction. Pure + exported for testing.
|
|
13
|
-
*/
|
|
14
1
|
export function applyPodSpecPatch(pod, patch) {
|
|
15
|
-
// 🔴 wq64gmm5e: a securityContext patch is a HARDENING seam — it may only TIGHTEN, never weaken the isolation
|
|
16
|
-
// the L8 gradeP1 premise depends on. A blind spread let privileged:true / allowPrivilegeEscalation:true /
|
|
17
|
-
// capabilities.add[SYS_ADMIN] / readOnlyRootFilesystem:false through verbatim (and the "cannot weaken" test was
|
|
18
|
-
// fake-green). Fail CLOSED on any privilege-escalating key so the docstring claim is actually enforced.
|
|
19
2
|
assertHardeningOnly(patch.podSecurityContext, "podSecurityContext");
|
|
20
3
|
assertHardeningOnly(patch.containerSecurityContext, "containerSecurityContext");
|
|
21
4
|
if (patch.labels) {
|
|
22
|
-
// Additive + identity-preserving: merge the patch labels, then re-assert the base identity labels LAST so a
|
|
23
|
-
// patch can never clobber the app/managed-by labels the RBAC selectors + reaper key on (wq64gmm5e).
|
|
24
5
|
const identity = {};
|
|
25
6
|
for (const k of ["app", "managed-by"]) {
|
|
26
7
|
const v = pod.metadata.labels?.[k];
|
|
@@ -30,12 +11,6 @@ export function applyPodSpecPatch(pod, patch) {
|
|
|
30
11
|
pod.metadata.labels = { ...pod.metadata.labels, ...patch.labels, ...identity };
|
|
31
12
|
}
|
|
32
13
|
const spec = pod.spec;
|
|
33
|
-
// 🔴 Volumes are the SAME isolation-breach class as a privileged securityContext (review: workflow HIGH). Kata
|
|
34
|
-
// passes a hostPath through into the guest VM (virtio-fs/9p host share — NOT blocked by the VM boundary), so a
|
|
35
|
-
// `hostPath:{path:"/"}` volume mounts the k3s NODE root (containerd sock, other pods, host secrets) into the
|
|
36
|
-
// sandbox — defeating the L8 immutable-oracle integrity premise as completely as privileged:true. Reject any
|
|
37
|
-
// host-backed volume source; only pod-local sources (emptyDir/configMap/secret/projected/downwardAPI/ephemeral)
|
|
38
|
-
// are additive-safe.
|
|
39
14
|
assertVolumesPodLocal(patch.volumes, "volumes");
|
|
40
15
|
if (patch.volumes?.length)
|
|
41
16
|
spec.volumes = [...(spec.volumes ?? []), ...patch.volumes];
|
|
@@ -49,8 +24,6 @@ export function applyPodSpecPatch(pod, patch) {
|
|
|
49
24
|
container.securityContext = { ...(container.securityContext ?? {}), ...patch.containerSecurityContext };
|
|
50
25
|
}
|
|
51
26
|
}
|
|
52
|
-
/** Fail-closed direction guard for a securityContext patch: reject any key that WEAKENS isolation. A patch may
|
|
53
|
-
* drop capabilities + tighten, never escalate. Throws (caller surfaces as a provisioning failure). */
|
|
54
27
|
function assertHardeningOnly(sc, where) {
|
|
55
28
|
if (!sc)
|
|
56
29
|
return;
|
|
@@ -70,16 +43,12 @@ function assertHardeningOnly(sc, where) {
|
|
|
70
43
|
const caps = sc.capabilities;
|
|
71
44
|
if (caps && Array.isArray(caps.add) && caps.add.length > 0)
|
|
72
45
|
bad(`capabilities.add ${JSON.stringify(caps.add)} (a hardening patch may only .drop)`);
|
|
73
|
-
// procMount:"Unmasked" re-exposes the masked /proc paths; seccompProfile:"Unconfined" disables syscall filtering —
|
|
74
|
-
// both WEAKEN confinement (review: workflow HIGH secondary), so they belong on the fail-closed list.
|
|
75
46
|
if (sc.procMount === "Unmasked")
|
|
76
47
|
bad('procMount:"Unmasked" (re-exposes masked /proc)');
|
|
77
48
|
const seccomp = sc.seccompProfile;
|
|
78
49
|
if (seccomp && seccomp.type === "Unconfined")
|
|
79
50
|
bad('seccompProfile.type:"Unconfined" (disables syscall filtering)');
|
|
80
51
|
}
|
|
81
|
-
/** Fail-closed guard for patch volumes: a Kata guest shares hostPath/csi/flexVolume sources from the NODE, so only
|
|
82
|
-
* POD-LOCAL sources are additive-safe. Reject anything host-backed. Throws (caller surfaces as a provisioning fail). */
|
|
83
52
|
function assertVolumesPodLocal(volumes, where) {
|
|
84
53
|
if (!volumes?.length)
|
|
85
54
|
return;
|
|
@@ -94,19 +63,10 @@ function assertVolumesPodLocal(volumes, where) {
|
|
|
94
63
|
}
|
|
95
64
|
}
|
|
96
65
|
}
|
|
97
|
-
/**
|
|
98
|
-
* TLS options for the exec WebSocket, shaped for BOTH runtimes (2026-06-11 in-cluster drill finding):
|
|
99
|
-
* Bun's `ws` shim IGNORES node-ws's top-level `ca`/`rejectUnauthorized` (handshake fails against a
|
|
100
|
-
* cluster CA even with rejectUnauthorized:false) and reads a Bun-specific `tls` sub-object instead;
|
|
101
|
-
* Node's real `ws` uses the top-level keys and passes the unknown `tls` key harmlessly through to
|
|
102
|
-
* tls.connect. One merged shape serves both. Live-proven on build-host (probe: top-level ca → TLS handshake
|
|
103
|
-
* failed; tls.ca → OPEN).
|
|
104
|
-
*/
|
|
105
66
|
export function execSocketTlsOptions(ca, insecureTls) {
|
|
106
67
|
const caList = ca ? [ca] : undefined;
|
|
107
68
|
return { ca: caList, rejectUnauthorized: !insecureTls, tls: { ca: caList, rejectUnauthorized: !insecureTls } };
|
|
108
69
|
}
|
|
109
|
-
/** Parse the v4.channel.k8s.io status frame (channel 3) into an exit code, or a typed failure. */
|
|
110
70
|
export function exitCodeFromStatus(statusJson) {
|
|
111
71
|
try {
|
|
112
72
|
const s = JSON.parse(statusJson);
|
|
@@ -114,7 +74,6 @@ export function exitCodeFromStatus(statusJson) {
|
|
|
114
74
|
return { exitCode: 0 };
|
|
115
75
|
const cause = s.details?.causes?.find((c) => c.reason === "ExitCode");
|
|
116
76
|
if (cause?.message?.trim()) {
|
|
117
|
-
// trim-guard: Number("")/Number(" ") === 0 — an empty message must NOT read as a fake exit 0 on a Failure
|
|
118
77
|
const code = Number(cause.message);
|
|
119
78
|
if (Number.isFinite(code))
|
|
120
79
|
return { exitCode: code };
|