@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,54 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Degenerate-repetition instrument (a/b classifier).
|
|
3
|
-
*
|
|
4
|
-
* When a task fails with `errorCode === "output.degenerate"` (core 1.59), core hands back
|
|
5
|
-
* `salvagedOutput` = the degenerate turn's text — **already tail-TRIMMED at the brain stream layer**.
|
|
6
|
-
* 🔴 A-057.52 correction (this note used to claim "the **whole** text, good head + looped garbage tail";
|
|
7
|
-
* that was true of core 1.59 and false of every engine that ships `trimDegenerateTail` — verified against
|
|
8
|
-
* the installed dist, not against JSDoc): on the main path `assemble-result` fills the seat from the same
|
|
9
|
-
* final message the brain already trimmed, so what arrives here is `head + exactly ONE copy of the
|
|
10
|
-
* repeating unit` (unit ≤ `MAX_PERIOD` = 100 chars). Untrimmed text only reaches us on the narrow
|
|
11
|
-
* fallbacks — the cut landed on the reasoning face, or `trimDegenerateTail` bailed (`reps < 2`, empty
|
|
12
|
-
* unit, unit-loop period mismatch). Consequence for the numbers below: {@link repetitionTail} is a
|
|
13
|
-
* **no-op** on the main-path input (one copy is not ≥ {@link MIN_REPEATS}), so `uniquePrefixLen`
|
|
14
|
-
* collapses onto `salvagedLen` and the a/unknown split degrades to a pure length test. The a-vs-b
|
|
15
|
-
* decision is unaffected (it is decided by `priorChars`, independently); the residual bias on a/unknown
|
|
16
|
-
* is bounded by one unit ≤ 100 chars. Pin: the "已裁形" case in `test/degenerate-instrument.test.ts`
|
|
17
|
-
* drives the REAL `inspectDegenerate` + `trimDegenerateTail` and asserts the tail measures 0.
|
|
18
|
-
* Core's *salvage ②* — recovering the "last substantive turn" instead of the current one — is being
|
|
19
|
-
* gated on REAL data: how often is the useful answer actually in an EARLIER turn vs. in the degenerate
|
|
20
|
-
* turn itself? This instrument answers that, per event, without changing any behaviour.
|
|
21
|
-
*
|
|
22
|
-
* Classification (since the last user message = this task's own turns):
|
|
23
|
-
* - **a** good+garbage SAME turn — the degenerate turn carries a substantive unique head before the
|
|
24
|
-
* loop. Whole-turn salvage ① + a tail-trim is enough; ② would add nothing.
|
|
25
|
-
* - **b** good EARLIER / degenerate LATER — a substantive assistant turn already exists before the
|
|
26
|
-
* degenerate one. ② ("last-substantive-turn") would recover it; ① (current turn) throws it away.
|
|
27
|
-
* - **unknown** — degenerate from the start, nothing substantive anywhere (neither ① nor ② helps).
|
|
28
|
-
*
|
|
29
|
-
* Output is measurement only: a `degenerate_total{class}` counter (→ `/metrics/summary`) plus one
|
|
30
|
-
* structured log line carrying the raw signals. The aggregate a/b/unknown frequencies are what gets
|
|
31
|
-
* reported back to the core AI to decide whether ② is worth building. The repetition-boundary estimate
|
|
32
|
-
* here is for *measuring* the unique head — it is NOT the production tail-trim (that lives in core).
|
|
33
|
-
*/
|
|
34
|
-
// 🔴 core 1.403 迁移(2026-07-25):runtime 构造用法,`Session` → `StoredSession`(见 audit.ts 同款注释)。
|
|
35
1
|
import { StoredSession } from "@sema-agent/core";
|
|
36
2
|
import { TiDBSessionStorage } from "./plugins/tidb-session-storage.js";
|
|
37
|
-
/** A "real" answer is at least this many non-looping chars (after the degenerate tail is stripped). */
|
|
38
3
|
const SUBSTANTIVE_CHARS = 200;
|
|
39
|
-
/** Loops live at the end — only inspect this much of the tail when hunting the repeating unit. */
|
|
40
4
|
const REP_WINDOW = 8192;
|
|
41
|
-
/** Need at least this many contiguous copies to call a periodic suffix a degenerate loop. */
|
|
42
5
|
const MIN_REPEATS = 3;
|
|
43
|
-
/** Longest repeating unit we look for (a sentence-ish block); bounds the scan. */
|
|
44
6
|
const MAX_UNIT = 256;
|
|
45
|
-
/** Two assistant turns are "the same turn" if their heads match (a degenerate head is distinctive). */
|
|
46
7
|
const TURN_MATCH_HEAD = 120;
|
|
47
|
-
/**
|
|
48
|
-
* Length of the longest periodic suffix of `text` (≥ {@link MIN_REPEATS} contiguous copies of some
|
|
49
|
-
* unit ≤ {@link MAX_UNIT}). 0 when no qualifying loop is found. Bounded to the last {@link REP_WINDOW}
|
|
50
|
-
* chars. This estimates where the degenerate repetition begins — a measurement aid, not a trimmer.
|
|
51
|
-
*/
|
|
52
8
|
export function repetitionTail(text) {
|
|
53
9
|
const n = text.length;
|
|
54
10
|
if (n < 2)
|
|
@@ -57,7 +13,6 @@ export function repetitionTail(text) {
|
|
|
57
13
|
const maxUnit = Math.min(MAX_UNIT, Math.floor((n - start) / MIN_REPEATS));
|
|
58
14
|
let best = 0;
|
|
59
15
|
for (let p = 1; p <= maxUnit; p++) {
|
|
60
|
-
// How many times does the final p-char block repeat contiguously, walking backwards?
|
|
61
16
|
let reps = 1;
|
|
62
17
|
for (;;) {
|
|
63
18
|
const blockStart = n - p * (reps + 1);
|
|
@@ -79,26 +34,20 @@ export function repetitionTail(text) {
|
|
|
79
34
|
}
|
|
80
35
|
return best;
|
|
81
36
|
}
|
|
82
|
-
/** Substantive (non-looping) length of one assistant turn's text. */
|
|
83
37
|
function substantiveLen(text) {
|
|
84
38
|
return Math.max(0, text.length - repetitionTail(text));
|
|
85
39
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Classify a degenerate failure from its salvaged text + the substantive content of the assistant
|
|
88
|
-
* turns that preceded it (since the last user message). Pure — unit-tested directly.
|
|
89
|
-
*/
|
|
90
40
|
export function classifyDegenerate(salvaged, priorChars, turnsSinceUser) {
|
|
91
41
|
const salvagedLen = salvaged.length;
|
|
92
42
|
const repeatedTailLen = repetitionTail(salvaged);
|
|
93
43
|
const uniquePrefixLen = Math.max(0, salvagedLen - repeatedTailLen);
|
|
94
44
|
const cls = priorChars >= SUBSTANTIVE_CHARS
|
|
95
|
-
? "b"
|
|
45
|
+
? "b"
|
|
96
46
|
: uniquePrefixLen >= SUBSTANTIVE_CHARS
|
|
97
|
-
? "a"
|
|
98
|
-
: "unknown";
|
|
47
|
+
? "a"
|
|
48
|
+
: "unknown";
|
|
99
49
|
return { class: cls, turnsSinceUser, priorChars, salvagedLen, uniquePrefixLen, repeatedTailLen };
|
|
100
50
|
}
|
|
101
|
-
/** Concatenated text-block content of one (assistant) message; "" for a tool-call-only turn. */
|
|
102
51
|
function turnText(msg) {
|
|
103
52
|
if (msg.role !== "assistant" || !Array.isArray(msg.content))
|
|
104
53
|
return "";
|
|
@@ -107,7 +56,6 @@ function turnText(msg) {
|
|
|
107
56
|
.map((b) => b.text)
|
|
108
57
|
.join("");
|
|
109
58
|
}
|
|
110
|
-
/** Heads match → treat as the same turn (core may or may not persist the failed degenerate turn). */
|
|
111
59
|
function sameTurn(a, b) {
|
|
112
60
|
if (!a || !b)
|
|
113
61
|
return false;
|
|
@@ -116,12 +64,6 @@ function sameTurn(a, b) {
|
|
|
116
64
|
const k = Math.min(TURN_MATCH_HEAD, a.length, b.length);
|
|
117
65
|
return k > 0 && a.slice(0, k) === b.slice(0, k);
|
|
118
66
|
}
|
|
119
|
-
/**
|
|
120
|
-
* Read this task's assistant turns (since the last user message) and total the substantive text that
|
|
121
|
-
* preceded the degenerate turn. The degenerate turn is located by head-matching `salvaged`; if it was
|
|
122
|
-
* not persisted, every persisted assistant turn in the window counts as prior (and the degenerate turn
|
|
123
|
-
* is added to the count). Best-effort — returns zeros if the session can't be read.
|
|
124
|
-
*/
|
|
125
67
|
async function readPriorTurns(pool, sessionId, salvaged) {
|
|
126
68
|
const storage = await TiDBSessionStorage.wake(pool, sessionId);
|
|
127
69
|
if (!storage)
|
|
@@ -148,15 +90,9 @@ async function readPriorTurns(pool, sessionId, salvaged) {
|
|
|
148
90
|
}
|
|
149
91
|
const priorTexts = degenIdx >= 0 ? assistantTexts.slice(0, degenIdx) : assistantTexts;
|
|
150
92
|
const priorChars = priorTexts.reduce((a, t) => a + substantiveLen(t), 0);
|
|
151
|
-
// turnsSinceUser counts the degenerate turn too; add 1 when it wasn't found among persisted turns.
|
|
152
93
|
const turnsSinceUser = degenIdx >= 0 ? assistantTexts.length : assistantTexts.length + 1;
|
|
153
94
|
return { priorChars, turnsSinceUser };
|
|
154
95
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Build the fire-and-forget instrument. Returns a sync `(result) => void` so the hot result paths
|
|
157
|
-
* (`recordTaskResult` / `recordDone`) stay unblocked; the session read + classification run in the
|
|
158
|
-
* background and never throw into the response. No-op for non-degenerate results.
|
|
159
|
-
*/
|
|
160
96
|
export function makeDegenerateInstrument(pool, metrics, logger) {
|
|
161
97
|
return (result) => {
|
|
162
98
|
if (result.errorCode !== "output.degenerate")
|
|
@@ -1,38 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/181 件一:**部署治理链的单一构造口**。
|
|
3
|
-
*
|
|
4
|
-
* 三条腿要装同一条「部署 ⊇ 操作员」治理链 —— HTTP 的 `resolveSpec`(boot/resolve-spec.ts)、durable park
|
|
5
|
-
* 的赎回腿(main.ts 的父约束重建)、`run-local`。此前只有第一条腿真装,另外两条各自手拼一小截,于是
|
|
6
|
-
* 「同一个部署旋钮在这条腿上生效、在那条腿上不存在」成了结构性可能。本文件把**构造**收成单一属主。
|
|
7
|
-
*
|
|
8
|
-
* 🔒 **折叠属主不在这里**:合成仍是 core 的 `tightenTaskSpec`,经由既有的 `applyRuntimeGovernance`
|
|
9
|
-
* (src/runtime-governance.ts)。本口**只产入参与基线**,一条 policy 都不合成 —— 折叠有六条规则
|
|
10
|
-
* (excludeTools/deferTools 并集、handsReadOnly/shellGate 放松即 throw、onAsk/hooks 冲突、键存在性
|
|
11
|
-
* profile),且 `applyRuntimeGovernance` 的 shellGate delete 臂读的是 `base.shellGate`、`governanceForced`
|
|
12
|
-
* 观察器包在其内部 —— 一个 base-free 的「已折好」新口复现不了这些,照抄=在 server 侧重写 core 的折叠。
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ **禁 memoize**:`autonomy` / `commandPolicy` / `manualModeShellGate` / `sensitiveWritePatterns` 都是
|
|
15
|
-
* 热改字段(registry 热应用换 config 引用,resume 腿按**当前** config 重折 —— 与审批基线读活
|
|
16
|
-
* `config.approvalRequire` 同一姿势)。每次调用现取,boot 期缓存一份 = 把热改静默冻住。
|
|
17
|
-
*/
|
|
18
1
|
import { posix } from "node:path";
|
|
19
2
|
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, err, ok, } from "@sema-agent/core";
|
|
20
3
|
import { createDurableAskPolicy } from "./approval.js";
|
|
21
|
-
/**
|
|
22
|
-
* #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
23
|
-
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
24
|
-
* 活流腿(bg/SSE,coordinator.runWithContext 包裹且投递面此刻可达,ALS 判)allow——工具执行落到
|
|
25
|
-
* RunnerDeps.onQuestion 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade、
|
|
26
|
-
* durable resume 驱动、断连后的 detach 腿)ask——durable park 原语义逐字保留(#166 后无活流腿放行执行
|
|
27
|
-
* 也不会产出空答:coordinator 无 ALS ctx ⇒ 冻结 `{kind:"unavailable"}`(src/question.ts),永不悬挂;
|
|
28
|
-
* park 仍是把问题送到人面前的唯一那条腿,正当性不变、只是反事实前提换了)。
|
|
29
|
-
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。
|
|
30
|
-
* ⚠️ **单一属主**(复审 A2):AskUserQuestion 的 durable 判决只有这一处。任何需要「同参重建」这条判决的
|
|
31
|
-
* 地方(main.ts 的 parkedReviveInheritedGate 父约束链)必须调本工厂,不得自折 core 原形——两份拷贝里
|
|
32
|
-
* 只改一份正是本条 finding 的成因。**登记豁免一处**:leader worker 腿(src/leader/wire.ts provisionWorker)
|
|
33
|
-
* 自折 core 原形——该腿无活体问答面可装且 leader 不 import boot 层(分层),core 原形+sentinel 即其完整
|
|
34
|
-
* 语义;豁免注在彼处互指,接活体面之日必须并回本工厂。
|
|
35
|
-
*/
|
|
36
4
|
export function createDurableQuestionGate(live) {
|
|
37
5
|
if (live === undefined)
|
|
38
6
|
return createDurableQuestionPolicy();
|
|
@@ -46,69 +14,17 @@ export function createDurableQuestionGate(live) {
|
|
|
46
14
|
},
|
|
47
15
|
};
|
|
48
16
|
}
|
|
49
|
-
/**
|
|
50
|
-
* 沙箱 lane 上**相对形**写目标的守卫补层用 env(codex 对抗复审 round1 finding 1,红先复现)。
|
|
51
|
-
*
|
|
52
|
-
* 缺口:沙箱 lane 的守卫策略拿不到 `rootPath`(沙箱 cwd 不是 server 能猜的,#165 裁定 1),而
|
|
53
|
-
* `DeferredSandboxPathEnv.absolutePath` 对相对形一律报错。core 的 `canonicalizeTarget` 在
|
|
54
|
-
* **absolutePath 失败**这一支不置 `unresolvedSymlink`,于是守卫策略走的是
|
|
55
|
-
* 「判不了就弃权」的 `allow`(dist 亲读)。写门在场时这条腿被门的 `ask` 兜住;而
|
|
56
|
-
* `bypassPermissions` / settings 缺席这几形**根本没有门**,于是 `Write(file_path: ".env")` 一路放行——
|
|
57
|
-
* 而 core 的结构化写工具会把相对形按 engine 跟踪的 cwd 解析后真写下去(fs-write.js `resolveKey`)。
|
|
58
|
-
*
|
|
59
|
-
* 补法:**同一只**守卫策略工厂再铸一个实例,只把「路径→canonical key」这一步换成
|
|
60
|
-
* 纯词法基准(本 env)。判定与提取(哪个参数是写目标、NotebookEdit 的 notebook_path 优先、段匹配)
|
|
61
|
-
* 全部仍是 core 的,server 侧零复刻——复刻 core 的裁决逻辑正是「同源谎」那一类错误。
|
|
62
|
-
*
|
|
63
|
-
* 三条不可动的边界:
|
|
64
|
-
* · **绝对形一律弃权**(absolutePath 报错 ⇒ canon 失败且非 unresolvedSymlink ⇒ core 判 allow):
|
|
65
|
-
* 绝对形归真身裁决那一层,#165「真身胜过名字」的裁定(域内良性软链名叫 `.ssh` 只 ask)不受影响。
|
|
66
|
-
* · **只会 deny,不会放行**:相对形自身拼写里出现的段,解析成绝对路径后仍在,所以词法命中即真命中;
|
|
67
|
-
* 反过来一条名叫 `.env` 而真身良性的相对软链会被误 deny —— 方向是 fail-closed,与守卫集语义同向。
|
|
68
|
-
* · **覆盖面(2026-08-08 按 core 5.19.0 #108 校正;旧文见下方「历史」段)**:本层只在
|
|
69
|
-
* `ToolCallRequest.cwd` **缺席**那一形上说话。core 5.19.0 起每条路径解析型守卫按 `req.cwd ?? rootPath`
|
|
70
|
-
* 解析写目标(dist `core/sensitive-path-policy.js`),而 `canonicalizeTarget` 拿到 baseCwd 后会先把
|
|
71
|
-
* 相对形**拼成绝对形**再交给 env(dist `tools/fs/safety.js` 的 `baseCwd && !isAbsolutePathForm(...)`
|
|
72
|
-
* 分支)—— 本层的 `absolutePath` 对绝对形一律报错弃权 ⇒ **有戳时本层自动让位**,由真身那一层
|
|
73
|
-
* (沙箱 `DeferredSandboxPathEnv` / host `NodeExecutionEnv`)按活 cwd 裁决,cwd 里的守卫段现在真看得见。
|
|
74
|
-
* ⇒ 本层今天的射程 = 「引擎没盖戳」的调用:相对形**自身拼写**里带守卫段的那一类(`Write(".env")`、
|
|
75
|
-
* `Write("cfg/.ssh/id_rsa")`),仍由本层 fail-closed 兜住。**不删臂**:让位与冗余不是一回事——
|
|
76
|
-
* 删掉它等于把「缺戳即无守卫」写死,而缺戳形在契约上是 core 明确保留的回落语义(直接调用形)。
|
|
77
|
-
* 两处特征化钉现在各带两臂(带戳 deny / 缺戳 allow):test/task-settings.test.ts 与
|
|
78
|
-
* test/run-local.test.ts(后者是真引擎端到端,已翻成 🔴 正控)。
|
|
79
|
-
*
|
|
80
|
-
* 📜 **历史(留档,别当现状读)**:2026-08-08 之前本层的覆盖面到「cwd 里的守卫段看不见」为止——
|
|
81
|
-
* 本层把相对形挂在 `/` 上,而工具挂在 engine 活 cwd 上,`cd .git` 后 `Write("config")` 真写
|
|
82
|
-
* `<root>/.git/config` 而本层只看得到 `/config` ⇒ 弃权。design/181 刀3 的口径更正查明这条残余面
|
|
83
|
-
* **不是沙箱 lane 局部的**:host 腿虽供了 `rootPath`,那也是装配期的静态值,一样追不上被 Bash `cd`
|
|
84
|
-
* 就地改写的 `cwdRef.current`(run-local 端到端真复现:`cd .git/hooks` 后 `Write("pre-commit")` 真落盘)。
|
|
85
|
-
* 当年判定属主是引擎那条缝(不是任何一条消费腿——在消费腿里自己拿静态 cwd 追 `cd`,是拿会漂的复制品
|
|
86
|
-
* 追引擎的真值,本文件反复点名的病),并把两条钉写成「引擎缝落地后一起翻面」。**该缝即 core backlog
|
|
87
|
-
* #108,已在 5.19.0 到货**,两条钉按上述翻面完毕。另一条备选收口(守卫集开启即把沙箱 lane 相对写
|
|
88
|
-
* 一律 deny,有真受损方且无对应旋钮)因此作废,无需部署方拍板。
|
|
89
|
-
*/
|
|
90
17
|
export class RelativeTargetLexicalEnv extends StubExecutionEnv {
|
|
91
|
-
/** 相对形 → `/<词法归一>`;绝对形 / 空串 / 含 NUL 一律报错(= 弃权,见类注)。 */
|
|
92
18
|
absolutePath(path) {
|
|
93
19
|
if (path.length === 0 || path.startsWith("/") || path.includes("\u0000")) {
|
|
94
20
|
return Promise.resolve(err(new FileError("not_supported", "this guard layer only adjudicates RELATIVE write targets (absolute forms are adjudicated against the real sandbox filesystem)", path)));
|
|
95
21
|
}
|
|
96
22
|
return Promise.resolve(ok(posix.normalize(`/${path}`)));
|
|
97
23
|
}
|
|
98
|
-
/** 恒「不存在」⇒ core 的 `canonicalizeNewPath` 逐级回退,最终把词法归一形当 canonical key 交给段匹配。
|
|
99
|
-
* 这里绝不能报错:报错会被 core 读成 `unresolvedSymlink` 而对**每一个**相对目标 deny(含普通文件)。 */
|
|
100
24
|
exists(_path, _abortSignal) {
|
|
101
25
|
return Promise.resolve(ok(false));
|
|
102
26
|
}
|
|
103
27
|
}
|
|
104
|
-
/**
|
|
105
|
-
* boot 期的守卫集**可编译性**门(#177 收口①,随 design/181 件一搬进本口 —— 三条消费腿同得)。
|
|
106
|
-
*
|
|
107
|
-
* 守卫集的编译发生在**每个请求**上。core 的 `compilePatterns` 对「一个路径段都没有」的模式(`"/"`、
|
|
108
|
-
* `"//"`)THROW,那条 throw 会变成**每一个任务一条 500**,且运维从错误里看不出是自己的 env 写错了。
|
|
109
|
-
* 消费腿在装配期先编译一次:非法旋钮值当场炸在启动上(与 config.ts 的 env fail-loud 同族),指名键与
|
|
110
|
-
* core 的原因。env 只是编译期的占位(compilePatterns 不碰它),真裁决用的是每请求按 lane 铸的那一个。
|
|
111
|
-
*/
|
|
112
28
|
export function assertGuardPatternsUsable(config) {
|
|
113
29
|
if (config.sensitiveWritePatterns.length === 0)
|
|
114
30
|
return;
|
|
@@ -119,20 +35,6 @@ export function assertGuardPatternsUsable(config) {
|
|
|
119
35
|
throw new Error(`SENSITIVE_WRITE_PATTERNS is not a usable guard set: ${e instanceof Error ? e.message : String(e)}`);
|
|
120
36
|
}
|
|
121
37
|
}
|
|
122
|
-
/**
|
|
123
|
-
* UNGATED 信号的**补偿**(design/181 件一收编 / 件三三腿同得)。
|
|
124
|
-
*
|
|
125
|
-
* 审批基线铺开之后 core 的 `hasEffectAwareGate` 恒真,于是它那条 "write-capable hand tools are present
|
|
126
|
-
* but UNGATED" 的 onError 不再触发(判据是 `policyLayers.length > 0`,prepare-task dist 亲读)。那条信号
|
|
127
|
-
* 此前是「这个部署一个门都没接」这个 misconfig 的**唯一**提示,而守卫集只挡那二十来个路径段、其余写
|
|
128
|
-
* 目标照旧无裁决 —— 信号不能因为我们铺了基线就静默消失,所以由我们自己按同一判据说一次。
|
|
129
|
-
*
|
|
130
|
-
* 判据(与信号消失的条件逐字互补):守卫集在场 ∧ 两条产**真**门的腿都不在场(durable 门关 ∧ 单用户
|
|
131
|
-
* auto-accept 基线不适用)。三个量都是部署常量 ⇒ 消费腿在 boot 期说一次,不是每任务一次。
|
|
132
|
-
*
|
|
133
|
-
* ⚠️ 单一属主(design/181 件三):HTTP 腿与 run-local 腿共用本判据与文案。两处各写一份 = 一处改了另一处
|
|
134
|
-
* 没改,而两份都长得像对的 —— 那正是本文件存在的理由。
|
|
135
|
-
*/
|
|
136
38
|
export function buildOnlySensitiveBaselineWarning(config, seat) {
|
|
137
39
|
if (config.sensitiveWritePatterns.length === 0 || seat.durableEnabled || seat.singleUserAutoAcceptBaseline)
|
|
138
40
|
return undefined;
|
|
@@ -145,13 +47,6 @@ export function buildOnlySensitiveBaselineWarning(config, seat) {
|
|
|
145
47
|
},
|
|
146
48
|
};
|
|
147
49
|
}
|
|
148
|
-
/**
|
|
149
|
-
* `applyRuntimeGovernance` 的 governance 实参预铸(design/181 件一)。
|
|
150
|
-
*
|
|
151
|
-
* 键存在性 profile 逐字保持消费腿原样:`autonomy`/`commandPolicy` 恒在场(值可为 `undefined`),
|
|
152
|
-
* `manualModeShellGate`/`sensitivePathPolicy` 按在场性条件展开 —— `applyRuntimeGovernance` 的
|
|
153
|
-
* `!== undefined` 判据对两者等价,但 profile 是折叠面的可观测字节,搬家不许顺手改。
|
|
154
|
-
*/
|
|
155
50
|
export function createDeploymentGovernanceInputs(config, pathAdjudication) {
|
|
156
51
|
const sensitivePathPolicy = config.sensitiveWritePatterns.length > 0
|
|
157
52
|
? (() => {
|
|
@@ -160,9 +55,6 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication) {
|
|
|
160
55
|
patterns: config.sensitiveWritePatterns,
|
|
161
56
|
...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
|
|
162
57
|
});
|
|
163
|
-
// cwd 在场(host 形)⇒ 相对形已被 core 按 rootPath 解析进真身裁决,一层就够。
|
|
164
|
-
// cwd 缺席(沙箱形)⇒ 相对形在真身那一层是「判不了 ⇒ 弃权 allow」,而写门恰恰在
|
|
165
|
-
// bypass/settings 缺席这几形不在场 ⇒ 补一层纯词法的相对形守卫(见 RelativeTargetLexicalEnv)。
|
|
166
58
|
if (pathAdjudication.cwd !== undefined)
|
|
167
59
|
return realTarget;
|
|
168
60
|
return combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns }));
|
|
@@ -175,22 +67,6 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication) {
|
|
|
175
67
|
...(sensitivePathPolicy ? { sensitivePathPolicy } : {}),
|
|
176
68
|
};
|
|
177
69
|
}
|
|
178
|
-
/**
|
|
179
|
-
* 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座(design/181 件一)。**恒非
|
|
180
|
-
* `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
|
|
181
|
-
*
|
|
182
|
-
* 两形:
|
|
183
|
-
* · `durable` 在场 ⇒ durable 轴 = AskUserQuestion 判决门 + F4 高危写审批门(deny/neverAuto/预算/
|
|
184
|
-
* 会话豁免全在 `createDurableAskPolicy` 里)。gated `ask` 由 core 变成 durable checkpoint suspend。
|
|
185
|
-
* · `durable` 缺席 ⇒ **adjudicated allow-all**(`createAllowDenyPolicy({})`):一条**在场的**、
|
|
186
|
-
* effect-aware 的策略。CC 的信任模型是 auto-accept,但门**机制**必须在场(core 原则「机制留、默认
|
|
187
|
-
* 可更宽」)—— 满足 core 的 `hasEffectAwareGate`,恢复可观测性与 hook/tighten 点,而运维照旧用
|
|
188
|
-
* `AUTONOMY`/`commandPolicy` 收紧不可逆操作(由 `applyRuntimeGovernance` tighten-only 叠上)。
|
|
189
|
-
*
|
|
190
|
-
* ⚠️ 「零门意图才铺 allow-all」这条准入判据**不在本口**:它是消费腿的部署形判断(单用户/多租户、
|
|
191
|
-
* 有无 checkpoint 店),属主是 `hasOperatorGateIntent`/`assertGateIntentServiceable`(src/approval.ts)。
|
|
192
|
-
* 本口只按调用方给的形铸策略,绝不替它判「这个部署该不该有门」。
|
|
193
|
-
*/
|
|
194
70
|
export function createApprovalBaselinePolicy(config, durable) {
|
|
195
71
|
if (durable === undefined)
|
|
196
72
|
return createAllowDenyPolicy({});
|
package/dist/digest-form.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/158 R12 收编叶子 — the sha256 content-address SHAPE constants, single-sourced. A neutral leaf (no
|
|
3
|
-
* imports of its own) so both `security.ts`-adjacent modules and prompt/image/session-sync consumers can
|
|
4
|
-
* depend on it without a layering edge. Six call sites across the tree each ran their OWN literal copy of one
|
|
5
|
-
* of these two regexes (a bare-hex form and a scheme-qualified form) — same shape, independently typed out.
|
|
6
|
-
* This module is the single point of truth for the SHAPE; each consumer's accept POLICY (prefix-stripping,
|
|
7
|
-
* case-folding order) is UNCHANGED by this collection — those policies differ across call sites (a real,
|
|
8
|
-
* pre-existing divergence, not introduced here) and are tracked for a follow-up cross-repo alignment, not
|
|
9
|
-
* unified in this refactor.
|
|
10
|
-
*/
|
|
11
|
-
/** A bare sha256 content-address: exactly 64 lowercase-hex characters, no scheme prefix. */
|
|
12
1
|
export const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
13
|
-
/** A scheme-qualified sha256 content-address: `sha256:` followed by 64 lowercase-hex characters. */
|
|
14
2
|
export const SHA256_DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
15
3
|
//# sourceMappingURL=digest-form.js.map
|
package/dist/elicitation.js
CHANGED
|
@@ -1,37 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* E23 (shell-host contract) — inbound-MCP-elicitation HITL flow, the SERVICE side of core 1.124.0's live-only
|
|
3
|
-
* `RunnerDeps.onElicit` seam (design/99 §E23).
|
|
4
|
-
*
|
|
5
|
-
* An opted-in MCP server (`McpServerSpec.elicitation === true`) may, MID-tool-call, ask the END USER for input.
|
|
6
|
-
* core hands that request to `onElicit` and awaits the answer SYNCHRONOUSLY on the still-open task-scoped
|
|
7
|
-
* connection (it replies to the server with whatever the promise resolves to). There is NO durable suspend —
|
|
8
|
-
* a crash/replica-change loses an in-flight elicitation (acceptable for an interactive shell; a durable variant
|
|
9
|
-
* waits on the deferred E22 longer-lived connection). So this whole flow is LIVE-ONLY + same-replica, the
|
|
10
|
-
* `steer()`-class of HITL, NOT the durable approval/checkpoint class.
|
|
11
|
-
*
|
|
12
|
-
* The service is pure WIRE glue (the abstraction is core's, §0.1): forward the request to whoever is tailing the
|
|
13
|
-
* run's stream (an `elicitation` SSE frame), park a promise, and resolve it from `POST /v1/elicitations/:id/respond`.
|
|
14
|
-
* Three responsibilities are explicitly the deployment's (core delegates them):
|
|
15
|
-
* 1. THROTTLE — core imposes no rate/count cap; an over-eager or hostile server could flood the human. We bound
|
|
16
|
-
* concurrency + total + per-server burst per run; a breach fail-closes to `decline`.
|
|
17
|
-
* 2. FENCING — `message`/`requestedSchema` are server-controlled UNTRUSTED text; `message` is `delimitUntrusted`-
|
|
18
|
-
* fenced + secret-redacted before it reaches a human (the shell must also fence `requestedSchema` on display).
|
|
19
|
-
* 3. FORM-MODE ONLY — v1 advertises form mode; url-mode (a phishing surface) is defensively rejected.
|
|
20
|
-
*
|
|
21
|
-
* ROUTING: `onElicit` lives on the SINGLETON runner's `RunnerDeps` and the request carries no taskId, so the run's
|
|
22
|
-
* executor establishes a per-run context (AsyncLocalStorage, like `withPrincipal`) carrying the taskId (throttle
|
|
23
|
-
* key), the owner (respond owner-gate), and an `emit` closure (durable append for background/resume legs; `res.write`
|
|
24
|
-
* for the synchronous stream leg). `node:async_hooks` propagates it through every await, so `onElicit` — invoked deep
|
|
25
|
-
* inside core's `callTool` — recovers it. No context ⇒ fail-closed `decline`.
|
|
26
|
-
*/
|
|
27
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
2
|
import { uuidv7, delimitUntrusted } from "@sema-agent/core";
|
|
29
3
|
import { redactSecrets, redactDeep } from "./trace/redact.js";
|
|
30
4
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
31
|
-
/** Size bounds on the UNTRUSTED server-controlled payload (the throttle bounds COUNT, these bound SIZE — an
|
|
32
|
-
* opted-in-but-hostile server otherwise amplifies a multi-MB prompt/schema into the durable log + SSE socket).
|
|
33
|
-
* The message is a human-facing prompt (a few KB is ample); a requestedSchema over the cap is dropped (the form
|
|
34
|
-
* can't render an abusive schema — fail-safe to no-schema). */
|
|
35
5
|
const MAX_ELICIT_MESSAGE_CHARS = 4096;
|
|
36
6
|
const MAX_ELICIT_SCHEMA_BYTES = 8192;
|
|
37
7
|
export const DEFAULT_ELICITATION_THROTTLE = {
|
|
@@ -40,9 +10,6 @@ export const DEFAULT_ELICITATION_THROTTLE = {
|
|
|
40
10
|
minIntervalMsPerServer: 1_000,
|
|
41
11
|
ttlMs: 5 * 60_000,
|
|
42
12
|
};
|
|
43
|
-
/** Validate the respond body = core's `McpElicitResponse`. `content` is accept-only OPAQUE passthrough (core never
|
|
44
|
-
* validates it against `requestedSchema`); we validate only the outer shape (a flat record of scalars / string[]),
|
|
45
|
-
* never the schema. A non-accept drops content (MCP convention). */
|
|
46
13
|
export function parseElicitationResponse(body) {
|
|
47
14
|
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
48
15
|
return { ok: false, error: "body must be an object" };
|
|
@@ -66,10 +33,6 @@ export function parseElicitationResponse(body) {
|
|
|
66
33
|
}
|
|
67
34
|
return { ok: true, value: { action: b.action } };
|
|
68
35
|
}
|
|
69
|
-
/** redactDeep + size-bound an UNTRUSTED `requestedSchema` before it is persisted to the durable log / streamed
|
|
70
|
-
* (the same redact-at-write contract every other untrusted structured field honors, e.g. tool_end.output). A JSON
|
|
71
|
-
* Schema's free-text carriers (description/title/default/enum) can smuggle a secret. Over the byte cap ⇒ undefined
|
|
72
|
-
* (drop — an abusive schema can't render a form); unserializable (circular) ⇒ undefined. */
|
|
73
36
|
function boundSchema(schema) {
|
|
74
37
|
if (schema === undefined)
|
|
75
38
|
return undefined;
|
|
@@ -83,12 +46,6 @@ function boundSchema(schema) {
|
|
|
83
46
|
}
|
|
84
47
|
return redacted;
|
|
85
48
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Coordinates inbound MCP elicitations for one server instance. Process-local + same-replica (the pending map is in
|
|
88
|
-
* memory, like `steerableRuns`): a respond that lands on another replica finds nothing → 404. Present (passed into
|
|
89
|
-
* `RunnerDeps.onElicit` + the respond route) ONLY when `MCP_ELICITATION_ENABLED` — absent ⇒ core advertises no
|
|
90
|
-
* elicitation capability to any server (doubly fail-closed with the per-server `McpServerSpec.elicitation` default OFF).
|
|
91
|
-
*/
|
|
92
49
|
export class ElicitationCoordinator {
|
|
93
50
|
als = new AsyncLocalStorage();
|
|
94
51
|
pending = new Map();
|
|
@@ -99,16 +56,12 @@ export class ElicitationCoordinator {
|
|
|
99
56
|
this.throttle = throttle;
|
|
100
57
|
this.now = now;
|
|
101
58
|
}
|
|
102
|
-
/** Run `fn` with the per-run elicitation context ambient. On exit, cancel any still-pending elicitation for this
|
|
103
|
-
* run (a live-only elicitation cannot outlive its leg) and drop the run's counters (no leak). The cancel covers
|
|
104
|
-
* the case where the leg ends/throws while an elicitation is parked but the leg's own loop has already drained. */
|
|
105
59
|
runWithContext(ctx, fn) {
|
|
106
60
|
return this.als.run(ctx, async () => {
|
|
107
61
|
try {
|
|
108
62
|
return await fn();
|
|
109
63
|
}
|
|
110
64
|
finally {
|
|
111
|
-
// Snapshot first — settle() mutates this.pending.
|
|
112
65
|
for (const [, p] of [...this.pending]) {
|
|
113
66
|
if (p.taskId === ctx.taskId)
|
|
114
67
|
p.settle({ action: "cancel" });
|
|
@@ -117,42 +70,26 @@ export class ElicitationCoordinator {
|
|
|
117
70
|
}
|
|
118
71
|
});
|
|
119
72
|
}
|
|
120
|
-
/** `RunnerDeps.onElicit`. Core calls this when an opted-in server elicits the END USER mid-tool-call; the resolved
|
|
121
|
-
* value is sent back to the server on the still-open connection. Fail-closed (`decline`/`cancel`) in every
|
|
122
|
-
* uncertain case. Arrow property so it can be passed as `onElicit: coordinator.elicit` with `this` bound. */
|
|
123
73
|
elicit = async (req, signal) => {
|
|
124
74
|
const ctx = this.als.getStore();
|
|
125
|
-
// No run context to route to a human (a leg not wrapped by runWithContext — synchronous /v1/tasks, verify/cascade,
|
|
126
|
-
// or a leader sub-task) ⇒ fail-closed decline. NB on SUB-TASKS: subagent/team run on the SAME runner inside the
|
|
127
|
-
// parent's ALS frame, so a sub-task's elicitation INHERITS the top-level run's ctx — by design it routes to the
|
|
128
|
-
// top-level stream + owner and shares the per-run-tree throttle budget (one human, one budget); the frame's
|
|
129
|
-
// `mcpServerName` attributes which server asked. (Same tenant: a sub-task inherits the parent principal.)
|
|
130
75
|
if (!ctx)
|
|
131
76
|
return { action: "decline" };
|
|
132
|
-
// v1 FORM-MODE ONLY: defensively reject url-mode. core advertises form-only so `url` is never populated today;
|
|
133
|
-
// this is defense-in-depth so a future/hostile url can never reach a human as a phishing link.
|
|
134
77
|
if (typeof req.url === "string" && req.url.length > 0)
|
|
135
78
|
return { action: "decline" };
|
|
136
79
|
if (signal?.aborted || ctx.abortSignal?.aborted)
|
|
137
80
|
return { action: "cancel" };
|
|
138
|
-
// THROTTLE (service's job — core imposes none): bound concurrency + total + per-server burst. Breach ⇒ decline.
|
|
139
81
|
const rc = this.countersFor(ctx.taskId);
|
|
140
82
|
const t = this.now();
|
|
141
|
-
const last = rc.lastByServer.get(req.server);
|
|
83
|
+
const last = rc.lastByServer.get(req.server);
|
|
142
84
|
if (rc.inflight >= this.throttle.maxConcurrentPerRun ||
|
|
143
85
|
rc.total >= this.throttle.maxTotalPerRun ||
|
|
144
86
|
(last !== undefined && t - last < this.throttle.minIntervalMsPerServer)) {
|
|
145
87
|
return { action: "decline" };
|
|
146
88
|
}
|
|
147
89
|
const id = uuidv7();
|
|
148
|
-
rc.inflight += 1;
|
|
149
|
-
// FENCE + secret-redact + SIZE-bound the untrusted human-facing prompt: delimitUntrusted neutralizes
|
|
150
|
-
// <system-reminder>/fence break-out on BOTH the body and the server-name label; redactSecrets strips a secret the
|
|
151
|
-
// server echoed; the slice bounds storage/CPU. requestedSchema is redactDeep'd + size-bounded (it is persisted to
|
|
152
|
-
// the durable log + streamed, served to trace readers — fencing-on-display alone does not contain secrets).
|
|
90
|
+
rc.inflight += 1;
|
|
153
91
|
const message = delimitUntrusted(req.server, redactSecrets((typeof req.message === "string" ? req.message : "").slice(0, MAX_ELICIT_MESSAGE_CHARS)));
|
|
154
92
|
const requestedSchema = boundSchema(req.requestedSchema);
|
|
155
|
-
// Deferred answer promise + idempotent settle (resolved by respond, the TTL timer, or an abort).
|
|
156
93
|
let done = false;
|
|
157
94
|
let timer;
|
|
158
95
|
let resolveAnswer;
|
|
@@ -169,58 +106,38 @@ export class ElicitationCoordinator {
|
|
|
169
106
|
rc.inflight = Math.max(0, rc.inflight - 1);
|
|
170
107
|
resolveAnswer(r);
|
|
171
108
|
};
|
|
172
|
-
// Both signals: the MCP per-request signal AND the run's own cancel signal (the former may not be chained to a
|
|
173
|
-
// run abort, which would otherwise leave a cancelled run parked here for the full TTL).
|
|
174
109
|
const onAbort = () => settle({ action: "cancel" });
|
|
175
110
|
timer = setTimeout(() => settle({ action: "decline" }), this.throttle.ttlMs);
|
|
176
111
|
timer.unref?.();
|
|
177
112
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
178
113
|
ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
179
|
-
// Register BEFORE emitting so a (fast) respond can never miss the entry.
|
|
180
114
|
this.pending.set(id, { settle, owner: ctx.owner, taskId: ctx.taskId });
|
|
181
|
-
// Emit the OPEN frame AWAITED so its durable append is ORDERED ahead of the answer/completion/terminal appends —
|
|
182
|
-
// a fire-and-forget append can commit out of seq order and be permanently skipped by the monotonic events-tail
|
|
183
|
-
// cursor (an invisible prompt that then TTL-declines). On a successful emit, charge the per-run-tree budget; a
|
|
184
|
-
// FAILED emit declines (nobody can answer) and must NOT burn maxTotal / the per-server interval.
|
|
185
115
|
try {
|
|
186
116
|
await ctx.emit({ type: "elicitation", elicitationId: id, mcpServerName: req.server, message, ...(requestedSchema !== undefined ? { requestedSchema } : {}), mode: "form", serverNowMs: this.now() });
|
|
187
117
|
rc.total += 1;
|
|
188
118
|
rc.lastByServer.set(req.server, t);
|
|
189
119
|
}
|
|
190
120
|
catch {
|
|
191
|
-
// #157:投递失败 ⇒ decline。方向已是 fail-closed(问不到人就是拒),缺的只是留痕。
|
|
192
121
|
recordFailOpen("server.elicitation.open-frame-undelivered", `elicitation=${id} server=${req.server}`);
|
|
193
|
-
settle({ action: "decline" });
|
|
122
|
+
settle({ action: "decline" });
|
|
194
123
|
}
|
|
195
124
|
const answer = await answerP;
|
|
196
|
-
// Completion breadcrumb (dialog dismiss) — FIRE-AND-FORGET so a slow/hung durable append can NEVER delay
|
|
197
|
-
// returning the answer to core (which replies to the MCP server on the still-open connection). Best-effort +
|
|
198
|
-
// ordering-uncritical: the shell already has the answer via respond; a late/lost dismiss is cosmetic.
|
|
199
125
|
void Promise.resolve()
|
|
200
126
|
.then(() => ctx.emit({ type: "elicitation_complete", elicitationId: id, mcpServerName: req.server, action: answer.action, serverNowMs: this.now() }))
|
|
201
|
-
// #157:F 类兜底,与 question.ts 的孪生面同形同 tag 形(丢收尾渲染,不丢答案)。
|
|
202
127
|
.catch(() => recordFailOpen("server.elicitation.complete-breadcrumb-dropped", `elicitation=${id}`));
|
|
203
128
|
return answer;
|
|
204
129
|
};
|
|
205
|
-
/** `POST /v1/elicitations/:id/respond` — resolve a parked elicitation with the shell's answer. Owner-gated with a
|
|
206
|
-
* 404 (no existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica) both get 404.
|
|
207
|
-
* Returns the HTTP {status, body}; the HTTP layer owns auth (gatedPrincipal + REQUIRE_PRINCIPAL) before calling. */
|
|
208
130
|
respond(id, principal, body) {
|
|
209
|
-
// Validate the body BEFORE the lookup (steer parity): a bad body is the caller's error regardless of run state,
|
|
210
|
-
// so 400 is existence-independent (a malformed respond to ANY id → 400, never an existence oracle via 400-vs-404).
|
|
211
131
|
const parsed = parseElicitationResponse(body);
|
|
212
132
|
if (!parsed.ok)
|
|
213
133
|
return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
|
|
214
134
|
const entry = this.pending.get(id);
|
|
215
|
-
// Owner-gate with 404 (no existence oracle): a non-owner AND an unknown id (answered / expired / wrong replica)
|
|
216
|
-
// both get 404, so a good-body caller can't tell another tenant's id from a nonexistent one.
|
|
217
135
|
if (!entry || (entry.owner !== null && entry.owner !== principal)) {
|
|
218
136
|
return { status: 404, body: { error: "no pending elicitation for this id (answered, expired, or not on this replica)", errorCode: "elicitation.not_pending" } };
|
|
219
137
|
}
|
|
220
138
|
entry.settle(parsed.value);
|
|
221
139
|
return { status: 200, body: { elicitationId: id, delivery: "applied", action: parsed.value.action } };
|
|
222
140
|
}
|
|
223
|
-
/** Test/observability hook: number of currently-parked elicitations. */
|
|
224
141
|
pendingCount() {
|
|
225
142
|
return this.pending.size;
|
|
226
143
|
}
|