@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
package/dist/auth-bridge.js
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 鉴权桥 verifier(REGISTRY-REBUILD §5 M3 service 半场)——验 registry 铸的用户 access JWT,
|
|
3
|
-
* `sub` 直喂既有 x-agent-principal 体系。规范=`@sema-agent/registry-core/api/auth-bridge` R1-R10(零 IO 纯契约,
|
|
4
|
-
* 决策函数直接复用);本模块补齐 IO+crypto 半场:JWKS 拉取/缓存、RS256 真验签(node:crypto,零新依赖)。
|
|
5
|
-
*
|
|
6
|
-
* 信任模型:worker 已信 registry 的 effective config,同一信任根延伸到它的 JWKS——零新配置项
|
|
7
|
-
* (issuer/jwksUrl 默认从 SEMA_REGISTRY_URL 派生;split 部署可 env 显式覆盖)。既有 SERVICE_AUTH_TOKEN(S)
|
|
8
|
-
* 分支不动:bearer 长得像 JWT(三段 base64url)才走本分支(looksLikeJwt=路由预过滤,非校验)。
|
|
9
|
-
*
|
|
10
|
-
* R10:本模块任何路径不 log token/签名段/密钥材料——只 log kid + 错误码 + iss。
|
|
11
|
-
*/
|
|
12
1
|
import { createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
13
2
|
import { AUTH_BRIDGE_ALGS, DEFAULT_CLOCK_SKEW_SEC, DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC, DEFAULT_OFFLINE_GRACE_SEC, REGISTRY_JWKS_PATH, RegistryJwks, classifyTimeClaims, jwksWithinOfflineGrace, shouldForceJwksRefresh, toVerifiedIdentity, } from "@sema-agent/registry-core/api/auth-bridge";
|
|
14
3
|
const DEFAULT_JWKS_TTL_SEC = 15 * 60;
|
|
15
|
-
/** JWKS 响应体字节上限(一个配错或超大响应的端点不应让 worker 无界缓冲;正常 JWKS 千字节级)。 */
|
|
16
4
|
const MAX_JWKS_BYTES = 256 * 1024;
|
|
17
|
-
/** 整 token 长度上限(签名段无独立上限,故对整 bearer 统一设限;正常 JWT ~1KiB)。 */
|
|
18
5
|
const MAX_TOKEN_CHARS = 16 * 1024;
|
|
19
|
-
/** 单段解码上限(header/payload)。 */
|
|
20
6
|
const MAX_SEGMENT_BYTES = 64 * 1024;
|
|
21
|
-
/** base64url 字符集严格校验——Node 的 base64url 解码会容忍集外字符(仍解出同一字节),而契约要求"三段
|
|
22
|
-
* base64url"形态严格,故解码前每段必须匹配该字符集。 */
|
|
23
7
|
const B64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
24
8
|
function b64urlJson(seg) {
|
|
25
9
|
if (seg.length > MAX_SEGMENT_BYTES || !B64URL_RE.test(seg))
|
|
@@ -33,10 +17,6 @@ function b64urlJson(seg) {
|
|
|
33
17
|
}
|
|
34
18
|
}
|
|
35
19
|
const stripSlash = (s) => s.replace(/\/+$/, "");
|
|
36
|
-
/**
|
|
37
|
-
* registry-JWT verifier(单例,进程级 JWKS 缓存)。`verify(bearer)` 全程不 throw——一切异常折为
|
|
38
|
-
* AUTH_BRIDGE_ERROR_CODES(HTTP 面由调用方恒答 generic 401,码只进日志/metrics,R9)。
|
|
39
|
-
*/
|
|
40
20
|
export function createRegistryJwtVerifier(opts) {
|
|
41
21
|
const issuer = stripSlash(opts.issuer);
|
|
42
22
|
const jwksUrl = opts.jwksUrl ?? `${issuer}${REGISTRY_JWKS_PATH}`;
|
|
@@ -46,14 +26,12 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
46
26
|
const jwksTtlSec = opts.jwksTtlSec ?? DEFAULT_JWKS_TTL_SEC;
|
|
47
27
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
48
28
|
const now = opts.nowMs ?? Date.now;
|
|
49
|
-
// 进程级缓存(R3/R8 的状态面;shouldForceJwksRefresh/jwksWithinOfflineGrace 消费的快照即它)。
|
|
50
29
|
const keys = new Map();
|
|
51
30
|
let lastFetchAttemptMs;
|
|
52
31
|
let lastFetchSuccessMs;
|
|
53
|
-
let inflight;
|
|
32
|
+
let inflight;
|
|
54
33
|
const fail = (error, kid) => {
|
|
55
34
|
opts.metrics?.inc("auth_bridge_verify_total", { outcome: error });
|
|
56
|
-
// R10:仅 kid+码+iss。kid 来自**调用方可控**的 token header(16K token 上限内可到 ~10KB)——截断防日志放大。
|
|
57
35
|
opts.logger?.warn?.("auth_bridge_rejected", { error, ...(kid ? { kid: kid.slice(0, 128) } : {}), iss: issuer });
|
|
58
36
|
return { ok: false, error };
|
|
59
37
|
};
|
|
@@ -66,19 +44,16 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
66
44
|
const res = await doFetch(jwksUrl, { redirect: "error", signal: AbortSignal.timeout(10_000) });
|
|
67
45
|
if (!res.ok)
|
|
68
46
|
throw new Error(`jwks HTTP ${res.status}`);
|
|
69
|
-
// 先看 Content-Length 早拒;chunked/无 CL 的响应会整体读入后按字节拒(非流式截断——jwksUrl 是
|
|
70
|
-
// operator 钉死的可信 registry + redirect:"error" + 10s timeout,瞬时内存风险接受,诚实记档)。
|
|
71
47
|
const cl = Number(res.headers.get("content-length"));
|
|
72
48
|
if (Number.isFinite(cl) && cl > MAX_JWKS_BYTES)
|
|
73
49
|
throw new Error("jwks content-length too large");
|
|
74
50
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
75
51
|
if (buf.byteLength > MAX_JWKS_BYTES)
|
|
76
52
|
throw new Error("jwks body too large");
|
|
77
|
-
const doc = RegistryJwks.parse(JSON.parse(buf.toString("utf8")));
|
|
53
|
+
const doc = RegistryJwks.parse(JSON.parse(buf.toString("utf8")));
|
|
78
54
|
const next = new Map();
|
|
79
55
|
for (const k of doc.keys) {
|
|
80
56
|
try {
|
|
81
|
-
// 公钥成员 only(契约警告:即便 registry 出 bug 带了私有成员也绝不使用/回显)。
|
|
82
57
|
next.set(k.kid, createPublicKey({ key: { kty: "RSA", n: k.n, e: k.e }, format: "jwk" }));
|
|
83
58
|
}
|
|
84
59
|
catch {
|
|
@@ -91,7 +66,6 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
91
66
|
lastFetchSuccessMs = now();
|
|
92
67
|
}
|
|
93
68
|
catch (e) {
|
|
94
|
-
// 失败=保留旧缓存(R8 离线宽限判定在 verify 侧);只记账不 throw。
|
|
95
69
|
opts.logger?.warn?.("auth_bridge_jwks_fetch_failed", { iss: issuer, error: String(e).slice(0, 200) });
|
|
96
70
|
}
|
|
97
71
|
finally {
|
|
@@ -101,8 +75,6 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
101
75
|
return inflight;
|
|
102
76
|
}
|
|
103
77
|
async function verify(bearer) {
|
|
104
|
-
// 延迟观测(auth_bridge_verify_seconds):热路径(kid 命中)应为亚毫秒级;冷路径(JWKS 拉取)带一次
|
|
105
|
-
// 网络 RTT——首请求/轮换时的尾延迟在这根直方图上可见,不用猜。
|
|
106
78
|
const t0 = now();
|
|
107
79
|
try {
|
|
108
80
|
return await verifyInner(bearer);
|
|
@@ -115,57 +87,43 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
115
87
|
if (bearer.length > MAX_TOKEN_CHARS)
|
|
116
88
|
return fail("malformed");
|
|
117
89
|
const parts = bearer.split(".");
|
|
118
|
-
// 三段非空 + 每段 base64url 字符集(b64urlJson 已校验 header/payload;签名段在此单独校验)。
|
|
119
90
|
if (parts.length !== 3 || parts.some((p) => p.length === 0) || !B64URL_RE.test(parts[2]))
|
|
120
91
|
return fail("malformed");
|
|
121
92
|
const header = b64urlJson(parts[0]);
|
|
122
93
|
const payload = b64urlJson(parts[1]);
|
|
123
94
|
if (!header || !payload)
|
|
124
95
|
return fail("malformed");
|
|
125
|
-
// R1 — alg 固定在调用侧:只接受 RS256,不读 token 的 alg 字段来决定验法(none/HS*/ES*/PS* 一律拒)。
|
|
126
|
-
// 错误码归类跟契约(AUTH_BRIDGE_ERROR_CODES 注释):disallowed alg / 缺 kid = `malformed`
|
|
127
|
-
// (invalid_signature 留给"键在、验签不过"——异源复审对齐,跨侧 metrics 口径一致)。
|
|
128
96
|
if (header.alg !== AUTH_BRIDGE_ALGS[0])
|
|
129
97
|
return fail("malformed");
|
|
130
98
|
const kid = typeof header.kid === "string" ? header.kid : undefined;
|
|
131
99
|
if (!kid)
|
|
132
100
|
return fail("malformed");
|
|
133
|
-
// R4 — issuer 先于取键比较(iss 不对根本不必碰 JWKS)。
|
|
134
101
|
const iss = typeof payload.iss === "string" ? stripSlash(payload.iss) : undefined;
|
|
135
102
|
if (iss !== issuer)
|
|
136
103
|
return fail("unknown_issuer", kid);
|
|
137
|
-
// R5 — 时间窗(60s 偏移;exp 必须在)。
|
|
138
104
|
if (classifyTimeClaims(payload, Math.floor(now() / 1000), clockSkewSec) !== "ok")
|
|
139
105
|
return fail("expired", kid);
|
|
140
|
-
// R2/R3 — 取键。热路径:kid 命中 ⇒ 直用,零 fetch(TTL 陈旧只在无在途拉取且过节流时【后台】刷一次
|
|
141
|
-
// [void,不 await],不阻塞验证)。kid miss ⇒ 先 await 任何在途拉取(并发请求不应被 lastFetchAttemptMs
|
|
142
|
-
// 节流跳过而错过正在完成的刷新),再查;仍 miss 且过节流 ⇒ 强制刷一次。
|
|
143
106
|
const snapshot = () => ({ cachedKids: [...keys.keys()], lastFetchAttemptMs, nowMs: now() });
|
|
144
107
|
if (keys.has(kid)) {
|
|
145
108
|
const ttlStale = lastFetchSuccessMs === undefined || now() - lastFetchSuccessMs > jwksTtlSec * 1000;
|
|
146
109
|
if (ttlStale && !inflight && (lastFetchAttemptMs === undefined || now() - lastFetchAttemptMs >= refreshMin * 1000)) {
|
|
147
|
-
void refreshJwks();
|
|
110
|
+
void refreshJwks();
|
|
148
111
|
}
|
|
149
112
|
}
|
|
150
113
|
else {
|
|
151
114
|
if (inflight)
|
|
152
|
-
await inflight;
|
|
115
|
+
await inflight;
|
|
153
116
|
if (!keys.has(kid) && shouldForceJwksRefresh(kid, snapshot(), { jwksRefreshMinIntervalSec: refreshMin }))
|
|
154
117
|
await refreshJwks();
|
|
155
118
|
}
|
|
156
119
|
const key = keys.get(kid);
|
|
157
120
|
if (!key) {
|
|
158
|
-
// R8 — kid 仍未命中:有【活】缓存(有成功拉取且在 24h 宽限内)⇒ 键该在却不在=token 与当前键集不符
|
|
159
|
-
// (invalid_signature)。从没成功拉过 / 超宽限 ⇒ jwks_unavailable(失败即拒:不用 `keys.size>0` 兜底,
|
|
160
|
-
// 因为超宽限的陈旧缓存 size>0 时也不应把 unavailable 归成 invalid_signature)。
|
|
161
121
|
const graceOk = lastFetchSuccessMs !== undefined && jwksWithinOfflineGrace({ lastFetchSuccessMs, nowMs: now() }, { offlineGraceSec });
|
|
162
122
|
return graceOk ? fail("invalid_signature", kid) : fail("jwks_unavailable", kid);
|
|
163
123
|
}
|
|
164
124
|
if (lastFetchSuccessMs !== undefined && !jwksWithinOfflineGrace({ lastFetchSuccessMs, nowMs: now() }, { offlineGraceSec })) {
|
|
165
|
-
// 缓存超出 24h 离线宽限——即便 kid 命中也拒(R8:宽限后不再使用陈旧键)。
|
|
166
125
|
return fail("jwks_unavailable", kid);
|
|
167
126
|
}
|
|
168
|
-
// 真验签(RS256 = RSASSA-PKCS1-v1_5 + SHA-256 over `header.payload`)。
|
|
169
127
|
let sigOk = false;
|
|
170
128
|
try {
|
|
171
129
|
sigOk = cryptoVerify("RSA-SHA256", Buffer.from(`${parts[0]}.${parts[1]}`, "utf8"), key, Buffer.from(parts[2], "base64url"));
|
|
@@ -175,7 +133,6 @@ export function createRegistryJwtVerifier(opts) {
|
|
|
175
133
|
}
|
|
176
134
|
if (!sigOk)
|
|
177
135
|
return fail("invalid_signature", kid);
|
|
178
|
-
// R6/R7/R9 — 身份映射(契约纯函数)。
|
|
179
136
|
const mapped = toVerifiedIdentity(payload, opts.audience ? { audience: opts.audience } : undefined);
|
|
180
137
|
if (!mapped.ok)
|
|
181
138
|
return fail(mapped.error, kid);
|
package/dist/auth-keys.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AUTH KEY-SET parsing (design/158 A4) — the env→key-set decoders for the two D-G credential planes, moved VERBATIM
|
|
3
|
-
* out of security.ts: {@link parseApprovalHmacKeys} (APPROVAL_HMAC_KEYS — the rotating HMAC secret set, decision
|
|
4
|
-
* INTEGRITY) and {@link parsePrincipalJwks} (PRINCIPAL_JWT_PUBKEYS — the rotating issuer public-key set, principal
|
|
5
|
-
* IDENTITY). config.ts consumes ONLY these two functions + their row types at env-load time, and importing them from
|
|
6
|
-
* security.ts made the base config layer value-depend on the whole 55KiB auth module (lens2 §F). This file is a pure
|
|
7
|
-
* leaf (JSON.parse + shape filters, zero repo imports); the VERIFIERS that consume the parsed key-sets
|
|
8
|
-
* (verifyApprovalHmac / verifyPrincipalJwt) stay in ./approval-hmac.ts and ./principal-jwt.ts; consumers import these four names from HERE ([2354] 兼容面全清).
|
|
9
|
-
*/
|
|
10
|
-
/** Parse APPROVAL_HMAC_KEYS — a sema-registry-managed, rotating key-SET, as a JSON array of {kid,key,status}.
|
|
11
|
-
* Empty / unset / unparseable ⇒ `[]` ⇒ HMAC verification is OFF (D-G inactive; back-compat, BFF-only door).
|
|
12
|
-
* fail-closed 语义与改前**逐字相同**;新增的只有 `onReject`(#210:每一条被丢弃的条目都要说出来)。 */
|
|
13
1
|
export function parseApprovalHmacKeys(raw, onReject) {
|
|
14
2
|
if (!raw)
|
|
15
3
|
return [];
|
|
@@ -18,9 +6,6 @@ export function parseApprovalHmacKeys(raw, onReject) {
|
|
|
18
6
|
v = JSON.parse(raw);
|
|
19
7
|
}
|
|
20
8
|
catch {
|
|
21
|
-
// ⚠️ 解析器自带的 message **不能**转述:V8 的 `JSON.parse` 报错里嵌了输入片段
|
|
22
|
-
// (`Unexpected token 'T', "TOP_SECRET"... is not valid JSON`),而这条 env 的内容就是对称密钥 ——
|
|
23
|
-
// 把它写进 boot 日志等于把密钥前缀印出来。只说「不是合法 JSON」,定位靠 env 名本身。
|
|
24
9
|
onReject({ index: "-", reason: "not valid JSON (value withheld: this env holds secrets)" });
|
|
25
10
|
return [];
|
|
26
11
|
}
|
|
@@ -43,10 +28,6 @@ export function parseApprovalHmacKeys(raw, onReject) {
|
|
|
43
28
|
});
|
|
44
29
|
return out;
|
|
45
30
|
}
|
|
46
|
-
/** Parse PRINCIPAL_JWT_PUBKEYS — a JSON array of {kid,alg,key,status}. Empty/unset/malformed ⇒ [] ⇒ no trust
|
|
47
|
-
* anchor ⇒ the direct door cannot open (D-G inactive). Each entry must have kid + a pinned alg + a PEM key.
|
|
48
|
-
* #210: fail-closed 语义逐字不变,但每条被丢弃的条目经 `onReject` 报出(序号 + 缺陷字段名,不带值)——
|
|
49
|
-
* 这里的 key 是**公**钥,但 reason 仍只说字段名,与 HMAC 那条同形(两个出口的口径不该分岔)。 */
|
|
50
31
|
export function parsePrincipalJwks(raw, onReject) {
|
|
51
32
|
if (!raw)
|
|
52
33
|
return [];
|
|
@@ -56,8 +37,6 @@ export function parsePrincipalJwks(raw, onReject) {
|
|
|
56
37
|
v = JSON.parse(raw);
|
|
57
38
|
}
|
|
58
39
|
catch {
|
|
59
|
-
// 同 parseApprovalHmacKeys:不转述 V8 的 message(它嵌输入片段)。这一口装的是**公**钥,但两个出口
|
|
60
|
-
// 的口径不该分岔 —— 一个「视情况才脱敏」的规矩,迟早会在错的那一侧被抄走。
|
|
61
40
|
onReject({ index: "-", reason: "not valid JSON (value withheld)" });
|
|
62
41
|
return [];
|
|
63
42
|
}
|
|
@@ -73,8 +52,6 @@ export function parsePrincipalJwks(raw, onReject) {
|
|
|
73
52
|
return;
|
|
74
53
|
}
|
|
75
54
|
const bad = ["kid", "key"].filter((f) => typeof e[f] !== "string");
|
|
76
|
-
// alg 单列:缺席与「不在闭集」是两种手滑,但都必须点名 `alg` —— 运维看到字段名才知道去改哪一格
|
|
77
|
-
// (alg-confusion 防线就挂在这条钉上,静默滤掉等于防线自己消失了没人知道)。
|
|
78
55
|
if (typeof e.alg !== "string")
|
|
79
56
|
bad.push("alg");
|
|
80
57
|
else if (!ALGS.has(e.alg))
|
package/dist/bake-runner/main.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bake-runner ENTRYPOINT (IMAGE-API-DESIGN.md §P2.0/§P2.12) — the privileged build-host daemon. It wires the REAL I/O
|
|
3
|
-
* (HTTP to image-api, the setsid-detached build.sh child, the fs manifest read, git checkout, df) into the
|
|
4
|
-
* I/O-injected `BakeRunner` core (./runner.ts). This process runs ONLY on the build-host build host — it CANNOT be
|
|
5
|
-
* unit-run here (no docker/ctr/recipes); the testable logic is the pure protocol + the runner orchestration.
|
|
6
|
-
*
|
|
7
|
-
* Crown-jewel discipline (§P2.4e): the only secrets are BAKE_RUNNER_TOKEN (the operator-principal bearer for
|
|
8
|
-
* claim/heartbeat) and the per-bake ingest secret minted by image-api at claim — neither is ever logged.
|
|
9
|
-
*/
|
|
10
1
|
import { spawn, exec as execCb } from "node:child_process";
|
|
11
2
|
import { readFile } from "node:fs/promises";
|
|
12
3
|
import { promisify } from "node:util";
|
|
@@ -15,12 +6,6 @@ import { parseNumOrFailNonNegative } from "../config.js";
|
|
|
15
6
|
import { shellQuote as shellSafe } from "../plugins/remote-shell.js";
|
|
16
7
|
import { BakeRunner, } from "./runner.js";
|
|
17
8
|
const exec = promisify(execCb);
|
|
18
|
-
// 鲁棒性批5 A3(2026-08-05):claim/ingest/heartbeat 三个 fetch 此前零超时——bake-runner 的整条 loop()
|
|
19
|
-
// 是这台构建宿主机唯一的主循环,一次悬挂的 image-api 连接(网络分区/黑洞 TCP)会让 claim/heartbeat 永久
|
|
20
|
-
// 挂起,直到底层 socket 超时(可能几十分钟),期间既不上报心跳也不能认领新 bake——整机静默停摆。
|
|
21
|
-
// 上界两档:claim/ingest 不在心跳热路径,给 30s 吃满慢网 RTT + 大 body;heartbeat 必须显著小于
|
|
22
|
-
// BAKE_HEARTBEAT_MS 的默认量级(30s)——否则一次超时本身就吃掉一整个心跳周期,取 10s(同
|
|
23
|
-
// fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS` 判据的普适形)。
|
|
24
9
|
const BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS = 30_000;
|
|
25
10
|
const BAKE_HEARTBEAT_FETCH_TIMEOUT_MS = 10_000;
|
|
26
11
|
function loadEnv() {
|
|
@@ -38,26 +23,14 @@ function loadEnv() {
|
|
|
38
23
|
recipeDir: process.env.RECIPE_DIR || "/opt/recipes",
|
|
39
24
|
buildShPath: process.env.BUILD_SH_PATH || "e2b-template/dev-sandbox/build.sh",
|
|
40
25
|
dataDir: process.env.BAKE_DATA_DIR || "/data",
|
|
41
|
-
// A2/E5 (gap-sweep 2026-08-01): these were bare `Number(process.env.X || default)` — a unit-suffixed or
|
|
42
|
-
// typo'd value (`"30s"`) silently became NaN, and NaN survives every downstream `??`/`?? default` guard
|
|
43
|
-
// (those only catch null/undefined). The fallout is a SILENT-LOOSENING pair: `clock.sleep(NaN)` resolves
|
|
44
|
-
// ~immediately (`setTimeout(fn, NaN)` behaves like a 0ms timer) turning the lease heartbeat into a tight
|
|
45
|
-
// POST loop against image-api, and `freeGb < NaN` is always false so the pre-flight disk guard silently
|
|
46
|
-
// never fires. `parseNumOrFailNonNegative` (config.ts, shared with leader/wire.ts's four resource knobs)
|
|
47
|
-
// fails loud at bake-runner startup instead — non-numeric OR negative both throw; unset/empty still falls
|
|
48
|
-
// back to the documented default (zero behavior change on the happy path).
|
|
49
26
|
heartbeatMs: parseNumOrFailNonNegative("BAKE_HEARTBEAT_MS", process.env.BAKE_HEARTBEAT_MS || "30000"),
|
|
50
27
|
idlePollMs: parseNumOrFailNonNegative("BAKE_IDLE_POLL_MS", process.env.BAKE_IDLE_POLL_MS || "5000"),
|
|
51
28
|
minFreeGb: parseNumOrFailNonNegative("IMAGE_BAKE_MIN_FREE_GB", process.env.IMAGE_BAKE_MIN_FREE_GB || "20"),
|
|
52
29
|
};
|
|
53
30
|
}
|
|
54
|
-
/** The real image-api HTTP client. Authenticates claim/heartbeat with the runner bearer; ingest additionally
|
|
55
|
-
* carries the per-bake ingest secret header (so image-api can reject a stale/rogue runner — §P2.4e). */
|
|
56
31
|
function makeApiClient(env, log) {
|
|
57
32
|
const auth = { authorization: `Bearer ${env.token}`, "content-type": "application/json" };
|
|
58
33
|
return {
|
|
59
|
-
// Long-poll claim (§P2.12): the bare `POST …/bakes/claim` finds the oldest queued bake AND leases it in one
|
|
60
|
-
// call (the discovery + the single-flight CAS server-side). 204 ⇒ the queue is empty (the runner idle-polls).
|
|
61
34
|
async claimNext() {
|
|
62
35
|
const res = await fetch(`${env.imageApiBase}/v1/images/bakes/claim`, {
|
|
63
36
|
method: "POST",
|
|
@@ -66,23 +39,13 @@ function makeApiClient(env, log) {
|
|
|
66
39
|
signal: AbortSignal.timeout(BAKE_CLAIM_INGEST_FETCH_TIMEOUT_MS),
|
|
67
40
|
});
|
|
68
41
|
if (res.status === 204)
|
|
69
|
-
return null;
|
|
70
|
-
// C6/C1 (2026-08-01): 401 (bad runner credential) / 409 (lost the single-flight CAS to another runner
|
|
71
|
-
// replica) / 5xx previously fell into this SAME silent-`null` branch as an empty queue — a misconfigured
|
|
72
|
-
// BAKE_RUNNER_TOKEN and a healthy idle runner were indistinguishable from the outside (both just idle-poll
|
|
73
|
-
// forever with zero log lines). `bake_claim_error` (loop()'s catch) only fires on a THROWN error — a non-ok
|
|
74
|
-
// HTTP response never throws — so this warn is the only trace a non-2xx claim response leaves anywhere.
|
|
42
|
+
return null;
|
|
75
43
|
if (!res.ok) {
|
|
76
44
|
log.warn("bake_claim_rejected", { status: res.status });
|
|
77
45
|
return null;
|
|
78
46
|
}
|
|
79
47
|
const c = (await res.json());
|
|
80
48
|
if (!c.bakeId || !Array.isArray(c.argv) || !c.ingestSecret) {
|
|
81
|
-
// A malformed 2xx body is worse than a rejection: image-api's claim call ATOMICALLY leases the bake
|
|
82
|
-
// server-side before returning it, so a shape we refuse to trust here is a bake already checked OUT
|
|
83
|
-
// from the queue that we are about to drop on the floor. We still can't safely act on an untrusted
|
|
84
|
-
// shape (no bakeId ⇒ no way to release/fail it), so this warn is the only surfacing point until
|
|
85
|
-
// image-api's stale-lease reaper reclaims it (§P2.7) — see runner.ts module doc for the reaper backstop.
|
|
86
49
|
log.warn("bake_claim_malformed", { status: res.status, bakeId: typeof c.bakeId === "string" ? c.bakeId : undefined });
|
|
87
50
|
return null;
|
|
88
51
|
}
|
|
@@ -108,11 +71,10 @@ function makeApiClient(env, log) {
|
|
|
108
71
|
return {
|
|
109
72
|
accepted: res.ok && body.accepted !== false,
|
|
110
73
|
cancelRequested: body.cancelRequested === true,
|
|
111
|
-
leaseValid: res.status !== 409,
|
|
74
|
+
leaseValid: res.status !== 409,
|
|
112
75
|
};
|
|
113
76
|
},
|
|
114
77
|
async heartbeat(bakeId, ingestSecret) {
|
|
115
|
-
// A heartbeat is a no-frame ingest: image-api advances the lease and echoes the cancel flag (§P2.7/§P2.11).
|
|
116
78
|
const res = await fetch(`${env.imageApiBase}/v1/images/bakes/${encodeURIComponent(bakeId)}/ingest`, {
|
|
117
79
|
method: "POST",
|
|
118
80
|
headers: { ...auth, "x-bake-ingest-secret": ingestSecret },
|
|
@@ -128,13 +90,9 @@ function makeApiClient(env, log) {
|
|
|
128
90
|
},
|
|
129
91
|
};
|
|
130
92
|
}
|
|
131
|
-
/** The real child spawner: `setsid`-style detached process GROUP so a cancel/deadline kill signals the whole tree
|
|
132
|
-
* (channel-close / a plain kill of the immediate child does NOT stop a detached build — §P2.11). */
|
|
133
93
|
function makeSpawner() {
|
|
134
94
|
return {
|
|
135
95
|
spawn(opts) {
|
|
136
|
-
// detached:true → the child becomes a process-group leader (its pgid == its pid); `process.kill(-pid, sig)`
|
|
137
|
-
// then signals the WHOLE group. We DON'T `unref()` — the runner waits on the exit promise.
|
|
138
96
|
const child = spawn(opts.argv[0], opts.argv.slice(1), {
|
|
139
97
|
cwd: opts.cwd,
|
|
140
98
|
detached: true,
|
|
@@ -146,13 +104,12 @@ function makeSpawner() {
|
|
|
146
104
|
bufferLines(child.stderr, opts.onStderrLine);
|
|
147
105
|
const exited = new Promise((resolve) => {
|
|
148
106
|
child.on("exit", (code, signal) => resolve({ code, signal }));
|
|
149
|
-
child.on("error", () => resolve({ code: 127, signal: null }));
|
|
107
|
+
child.on("error", () => resolve({ code: 127, signal: null }));
|
|
150
108
|
});
|
|
151
109
|
const handle = {
|
|
152
110
|
pgid,
|
|
153
111
|
exited,
|
|
154
112
|
killGroup(signal) {
|
|
155
|
-
// Negative pid → signal the process GROUP (the detached tree), not just the immediate child.
|
|
156
113
|
if (pgid > 0)
|
|
157
114
|
process.kill(-pgid, signal);
|
|
158
115
|
},
|
|
@@ -161,8 +118,6 @@ function makeSpawner() {
|
|
|
161
118
|
},
|
|
162
119
|
};
|
|
163
120
|
}
|
|
164
|
-
/** Split a readable stream into lines, invoking `onLine` per complete line (the runner parses ONLY stdout as JSON;
|
|
165
|
-
* stderr lines are the failure tail). Carries a partial trailing line across chunks; flushes it on stream end. */
|
|
166
121
|
function bufferLines(stream, onLine) {
|
|
167
122
|
if (!stream)
|
|
168
123
|
return;
|
|
@@ -182,7 +137,6 @@ function bufferLines(stream, onLine) {
|
|
|
182
137
|
onLine(buf);
|
|
183
138
|
});
|
|
184
139
|
}
|
|
185
|
-
/** The real host ops: fs manifest read, df /data, recipe checkout, and the thin docker-push big-blob retry. */
|
|
186
140
|
function makeHostOps(env) {
|
|
187
141
|
return {
|
|
188
142
|
async readManifest(path) {
|
|
@@ -193,14 +147,6 @@ function makeHostOps(env) {
|
|
|
193
147
|
return stdout;
|
|
194
148
|
},
|
|
195
149
|
async checkoutRecipe(ref) {
|
|
196
|
-
// The recipe dir is a git checkout of the service repo pinned to the runner's deployed commit (NOT a frozen
|
|
197
|
-
// copy) so build.sh's `git rev-parse` gitsha is REAL and the manifest's recipeGitSha is replay-sufficient.
|
|
198
|
-
// A fixed ref (the deployed image's commit) → fetch + checkout is idempotent; a non-default ref is fetched.
|
|
199
|
-
// 🔴 A ref may be a branch/tag (fetchable by name) OR a BARE COMMIT SHA — and `git fetch origin <sha>` fails
|
|
200
|
-
// on most servers ("couldn't find remote ref <sha>"; only refs are fetchable). So try the cheap shallow
|
|
201
|
-
// ref-fetch, and on failure fall back to fetching the repo and checking the SHA out of the local history
|
|
202
|
-
// (the init-container clone usually already contains it). RECIPE_REF=HEAD skips this entirely (uses the
|
|
203
|
-
// checkout as-is — the prod path, the init-container pins the commit). [live-caught on center's prod deploy]
|
|
204
150
|
if (ref && ref !== "HEAD") {
|
|
205
151
|
const dir = shellSafe(env.recipeDir);
|
|
206
152
|
try {
|
|
@@ -211,14 +157,9 @@ function makeHostOps(env) {
|
|
|
211
157
|
await exec(`git -C ${dir} fetch origin && git -C ${dir} checkout --quiet --detach ${shellSafe(ref)}`);
|
|
212
158
|
}
|
|
213
159
|
}
|
|
214
|
-
// build.sh resolves itself relative to its own dir; the cwd for the spawn is the recipe checkout root so the
|
|
215
|
-
// path RECIPE_DIR + BUILD_SH_PATH is the script the runner invokes.
|
|
216
160
|
return env.recipeDir;
|
|
217
161
|
},
|
|
218
162
|
async dockerPushRetry(tag, attempts) {
|
|
219
|
-
// Thin `docker push` wrapper (NOT build.sh --replay — that re-bakes + hits the dup-check, exit 10). Retries a
|
|
220
|
-
// flaky big-blob registry layer push with backoff (born on SWR — deprecated; pool now docker.io/claybobby);
|
|
221
|
-
// NO_PROXY (incl. the registry domain) lives in this process's env.
|
|
222
163
|
for (let i = 0; i < attempts; i++) {
|
|
223
164
|
try {
|
|
224
165
|
await exec(`docker push ${shellSafe(tag)}`);
|
|
@@ -251,7 +192,7 @@ async function main() {
|
|
|
251
192
|
recipeRef: env.recipeRef,
|
|
252
193
|
heartbeatMs: env.heartbeatMs,
|
|
253
194
|
minFreeGb: env.minFreeGb,
|
|
254
|
-
});
|
|
195
|
+
});
|
|
255
196
|
const runner = new BakeRunner({
|
|
256
197
|
api: makeApiClient(env, log),
|
|
257
198
|
spawner: makeSpawner(),
|
|
@@ -272,11 +213,9 @@ async function main() {
|
|
|
272
213
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
273
214
|
await runner.loop();
|
|
274
215
|
}
|
|
275
|
-
// Only run when invoked directly (not when imported by a test).
|
|
276
216
|
const invokedDirectly = process.argv[1] !== undefined && /bake-runner[/\\]main\.(ts|js)$/.test(process.argv[1]);
|
|
277
217
|
if (invokedDirectly) {
|
|
278
218
|
main().catch((err) => {
|
|
279
|
-
// eslint-disable-next-line no-console
|
|
280
219
|
console.error(JSON.stringify({ ts: new Date().toISOString(), level: "error", msg: "bake_runner_fatal", err: String(err) }));
|
|
281
220
|
process.exit(1);
|
|
282
221
|
});
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bake-runner PURE protocol layer (IMAGE-API-DESIGN.md §P2.12) — the line→frame mapping + the small policy
|
|
3
|
-
* helpers, factored free of I/O so they are unit-testable WITHOUT the privileged build-host build host (the runner
|
|
4
|
-
* itself can only be exercised live; this is the part a unit test pins).
|
|
5
|
-
*
|
|
6
|
-
* The runner spawns `build.sh <vetted argv> --json` and forwards each STDOUT line to image-api's ingest. build.sh
|
|
7
|
-
* emits a small set of JSON object lines (`resolved | state | step | image | manifest | done`) on stdout and tees
|
|
8
|
-
* the raw `docker build` output to `$BUILD_DIR/build.log` (NOT onto stdout). On failure it does `tail -40 build.log
|
|
9
|
-
* >&2` — that is STDERR, the failure tail, NOT JSON; we parse JSON ONLY from stdout (a naive JSON.parse over both
|
|
10
|
-
* crashes the parser — §P2.5). Each forwarded frame carries an incrementing `lineOrd` (at-least-once ingest dedupe;
|
|
11
|
-
* image-api's `uq_line` ignores a re-sent ordinal — §P2.10).
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Parse ONE raw stdout line into a build.sh event, or null when it is not a JSON object line (a blank line, a
|
|
15
|
-
* stray non-JSON print). build.sh's protocol is strictly one JSON OBJECT per stdout line (`jq -nc`), so anything
|
|
16
|
-
* that is not a `{…}` object is treated as non-protocol noise the caller forwards as a `log` frame (NOT crashed on).
|
|
17
|
-
*/
|
|
18
1
|
export function parseBuildShLine(raw) {
|
|
19
2
|
const s = raw.trim();
|
|
20
3
|
if (s.length === 0 || s[0] !== "{")
|
|
21
|
-
return null;
|
|
4
|
+
return null;
|
|
22
5
|
let parsed;
|
|
23
6
|
try {
|
|
24
7
|
parsed = JSON.parse(s);
|
|
@@ -30,49 +13,26 @@ export function parseBuildShLine(raw) {
|
|
|
30
13
|
return null;
|
|
31
14
|
const obj = parsed;
|
|
32
15
|
if (typeof obj.event !== "string")
|
|
33
|
-
return null;
|
|
16
|
+
return null;
|
|
34
17
|
return obj;
|
|
35
18
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Map ONE stdout line → the ingest frame the runner POSTs (the PURE core a unit test pins). A parseable protocol
|
|
38
|
-
* line is forwarded under its own `event`; an unparseable stdout line becomes an `info` log frame (build.sh's
|
|
39
|
-
* structured stdout is naturally quiet, so this is rare — usually a generator print). The raw `docker build` tail
|
|
40
|
-
* never reaches here: it is on stderr (the runner only feeds stdout through this) — §P2.5.
|
|
41
|
-
*/
|
|
42
19
|
export function lineToFrame(raw, lineOrd) {
|
|
43
20
|
const ev = parseBuildShLine(raw);
|
|
44
21
|
if (ev === null) {
|
|
45
|
-
// Non-protocol stdout noise → an info log frame (NOT crashed on, NOT a raw docker tail — that is stderr).
|
|
46
22
|
return { event: "log", lineOrd, level: "info", line: raw.replace(/\s+$/, "") };
|
|
47
23
|
}
|
|
48
24
|
const { event, ...rest } = ev;
|
|
49
25
|
return { event, lineOrd, ...rest };
|
|
50
26
|
}
|
|
51
|
-
/**
|
|
52
|
-
* Synthesize the `docker-build status:running elapsedSec` heartbeat step (§P2.2) — emitted every ~30s during the
|
|
53
|
-
* long, silent `docker build` (with logs off) so a resuming reader sees forward motion instead of a dead spinner.
|
|
54
|
-
* It is a synthesized frame, so it still carries a `lineOrd` (image-api dedupes the same way).
|
|
55
|
-
*/
|
|
56
27
|
export function dockerBuildTickFrame(lineOrd, elapsedSec) {
|
|
57
28
|
return { event: "step", lineOrd, step: "docker-build", status: "running", elapsedSec };
|
|
58
29
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Synthesize the terminal `done` the runner sends when build.sh did NOT emit one (a hard cancel/timeout kill —
|
|
61
|
-
* build.sh has no SIGTERM trap, so a killed process never prints `done:CANCELLED`/`done:FAILED`; the runner MUST
|
|
62
|
-
* synthesize it — §P2.11). `cancelled` → `CANCELLED`; a timeout → `FAILED exitCode:124`.
|
|
63
|
-
*/
|
|
64
30
|
export function synthDoneFrame(lineOrd, kind, detail) {
|
|
65
31
|
if (kind === "cancelled") {
|
|
66
32
|
return { event: "done", lineOrd, status: "CANCELLED", exitCode: null, error: detail?.error ?? "cancelled by operator" };
|
|
67
33
|
}
|
|
68
34
|
return { event: "done", lineOrd, status: "FAILED", exitCode: 124, error: detail?.error ?? "bake hard-deadline exceeded (killed)" };
|
|
69
35
|
}
|
|
70
|
-
/**
|
|
71
|
-
* The runner-side terminal `done` for a build.sh CHILD that exited non-zero WITHOUT printing its own `done` (build.sh
|
|
72
|
-
* normally dies via `die()` which DOES print a `done:FAILED`, but a SIGKILL or a crash before the trap can leave the
|
|
73
|
-
* child exiting non-zero silently). The runner backstops with a FAILED done carrying the child exit code + the
|
|
74
|
-
* captured stderr failure tail (the `tail -40 build.log` build.sh prints on failure), redacted by image-api on store.
|
|
75
|
-
*/
|
|
76
36
|
export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
|
|
77
37
|
const tail = stderrTail.trim();
|
|
78
38
|
return {
|
|
@@ -83,16 +43,10 @@ export function exitDoneFrame(lineOrd, exitCode, stderrTail) {
|
|
|
83
43
|
error: tail.length > 0 ? `build.sh exited ${exitCode}: ${lastLines(tail, 12)}` : `build.sh exited ${exitCode}`,
|
|
84
44
|
};
|
|
85
45
|
}
|
|
86
|
-
/** Keep only the last `n` lines of a (possibly long) stderr tail — bounds the error string we forward. */
|
|
87
46
|
export function lastLines(text, n) {
|
|
88
47
|
const lines = text.split(/\r?\n/).filter((l) => l.length > 0);
|
|
89
48
|
return lines.slice(Math.max(0, lines.length - n)).join("\n");
|
|
90
49
|
}
|
|
91
|
-
/**
|
|
92
|
-
* The profile-aware HARD DEADLINE (§P2.7/§P2.11) the runner enforces by killing the process group. flutter/full are
|
|
93
|
-
* the heavy profiles (~60min); everything else ~45min. A bands build ("custom") is treated as heavy if it includes
|
|
94
|
-
* a heavy band. The reaper backstops this; the runner is the primary enforcer (it owns the child).
|
|
95
|
-
*/
|
|
96
50
|
export function deadlineMsForProfile(profile, bands) {
|
|
97
51
|
const HEAVY_MS = 60 * 60 * 1000;
|
|
98
52
|
const NORMAL_MS = 45 * 60 * 1000;
|
|
@@ -104,25 +58,19 @@ export function deadlineMsForProfile(profile, bands) {
|
|
|
104
58
|
return HEAVY_MS;
|
|
105
59
|
return NORMAL_MS;
|
|
106
60
|
}
|
|
107
|
-
/** Parse `df -P -BG /data` (POSIX, 1G blocks) → free GiB on the build-host data fs. Returns null if unparseable
|
|
108
|
-
* (the caller then fails OPEN — a df parse miss must not block a bake; the reaper still guards a mid-build fill). */
|
|
109
61
|
export function parseDfFreeGb(dfOutput) {
|
|
110
|
-
// `df -P -BG <path>` prints a header line then one data line; the "Available" column is field index 3 (0-based),
|
|
111
|
-
// a value like "812G". POSIX `-P` guarantees one physical line per fs (no wrapping), so field positions are fixed.
|
|
112
62
|
const lines = dfOutput.trim().split(/\r?\n/);
|
|
113
63
|
if (lines.length < 2)
|
|
114
64
|
return null;
|
|
115
65
|
const data = lines[lines.length - 1].trim().split(/\s+/);
|
|
116
66
|
if (data.length < 4)
|
|
117
67
|
return null;
|
|
118
|
-
const avail = data[3];
|
|
68
|
+
const avail = data[3];
|
|
119
69
|
const m = /^(\d+)G?$/.exec(avail);
|
|
120
70
|
if (!m)
|
|
121
71
|
return null;
|
|
122
72
|
return Number(m[1]);
|
|
123
73
|
}
|
|
124
|
-
/** The path build.sh's `done`/`manifest` line points at (a build-host FS path). The done carries `manifest:<path>`, the
|
|
125
|
-
* manifest event carries `path` — the runner reads whichever is present off disk for the auto-register body. */
|
|
126
74
|
export function manifestPathFromLine(doneLine, lastManifestPath) {
|
|
127
75
|
if (typeof doneLine.manifest === "string" && doneLine.manifest.length > 0)
|
|
128
76
|
return doneLine.manifest;
|
|
@@ -130,9 +78,6 @@ export function manifestPathFromLine(doneLine, lastManifestPath) {
|
|
|
130
78
|
return doneLine.path;
|
|
131
79
|
return lastManifestPath;
|
|
132
80
|
}
|
|
133
|
-
/** Pull the post-push runtime digest the auto-register should key on. build.sh rewrites DIGEST from RepoDigests[0]
|
|
134
|
-
* AFTER a push, so the `done`/`image` line's `digest` is already the post-push registry RepoDigest on a push bake and the
|
|
135
|
-
* LOCAL_ID on a no-push bake — the runner forwards it verbatim; this just normalizes the field name (§P2.10). */
|
|
136
81
|
export function digestFromDoneLine(doneLine, lastImageDigest) {
|
|
137
82
|
if (typeof doneLine.digest === "string" && doneLine.digest.length > 0)
|
|
138
83
|
return doneLine.digest;
|