@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,19 +1,4 @@
|
|
|
1
1
|
import { redactSecrets } from "../trace/redact.js";
|
|
2
|
-
/**
|
|
3
|
-
* The gate sources core's `PermissionDeniedPayload.source` names. Treated as an OPEN enum on the WIRE
|
|
4
|
-
* (a core newer than this build may send a word this table has never heard of — that must not explode
|
|
5
|
-
* the Prometheus label set), but as a CLOSED SET against the core we compile with.
|
|
6
|
-
*
|
|
7
|
-
* 🔴 core 5.23.0([3372] 提货批件②):这张表**曾是手抄的**(`new Set([...五个字面量])`,与 core 的类型
|
|
8
|
-
* 零编译期联系),于是 core 先加 `classifier`、又在 5.23.0(design/182 §7)加 `org` 时,两个**真词**都
|
|
9
|
-
* 被静默折进了 `other`。那不是「少一个标签」——`permission_denied_total{source="other"}` 的告警语义是
|
|
10
|
-
* 「引擎发了本 build 不认识的词,去对表」,真词混进来就把这个信号读废了,而 `org`(组织治理层的拒)
|
|
11
|
-
* 恰恰是运维最该单独看见的一类。
|
|
12
|
-
*
|
|
13
|
-
* ⇒ 表改为 `Record<PermissionDeniedSource, true>` 穷举:**core 增删一个词,本表编译期先红**(闭集词表
|
|
14
|
-
* 禁手抄、必须从 core 类型推导的成文纪律)。运行期配对锚在 `test/tool-trace.test.ts`(从装树 core 的
|
|
15
|
-
* `.d.ts` 解析联合词表逐词断言),因为 vitest 走 esbuild 不查类型 —— 两道锚缺一不可。
|
|
16
|
-
*/
|
|
17
2
|
const KNOWN_DENY_SOURCE_TABLE = {
|
|
18
3
|
policy: true,
|
|
19
4
|
hook: true,
|
|
@@ -23,21 +8,9 @@ const KNOWN_DENY_SOURCE_TABLE = {
|
|
|
23
8
|
classifier: true,
|
|
24
9
|
org: true,
|
|
25
10
|
};
|
|
26
|
-
/** 表内 ⇒ 收窄成 `PermissionDeniedSource`(类型守卫)。计量点用它判「这个词是不是本 build 认得的真词」。
|
|
27
|
-
* 导出是为了让运行期锚(`test/tool-trace.test.ts`,从装树 core 的 `.d.ts` 解析真词表)**复用同一个谓词**
|
|
28
|
-
* —— 测试自己另写一份判据 = 又一张手抄表,正是本文件刚消灭的那个病。 */
|
|
29
11
|
export function isKnownDenySource(v) {
|
|
30
12
|
return typeof v === "string" && Object.hasOwn(KNOWN_DENY_SOURCE_TABLE, v);
|
|
31
13
|
}
|
|
32
|
-
/**
|
|
33
|
-
* ALWAYS-ON gate-deny meter (goal B4, 2026-07-08): `permission_denied_total{source}` — how often the
|
|
34
|
-
* adjudicate chain DENIES a tool call, by gate source. Deliberately SEPARATE from {@link createToolTracer}:
|
|
35
|
-
* the tracer is a default-OFF diagnostic (TOOL_TRACE=true), but deny frequency is an operational/security
|
|
36
|
-
* signal that must reach /metrics on every deployment — a spike in `source="policy"` means a task keeps
|
|
37
|
-
* asking for something the operator forbids; `source="safety"`/`"shellGate"` spikes flag risky-command
|
|
38
|
-
* pressure. main.ts composes this UNDER the tracer via composeHooks (both are void observers on the
|
|
39
|
-
* permissionDenied slot, which composeHooks double-runs deployment-first).
|
|
40
|
-
*/
|
|
41
14
|
export function createPermissionDeniedMeter(metrics) {
|
|
42
15
|
return {
|
|
43
16
|
permissionDenied(payload) {
|
|
@@ -45,34 +18,6 @@ export function createPermissionDeniedMeter(metrics) {
|
|
|
45
18
|
},
|
|
46
19
|
};
|
|
47
20
|
}
|
|
48
|
-
/**
|
|
49
|
-
* prove-before-scale stopgap (design/50): a no-op observer hook that logs every tool call (pre + post)
|
|
50
|
-
* to the worker log — INCLUDING resumed legs that `GET /v1/runs/:id/events` does NOT surface (the
|
|
51
|
-
* streaming-resume observability gap, service[15] API request #2). Gated by `TOOL_TRACE=true`; OFF by
|
|
52
|
-
* default (no prod noise).
|
|
53
|
-
*
|
|
54
|
-
* Why: a durable-HITL-gated coding task that keeps re-suspending is ambiguous from /events alone —
|
|
55
|
-
* could be a model restart-on-resume loop (the SAME tool re-issued each resume) or slow legit iteration.
|
|
56
|
-
* The pre-hook logs `{tool, id, arg:{path,contentLen,…}}` per call, so grepping `tool_trace` reconstructs
|
|
57
|
-
* the full sequence: a repeated `write_file path=X contentLen=Y` across resumes ⇒ restart-loop;
|
|
58
|
-
* write X → write Y → bash ⇒ iteration.
|
|
59
|
-
*
|
|
60
|
-
* Pure observation: every hook returns `undefined`, so it never rewrites args, never gates, never alters
|
|
61
|
-
* the result (and per core's hooks invariant a hook's `allow` can't bypass the policy gate anyway).
|
|
62
|
-
*
|
|
63
|
-
* 🔴 core 5.20.0 起这条「纯观察」性质必须**声明**出来(`preToolUseObservational`),不能只写在注释里:
|
|
64
|
-
* core 5.19.0 的委派折叠按「PreToolUse 席位在不在」把父的 screening 面折进子代的祖先约束链——它分不出
|
|
65
|
-
* 诊断 tracer 与真裁决者。后果是**这个默认关的诊断旋钮**一开,该部署所有 hook-wired 父的 parked 子代
|
|
66
|
-
* checkpoint 就记 `parentConstraintCount=2`,而跨副本赎回腿(`boot/parked-revive-gate.ts`)恒供 1 ⇒
|
|
67
|
-
* core pre-CAS 响亮拒、行永久 pending(层数 park 时写死,事后关旋钮/重批/回滚都不改)。
|
|
68
|
-
* 声明之后该面不铸链条目,count 回到 1,病路消灭。
|
|
69
|
-
* ⛔ 打这个标的前提是**真的一条裁决都不出**:core 对声明了却返回判词的面是「拒绝采纳 + onError 告警」
|
|
70
|
-
* (classification `observational-hook-verdict-ignored`),即判词被静默丢弃——把标打到会裁决的面上比原
|
|
71
|
-
* 缺陷更坏。本文件三个 pre/post 回调恒返 `undefined`,`permissionDenied` 本就是 void 观察槽。
|
|
72
|
-
* 📎 连带的**可观测**变化(不是回归):core 的「可写工具面无 effect-aware 门」启动告警此前把本 tracer
|
|
73
|
-
* 算作一道门而被抑制;声明之后它不再算,于是「没配任何 policy 层 + 开着 TOOL_TRACE」的部署升级后会
|
|
74
|
-
* 多出那行告警。方向正确——tracer 从来没有门住任何东西,原先的抑制本身就是一次静默 fail-open。
|
|
75
|
-
*/
|
|
76
21
|
export function createToolTracer(logger) {
|
|
77
22
|
return {
|
|
78
23
|
preToolUseObservational: true,
|
|
@@ -90,10 +35,6 @@ export function createToolTracer(logger) {
|
|
|
90
35
|
});
|
|
91
36
|
return undefined;
|
|
92
37
|
},
|
|
93
|
-
// core 1.252 (design/134) BREAKING: postToolUse and postToolUseFailure are MUTUALLY EXCLUSIVE — an
|
|
94
|
-
// isError result now fires ONLY this callback. Without it, every failed tool call silently vanished
|
|
95
|
-
// from the trace (the exact silent-fallback class this log exists to catch). Same line shape as the
|
|
96
|
-
// success path (phase "post", isError true) so existing `tool_trace` grep workflows keep working.
|
|
97
38
|
postToolUseFailure(toolName, _input, failure, ctx) {
|
|
98
39
|
logger.info("tool_trace", {
|
|
99
40
|
phase: "post",
|
|
@@ -106,12 +47,6 @@ export function createToolTracer(logger) {
|
|
|
106
47
|
});
|
|
107
48
|
return undefined;
|
|
108
49
|
},
|
|
109
|
-
// core 1.257: the gate's DENY short-circuit never reaches postToolUse* (the call
|
|
110
|
-
// doesn't execute), so denied calls were INVISIBLE to this trace — the exact observability hole
|
|
111
|
-
// asked about. `source` = core's adjudicate-chain enum (the closed set is `KNOWN_DENY_SOURCE_TABLE`
|
|
112
|
-
// above — deliberately NOT re-listed here, a second hand-written copy is how the meter drifted); the
|
|
113
|
-
// deny `reason` can quote the adjudicated args (an approver's text, a policy message), so redact+clip
|
|
114
|
-
// like every other line. Same `tool_trace` key so existing grep workflows see denies in sequence.
|
|
115
50
|
permissionDenied(payload) {
|
|
116
51
|
logger.info("tool_trace", {
|
|
117
52
|
phase: "denied",
|
|
@@ -124,15 +59,11 @@ export function createToolTracer(logger) {
|
|
|
124
59
|
},
|
|
125
60
|
};
|
|
126
61
|
}
|
|
127
|
-
/** Compact, restart-diagnosable summary: key path/command fields + content length (not full content). */
|
|
128
62
|
function summarize(input) {
|
|
129
63
|
if (!input || typeof input !== "object")
|
|
130
64
|
return { v: clip(String(input), 120) };
|
|
131
65
|
const o = input;
|
|
132
66
|
const s = {};
|
|
133
|
-
// Redact secrets BEFORE clipping: a traced `command`/`old_string` (e.g. `curl -H "Authorization: Bearer …"`,
|
|
134
|
-
// an edited .env line) can carry credentials into the worker log → its aggregation pipeline. TOOL_TRACE is a
|
|
135
|
-
// default-off diagnostic, but 120 chars is enough to hold a full token, so mask it.
|
|
136
67
|
for (const k of ["path", "file_path", "command", "cmd", "pattern", "glob", "old_string", "new_string"]) {
|
|
137
68
|
if (typeof o[k] === "string")
|
|
138
69
|
s[k] = clip(redactSecrets(o[k]), 120);
|
|
@@ -146,7 +77,7 @@ function textOf(content) {
|
|
|
146
77
|
.map((c) => (c && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
|
|
147
78
|
.join("")
|
|
148
79
|
.trim();
|
|
149
|
-
return clip(redactSecrets(text), 200);
|
|
80
|
+
return clip(redactSecrets(text), 200);
|
|
150
81
|
}
|
|
151
82
|
function clip(s, n) {
|
|
152
83
|
return s.length > n ? `${s.slice(0, n)}…(${s.length})` : s;
|
|
@@ -7,17 +7,7 @@ const DEFAULTS = {
|
|
|
7
7
|
concurrency: 12,
|
|
8
8
|
maxHeapMb: 128,
|
|
9
9
|
};
|
|
10
|
-
/** Own keys that carry a prototype-tampering gadget across the JSON membrane. */
|
|
11
10
|
const PROTO_TAMPER_KEYS = ["__proto__", "constructor", "prototype"];
|
|
12
|
-
/**
|
|
13
|
-
* Recursively delete own `__proto__` / `constructor` / `prototype` keys from JSON-parsed data. Closes the
|
|
14
|
-
* tampering gadget a script passes across the membrane: `Reflect.defineProperty(o, "__proto__",
|
|
15
|
-
* {enumerable:true, value:{...}})` (Reflect is reachable; `codeGeneration:{strings:false}` does NOT block it)
|
|
16
|
-
* makes `__proto__` an OWN ENUMERABLE key, which in-context `JSON.stringify` emits and host `JSON.parse`
|
|
17
|
-
* re-materializes as a real own data property — feeding a host fn (or the returned result) a crafted
|
|
18
|
-
* prototype a downstream deep-merge / key-assignment sink splats onto the HOST realm's `Object.prototype`
|
|
19
|
-
* (process-wide, cross-tenant). Caught by the s8-sandbox-robustness marshal-bridge lens (core's battery + the
|
|
20
|
-
* result-path probes both miss it). Apply ONLY to JSON.parse output (plain, acyclic, getter-free). */
|
|
21
11
|
function scrubProtoDeep(value) {
|
|
22
12
|
if (value === null || typeof value !== "object")
|
|
23
13
|
return;
|
|
@@ -34,19 +24,6 @@ function scrubProtoDeep(value) {
|
|
|
34
24
|
scrubProtoDeep(value[key]);
|
|
35
25
|
}
|
|
36
26
|
}
|
|
37
|
-
/**
|
|
38
|
-
* [1818]§二邻位 — script-realm rejection guard. The vm context shares the HOST isolate's unhandledRejection
|
|
39
|
-
* machinery, and Node's default posture is CRASH: an untrusted script's un-awaited rejection (a bare
|
|
40
|
-
* `(async () => { throw ... })()`, an un-awaited `agent()`/`phase()` whose body fails) would take the whole
|
|
41
|
-
* server process down — a resource/availability hole, not a data-boundary one (clay's live incident hit the
|
|
42
|
-
* sibling shape that HANGS instead of rejecting, which is the only reason it didn't crash). There is no
|
|
43
|
-
* per-context hook in Node, so the guard is a process listener that tells realms apart by intrinsic:
|
|
44
|
-
* a promise minted INSIDE the context fails `instanceof` against the HOST `Promise`. Context-realm rejections
|
|
45
|
-
* are contained (logged, never fatal); host-realm rejections keep Node's fail-loud default (rethrown as an
|
|
46
|
-
* uncaughtException) UNLESS the deployment installed its own handler — then we defer to it entirely.
|
|
47
|
-
* Installed lazily on the first hardened run: only processes that actually execute untrusted scripts change
|
|
48
|
-
* their global posture, and only then is the hazard live. Idempotent.
|
|
49
|
-
*/
|
|
50
27
|
let rejectionGuardInstalled = false;
|
|
51
28
|
export function installScriptRealmRejectionGuard() {
|
|
52
29
|
if (rejectionGuardInstalled)
|
|
@@ -54,32 +31,24 @@ export function installScriptRealmRejectionGuard() {
|
|
|
54
31
|
rejectionGuardInstalled = true;
|
|
55
32
|
process.on("unhandledRejection", (reason, promise) => {
|
|
56
33
|
if (!(promise instanceof Promise)) {
|
|
57
|
-
// cross-realm ⇒ minted in a vm context ⇒ an un-awaited rejection inside an untrusted script. Contain.
|
|
58
34
|
const summary = reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason);
|
|
59
35
|
try {
|
|
60
36
|
console.warn(`hardened-vm: un-awaited script rejection contained: ${summary}`);
|
|
61
37
|
}
|
|
62
38
|
catch {
|
|
63
|
-
/* the guard must never become a crash source itself */
|
|
64
39
|
}
|
|
65
40
|
return;
|
|
66
41
|
}
|
|
67
|
-
// Host-realm: preserve fail-loud unless another handler exists (a deployment that installed one chose
|
|
68
|
-
// its own posture — a rethrow here would override that choice).
|
|
69
42
|
if (process.listenerCount("unhandledRejection") > 1)
|
|
70
43
|
return;
|
|
71
44
|
throw reason;
|
|
72
45
|
});
|
|
73
46
|
}
|
|
74
|
-
/** Host-side parse that strips the tampering gadget from data crossing INTO a host fn. */
|
|
75
47
|
function sterilizingParse(json) {
|
|
76
48
|
const parsed = JSON.parse(json);
|
|
77
49
|
scrubProtoDeep(parsed);
|
|
78
50
|
return parsed;
|
|
79
51
|
}
|
|
80
|
-
/** Marshal a (possibly live cross-realm) script value to PLAIN, sterilized host data: JSON round-trips it
|
|
81
|
-
* (firing in-context getters/toJSON harmlessly in-context → plain data; drops functions; no live context
|
|
82
|
-
* object with traps reaches the host) then strips the tampering gadget. Throws on a non-serializable result. */
|
|
83
52
|
function marshalOut(value) {
|
|
84
53
|
let json;
|
|
85
54
|
try {
|
|
@@ -90,21 +59,14 @@ function marshalOut(value) {
|
|
|
90
59
|
}
|
|
91
60
|
return sterilizingParse(json === undefined ? "null" : json);
|
|
92
61
|
}
|
|
93
|
-
/**
|
|
94
|
-
* The shared hardened-vm core. Builds the membrane, lets `buildGlobals` construct the context-local globals,
|
|
95
|
-
* runs the untrusted `body` as an async function under the caps, and returns the MARSHALLED (plain, sterilized)
|
|
96
|
-
* result. Both public surfaces delegate here.
|
|
97
|
-
*/
|
|
98
62
|
async function runHardened(opts) {
|
|
99
63
|
const cfg = { ...DEFAULTS, ...opts.limits };
|
|
100
64
|
const { signal } = opts;
|
|
101
65
|
if (signal?.aborted)
|
|
102
66
|
throw new WorkflowScriptError("hardened-vm aborted before start");
|
|
103
67
|
installScriptRealmRejectionGuard();
|
|
104
|
-
// 1. hardened context
|
|
105
68
|
const sandbox = { __proto__: null };
|
|
106
69
|
const ctx = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
107
|
-
// 2. determinism guard (resume-safety; matches CC): nondeterministic clocks/RNG throw in-context.
|
|
108
70
|
vm.runInContext(`(() => {
|
|
109
71
|
const block = (l) => { throw new Error(l + " is unavailable in workflow scripts (nondeterministic — breaks resume). Pass timestamps via args; vary by index for randomness."); };
|
|
110
72
|
Date.now = () => block("Date.now()");
|
|
@@ -121,30 +83,17 @@ async function runHardened(opts) {
|
|
|
121
83
|
globalThis[n] = () => noTimer(n + "()");
|
|
122
84
|
}
|
|
123
85
|
})()`, ctx);
|
|
124
|
-
// 3. membrane helpers (factory bodies COMPILED IN-CONTEXT → context-rooted prototypes)
|
|
125
86
|
const ctxJsonParse = vm.runInContext("(s => JSON.parse(s))", ctx);
|
|
126
87
|
const dataIn = (v) => {
|
|
127
|
-
// sterilize the gadget host-side BEFORE it re-materializes in-context (a __proto__ own key would otherwise
|
|
128
|
-
// pollute the CONTEXT's Object.prototype — sandboxed, but cheap to close).
|
|
129
88
|
const clean = JSON.parse(JSON.stringify(v === undefined ? null : v));
|
|
130
89
|
scrubProtoDeep(clean);
|
|
131
90
|
return ctxJsonParse(JSON.stringify(clean));
|
|
132
91
|
};
|
|
133
|
-
// a {ok,v} | {ok:false,err,name} JSON round-trip; the IN-CONTEXT wrapper throws a CONTEXT-LOCAL Error on
|
|
134
|
-
// failure so a host error never reaches the script (beyond-battery fix).
|
|
135
92
|
const mkBridge = vm.runInContext(`(hostCall => async function (...a) {
|
|
136
93
|
const r = JSON.parse(await hostCall(JSON.stringify(a)));
|
|
137
94
|
if (r.ok) return r.v;
|
|
138
95
|
const e = new Error(r.err); e.name = r.name || "Error"; throw e;
|
|
139
96
|
})`, ctx);
|
|
140
|
-
// [1818]§二 — quiescence deadlock detector. Inside the membrane the ONLY way a suspended script can ever be
|
|
141
|
-
// resumed is a bridged host call settling: timers are blocked, there is no I/O, and every other await is a
|
|
142
|
-
// microtask chain that drains on its own. So "zero bridged calls in flight + the microtask AND macrotask
|
|
143
|
-
// queues have both drained + scriptPromise still pending" is not a heuristic — it PROVES the script can never
|
|
144
|
-
// progress again (clay's live shape: an un-awaited `const r = phase(...)` whose body returns `r` → a promise
|
|
145
|
-
// cycle that neither rejects nor resolves; pre-fix it burned the full totalTimeoutMs blind and then blamed
|
|
146
|
-
// the timeout). setImmediate (a macrotask) is the drain fence: any continuation the settling host call woke
|
|
147
|
-
// runs first, and a continuation that starts a new bridged call increments the counter synchronously.
|
|
148
97
|
let inFlightHostCalls = 0;
|
|
149
98
|
let scriptSettled = false;
|
|
150
99
|
let fireDeadlock;
|
|
@@ -160,8 +109,6 @@ async function runHardened(opts) {
|
|
|
160
109
|
const bridge = (hostFn) => mkBridge(async (argsJson) => {
|
|
161
110
|
inFlightHostCalls++;
|
|
162
111
|
try {
|
|
163
|
-
// sterilizingParse (not bare JSON.parse): strip a slipped-in __proto__/constructor/prototype gadget
|
|
164
|
-
// BEFORE the args reach the host fn — the dangerous (script→host) direction.
|
|
165
112
|
const out = await hostFn(...sterilizingParse(argsJson));
|
|
166
113
|
return JSON.stringify({ ok: true, v: out ?? null });
|
|
167
114
|
}
|
|
@@ -178,7 +125,6 @@ async function runHardened(opts) {
|
|
|
178
125
|
}
|
|
179
126
|
});
|
|
180
127
|
const membrane = { bridge, dataIn, ctxEval: (src) => vm.runInContext(src, ctx) };
|
|
181
|
-
// 4. expose the surface's context-local globals + run the body under the sync vm timeout
|
|
182
128
|
for (const [name, value] of Object.entries(opts.buildGlobals(membrane)))
|
|
183
129
|
sandbox[name] = value;
|
|
184
130
|
let invocation;
|
|
@@ -188,14 +134,9 @@ async function runHardened(opts) {
|
|
|
188
134
|
catch (err) {
|
|
189
135
|
throw new WorkflowScriptError(`script failed to compile: ${err instanceof Error ? err.message : String(err)}`);
|
|
190
136
|
}
|
|
191
|
-
// `timeout` bounds the SYNCHRONOUS run up to the first await (a sync infinite loop is killed here); an async
|
|
192
|
-
// loop is bounded by the wall-clock race below.
|
|
193
137
|
const scriptPromise = invocation.runInContext(ctx, { timeout: cfg.syncTimeoutMs });
|
|
194
138
|
scriptPromise.then(() => (scriptSettled = true), () => (scriptSettled = true));
|
|
195
|
-
// Initial probe: a script that suspends without EVER touching the bridge (`await new Promise(() => {})`)
|
|
196
|
-
// never decrements the counter, so the settle-side probe alone would miss it.
|
|
197
139
|
probeQuiescence();
|
|
198
|
-
// 5. wall-clock + external-abort + quiescence-deadlock race
|
|
199
140
|
let timer;
|
|
200
141
|
let onAbort;
|
|
201
142
|
try {
|
|
@@ -208,24 +149,14 @@ async function runHardened(opts) {
|
|
|
208
149
|
onAbort = () => reject(new WorkflowScriptError("hardened-vm aborted"));
|
|
209
150
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
210
151
|
}
|
|
211
|
-
// Diagnosis-first message: it reaches the model via failureSummary (bounded), so the self-correction
|
|
212
|
-
// hint has to be up front.
|
|
213
152
|
fireDeadlock = () => reject(new WorkflowScriptError("script deadlocked: no host call (agent/phase) in flight and nothing can resume the script — usually an un-awaited promise cycle, e.g. `const r = phase(...)` without await whose body reads `r`."));
|
|
214
153
|
}),
|
|
215
154
|
]);
|
|
216
|
-
// hand the host PLAIN sterilized data, never a live cross-realm object (getter/toJSON/then traps) and
|
|
217
|
-
// never a slipped-in __proto__ (return-path tampering).
|
|
218
155
|
return marshalOut(result);
|
|
219
156
|
}
|
|
220
157
|
catch (err) {
|
|
221
|
-
// Live dogfood catch 2026-07-03 ( `setTimeout` blew up as a bare "workflow failed"): a RUNTIME script-realm
|
|
222
|
-
// rejection (ReferenceError, an uncaught script throw, a bridged agent() error the script didn't catch) used
|
|
223
|
-
// to propagate UN-typed — `failureSummary` only surfaces the message for `workflow.*`-coded errors, so the model/
|
|
224
|
-
// operator got zero diagnosis. Wrap it as `workflow.script_error`; core's failureSummary already bounds (200
|
|
225
|
-
// chars) + `redactHostLeaks`-redacts the message before it reaches the notification, so a bridged host error
|
|
226
|
-
// message is safe to carry here.
|
|
227
158
|
if (err instanceof WorkflowScriptError)
|
|
228
|
-
throw err;
|
|
159
|
+
throw err;
|
|
229
160
|
throw new WorkflowScriptError(`script failed at runtime: ${err instanceof Error ? err.message : String(err)}`);
|
|
230
161
|
}
|
|
231
162
|
finally {
|
|
@@ -235,23 +166,6 @@ async function runHardened(opts) {
|
|
|
235
166
|
signal.removeEventListener("abort", onAbort);
|
|
236
167
|
}
|
|
237
168
|
}
|
|
238
|
-
/**
|
|
239
|
-
* phases=[] ROOT CAUSE + fix — the SCOPED-PHASE GATE. The old in-context `phase` was pure sugar
|
|
240
|
-
* (log a "◆ phase" line, run the body) that NEVER called the host `primitives.phase`, so core recorded ZERO
|
|
241
|
-
* phase state: terminal rows had `phases: []` and agents carried no `phase` key (exactly the storage-row
|
|
242
|
-
* autopsy — agents fine, phase accounting gone). The membrane's JSON bridge cannot carry the script's BODY
|
|
243
|
-
* function across, so the bridge is split into three JSON-safe host calls, and the body stays IN-CONTEXT:
|
|
244
|
-
* - `mark(title)` → `primitives.phase(title, undefined)` (core's bare CC-marker path);
|
|
245
|
-
* - `enter(title)` → opens core's scoped phase with a DEFERRED body (`await gate`), returns a numeric token.
|
|
246
|
-
* core pushes the phase record + sets `currentPhase` SYNCHRONOUSLY before its first await, so by the time
|
|
247
|
-
* `enter` returns, `agent()` calls made inside the VM body land with the right phase label;
|
|
248
|
-
* - `exit(token, ok, err?)` → releases the gate (ok → core closes the phase `completed`; !ok → the deferred
|
|
249
|
-
* body throws and core closes it `failed`), then awaits core's settle so the close is persisted before the
|
|
250
|
-
* VM continues.
|
|
251
|
-
* A finalized-run `phase()` (core throws) surfaces on `enter` via the settled-race; an unknown/double `exit`
|
|
252
|
-
* is idempotent. The VM wrapper rethrows the body's own error — the gate's synthetic error never reaches the
|
|
253
|
-
* script.
|
|
254
|
-
*/
|
|
255
169
|
export function scopedPhaseGate(phase) {
|
|
256
170
|
let seq = 0;
|
|
257
171
|
const open = new Map();
|
|
@@ -264,9 +178,7 @@ export function scopedPhaseGate(phase) {
|
|
|
264
178
|
if (!r.ok)
|
|
265
179
|
throw new Error(r.err ?? "phase body failed");
|
|
266
180
|
}));
|
|
267
|
-
settled.catch(() => undefined);
|
|
268
|
-
// Surface an IMMEDIATE rejection (core's finalized guard rejects before its first await settles) while a
|
|
269
|
-
// healthy still-pending phase resolves the race via the resolved sentinel.
|
|
181
|
+
settled.catch(() => undefined);
|
|
270
182
|
await Promise.race([settled, Promise.resolve()]);
|
|
271
183
|
const token = ++seq;
|
|
272
184
|
open.set(token, { release, settled });
|
|
@@ -275,19 +187,13 @@ export function scopedPhaseGate(phase) {
|
|
|
275
187
|
async exit(token, ok, err) {
|
|
276
188
|
const o = open.get(token);
|
|
277
189
|
if (!o)
|
|
278
|
-
return;
|
|
190
|
+
return;
|
|
279
191
|
open.delete(token);
|
|
280
192
|
o.release({ ok, ...(err !== undefined ? { err } : {}) });
|
|
281
|
-
await o.settled.catch(() => undefined);
|
|
193
|
+
await o.settled.catch(() => undefined);
|
|
282
194
|
},
|
|
283
195
|
};
|
|
284
196
|
}
|
|
285
|
-
/** Build the orchestration context-local globals (agent/parallel/pipeline/phase/log/budget/args) from the
|
|
286
|
-
* host {@link WorkflowPrimitives}. agent/log/phase are host-bridged (phase via the three-verb
|
|
287
|
-
* {@link scopedPhaseGate} — the body function itself never crosses the membrane); parallel/pipeline are
|
|
288
|
-
* in-context sugar; budget is a frozen null-proto object over host accessors.
|
|
289
|
-
* Exported so the worker-isolated runner (hardened-vm-worker.ts) reuses the EXACT same surface with
|
|
290
|
-
* IPC-backed primitives — one definition of the orchestration globals, no drift. */
|
|
291
197
|
export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
|
|
292
198
|
const cfg = { ...DEFAULTS, ...limits };
|
|
293
199
|
return (m) => {
|
|
@@ -312,8 +218,6 @@ export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
|
|
|
312
218
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
313
219
|
return out;
|
|
314
220
|
})`)(cfg.concurrency);
|
|
315
|
-
// Phase-gate fix: REAL phase bridging via the three-verb gate (see scopedPhaseGate) — the old in-context sugar
|
|
316
|
-
// never told core a phase existed (phases:[] / label-less agents in every hardened-runner deployment).
|
|
317
221
|
const gate = scopedPhaseGate((t, b) => primitives.phase(t, b));
|
|
318
222
|
const phaseMark = m.bridge(async (title) => void (await primitives.phase(String(title), undefined)));
|
|
319
223
|
const phaseEnter = m.bridge(async (title) => gate.enter(String(title)));
|
|
@@ -327,9 +231,6 @@ export function orchestrationGlobals(primitives, scriptArgs, limits = {}) {
|
|
|
327
231
|
return { agent, parallel, pipeline, phase, log, budget, args: m.dataIn(scriptArgs) };
|
|
328
232
|
};
|
|
329
233
|
}
|
|
330
|
-
/** Build the hardened-vm S8 `WorkflowScriptRunner` (TOC+TOB workflow orchestration). `safeForUntrustedScripts`
|
|
331
|
-
* is `true` — it has passed the conformance battery + robustness suite; the S8 gate mounts `run_workflow` only for
|
|
332
|
-
* a runner that asserts this. */
|
|
333
234
|
export function createHardenedVmRunner(limits = {}) {
|
|
334
235
|
const cfg = { ...DEFAULTS, ...limits };
|
|
335
236
|
return {
|
|
@@ -342,25 +243,10 @@ export function createHardenedVmRunner(limits = {}) {
|
|
|
342
243
|
...(signal ? { signal } : {}),
|
|
343
244
|
buildGlobals: orchestrationGlobals(primitives, scriptArgs, cfg),
|
|
344
245
|
});
|
|
345
|
-
// `meta` is core-parsed (AST, not eval) but its values are LLM-controlled — sterilize it too.
|
|
346
246
|
return { result, meta: marshalOut(meta) };
|
|
347
247
|
},
|
|
348
248
|
};
|
|
349
249
|
}
|
|
350
|
-
/**
|
|
351
|
-
* General untrusted-code exec on the SAME hardened membrane — the TOC ephemeral execution sandbox. Runs
|
|
352
|
-
* `code` (an async function body that may `return`) with ONLY the safe built-ins plus whatever host
|
|
353
|
-
* capabilities the caller bridges via `bindings` (each MUST go through `m.bridge` / `m.dataIn` — never a raw
|
|
354
|
-
* host reference). Returns the marshalled (plain, sterilized) result. With no `bindings`, the code is pure
|
|
355
|
-
* computation (no host reach at all).
|
|
356
|
-
*
|
|
357
|
-
* @example
|
|
358
|
-
* const out = await runInHardenedVm({
|
|
359
|
-
* code: "return await fetchJson(args.url)",
|
|
360
|
-
* bindings: (m) => ({ fetchJson: m.bridge((u) => safeFetch(String(u))), args: m.dataIn({ url }) }),
|
|
361
|
-
* limits: { totalTimeoutMs: 5000 },
|
|
362
|
-
* });
|
|
363
|
-
*/
|
|
364
250
|
export async function runInHardenedVm(opts) {
|
|
365
251
|
return runHardened({
|
|
366
252
|
body: opts.code,
|
|
@@ -2,26 +2,9 @@ import { Worker } from "node:worker_threads";
|
|
|
2
2
|
import { WorkflowScriptError } from "@sema-agent/core";
|
|
3
3
|
import { scopedPhaseGate } from "./hardened-vm-runner.js";
|
|
4
4
|
const DEFAULTS = { totalTimeoutMs: 600_000, syncTimeoutMs: 5_000, maxAgents: 1000, concurrency: 12, maxHeapMb: 128 };
|
|
5
|
-
// Resolve the worker bootstrap next to this module. In a tsc build it's the sibling `.js`; under vitest/tsx
|
|
6
|
-
// (this module's URL ends in `.ts`) point at the `.ts` and register tsx as the worker's loader so the worker
|
|
7
|
-
// can import TypeScript. Prod (node dist/) never needs tsx.
|
|
8
5
|
const IS_TS = import.meta.url.endsWith(".ts");
|
|
9
6
|
const WORKER_URL = new URL(IS_TS ? "./hardened-vm-worker.ts" : "./hardened-vm-worker.js", import.meta.url);
|
|
10
7
|
const WORKER_EXEC_ARGV = IS_TS ? ["--import", "tsx/esm"] : undefined;
|
|
11
|
-
/**
|
|
12
|
-
* Worker-isolated S8 hard runner (design/98 v2). Runs the hardened-vm membrane inside a `worker_thread`, adding
|
|
13
|
-
* RESOURCE isolation on top of the in-process {@link import("./hardened-vm-runner.js").createHardenedVmRunner}'s
|
|
14
|
-
* BOUNDARY isolation:
|
|
15
|
-
* - HEAP cap (`resourceLimits.maxOldGenerationSizeMb`): a runaway-allocation script OOMs the WORKER (an `error`
|
|
16
|
-
* event), never the shared main process — closes the in-process resource-exhaustion gap.
|
|
17
|
-
* - `terminate()` on wall-clock timeout / abort: kills a MICROTASK-starvation loop (`while(true){await null}`)
|
|
18
|
-
* the in-process wall-clock race cannot pre-empt (it starves its own event loop).
|
|
19
|
-
* - credential isolation: the worker gets only the script + args; `agent`/`log` IPC to the main thread (which
|
|
20
|
-
* holds the real Runner/DB/keys); a (validated-impossible) boundary crossing lands in a credential-free isolate.
|
|
21
|
-
*
|
|
22
|
-
* `safeForUntrustedScripts: true` — the membrane it runs is the conformance-validated + robustness-tested one. Use
|
|
23
|
-
* this (over the in-process runner) for genuinely-untrusted multi-tenant scripts where resource isolation matters.
|
|
24
|
-
*/
|
|
25
8
|
export function createWorkerHardenedVmRunner(limits = {}) {
|
|
26
9
|
const cfg = { ...DEFAULTS, ...limits };
|
|
27
10
|
return {
|
|
@@ -31,8 +14,6 @@ export function createWorkerHardenedVmRunner(limits = {}) {
|
|
|
31
14
|
return Promise.reject(new WorkflowScriptError("worker hardened-vm aborted before start"));
|
|
32
15
|
return new Promise((resolve, reject) => {
|
|
33
16
|
const worker = new Worker(WORKER_URL, {
|
|
34
|
-
// budgetSpent seeds the worker's cache with the CURRENT spend (core 1.238 wiring battery caught the
|
|
35
|
-
// cold-start gap: a cache seeded 0 made remaining() read total until the first agent reply refreshed it).
|
|
36
17
|
workerData: { scriptSource, scriptArgs, limits: cfg, budgetTotal: primitives.budget.total, budgetSpent: primitives.budget.spent() },
|
|
37
18
|
resourceLimits: { maxOldGenerationSizeMb: cfg.maxHeapMb },
|
|
38
19
|
...(WORKER_EXEC_ARGV ? { execArgv: WORKER_EXEC_ARGV } : {}),
|
|
@@ -45,7 +26,7 @@ export function createWorkerHardenedVmRunner(limits = {}) {
|
|
|
45
26
|
clearTimeout(timer);
|
|
46
27
|
if (signal)
|
|
47
28
|
signal.removeEventListener("abort", onAbort);
|
|
48
|
-
void worker.terminate();
|
|
29
|
+
void worker.terminate();
|
|
49
30
|
};
|
|
50
31
|
const finish = (settle) => {
|
|
51
32
|
if (settled)
|
|
@@ -54,15 +35,10 @@ export function createWorkerHardenedVmRunner(limits = {}) {
|
|
|
54
35
|
cleanup();
|
|
55
36
|
settle();
|
|
56
37
|
};
|
|
57
|
-
// Wall-clock backstop (terminate). Grace over the in-worker totalTimeoutMs so the worker's CLEAN error
|
|
58
|
-
// (stuck host primitive) wins normally; terminate is the backstop for microtask-starvation.
|
|
59
38
|
timer = setTimeout(() => finish(() => reject(new WorkflowScriptError(`worker hardened-vm exceeded totalTimeoutMs (${cfg.totalTimeoutMs})`))), cfg.totalTimeoutMs + 1500);
|
|
60
39
|
timer.unref?.();
|
|
61
40
|
if (signal)
|
|
62
41
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
-
// Phase-gate fix: the worker's phase IPC verbs land on the REAL primitives.phase via the same three-verb gate
|
|
64
|
-
// the in-process runner uses (the body function stays in the worker; only mark/enter/exit cross). Replies
|
|
65
|
-
// reuse the worker's id-keyed "agentResult" resolver (same shape — no second pending map in the worker).
|
|
66
42
|
const phaseGate = scopedPhaseGate((t, b) => primitives.phase(t, b));
|
|
67
43
|
const reply = (id, p) => void p.then((value) => worker.postMessage({ type: "agentResult", id, ok: true, value: value ?? null, budgetSpent: primitives.budget.spent() }), (e) => worker.postMessage({ type: "agentResult", id, ok: false, error: e instanceof Error ? e.message : String(e), name: e instanceof Error ? e.name : "Error", budgetSpent: primitives.budget.spent() }));
|
|
68
44
|
worker.on("message", (msg) => {
|
|
@@ -91,7 +67,6 @@ export function createWorkerHardenedVmRunner(limits = {}) {
|
|
|
91
67
|
finish(() => reject(new WorkflowScriptError(msg.message ?? "worker hardened-vm script failed")));
|
|
92
68
|
}
|
|
93
69
|
});
|
|
94
|
-
// An `error` event = an UNCAUGHT worker exception, most often the heap-cap OOM ("Reached heap limit").
|
|
95
70
|
worker.on("error", (err) => finish(() => reject(new WorkflowScriptError(`worker hardened-vm crashed (likely heap/OOM): ${err.message}`))));
|
|
96
71
|
worker.on("exit", (code) => {
|
|
97
72
|
if (!settled)
|
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S8 hardened-vm WORKER bootstrap (design/98 v2 resource isolation). Runs inside a `worker_thread` spawned by
|
|
3
|
-
* {@link import("./hardened-vm-worker-runner.js").createWorkerHardenedVmRunner}. The SAME hardened-vm membrane
|
|
4
|
-
* ({@link runInHardenedVm} + {@link orchestrationGlobals}) executes the untrusted script HERE — so on top of
|
|
5
|
-
* the membrane's BOUNDARY isolation we get the worker's RESOURCE isolation: a separate V8 isolate + heap (capped
|
|
6
|
-
* by `resourceLimits.maxOldGenerationSizeMb` → a memory bomb OOMs the WORKER, not the shared main process) and
|
|
7
|
-
* `terminate()` (the main thread kills a microtask-starvation / runaway loop the in-process wall-clock cannot).
|
|
8
|
-
*
|
|
9
|
-
* The worker holds NO credentials: `agent`/`log` are STUBS that IPC to the main thread (which runs the real
|
|
10
|
-
* primitives with the real Runner/DB/model keys); `budget` reads a worker-local cache the main thread refreshes
|
|
11
|
-
* on each agent reply. So even a (validated-impossible) membrane boundary crossing lands in a credential-free isolate.
|
|
12
|
-
*/
|
|
13
1
|
import { parentPort, workerData } from "node:worker_threads";
|
|
14
2
|
import { splitWorkflowMeta } from "@sema-agent/core";
|
|
15
|
-
// Resolve the sibling membrane module with an EXPLICIT extension so it loads under both a tsc build (`.js`) and
|
|
16
|
-
// tsx (`.ts`) — tsx does not map a `.js` specifier to `.ts` for a worker's static import. The template literal
|
|
17
|
-
// keeps tsc from rejecting a literal `.ts` specifier; the cast restores the types.
|
|
18
3
|
const ext = import.meta.url.endsWith(".ts") ? "ts" : "js";
|
|
19
4
|
const { runInHardenedVm, orchestrationGlobals } = (await import(`./hardened-vm-runner.${ext}`));
|
|
20
5
|
const port = parentPort;
|
|
21
6
|
if (!port)
|
|
22
7
|
throw new Error("hardened-vm-worker must run inside a worker_thread");
|
|
23
8
|
const input = workerData;
|
|
24
|
-
// agent: async IPC (request/response keyed by id) to the main thread's real primitive.
|
|
25
9
|
let nextId = 0;
|
|
26
10
|
const pending = new Map();
|
|
27
|
-
// budget: worker-local cache the main thread refreshes (piggybacked on every agentResult).
|
|
28
11
|
const budgetCache = { total: input.budgetTotal, spent: input.budgetSpent ?? 0 };
|
|
29
12
|
port.on("message", (msg) => {
|
|
30
13
|
if (msg.type !== "agentResult" || msg.id === undefined)
|
|
@@ -43,8 +26,6 @@ port.on("message", (msg) => {
|
|
|
43
26
|
p.rej(e);
|
|
44
27
|
}
|
|
45
28
|
});
|
|
46
|
-
// IPC-backed WorkflowPrimitives handed to orchestrationGlobals — agent/log/phase cross to the main thread;
|
|
47
|
-
// budget is the live cache; parallel/pipeline are built in-context by orchestrationGlobals.
|
|
48
29
|
const ipcCall = (type, extra) => new Promise((res, rej) => {
|
|
49
30
|
const id = nextId++;
|
|
50
31
|
pending.set(id, { res, rej });
|
|
@@ -58,14 +39,8 @@ const ipcPrimitives = {
|
|
|
58
39
|
spent: () => budgetCache.spent,
|
|
59
40
|
remaining: () => (budgetCache.total === null ? Infinity : budgetCache.total - budgetCache.spent),
|
|
60
41
|
},
|
|
61
|
-
// never invoked (orchestrationGlobals builds parallel/pipeline in-context), but required by the type.
|
|
62
42
|
parallel: async () => [],
|
|
63
43
|
pipeline: async () => [],
|
|
64
|
-
// Phase-gate fix: phase accounting must reach the MAIN thread's real primitives (the old stub ran the body and told
|
|
65
|
-
// core nothing — phases:[] on every worker-isolated run). The body function cannot cross the IPC boundary, so
|
|
66
|
-
// the same three-verb protocol as the in-process gate: mark (bare) / enter→token / exit(token, ok, err).
|
|
67
|
-
// orchestrationGlobals' own gate wraps THIS phase — its deferred body resolves when the in-VM body settles,
|
|
68
|
-
// which chains 1:1 onto these IPC verbs (enter here ⇒ the main-side gate's enter; exit ⇒ its exit).
|
|
69
44
|
phase: async (t, b) => {
|
|
70
45
|
if (b === undefined) {
|
|
71
46
|
await ipcCall("phaseMark", { title: String(t) });
|
|
@@ -82,7 +57,6 @@ const ipcPrimitives = {
|
|
|
82
57
|
await ipcCall("phaseExit", { token, ok: false, err: e instanceof Error ? e.message : String(e) });
|
|
83
58
|
}
|
|
84
59
|
catch {
|
|
85
|
-
/* exit is best-effort — the body's own error wins */
|
|
86
60
|
}
|
|
87
61
|
throw e;
|
|
88
62
|
}
|
|
@@ -96,7 +70,6 @@ void (async () => {
|
|
|
96
70
|
bindings: orchestrationGlobals(ipcPrimitives, input.scriptArgs, input.limits),
|
|
97
71
|
limits: input.limits,
|
|
98
72
|
});
|
|
99
|
-
// meta is core-parsed; JSON round-trip strips any slipped-in gadget before it crosses back via structuredClone.
|
|
100
73
|
const safeMeta = JSON.parse(JSON.stringify(meta));
|
|
101
74
|
port.postMessage({ type: "done", result, meta: safeMeta });
|
|
102
75
|
}
|
|
@@ -1,53 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* C2 (core 1.219) + design/122 (core 1.225) — the SERVICE-side registry for
|
|
3
|
-
* STEERING and RESUMING a Task SUB-AGENT (a sync delegation spawned by the `Agent` tool), the sibling of
|
|
4
|
-
* {@link import("./workflow-agent-steer.js").WorkflowAgentRegistry} for workflow agents.
|
|
5
|
-
*
|
|
6
|
-
* core emits a {@link SubagentSteerHandle} to the deployment's `RunInternals.onSubagentSpawn` sink the
|
|
7
|
-
* moment a SYNC child spawns (the handle NEVER reaches the model). The service registers it under the
|
|
8
|
-
* PARENT RUN's taskId so `POST /v1/runs/:runId/subagents/:target/{steer,resume}` can route into it.
|
|
9
|
-
* Delivery is replica-local (the handle is an in-memory bridge, like `steerableRuns`); a cross-replica
|
|
10
|
-
* steer/resume is an honest 409, never a silent drop.
|
|
11
|
-
*
|
|
12
|
-
* 🔴 LIFECYCLE (design/122 item ③ — the "不改必踩" change): handles are NO LONGER evicted on child settle.
|
|
13
|
-
* `resume` is only legal AFTER the child settled (core rejects `steering.still_running` before), so a
|
|
14
|
-
* settle-evicted registry would make resume structurally unreachable. Instead:
|
|
15
|
-
* - one handle PER parentToolCallId, NEWEST WINS (core D2: each resume re-emits a FRESH handle under the
|
|
16
|
-
* same id whose `settled` tracks the revived run — `Map.set` replace is exactly that contract);
|
|
17
|
-
* - handles live until the OWNING LEG ends (each run leg collects its `register()` unregister thunks and
|
|
18
|
-
* runs them in its finally — identity-guarded, so a suspend→fast-resume sibling leg's fresh handles are
|
|
19
|
-
* never clobbered by the old leg's teardown, mirroring the `steerableRuns` identity guard);
|
|
20
|
-
* - a per-run cap (oldest-evicted) bounds the map for a delegation-heavy parent (handles now outlive
|
|
21
|
-
* settle, so an unbounded list would grow for the whole run).
|
|
22
|
-
* After the parent run ends core disposes the retain ledger anyway (abort in-flight resumes + release all),
|
|
23
|
-
* so a post-teardown resume would only ever 409 — the eviction just keeps the map honest.
|
|
24
|
-
*
|
|
25
|
-
* Addressing: `target` matches `parentToolCallId` FIRST (spawn-time identity, unique per delegation), then
|
|
26
|
-
* falls back to `agentName` (display name; two children of the same agent type collide → the route 409s
|
|
27
|
-
* rather than steering/resuming an arbitrary sibling). Steer-in redaction is shared with the workflow route
|
|
28
|
-
* (`redactSteerIn`) — one owner of the trust gate. 🔐 `handle.childSessionId` is a continuation
|
|
29
|
-
* CAPABILITY: control-plane only — no route response may include it.
|
|
30
|
-
*
|
|
31
|
-
* SCOPE (core r1-M3): only SYNC delegations emit a handle — `run_in_background` children have neither steer
|
|
32
|
-
* nor resume (poll/stop via TaskOutput/TaskStop; durable bg revival is a separate core item).
|
|
33
|
-
*/
|
|
34
|
-
/** Per-run handle cap — handles now live until the leg ends (not settle), so bound the map. Oldest-evicted;
|
|
35
|
-
* 256 mirrors the ToolDetachHub bound (a run with >256 live-or-resumable delegations is pathological). */
|
|
36
1
|
export const SUBAGENT_HANDLES_PER_RUN_CAP = 256;
|
|
37
2
|
export class SubagentSteerRegistry {
|
|
38
|
-
/** parent run taskId → (parentToolCallId → NEWEST handle). Map iteration order = insertion = age. */
|
|
39
3
|
byRun = new Map();
|
|
40
|
-
/** Register a child handle under its parent run. SAME parentToolCallId replaces (core D2: a resume
|
|
41
|
-
* re-emits a fresh handle for the revived run — newest wins). Returns an IDENTITY-GUARDED unregister
|
|
42
|
-
* thunk: it removes the entry only while it still holds THIS handle (a fresh sibling-leg registration
|
|
43
|
-
* under the same id survives the old leg's teardown). Call it at LEG END, not on settle (design/122 ③). */
|
|
44
4
|
register(runId, handle) {
|
|
45
5
|
let handles = this.byRun.get(runId);
|
|
46
6
|
if (!handles) {
|
|
47
7
|
handles = new Map();
|
|
48
8
|
this.byRun.set(runId, handles);
|
|
49
9
|
}
|
|
50
|
-
handles.delete(handle.parentToolCallId);
|
|
10
|
+
handles.delete(handle.parentToolCallId);
|
|
51
11
|
handles.set(handle.parentToolCallId, handle);
|
|
52
12
|
if (handles.size > SUBAGENT_HANDLES_PER_RUN_CAP) {
|
|
53
13
|
const oldest = handles.keys().next().value;
|
|
@@ -65,9 +25,6 @@ export class SubagentSteerRegistry {
|
|
|
65
25
|
}
|
|
66
26
|
};
|
|
67
27
|
}
|
|
68
|
-
/** Resolve `runId` + `target` to its handle on this replica. Exact `parentToolCallId` match wins (unique —
|
|
69
|
-
* newest-per-id by construction → count is 0|1); otherwise match by `agentName` (count may be >1 →
|
|
70
|
-
* ambiguous, `handle` undefined → the route 409s). count=0 → no such child here. */
|
|
71
28
|
resolve(runId, target) {
|
|
72
29
|
const handles = this.byRun.get(runId);
|
|
73
30
|
if (!handles || handles.size === 0)
|
|
@@ -78,7 +35,6 @@ export class SubagentSteerRegistry {
|
|
|
78
35
|
const byName = [...handles.values()].filter((h) => h.agentName === target);
|
|
79
36
|
return byName.length === 1 ? { handle: byName[0], count: 1 } : { count: byName.length };
|
|
80
37
|
}
|
|
81
|
-
/** Total registered handles (test/observability). */
|
|
82
38
|
get size() {
|
|
83
39
|
let n = 0;
|
|
84
40
|
for (const handles of this.byRun.values())
|