@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
|
@@ -3,19 +3,15 @@ import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_
|
|
|
3
3
|
import { verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
4
4
|
import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
|
|
5
5
|
import { redactedPreview, redactSecrets } from "../../trace/redact.js";
|
|
6
|
-
import { fleetRunLabels } from "../../fleet/fleet-bus.js";
|
|
6
|
+
import { fleetRunLabels } from "../../fleet/fleet-bus.js";
|
|
7
7
|
import { sleep } from "../sse-log.js";
|
|
8
8
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
9
9
|
import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
|
|
10
10
|
import { gatedPrincipal, explicitOperatorOk, isOperator } from "../principal-gate.js";
|
|
11
11
|
import { governanceOriginOf } from "../active-run-conflict.js";
|
|
12
|
-
// design/80 seam #2 (assistant-scheduler): graceful preempt (durable yield) + resource_limit resume of one task.
|
|
13
12
|
export const ASSISTANT_PREEMPT_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/preempt$/;
|
|
14
13
|
export const ASSISTANT_RESUME_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/resume$/;
|
|
15
14
|
export const ASSISTANT_PLAN_REVIEW_RE = /^\/v1\/assistant\/tasks\/([^/]+)\/plan_review$/;
|
|
16
|
-
/** Structural check for an operator-supplied QuestionAnswer (durable ask resume, TC-5.4). Strict on the
|
|
17
|
-
* load-bearing shape — `answers[]` non-empty, each `{header: string, selected: string[], note?: string}` —
|
|
18
|
-
* so a typo'd payload fails the request instead of resuming the task with an answer the tool can't use. */
|
|
19
15
|
export function isQuestionAnswer(v) {
|
|
20
16
|
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
21
17
|
return false;
|
|
@@ -38,28 +34,15 @@ export async function handleApprovalsAssistant(req, res, url, ctx) {
|
|
|
38
34
|
await handleApprovalsAssistantBody(req, res, url, ctx, miss);
|
|
39
35
|
return !miss.fell;
|
|
40
36
|
}
|
|
41
|
-
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
|
|
42
|
-
* = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
|
|
43
37
|
async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
44
38
|
const { deps } = ctx;
|
|
45
39
|
const { preemptableRuns } = ctx.registry;
|
|
46
40
|
const { readJson, rateLimited, quotaExceeded } = ctx.helpers;
|
|
47
41
|
const { resumeCheckpoint, resumePreempted, resumePlanReview } = ctx.legs;
|
|
48
|
-
// Durable F4 (design/45): the approval queue over the CHECKPOINT store. Operators list pending (non-secret)
|
|
49
|
-
// and decide by the session handle → the service maps session→token INTERNALLY and resumes (token never in
|
|
50
|
-
// a URL/body/log). Takes precedence over the poll gate when a checkpointStore is wired.
|
|
51
42
|
if (deps.checkpointStore && (url.startsWith("/v1/approvals") || url.startsWith("/v1/assistant"))) {
|
|
52
43
|
const cs = deps.checkpointStore;
|
|
53
|
-
|
|
54
|
-
// operator header leaks every tenant's pending tool-call args via GET/stream, adversarial HIGH). For reads there
|
|
55
|
-
// is no per-decision checkpointToken, so identity comes from a verified principal JWT WITHOUT a cnf.bnd; an
|
|
56
|
-
// absent/invalid token ⇒ undefined principal ⇒ scope "__none__" + operator=false (fail-closed). The POST /decide
|
|
57
|
-
// path below re-verifies the SAME token WITH cnf.bnd (channel-bound to the action).
|
|
58
|
-
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
44
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
59
45
|
const operator = isOperator(principal, deps.config.operatorPrincipals);
|
|
60
|
-
// design/80 (native push): SSE stream of pending-approval deltas so the portal SUBSCRIBES ONCE instead of
|
|
61
|
-
// polling GET /v1/approvals every ~10s. Same operator/owner scope as the list. Must precede the list/decide
|
|
62
|
-
// matches (the decide regex would otherwise treat "stream" as a sessionId on a POST — but this is GET-only).
|
|
63
46
|
if (req.method === "GET" && url === "/v1/approvals/stream") {
|
|
64
47
|
const scope = operator
|
|
65
48
|
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
|
|
@@ -69,26 +52,14 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
69
52
|
}
|
|
70
53
|
if (req.method === "GET" && url === "/v1/approvals") {
|
|
71
54
|
const scope = operator
|
|
72
|
-
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
|
|
73
|
-
: (principal ?? "__none__");
|
|
74
|
-
// #209 件4 + [3684]②:行整只上 wire(键集契约照旧「整只透传、不按键投影」),但两格由投影函数
|
|
75
|
-
// 负责——`riskDescriptor.shadowedRule` 脱敏、`governanceForced` 归因。与 `/v1/approvals/stream`
|
|
76
|
-
// **共用同一个** `projectPendingForWire`(两条读面各投各的必漂,见其顶注)。
|
|
77
|
-
// #315([4658] 案):第二顶层键 `livePending` = streamApproval 窗内的流内 ask(此前对轮询消费端
|
|
78
|
-
// 结构性不可见,要等窗超时转 park 才浮现)。**分数组不混编**:两族行形与决议路由不同(live 行
|
|
79
|
-
// 决议口=respond,durable 行=decide;数组名即路由判据)。scope 复用同一表达式(operator=全量,
|
|
80
|
-
// principal=只见自己;"__none__" 恒空)。协调器未装配 ⇒ 键**缺席**不编 [](缺席=「本部署无
|
|
81
|
-
// live 审批面」,空数组=「有面、此刻无待批」,两义须可判别)。
|
|
55
|
+
? (new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined)
|
|
56
|
+
: (principal ?? "__none__");
|
|
82
57
|
sendJson(res, 200, {
|
|
83
58
|
pending: (await cs.listPending(scope)).map((r) => projectPendingForWire(r, deps.config)),
|
|
84
59
|
...(deps.toolApproval ? { livePending: deps.toolApproval.listLivePending(scope) } : {}),
|
|
85
60
|
});
|
|
86
61
|
return;
|
|
87
62
|
}
|
|
88
|
-
// exemptions surface — the UI's "本会话不再询问" state (list) + revoke. Same authz shape as
|
|
89
|
-
// /decide: an explicit operator may inspect any session; otherwise the caller must OWN the session's scope
|
|
90
|
-
// (peekPendingScope only knows PENDING sessions, so ownership here reads the session store's owner — the
|
|
91
|
-
// durable authority). Non-owner ⇒ 404 (no existence oracle, decide-route parity).
|
|
92
63
|
{
|
|
93
64
|
const em = /^\/v1\/approvals\/([^/]+)\/exemptions(?:\/([^/]+))?$/.exec(url);
|
|
94
65
|
if (em && (req.method === "GET" || req.method === "DELETE")) {
|
|
@@ -107,14 +78,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
107
78
|
return;
|
|
108
79
|
}
|
|
109
80
|
const operators = deps.config.operatorPrincipals;
|
|
110
|
-
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
81
|
+
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
111
82
|
if (!isExplicitOperator) {
|
|
112
|
-
// 🔴 fail-CLOSED owner gate (both reviewers, 2026-07-13): mirror the sibling owner-gated routes
|
|
113
|
-
// (session policy/delete) — ownerOf REQUIRED (absent ⇒ 501, never open), a store error PROPAGATES
|
|
114
|
-
// (500, never open; the old `.catch(() => undefined)` flipped a DB blip into authorization), and an
|
|
115
|
-
// UNKNOWN session (owner === undefined) 404s with no existence oracle. owner === null (ownerless
|
|
116
|
-
// anonymous/dev session) stays open — decide-route parity. No uuid shape-gate: this population is
|
|
117
|
-
// the checkpoint sessionId space (decide-route parity), not the sessions-API surface.
|
|
118
83
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
119
84
|
if (!ownerOf) {
|
|
120
85
|
sendError(res, 501, "capability.session_store_required", "approval exemptions require a session-store backend");
|
|
@@ -128,7 +93,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
128
93
|
}
|
|
129
94
|
if (req.method === "GET") {
|
|
130
95
|
if (exToolName !== undefined) {
|
|
131
|
-
sendError(res, 404, "request.route_unsupported", "not found");
|
|
96
|
+
sendError(res, 404, "request.route_unsupported", "not found");
|
|
132
97
|
return;
|
|
133
98
|
}
|
|
134
99
|
sendJson(res, 200, { exemptions: await deps.approvalExemptionStore.list(exSessionId) });
|
|
@@ -138,46 +103,17 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
138
103
|
sendError(res, 400, "request.route_unsupported", "DELETE needs /exemptions/:toolName");
|
|
139
104
|
return;
|
|
140
105
|
}
|
|
141
|
-
const revoked = await deps.approvalExemptionStore.revoke(exSessionId, exToolName);
|
|
106
|
+
const revoked = await deps.approvalExemptionStore.revoke(exSessionId, exToolName);
|
|
142
107
|
sendJson(res, revoked ? 200 : 404, revoked ? { revoked: true } : { error: "not found", errorCode: "not_found.exemption" });
|
|
143
108
|
return;
|
|
144
109
|
}
|
|
145
110
|
}
|
|
146
|
-
// design/80 #2 (assistant-scheduler, design/75 §3): the SINGLE INBOX — one prioritized view of the HITL gates
|
|
147
|
-
// across ALL of a user's N concurrent durable tasks. = listPending(scope) (already severity-sorted with the
|
|
148
|
-
// riskDescriptor) ENRICHED with each task's objective so the user knows WHICH of their tasks each gate belongs
|
|
149
|
-
// to (the missing cross-task aggregation). Scheduling priority = the severity sort; preemption is
|
|
150
|
-
// a later policy layer. Same owner-scoping as listPending (a non-operator sees only its own tasks' gates).
|
|
151
111
|
if (req.method === "GET" && url === "/v1/assistant/inbox") {
|
|
152
|
-
// SECURITY (review w16yqkkxv): the assistant inbox is a PER-USER view — ALWAYS scope to a CONCRETE owner,
|
|
153
|
-
// never a system-wide dump. A non-operator sees its own principal; an operator may inspect ONE owner via
|
|
154
|
-
// ?owner, else its own. (≠ /v1/approvals, the admin queue, which intentionally lists all.) This removes the
|
|
155
|
-
// scope=undefined / empty-?owner path that made listPending() return every tenant's pendings unbounded.
|
|
156
112
|
const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
|
|
157
113
|
const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
|
|
158
|
-
// 🔴 (BUG FIX) the inbox is the documented CheckpointSummary view (gateKind + severity SCALAR +
|
|
159
|
-
// spentMicroUsd + deadline), NOT the rich operator-queue PendingCheckpoint. It USED to spread listPending()'s
|
|
160
|
-
// raw rich rows — which carried `riskDescriptor.severity` (an object, not the scalar), the tool input + the
|
|
161
|
-
// decide binding, and NO gateKind — diverging from the contract + the tasks view (which already projects via
|
|
162
|
-
// summarizeCheckpoint). Switched to listByScope (the SAME summarizeCheckpoint projection tasks uses) so the
|
|
163
|
-
// inbox + tasks are consistent; the rich decide-binding lives on /v1/approvals (the operator queue), not here.
|
|
164
114
|
const summaries = await cs.listByScope(scope);
|
|
165
115
|
const inbox = (await Promise.all(summaries.map(async (s) => {
|
|
166
|
-
// `createdAt` + `toolInput` are now PROJECTED into CheckpointSummary by core's summarizeCheckpoint
|
|
167
|
-
// (core 1.116.0): listByScope returns them in
|
|
168
|
-
// ONE query, so the per-entry `cs.get(token)` N+1 is GONE. `objective` lives in the task body
|
|
169
|
-
// (checkpoint_ctx), not the summary, so it still needs getCtx (best-effort; missing ⇒ null).
|
|
170
116
|
const ctx = await cs.getCtx(s.sessionId).catch(() => null);
|
|
171
|
-
// 🔴 STRIP the resume `token` (the unexposed secret capability — resume is by sessionId, never the
|
|
172
|
-
// token) AND the RAW `toolInput` (core bounds but does NOT redact it — redaction is
|
|
173
|
-
// the consumer's job). Re-emit the REDACTED `input` instead.
|
|
174
|
-
// 🔴 EXPLICIT projection([2029] cli 批准的收窄,辖域=仅本 inbox 面)—— 此前是「剥 token/toolInput、
|
|
175
|
-
// 其余整行展开」,于是 core summarizeCheckpoint 在 tool_approval 门上填的 toolName/toolCallId/preview/
|
|
176
|
-
// principal/sourceTaskId/contentKind 全部漏上 wire(其中 preview 还没过脱敏,而它同源的 toolInput 是
|
|
177
|
-
// 显式脱敏的)。四客户端仓结构性零消费(inbox verb 无调用点,[2029] 取证),按 §2 原意收成显式白名单:
|
|
178
|
-
// §1 镜像键(OMIT-when-absent)+ 两个显式 nullable 的 enrichment(objective/input)。富行
|
|
179
|
-
// (toolName/toolCallId/decide 绑定)只住 /v1/approvals —— 那面有具名消费者(desktop session-host
|
|
180
|
-
// 冷恢复),不在本刀辖域。`input` = REDACTED preview,只有 tool_approval 门携带(其余 ⇒ null)。
|
|
181
117
|
const input = s.toolInput != null ? redactedPreview(s.toolInput) : null;
|
|
182
118
|
return {
|
|
183
119
|
sessionId: s.sessionId,
|
|
@@ -190,70 +126,25 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
190
126
|
objective: ctx?.body?.objective ?? null,
|
|
191
127
|
input,
|
|
192
128
|
};
|
|
193
|
-
}))
|
|
194
|
-
|
|
195
|
-
// test/assistant-triage-ordering.test.ts)。旧姿势只比 severity,靠「listByScope 是 created_at_ms ASC
|
|
196
|
-
// + Array#sort 稳定」来兑现 §2 那句 oldest-first —— 而那个前提**只在本仓的 SQL 后端成立**
|
|
197
|
-
// (checkpoint-store-sql.ts 的 `ORDER BY created_at_ms ASC`):core 的 `CheckpointStore.listByScope` 接口
|
|
198
|
-
// 零顺序声明,InMemory/File 两个实现直接遍历 Map(LOCAL 车道包的正是 File store)⇒ 那句承诺过去是
|
|
199
|
-
// **后端相关**的。比较器自带兜底后它与 store 顺序无关。形与 listPending 的同族 sort 逐字一致
|
|
200
|
-
// (checkpoint-store-sql.ts `|| a.createdAt - b.createdAt`)。createdAt 缺席(pre-1.116 投影的老行)
|
|
201
|
-
// 计 0 = 档内最老:确定性优于「看它恰好落在哪」。
|
|
202
|
-
).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0) || (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
203
|
-
sendJson(res, 200, { inbox }); // CheckpointSummary (token-stripped) + objective, severity-prioritized — the assistant single inbox
|
|
129
|
+
}))).sort((a, b) => (b.severity ?? 0) - (a.severity ?? 0) || (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
130
|
+
sendJson(res, 200, { inbox });
|
|
204
131
|
return;
|
|
205
132
|
}
|
|
206
|
-
// design/80 #2 (assistant-scheduler): the N-TASKS overview = the cross-task SCHEDULING view. A user's active
|
|
207
|
-
// durable agent tasks, each joined (when suspended on a HITL gate) with the core scheduler seam
|
|
208
|
-
// listByScope(scope) → {gateKind, severity, spentMicroUsd, deadline} (core 1.105, ONE query, no N+1). The
|
|
209
|
-
// result is ordered for triage: blocked-on-a-gate first, then by risk severity DESC, then by budget spent
|
|
210
|
-
// DESC — i.e. the order a scheduler would advance/escalate them. Same owner-scoping. Needs the run-store.
|
|
211
133
|
if (req.method === "GET" && url === "/v1/assistant/tasks") {
|
|
212
|
-
// SECURITY (review w16yqkkxv): per-user view — ALWAYS a CONCRETE owner (operator inspects ONE via ?owner,
|
|
213
|
-
// else its own; non-operator = its own). No scope=undefined → no unbounded cross-tenant listPending()/listRuns.
|
|
214
134
|
const ownerParam = new URL(req.url ?? "", "http://x").searchParams.get("owner") || undefined;
|
|
215
135
|
const scope = operator && ownerParam ? ownerParam : (principal ?? "__none__");
|
|
216
136
|
if (!deps.runStore) {
|
|
217
137
|
sendJson(res, 200, { tasks: [] });
|
|
218
138
|
return;
|
|
219
139
|
}
|
|
220
|
-
// Rich per-gate summary via the core seam (severity + budget) — ONE scoped query, bounded (LIMIT 500).
|
|
221
140
|
const summaries = await cs.listByScope(scope);
|
|
222
141
|
const byGate = new Map(summaries.map((s) => [s.sessionId, s]));
|
|
223
142
|
const runs = await deps.runStore.listRuns({ owner: scope, limit: 100 });
|
|
224
143
|
const tasks = runs
|
|
225
|
-
// 🔴 wlzq75oac HIGH: a plan_review / dry-run review park persists as status='needs_review' (NOT 'suspended'),
|
|
226
|
-
// so a running/suspended-only filter DROPPED exactly the HITL-gated tasks §3 promises to surface "needs-
|
|
227
|
-
// attention first" — the operator could never discover the very plan_review tasks §4c says to resolve.
|
|
228
|
-
// Same class as the inbox leak (handler vs contract drift). needs_review is a non-terminal park → include it.
|
|
229
|
-
// 判据取词表属主(合并码重扫):park 两个词由 `isParkedRunStatus` 穷举,手抄形对 core 加词无编译期钉。
|
|
230
144
|
.filter((r) => r.status === "running" || isParkedRunStatus(r.status))
|
|
231
145
|
.map((r) => {
|
|
232
146
|
const s = byGate.get(r.sessionId);
|
|
233
147
|
const gate = s ? { kind: s.gateKind, severity: s.severity ?? null, spentMicroUsd: s.spentMicroUsd ?? null, deadline: s.deadline ?? null } : null;
|
|
234
|
-
// 🔴 `needsAttention` = 「此刻有一个**可解的活门**」= `!!gate`,**不是**「run 行停着」([2034]/[2035]
|
|
235
|
-
// 附带登记的 handler↔§3 文档出入,2026-07-30 裁定 (b):改文档对齐本行,常驻钉
|
|
236
|
-
// `test/assistant-tasks-needs-attention.test.ts`)。gate 来自 `listByScope`,三个实现都只回
|
|
237
|
-
// `status='pending'` 的行 ⇒ 分歧态 = run 停在 suspended/needs_review 而 pending checkpoint 已不在。
|
|
238
|
-
// 该态**可达且周期性发生**:TTL 收割的两半(checkpointStore.reapExpired 只动 checkpoint /
|
|
239
|
-
// runStore.failSuspendedWithExpiredCheckpoint 才动 run 行)在 boot/reapers.ts 同一 tick 里互不 await。
|
|
240
|
-
// 三问:① 需要方=triage 视图,它拿这一位只做「哪张卡现在能动 + 排前面」(比较器第一级);
|
|
241
|
-
// ② 若改按 status 算,分歧态会产出**不可解之卡**——标成待处理、排进前段,而 /decide 必 404、
|
|
242
|
-
// /plan_review 与 /resume 同族拒,且与同一行的 `gate:null` 自相矛盾;现写法的代价是这段窗口里
|
|
243
|
-
// 「只渲染 needsAttention 段」的客户端看不到它;
|
|
244
|
-
// ③ 补偿=(a) 行**不消失**——它仍在本列表里带真实 `status`,「所有还停着的任务」由 status 回答;
|
|
245
|
-
// (b) failSuspendedWithExpiredCheckpoint 三个 run store 全实装、每 tick 无条件跑(不受
|
|
246
|
-
// APPROVAL_TIMEOUT_SEC 开关),窗口由 REAP_INTERVAL_SEC 封顶后收敛成 failed。
|
|
247
|
-
// [2069]④(cli 2026-07-30):additive 展示位。行此前只有 taskId/status/needsAttention,面板
|
|
248
|
-
// 渲不出「这是哪个任务」。源 = run 行自己的 `objectivePreview`(写入时已脱敏 + ~120 截断)——
|
|
249
|
-
// **不**走 §2 inbox 那套 per-row `getCtx`:本视图头注逐字承诺「ONE query, no N+1」,为两个
|
|
250
|
-
// 展示位把它变成 N+1 是拿契约换排版。
|
|
251
|
-
// · `name` 经 `fleetRunLabels()`,与 fleet 行**同一个函数** ⇒ 同一个 run 在两个面板里名字一致
|
|
252
|
-
// (两处各写一遍必然漂),顺带白拿 [1414]④ 的控制帧形保护(`<task-notification…>` 给中性名)。
|
|
253
|
-
// · `description` = 未截断原值,**仅在与 name 不同时才发** —— 相同就是同一句话印两遍
|
|
254
|
-
// (BC-1 在 fleet 行上杀掉的 objective-shown-twice)。
|
|
255
|
-
// · 两键 OMIT-when-absent(无 preview ⇒ 都不发,占位归客户端),同批 [2070]③ 同一条纪律。
|
|
256
|
-
// 钉:test/assistant-tasks-display-keys.test.ts。
|
|
257
148
|
const preview = r.objectivePreview?.trim() ? r.objectivePreview : undefined;
|
|
258
149
|
const label = preview !== undefined ? fleetRunLabels(preview).name : undefined;
|
|
259
150
|
return {
|
|
@@ -267,32 +158,18 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
267
158
|
sendJson(res, 200, { tasks });
|
|
268
159
|
return;
|
|
269
160
|
}
|
|
270
|
-
// design/80 seam #2: POST /v1/assistant/tasks/:id/preempt — the scheduler's graceful "yield this task"
|
|
271
|
-
// (durable + resumable; the OPPOSITE of /v1/runs/:id/cancel's kill). Durable cross-replica preempt flag
|
|
272
|
-
// (the owning instance's heartbeat honors it) + a same-replica preempt-controller fast path. On a direct
|
|
273
|
-
// worker the caller is the crypto-VERIFIED JWT principal (above) — owner-scoped, no trusted-header bypass.
|
|
274
161
|
const preemptMatch = req.method === "POST" ? ASSISTANT_PREEMPT_RE.exec(url) : null;
|
|
275
162
|
if (preemptMatch) {
|
|
276
163
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
277
|
-
return;
|
|
278
|
-
// (yields a task, runs no model — see the billable-route classifier) → NO lease gate, else an exhausted
|
|
279
|
-
// tenant could not stop its own spending.
|
|
164
|
+
return;
|
|
280
165
|
if (!deps.runStore) {
|
|
281
166
|
sendError(res, 501, "capability.run_store_required", "preemption requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
282
167
|
return;
|
|
283
168
|
}
|
|
284
|
-
// requirePrincipal parity with the sibling mutating endpoints (runOwnerOk / cancel / runs): 401 before the
|
|
285
|
-
// lookup when configured (uses the crypto-verified `principal` — never the trusted header on a direct door).
|
|
286
169
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
287
170
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
288
171
|
return;
|
|
289
172
|
}
|
|
290
|
-
// Precondition: a preempt only durably suspends when the worker opts tasks into resourceSuspend
|
|
291
|
-
// (RESOURCE_SUSPEND=true) AND has the durable-suspend substrate (a checkpoint store — resolveSpec wires
|
|
292
|
-
// resourceSuspend ONLY inside the durableEnabled block, so without a checkpoint store the preemptSignal is
|
|
293
|
-
// never armed) AND core deems the task eligible (remote env + durable tool-results). Reject honestly when the
|
|
294
|
-
// worker can't preempt at all (e.g. the local in-memory backend has no checkpoint store), rather than 202
|
|
295
|
-
// "preempting" a signal nothing honors.
|
|
296
173
|
if (!deps.config.resourceSuspend || !deps.checkpointStore) {
|
|
297
174
|
sendError(res, 501, "feature.preemption_disabled", "preemption not enabled on this worker (needs RESOURCE_SUSPEND=true + a durable checkpoint store)");
|
|
298
175
|
return;
|
|
@@ -303,41 +180,19 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
303
180
|
sendError(res, 404, "not_found.run", "task not found");
|
|
304
181
|
return;
|
|
305
182
|
}
|
|
306
|
-
// Owner-gate: an EXPLICIT operator may preempt any tenant's task (preemption is a scheduler action); a
|
|
307
|
-
// normal caller only its own. 404 (not 403) for a non-owner — no existence oracle (parity with runOwnerOk
|
|
308
|
-
// + the cancel/decide precedent). NOT the bare isOperator (its empty-list "true-for-all" would let any
|
|
309
|
-
// caller preempt anyone — same trap the /decide owner-gate avoids).
|
|
310
183
|
const operators = deps.config.operatorPrincipals;
|
|
311
|
-
const explicitOperator = explicitOperatorOk(principal, operators);
|
|
184
|
+
const explicitOperator = explicitOperatorOk(principal, operators);
|
|
312
185
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
313
186
|
sendError(res, 404, "not_found.run", "task not found");
|
|
314
187
|
return;
|
|
315
188
|
}
|
|
316
189
|
if (run.status === "running") {
|
|
317
|
-
// Honesty guard (review wtyc7ist3): if THIS replica owns the run (instance match) yet it is NOT in
|
|
318
|
-
// preemptableRuns, it is a verify/cascade leg — those durably suspend as a FAILED result, so they are
|
|
319
|
-
// deliberately never registered + never honored (runs.ts). Don't set a dead durable flag or claim
|
|
320
|
-
// "preempting"; report the honest no-op. (Owned-elsewhere → instanceId mismatch → fall through to the
|
|
321
|
-
// durable flag so the owning replica's heartbeat honors it iff that leg is eligible — best-effort.)
|
|
322
190
|
const ownedHere = run.instanceId != null && run.instanceId === deps.instanceId;
|
|
323
191
|
if (ownedHere && !preemptableRuns.has(taskId)) {
|
|
324
192
|
sendJson(res, 202, { taskId, status: run.status, note: "task is not preempt-eligible (verify/cascade leg) — preempt is a no-op" });
|
|
325
193
|
return;
|
|
326
194
|
}
|
|
327
|
-
|
|
328
|
-
// the getRun→here window the flag affects 0 rows, so re-read and answer by the ACTUAL state.
|
|
329
|
-
let flagged = await deps.runStore.requestPreempt(taskId, run.owner); // owner guard: single-DB defense-in-depth (owner-gate already enforced above)
|
|
330
|
-
// 🔴 抢跑输了(旗打空 0 行)⇒ 按 run 的**真实**状态**三向**重判(重扫二轮红先修;与 `runs.ts` 取消腿
|
|
331
|
-
// 的孪生臂 `CANCEL_RECLASSIFY_ATTEMPTS` 那一段同源同形)。旧形无论重读到什么都发 202 no-op,于是
|
|
332
|
-
// **同一条 park 行**:不经这个竞态窗时回 409 `conflict.already_suspended`(前置检查那一支,`403a841`),
|
|
333
|
-
// 经过它时回 202「task no longer running」。按状态码分支的调度器对同一个状态拿到两种答案,而契约文
|
|
334
|
-
// §4d 把它写成了绝对规则(parked ⇒ 409 / genuinely terminal ⇒ 202),只成文了其中一种。
|
|
335
|
-
// · **park**(判据取词表属主 `isParkedRunStatus`,穷举 switch + never 臂)⇒ 409,`status` 带**行的真词**;
|
|
336
|
-
// · **running**(park→running 二次跃迁真实存在:并发 approve 的恢复腿把行翻回来)⇒ **重挂旗**,
|
|
337
|
-
// 有界重试;行活过来正是该重挂旗的时刻,不是该撒谎的时刻;
|
|
338
|
-
// · 真终局 ⇒ 照旧幂等 202 no-op(带真实状态)。
|
|
339
|
-
// 有界(park↔running 反复跳时不许把请求钉在这):次数用尽仍 running ⇒ 如实 409 让调用方重试,
|
|
340
|
-
// 码与取消腿的同名情形共用(同一件事:并发决议赢了,它的恢复腿正在跑)。
|
|
195
|
+
let flagged = await deps.runStore.requestPreempt(taskId, run.owner);
|
|
341
196
|
const PREEMPT_RECLASSIFY_ATTEMPTS = 3;
|
|
342
197
|
for (let attempt = 0; !flagged; attempt++) {
|
|
343
198
|
const now = await deps.runStore.getRun(taskId);
|
|
@@ -346,7 +201,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
346
201
|
return;
|
|
347
202
|
}
|
|
348
203
|
if (now?.status === "running" && attempt < PREEMPT_RECLASSIFY_ATTEMPTS) {
|
|
349
|
-
flagged = await deps.runStore.requestPreempt(taskId, run.owner);
|
|
204
|
+
flagged = await deps.runStore.requestPreempt(taskId, run.owner);
|
|
350
205
|
continue;
|
|
351
206
|
}
|
|
352
207
|
if (now?.status === "running") {
|
|
@@ -356,12 +211,10 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
356
211
|
sendJson(res, 202, { taskId, status: now?.status ?? "failed", note: "task no longer running — preempt is a no-op" });
|
|
357
212
|
return;
|
|
358
213
|
}
|
|
359
|
-
preemptableRuns.get(taskId)?.abort();
|
|
214
|
+
preemptableRuns.get(taskId)?.abort();
|
|
360
215
|
sendJson(res, 202, { taskId, status: "preempting", note: "graceful durable yield — the task suspends at the next clean turn boundary if preempt-eligible; resume via POST /v1/assistant/tasks/:id/resume" });
|
|
361
216
|
}
|
|
362
217
|
else if (isParkedRunStatus(run.status)) {
|
|
363
|
-
// 扫描P2 顺带件:park 是两个词(isParkedRunStatus 属主)——needs_review(plan_review/dry-run 门)
|
|
364
|
-
// 此前掉进 else 被谎报「already terminal — no-op」(无状态破坏,纯指路错)。extras.status 带真词。
|
|
365
218
|
sendError(res, 409, "conflict.already_suspended", "task is already suspended", { taskId, status: run.status });
|
|
366
219
|
}
|
|
367
220
|
else {
|
|
@@ -369,13 +222,10 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
369
222
|
}
|
|
370
223
|
return;
|
|
371
224
|
}
|
|
372
|
-
// design/80 seam #2: POST /v1/assistant/tasks/:id/resume — resume a task durably SUSPENDED on a
|
|
373
|
-
// resource_limit gate (a scheduler preempt OR a budget/turns/walltime slice, design/74). NOT an approval
|
|
374
|
-
// gate (resumePreempted's gate-kind guard 409s a policy_ask — that goes through POST /v1/approvals/:id/decide).
|
|
375
225
|
const resumeMatch = req.method === "POST" ? ASSISTANT_RESUME_RE.exec(url) : null;
|
|
376
226
|
if (resumeMatch) {
|
|
377
227
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
378
|
-
return;
|
|
228
|
+
return;
|
|
379
229
|
if (!deps.runStore) {
|
|
380
230
|
sendError(res, 501, "capability.run_store_required", "resume requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
381
231
|
return;
|
|
@@ -383,7 +233,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
383
233
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
384
234
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
385
235
|
return;
|
|
386
|
-
}
|
|
236
|
+
}
|
|
387
237
|
const taskId = resumeMatch[1];
|
|
388
238
|
const run = await deps.runStore.getRun(taskId);
|
|
389
239
|
if (!run) {
|
|
@@ -391,33 +241,19 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
391
241
|
return;
|
|
392
242
|
}
|
|
393
243
|
const operators = deps.config.operatorPrincipals;
|
|
394
|
-
const explicitOperator = explicitOperatorOk(principal, operators);
|
|
244
|
+
const explicitOperator = explicitOperatorOk(principal, operators);
|
|
395
245
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
396
246
|
sendError(res, 404, "not_found.run", "task not found");
|
|
397
247
|
return;
|
|
398
248
|
}
|
|
399
|
-
|
|
400
|
-
// 在响应之前同步跑完整条 resume 流,响应时长 = 模型往返、无服务端上限。#316 只对 legacy decide 腿
|
|
401
|
-
// 做了 200 受理化(那是 [4660] 雪崩的实测首环),本腿维持终局形:**wire BREAKING 需要消费方先表态**
|
|
402
|
-
// (能力显式表态制,[3780] 裁 A)。候消费方认领后同形改造 —— 改法现成:`resumePreempted` 一路把
|
|
403
|
-
// `acceptEarly: true` 透到 `driveResumeIntoRunLog` 即可(受理点/判定边界全在 `DriveResumeArgs.acceptEarly`)。
|
|
404
|
-
const out = await resumePreempted(run.sessionId, req); // gate-kind guard + drive into the run log (shared)
|
|
249
|
+
const out = await resumePreempted(run.sessionId, req);
|
|
405
250
|
sendJson(res, out.status, out.body);
|
|
406
251
|
return;
|
|
407
252
|
}
|
|
408
|
-
// design/80 D-B: POST /v1/assistant/tasks/:id/plan_review — resolve a PRE-ACTION `plan_review` gate, 3-state
|
|
409
|
-
// (approve | edit | reject). A SIBLING of preempt/resume (a scheduler-resolvable gate with NO bound tool
|
|
410
|
-
// action — the human reviews the PLAN, not a tool call), NOT the /decide wire (a tool-approval allow/deny +
|
|
411
|
-
// action binding) and NOT the needs_review wire (never reuse it). `edit` carries the
|
|
412
|
-
// operator's REVISED plan (core threads `editedPlan` into the resumed run). 🔴 D-G ruling:
|
|
413
|
-
// a plan_review binds NO action, so on a direct-door worker the self-asserted x-agent-principal is NEVER
|
|
414
|
-
// trusted — it carries the PRINCIPAL-ONLY proof (verifyDirectDoorProof actionBinding:false = JWT identity +
|
|
415
|
-
// cnf.bnd + HMAC over {sessionId,decision}, no boundInputHash), then the SAME operator-or-owner gate on the
|
|
416
|
-
// CRYPTO-VERIFIED principal. On a BFF worker the trusted-header principal owner-gate applies (parity with resume).
|
|
417
253
|
const planReviewMatch = req.method === "POST" ? ASSISTANT_PLAN_REVIEW_RE.exec(url) : null;
|
|
418
254
|
if (planReviewMatch) {
|
|
419
255
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
420
|
-
return;
|
|
256
|
+
return;
|
|
421
257
|
if (!deps.runStore) {
|
|
422
258
|
sendError(res, 501, "capability.run_store_required", "plan_review requires a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
423
259
|
return;
|
|
@@ -425,13 +261,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
425
261
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
426
262
|
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
427
263
|
return;
|
|
428
|
-
}
|
|
264
|
+
}
|
|
429
265
|
const taskId = planReviewMatch[1];
|
|
430
|
-
// 3-state decision; `edit` MUST carry the revised plan, `approve`/`reject` must NOT (a stray editedPlan
|
|
431
|
-
// signals a wrong-gate call — reject, don't drop it). Parsed + validated BEFORE the task is resolved so
|
|
432
|
-
// every bad-body 400 is existence-INDEPENDENT (depends only on the body). 🔴 wq64gmm5e LOW: getRun's 404
|
|
433
|
-
// used to fire BEFORE the body/proof — a task-existence oracle (404-vs-401) on a direct-door worker,
|
|
434
|
-
// divergent from /decide. Now the existence branch comes AFTER and is status-uniform with a bad proof.
|
|
435
266
|
const body = (await readJson(req));
|
|
436
267
|
const decision = body?.decision === "approve" ? "approve" : body?.decision === "edit" ? "edit" : body?.decision === "reject" ? "reject" : undefined;
|
|
437
268
|
if (!decision) {
|
|
@@ -450,38 +281,24 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
450
281
|
sendError(res, 400, "request.field_invalid", "reason must be a string");
|
|
451
282
|
return;
|
|
452
283
|
}
|
|
453
|
-
// 与 /decide 腿同一上限:reason 入签之后,超长只能**拒**(截断产出的字节签名器复现不了 ⇒ 恒 401)。
|
|
454
284
|
if (typeof body.reason === "string" && body.reason.length > MAX_APPROVAL_REASON_CHARS) {
|
|
455
285
|
sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
|
|
456
286
|
return;
|
|
457
287
|
}
|
|
458
288
|
const run = await deps.runStore.getRun(taskId);
|
|
459
289
|
if (!run) {
|
|
460
|
-
// No such task. On a DIRECT-DOOR worker return the SAME 401 a bad proof gets (parity with /decide): a
|
|
461
|
-
// missing task is indistinguishable from a bad proof — the caller cannot mint a valid proof for an
|
|
462
|
-
// unknown sessionId anyway — so existence never leaks. On a BFF worker the trusted-header owner posture 404s.
|
|
463
290
|
if (deps.config.directDoorActive) {
|
|
464
291
|
sendError(res, 401, "principal_unverified", "principal proof required");
|
|
465
292
|
return;
|
|
466
293
|
}
|
|
467
|
-
// [4268]/[4274] 件5(#43 P0 联调观测件):**只进日志,不上 wire**。两支 404 在 wire 面必须逐字同形
|
|
468
|
-
// (反枚举:「不存在」与「不属于你」不可区分是本门的要求,不是疏漏),代价是服务端自己也说不清撞的
|
|
469
|
-
// 是哪一支 —— 联调一次 404 只能靠猜。判别位放在服务端日志:运维/联调拿得到成因,枚举者拿不到。
|
|
470
|
-
// `principalPresent` 只记**有无**([4288]):principal / owner 两个身份串一个都不许进日志 ——
|
|
471
|
-
// 日志的读者面比 wire 宽,把值写进去等于在日志面重建一条本来被封住的枚举通道。
|
|
472
294
|
deps.logger?.info?.("decide_404", { reason: "no-row", taskId, principalPresent: principal !== undefined });
|
|
473
295
|
sendError(res, 404, "not_found.run", "task not found");
|
|
474
296
|
return;
|
|
475
297
|
}
|
|
476
|
-
// ── D-G principal-only proof (non-action gate): on a direct-door worker derive the CRYPTO-VERIFIED principal
|
|
477
|
-
// from the signed proof, never the self-asserted header. The envelope binds {sessionId, decision} so a forged
|
|
478
|
-
// or replayed plan decision is rejected; no boundInputHash (there is no tool action). fail-closed.
|
|
479
298
|
let deciderPrincipal = principal;
|
|
480
299
|
if (deps.config.directDoorActive) {
|
|
481
300
|
const hdr = (n) => { const h = req.headers[n]; return Array.isArray(h) ? h[0] : h; };
|
|
482
|
-
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) },
|
|
483
|
-
// reason 入签:同一条规则也适用于身份绑定腿(两个绑定位为 null,第五位照常参与)。
|
|
484
|
-
{ sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
|
|
301
|
+
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId: run.sessionId, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config, { actionBinding: false });
|
|
485
302
|
if (!proof.ok) {
|
|
486
303
|
sendError(res, proof.status, proof.errorCode, proof.error);
|
|
487
304
|
return;
|
|
@@ -489,34 +306,23 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
489
306
|
deciderPrincipal = proof.principal;
|
|
490
307
|
}
|
|
491
308
|
const operators = deps.config.operatorPrincipals;
|
|
492
|
-
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators);
|
|
309
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators);
|
|
493
310
|
if (!explicitOperator && run.owner !== null && run.owner !== deciderPrincipal) {
|
|
494
|
-
// no existence oracle (verified principal on a direct worker) —— wire 面与上面的「查无此行」逐字同形;
|
|
495
|
-
// 判别位同样只进服务端日志(件5,理由见上一处注)。刻意**不记** owner/principal 两个值:判别成因用
|
|
496
|
-
// reason 一个词就够,把两个身份写进日志等于在日志面重建一条本来被封住的枚举通道。
|
|
497
311
|
deps.logger?.info?.("decide_404", { reason: "owner-mismatch", taskId, principalPresent: principal !== undefined });
|
|
498
312
|
sendError(res, 404, "not_found.run", "task not found");
|
|
499
313
|
return;
|
|
500
314
|
}
|
|
501
|
-
|
|
502
|
-
// resume 流(时长 = 模型往返、无上限)。改造形现成(`acceptEarly` 透传),候消费方表态后同形改。
|
|
503
|
-
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req,
|
|
504
|
-
// [4277]/[4288] 件5:让四个 reason 的判别日志共用同一个 id 字段 + 同一个 principalPresent 口径。
|
|
505
|
-
{ taskId, principalPresent: principal !== undefined });
|
|
315
|
+
const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined });
|
|
506
316
|
sendJson(res, out.status, out.body);
|
|
507
317
|
return;
|
|
508
318
|
}
|
|
509
|
-
// [2373]C-4(5.0.0):裸 `POST /v1/approvals/:id` 装饰臂删——/decide 是唯一决裁形(裸形曾与
|
|
510
|
-
// `GET /v1/approvals/:id` 撞 URL 空间,并把限流/计费/wire 注三处记账劈成两条)。
|
|
511
319
|
const m = /^\/v1\/approvals\/([^/]+)\/decide$/.exec(url);
|
|
512
320
|
if (m && req.method === "POST") {
|
|
513
321
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
514
|
-
return;
|
|
515
|
-
// NOTE: the operator gate is checked AFTER the body parse below — D-G's direct door needs the parsed
|
|
516
|
-
// envelope (checkpointToken + decision) to verify the HMAC before it can derive the deciding principal.
|
|
322
|
+
return;
|
|
517
323
|
let sessionId;
|
|
518
324
|
try {
|
|
519
|
-
sessionId = decodeURIComponent(m[1]);
|
|
325
|
+
sessionId = decodeURIComponent(m[1]);
|
|
520
326
|
}
|
|
521
327
|
catch {
|
|
522
328
|
sendError(res, 400, "request.path_malformed", "malformed approval id (invalid percent-encoding)");
|
|
@@ -528,11 +334,6 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
528
334
|
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember? }");
|
|
529
335
|
return;
|
|
530
336
|
}
|
|
531
|
-
// `reason` 现在**在签名载荷里**(三方裁 (a),见 security.ts `approvalHmacMessage` 顶注)⇒ 它的形必须先钉死:
|
|
532
|
-
// · 非字符串会让规范化编码产出一个签名器不可能复现的消息(对象会被内联进 JSON)—— 此前这条腿
|
|
533
|
-
// 根本没校验 reason 的类型(plan_review 腿有),入签之后这就成了硬前置。
|
|
534
|
-
// · 超长**拒而不截**:服务端截过的字节与签名方签的字节必然不同 ⇒ MAC 恒不匹配,截断只会把一个
|
|
535
|
-
// 可诊断的 413 变成费解的 401(与 steer 入参上限同一判据:诚实拒绝优于自作主张改写)。
|
|
536
337
|
if (body.reason !== undefined && typeof body.reason !== "string") {
|
|
537
338
|
sendError(res, 400, "request.field_invalid", "reason must be a string");
|
|
538
339
|
return;
|
|
@@ -541,11 +342,6 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
541
342
|
sendError(res, 413, "reason_too_large", `reason too large (max ${MAX_APPROVAL_REASON_CHARS} chars)`);
|
|
542
343
|
return;
|
|
543
344
|
}
|
|
544
|
-
// `remember:"session"` — record a per-session per-toolName approval exemption alongside an
|
|
545
|
-
// approve ("本会话不再询问"). Closed enum (extensible later, e.g. "principal"); deny+remember = 400 (a
|
|
546
|
-
// "remembered deny" is a different semantic — not smuggled in here); direct-door v1 = 400 (remember is
|
|
547
|
-
// NOT in the HMAC envelope's signed payload — accepting it unsigned would be a tamper hole; extending
|
|
548
|
-
// the MAC payload is a v2 with the BFF, see docs/design/APPROVAL-SESSION-EXEMPTION.md).
|
|
549
345
|
let remember = false;
|
|
550
346
|
if (body.remember !== undefined) {
|
|
551
347
|
if (body.remember !== "session") {
|
|
@@ -564,22 +360,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
564
360
|
sendError(res, 501, "capability.store_required", "approval exemptions need a store backend (DB_BACKEND / LOCAL lane)");
|
|
565
361
|
return;
|
|
566
362
|
}
|
|
567
|
-
// 🔴 remember REQUIRES the D-1 decision-binding echo (workflow audit 2026-07-13, my override of the
|
|
568
|
-
// verify-reject): an unbound decide can consume a gate that was SWAPPED after the operator's GET (see
|
|
569
|
-
// gate A, approve lands on gate B) — tolerable as a one-shot legacy quirk, NOT as the mint of a
|
|
570
|
-
// SESSION-PERSISTENT auto-approval for a tool the operator never saw. remember is a NEW contract, so
|
|
571
|
-
// it can demand the strict calling shape — the BFF receives boundCallId on the pending row
|
|
572
|
-
// (HITL-7 注纠,[2400] 审计:checkpointToken 从不上 pending 行,listPending 的 SELECT 无 token 列;
|
|
573
|
-
// 下面的门是「或」关系,boundCallId 单独即满足,所以旧措辞没造成真故障)。
|
|
574
|
-
// Plain decide without remember keeps the legacy unbound fallback untouched.
|
|
575
363
|
if (!body.checkpointToken && !body.boundCallId) {
|
|
576
364
|
sendError(res, 400, "remember_requires_binding", "remember requires the decision binding (echo checkpointToken and/or boundCallId from the pending approval)");
|
|
577
365
|
return;
|
|
578
366
|
}
|
|
579
367
|
remember = true;
|
|
580
368
|
}
|
|
581
|
-
// Durable ask (TC-5.4): the operator's out-of-band QuestionAnswer rides the approve. Trusted
|
|
582
|
-
// control-plane input (the model never produces it); malformed = client error, absent = unchanged.
|
|
583
369
|
let answer;
|
|
584
370
|
if (body.answer !== undefined) {
|
|
585
371
|
if (!isQuestionAnswer(body.answer)) {
|
|
@@ -588,27 +374,12 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
588
374
|
}
|
|
589
375
|
answer = body.answer;
|
|
590
376
|
}
|
|
591
|
-
// D-1 (design/80) decision-action binding: the operator echoes back the checkpointToken + boundCallId +
|
|
592
|
-
// boundInputHash they SAW in GET /v1/approvals. The service binds the resume to that exact action (core
|
|
593
|
-
// verifies opaque equality, fail-closed PRE-CAS) and rejects a stale view distinctly (approval_stale).
|
|
594
|
-
// All OPTIONAL — a legacy BFF caller that omits them falls back to the current-pending values (deprecated).
|
|
595
377
|
for (const f of ["checkpointToken", "boundCallId", "boundInputHash"]) {
|
|
596
378
|
if (body[f] !== undefined && typeof body[f] !== "string") {
|
|
597
379
|
sendError(res, 400, "request.field_invalid", `${f} must be a string (the value surfaced on the pending approval record)`);
|
|
598
380
|
return;
|
|
599
381
|
}
|
|
600
382
|
}
|
|
601
|
-
// 🔴 直连门上拒 `updatedInput`(2026-07-26 修),判据与上面拒 `remember` **逐字同一条**:
|
|
602
|
-
// 「不在决策证明的签名载荷里」。此前只 `remember` 有这条门,而 `updatedInput` 改写的是
|
|
603
|
-
// **将被执行的工具实参**(core `runtask.js` 在哈希比对**之后**用 `outcome.updatedInput ?? pendingAction.args`)
|
|
604
|
-
// ⇒ 同一份 JWT+MAC 对「原 body」与「加了 updatedInput 的 body」**都放行**,而 `boundInputHash` 仍是
|
|
605
|
-
// 原动作的哈希。直连门的威胁模型就是「线上字节不可信」(否则不需要 HMAC),能改字节的一方
|
|
606
|
-
// (反代 / 被劫持的客户端)可在操作员批准 `ls` 的同一次决定里换掉实参。
|
|
607
|
-
// ⚠️ 已亲验的缓解:core 会把改写后的实参**重新过一遍 base tool policy**,所以越权面被限制在策略仍允许的
|
|
608
|
-
// 范围内、不是任意命令 —— 但那是"减轻",不是"这个字段被签名覆盖了"。
|
|
609
|
-
// 修法刻意**不是**把它塞进 HMAC 载荷(那要求所有直连门客户端改签名 = 跨仓 wire 变更),而是把**既有的、
|
|
610
|
-
// 代码自己写下的**判据施加到它漏掉的那个字段上:fail-closed、server 单方面可做、与 `remember` 对称。
|
|
611
|
-
// BFF/gated 门(头已由上游验过)**不受影响** —— 这条只在 `directDoorActive` 生效。
|
|
612
383
|
if (deps.config.directDoorActive && body.updatedInput !== undefined) {
|
|
613
384
|
sendError(res, 400, "updated_input_not_in_proof", "updatedInput is not supported on a direct-door worker (not covered by the decision proof)");
|
|
614
385
|
return;
|
|
@@ -619,75 +390,44 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
619
390
|
boundInputHash: body.boundInputHash,
|
|
620
391
|
updatedInput: body.updatedInput,
|
|
621
392
|
};
|
|
622
|
-
// ── design/80 D-G: the authorization gate. On a DIRECT-MODE worker the trusted-header path is DISABLED —
|
|
623
|
-
// every /decide MUST carry the crypto proof (signed principal JWT + cnf.bnd + HMAC envelope), and the
|
|
624
|
-
// self-asserted x-agent-principal is NEVER consulted. On a normal (BFF) worker, the existing trusted-header
|
|
625
|
-
// operator gate is byte-identical. Fail-closed: a direct-door failure rejects, never falls back to the header.
|
|
626
393
|
let deciderPrincipal = principal;
|
|
627
394
|
if (deps.config.directDoorActive) {
|
|
628
395
|
const hdr = (n) => {
|
|
629
396
|
const h = req.headers[n];
|
|
630
397
|
return Array.isArray(h) ? h[0] : h;
|
|
631
398
|
};
|
|
632
|
-
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) },
|
|
633
|
-
// reason 入签(三方裁 (a) + clay 干净切):落账理由属于决定内容,与 decision 同受完整性保护。
|
|
634
|
-
{ sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
|
|
399
|
+
const proof = verifyDirectDoorProof({ jwt: hdr(PRINCIPAL_TOKEN_HEADER), mac: hdr(APPROVAL_MAC_HEADER), kid: hdr(APPROVAL_MAC_KID_HEADER) }, { sessionId, boundCallId: binding.boundCallId, boundInputHash: binding.boundInputHash, decision, reason: typeof body.reason === "string" ? body.reason : null }, deps.config);
|
|
635
400
|
if (!proof.ok) {
|
|
636
401
|
sendError(res, proof.status, proof.errorCode, proof.error);
|
|
637
402
|
return;
|
|
638
403
|
}
|
|
639
|
-
deciderPrincipal = proof.principal;
|
|
404
|
+
deciderPrincipal = proof.principal;
|
|
640
405
|
}
|
|
641
|
-
// Authorization (council finding): an EXPLICIT operator may decide ANY approval; otherwise the
|
|
642
|
-
// caller must OWN the checkpoint's scope. 🔴 NOT the bare isOperator — its empty-OPERATOR_PRINCIPALS
|
|
643
|
-
// "true-for-all" would let ANY authenticated caller decide ANOTHER user's checkpoint via a leaked, NON-
|
|
644
|
-
// credential sessionId+boundInputHash (from a log/screenshot/ticket). A non-owner-non-operator gets 404
|
|
645
|
-
// (no existence oracle — parity with runOwnerOk + the cancel P0 precedent), never 403.
|
|
646
406
|
const operators = deps.config.operatorPrincipals;
|
|
647
|
-
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators);
|
|
407
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators);
|
|
648
408
|
if (!explicitOperator) {
|
|
649
409
|
const cpScope = await cs.peekPendingScope(sessionId);
|
|
650
|
-
// "_" = the anonymous-submit scope SENTINEL (main.ts writes auth?.principal ?? "_" at suspend) — treat it
|
|
651
|
-
// like null (open), as the sibling sites already do (the 5040/5599/5653 normalizations). Without this an
|
|
652
|
-
// anonymous single-user durable worker DEADLOCKS its own approvals (decide → 404 forever; workflow audit
|
|
653
|
-
// 2026-07-13, confirmed by both verify lenses).
|
|
654
410
|
const decodedCpScope = cpScope != null ? decodeCheckpointScope(cpScope) : undefined;
|
|
655
|
-
const ownsIt = cpScope == null || decodedCpScope === undefined || decodedCpScope === deciderPrincipal;
|
|
411
|
+
const ownsIt = cpScope == null || decodedCpScope === undefined || decodedCpScope === deciderPrincipal;
|
|
656
412
|
if (!ownsIt) {
|
|
657
413
|
sendError(res, 404, "not_found.approval", "approval not found");
|
|
658
414
|
return;
|
|
659
415
|
}
|
|
660
416
|
}
|
|
661
|
-
// remember: capture the pending gate's toolName BEFORE the resume consumes the row. AskUserQuestion
|
|
662
|
-
// gates are excluded (the durable-question policy owns them — an exemption row would be dead weight the
|
|
663
|
-
// ask-gate probe explicitly skips). null toolName (legacy row) ⇒ nothing to remember.
|
|
664
417
|
let rememberToolName = null;
|
|
665
418
|
if (remember) {
|
|
666
|
-
// [1.202 live 复验抓获] 旧姿势 `listPending(deciderPrincipal ?? "__none__")` 在匿名单用户 lane
|
|
667
|
-
// (decider 无 principal、checkpoint scope="_" 哨兵)永远滤空 ⇒ remember 结构性失灵(rememberApplied
|
|
668
|
-
// 恒 false,豁免从不落店)。授权已由上方 owner gate 把过(cpScope null/"_"/相符,同 5040/5599/5653
|
|
669
|
-
// 的 "_" 归一先例)——这里直接读该 session 的 pending 行拿 toolName,不再按 decider 二次过滤。
|
|
670
419
|
const tok = await cs.findPendingTokenBySession(sessionId);
|
|
671
420
|
const cp0 = tok ? await cs.get(tok) : null;
|
|
672
421
|
const pa0 = cp0?.pendingAction;
|
|
673
422
|
const tn = pa0 && pa0.kind === "tool_approval" ? pa0.toolName : null;
|
|
674
423
|
rememberToolName = tn && tn !== "AskUserQuestion" ? tn : null;
|
|
675
424
|
}
|
|
676
|
-
// grant timing(codex M2,1.294 isExempt 同腿生效):grant 挪进 onResumeCommitted——markResuming CAS
|
|
677
|
-
// 赢了(decide 已被接受、并发 sibling 已输)之后、模型腿驱动**之前**落店,所以 resumed leg 自己的下一个
|
|
678
|
-
// ask 就能命中豁免(gate isExempt + ask 政策层探针都实时读店);旧姿势 await 整条 resume 腿(可跑数分钟)
|
|
679
|
-
// 之后才 grant,同腿第二次同工具 ask 必再 park 一轮。输掉 CAS(409)/stale(404)⇒ 钩不触发,绝不留
|
|
680
|
-
// 豁免;CAS 赢后模型腿再失败 ⇒ 豁免保留(operator 的 approve+remember 意图在 accept 时已成立,与
|
|
681
|
-
// 「grant 失败不回滚 approve」同向)。grant 失败只 warn 不阻断 resume,响应带 rememberApplied 如实。
|
|
682
425
|
let rememberApplied = false;
|
|
683
|
-
// overrideSessionId([1591] 候裁② server 修):parked 赎回腿把 grant 锚改到行的 root/host 会话
|
|
684
|
-
// (部署侧豁免探针闭包统一捕获 host 会话键——落子代会话则键永不相交,rememberApplied 会是假话)。
|
|
685
|
-
// legacy 任务腿不传 override,行为逐字不变。
|
|
686
426
|
const grantOnCommit = remember && deps.approvalExemptionStore && rememberToolName
|
|
687
427
|
? async (overrideSessionId) => {
|
|
688
428
|
const grantSessionId = overrideSessionId ?? sessionId;
|
|
689
429
|
try {
|
|
690
|
-
const canonical = rememberToolName;
|
|
430
|
+
const canonical = rememberToolName;
|
|
691
431
|
await deps.approvalExemptionStore.grant(grantSessionId, canonical, deciderPrincipal ?? null);
|
|
692
432
|
rememberApplied = true;
|
|
693
433
|
deps.logger?.info?.("approval_exemption_granted", { sessionId: grantSessionId, toolName: canonical, grantedBy: deciderPrincipal ?? null });
|
|
@@ -697,12 +437,6 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
697
437
|
}
|
|
698
438
|
}
|
|
699
439
|
: undefined;
|
|
700
|
-
// #204 件6①:出处 = `"human"` —— 这条腿是运维**亲自**在 HTTP 上给的终局(approve/deny 两向都是)。
|
|
701
|
-
// #316([4660] Inkglow 案 → [4664]① 定谳):末位 `true` = **200 受理语义**。这条腿此前在响应之前
|
|
702
|
-
// 同步跑完整条 resume 流(模型往返、无服务端上限)⇒ 壳侧 60s AbortSignal 必然超时,而重试打回来
|
|
703
|
-
// 又撞 CAS —— [4660] 雪崩的首环。改后 HTTP 立刻回 `{taskId, status:"resuming", bindingEnforced:true}`,
|
|
704
|
-
// 结果面走既有 `GET /v1/runs/:id`(poll)/ `/events`(SSE)。**受理不是免检**:429/409/404 三类判定
|
|
705
|
-
// 逐字仍在响应前给出(判据全文见 `DriveResumeArgs.acceptEarly`)。
|
|
706
440
|
const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true);
|
|
707
441
|
if (remember && deps.approvalExemptionStore) {
|
|
708
442
|
sendJson(res, out.status, { ...out.body, rememberApplied });
|
|
@@ -714,154 +448,57 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
714
448
|
sendError(res, 404, "not_found.route", "not found");
|
|
715
449
|
return;
|
|
716
450
|
}
|
|
717
|
-
// [2354]/[2356] A-5(5.0.0):legacy F4 轮询腿(approvalStore list/by-id/decide,trusted-header 决裁)
|
|
718
|
-
// 整体退役。无 checkpointStore 的部署打 /v1/approvals 一律落到本域尾部的未匹配路径(404 诚实缺席)。
|
|
719
451
|
miss.fell = true;
|
|
720
452
|
}
|
|
721
|
-
/** Server-side poll cadence for the native-push approvals stream. The CLIENT subscribes ONCE and never polls;
|
|
722
|
-
* the server diffs at this cadence (TiDB has no LISTEN/NOTIFY) and pushes deltas. 3s < the ~10s client poll it
|
|
723
|
-
* replaces, so the operator sees a new suspend faster AND the portal loses its polling loop. (Each SSE
|
|
724
|
-
* connection runs its OWN poll loop — fine for a handful of operators; a shared fan-out poll is a future
|
|
725
|
-
* optimization if the operator count grows.) */
|
|
726
453
|
const APPROVALS_STREAM_POLL_MS = 3000;
|
|
727
|
-
/**
|
|
728
|
-
* 🔴 #209 件4(codex 对抗复审 R3-[high],验真后修)—— **durable 读面的脱敏投影**。
|
|
729
|
-
*
|
|
730
|
-
* core 5.25.0 的 `RiskDescriptor.shadowedRule` = 被越级的那条持久规则的**原文**。core 侧只过
|
|
731
|
-
* `inlineUntrusted`(中和 + 限长),**不**做秘密脱敏 —— 它的 JSDoc 也只承诺中和,不承诺无秘密
|
|
732
|
-
* (与同结构里的 `summary` 不同:那一格 core 明写「NEVER raw secrets / full args / env」)。
|
|
733
|
-
* 而规则原文是**人手写的文本**,一条 `Bash(curl -H "Authorization: Bearer …")` 形的规则完全拼得出来。
|
|
734
|
-
*
|
|
735
|
-
* 同步路(`tool_approval` 帧 / `card_json`)在铸点就 `redactSecrets` 了;durable 路两条读面
|
|
736
|
-
* (`GET /v1/approvals` 与 `/v1/approvals/stream`)此前把 `listPending()` 的行**整只**上 wire ⇒
|
|
737
|
-
* 同一份内容在两条腿上一条脱敏、一条不脱敏,而 operator 的队列是**跨租户**可见的那一条。
|
|
738
|
-
*
|
|
739
|
-
* 修在**读面**而不是落库点,理由是本仓成文的分工(同文件 inbox 段那条 `redactedPreview(s.toolInput)` 的
|
|
740
|
-
* 逐字理由):「core bounds but does NOT redact it — redaction is the consumer's job」。落库点改写会与
|
|
741
|
-
* core 的属主面打架,而且救不了**别的副本**已经写下的行。
|
|
742
|
-
*
|
|
743
|
-
* 🔴 只动这一格,不顺手洗整只 descriptor:`summary` 有 core 的无秘密承诺、`touchedPaths` 是路径,
|
|
744
|
-
* 两者的现行为不在本批辖域内(要改得走各自的三问)。键集**不变** —— 「整只透传、不按键投影」那条契约
|
|
745
|
-
* (`test/approval-shadowed-rule-wire.test.ts` §3)照旧成立,变的只是这一格的**内容**。
|
|
746
|
-
*/
|
|
747
454
|
function redactPendingDisclosures(row) {
|
|
748
455
|
const rd = row.riskDescriptor;
|
|
749
456
|
if (rd === null || rd === undefined || typeof rd.shadowedRule !== "string")
|
|
750
457
|
return row;
|
|
751
458
|
return { ...row, riskDescriptor: { ...rd, shadowedRule: redactSecrets(rd.shadowedRule) } };
|
|
752
459
|
}
|
|
753
|
-
/**
|
|
754
|
-
* 🔴 [3683]-2 / [3684]②:两条 durable 读面(`GET /v1/approvals` + `/v1/approvals/stream`)的**唯一**
|
|
755
|
-
* 投影 —— 脱敏(#209 件4)+ 出身归因(#220 的另一半)一次做完。
|
|
756
|
-
*
|
|
757
|
-
* 出身格的病灶如实记:`governanceForced` 此前只在 409 体上算(`buildActiveRunConflict`),而运维队列
|
|
758
|
-
* 的行不算 —— 同一道 park 门,客户端从 409 看得见「这是运维治理层下的」,从队列看不见。它**不是**一个
|
|
759
|
-
* 落库缺口:判据的两半(行上 `riskDescriptor.shellGateDoctrine` + 本部署的治理姿态)在这条读面上都够得着,
|
|
760
|
-
* 缺的只是**没有人算**。所以补法 = 让两面共用 `governanceOriginOf` 这一个属主(在读面算的已知失真、
|
|
761
|
-
* 以及「根治要 core 在 mint 写进行里」的登记,逐字在 `runtime-governance.ts` 那个函数的顶注,不复述)。
|
|
762
|
-
*
|
|
763
|
-
* 🔴 `true` 才写键,恒不写 `false` —— 与活卡帧 / 409 体 / `card_json` 同一条纪律:缺席 = 「没有治理来源
|
|
764
|
-
* 的证据」,不是「这不是治理门」。
|
|
765
|
-
*
|
|
766
|
-
* ⚠️ **`delegation` 刻意不在这里**(同批如实登记,禁编造键):park 行上**没有**这条信息 —— core 的
|
|
767
|
-
* `Checkpoint` / `PendingAction` / `CheckpointGate` 三个面都不带 `{parentToolCallId, depth, agentName}`
|
|
768
|
-
* (`Checkpoint.state.delegationProvenance` 是 design/180 的内容安全聚合 `{version, coverageStartTurn,
|
|
769
|
-
* sawExternal, incomplete}`,与 ask 的出处链是两件事)。同步腿的这一格来自 `AskRequest.delegation`,
|
|
770
|
-
* 那条路 park 时不随行。⇒ 无供给,诚实缺席;要它得先有 core 侧的属主面。
|
|
771
|
-
*/
|
|
772
460
|
function projectPendingForWire(row, governance) {
|
|
773
461
|
const redacted = redactPendingDisclosures(row);
|
|
774
462
|
const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
|
|
775
463
|
return governanceForced ? { ...redacted, governanceForced } : redacted;
|
|
776
464
|
}
|
|
777
|
-
|
|
778
|
-
* GET /v1/approvals/stream (design/80 native push): SSE — pushes pending-approval deltas so the portal
|
|
779
|
-
* SUBSCRIBES ONCE instead of polling GET /v1/approvals every ~10s (better UX: near-real-time + no client poll
|
|
780
|
-
* loop). Polls listPending(scope) and diffs against the prior snapshot: `pending` for a new suspend, `resolved`
|
|
781
|
-
* for a decided/expired/gone one. Cross-replica BY CONSTRUCTION — the poll reads the SHARED checkpoint table,
|
|
782
|
-
* so a suspend on ANY replica is seen by an operator streaming on a DIFFERENT replica. A transient DB blip
|
|
783
|
-
* heartbeats + retries (never kills the stream); a 15-min cap + req-close end it (parity with streamTaskTrace).
|
|
784
|
-
* Poll-granularity caveat (**narrowed** by [3684]②): the delta now re-emits a `pending` frame whenever a key's
|
|
785
|
-
* WIRE PROJECTION changes (upsert), not only when the key enters — so a same-key re-suspend within one interval
|
|
786
|
-
* IS visible as long as anything on the row differs. What still shows no delta is the strictly degenerate case:
|
|
787
|
-
* a resolve + re-suspend on the same (session,toolCallId) whose projection is BYTE-IDENTICAL — indistinguishable
|
|
788
|
-
* by construction, and harmless (the card the operator sees is the card that is pending). Pins: the two halves of
|
|
789
|
-
* `test/wire-pairing-approvals-stream.test.ts` S3b (changed ⇒ re-emit with the NEW payload; unchanged ⇒ silence).
|
|
790
|
-
*
|
|
791
|
-
* A-002.19(2026-08-09 亲验定性=设计接受,非欠账):每连接自跑 pollMs 轮询打共享 checkpoint 表,
|
|
792
|
-
* 之所以不建 fan-out/单轮询器基建——①消费方=portal 运营面,并发连接数量级为个位(不是用户面);
|
|
793
|
-
* ②listPending 走 (scope,status) 索引,单查成本与一次 /v1/approvals 轮询同阶,而本路正是替代客户端
|
|
794
|
-
* ~10s 轮询的(净负载持平或更低);③DB blip 已 fail-soft(心跳+下拍重试),15-min cap 兜顶。连接
|
|
795
|
-
* 数假设若被打破(portal 多开成常态),届时把 poll 收敛为进程内单轮询器多路复用——那是量级触发的
|
|
796
|
-
* 演化,不是现在的缺陷。
|
|
797
|
-
*/
|
|
798
|
-
export async function streamApprovals(req, res, cs, scope,
|
|
799
|
-
/** [3684]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
|
|
800
|
-
* `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
|
|
801
|
-
governance, pollMs = APPROVALS_STREAM_POLL_MS) {
|
|
802
|
-
// JSON-encode the (sessionId, toolCallId) pair so distinct pendings can NEVER collide into one Map key,
|
|
803
|
-
// regardless of what a caller-supplied sessionId contains (any single-char delimiter — space OR even NUL — is
|
|
804
|
-
// injectable by an adversarial id, masking/wrong-removing an approval card; adversarial finding). JSON escaping
|
|
805
|
-
// makes every distinct (sessionId, toolCallId) pair a distinct key.
|
|
465
|
+
export async function streamApprovals(req, res, cs, scope, governance, pollMs = APPROVALS_STREAM_POLL_MS) {
|
|
806
466
|
const keyOf = (p) => JSON.stringify([p.sessionId, p.toolCallId ?? null]);
|
|
807
467
|
sseHeaders(res);
|
|
808
|
-
// Each data payload ALSO carries a `type` field mirroring the SSE event name — so a client can dispatch on
|
|
809
|
-
// `data.type` without relying on the SSE `event:` line (some proxies strip event names; `onmessage` ignores
|
|
810
|
-
// named events). The SSE event name stays for native EventSource addEventListener.
|
|
811
468
|
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "approvals-delta", pollMs: APPROVALS_STREAM_POLL_MS })}\n\n`);
|
|
812
469
|
let closed = false;
|
|
813
|
-
// design/245 件1:req+res 双挂 + 一次性闸(理由全文在 `sse-lifecycle.ts` 顶注)。本腿每连接自跑一条
|
|
814
|
-
// 打共享 checkpoint 表的轮询(见上方 A-002.19 的定性),旗翻不过来的代价就是这条 DB 轮询对着死
|
|
815
|
-
// socket 跑满 15 分钟 —— 断连清理在本腿是**负载**问题,不只是收摊问题。
|
|
816
470
|
bindSseLifecycle(req, res, () => { closed = true; });
|
|
817
471
|
const start = Date.now();
|
|
818
472
|
let lastBeat = Date.now();
|
|
819
473
|
let prev = new Map();
|
|
820
|
-
/**
|
|
821
|
-
* 🔴 [3684]② / codex 对抗复审 [medium](验真后修):**上一拍每行的 wire 投影字节**。
|
|
822
|
-
*
|
|
823
|
-
* 旧 delta 只判「键进/键出」——对一条内容**不变**的 park 行那是对的,而 `governanceForced` 是**派生位**,
|
|
824
|
-
* 它随 `config.autonomy`(config-center 的 `apply-effective` 就地热改)在**同一条行还挂着**的时候翻面。
|
|
825
|
-
* 只判键的话,常驻订阅的 portal 会一直渲着旧徽标(最长到 15 分钟 cap 重连),而同一时刻
|
|
826
|
-
* `GET /v1/approvals` 已经给出另一个答案 —— 两条本该同源的读面当场分家。
|
|
827
|
-
*
|
|
828
|
-
* ⇒ 判据改成**投影字节变了就重发**。`pending` 帧对同一 (sessionId, toolCallId) 键因此是 **upsert**
|
|
829
|
-
* 语义(消费端本来就按这个键建表);重复发一帧是无害的(幂等覆盖),漏发一帧是错的徽标。
|
|
830
|
-
*/
|
|
831
474
|
let prevFrames = new Map();
|
|
832
475
|
let first = true;
|
|
833
476
|
while (!closed) {
|
|
834
477
|
let pending;
|
|
835
478
|
try {
|
|
836
|
-
// #209 件4 + [3684]②:与 `GET /v1/approvals` **同一个**投影函数(脱敏 + 出身归因)—— 两条 durable
|
|
837
|
-
// 读面各投各的就会漂(SSE 那条恰恰是 operator 常驻订阅的那条,漏掉它等于没修)。
|
|
838
479
|
pending = (await cs.listPending(scope)).map((r) => projectPendingForWire(r, governance));
|
|
839
480
|
}
|
|
840
481
|
catch {
|
|
841
|
-
// a transient TiDB blip must NOT kill the subscription — heartbeat + retry next tick (fail-soft)
|
|
842
482
|
if (!res.writableEnded)
|
|
843
483
|
res.write(`event: heartbeat\ndata: ${JSON.stringify({ type: "heartbeat" })}\n\n`);
|
|
844
484
|
await sleep(pollMs);
|
|
845
485
|
continue;
|
|
846
486
|
}
|
|
847
487
|
const cur = new Map(pending.map((p) => [keyOf(p), p]));
|
|
848
|
-
// 一行只序列化一次:既是要发的帧体,也是与上一拍比对的判据(两处各算一遍必然漂)。
|
|
849
488
|
const curFrames = new Map([...cur].map(([k, p]) => [k, JSON.stringify({ type: "pending", ...p })]));
|
|
850
489
|
if (first) {
|
|
851
|
-
// initial snapshot so a freshly-subscribed portal renders the current queue immediately (no first-poll gap)
|
|
852
490
|
for (const frame of curFrames.values())
|
|
853
491
|
res.write(`event: pending\ndata: ${frame}\n\n`);
|
|
854
492
|
res.write(`event: synced\ndata: ${JSON.stringify({ type: "synced", count: pending.length })}\n\n`);
|
|
855
493
|
first = false;
|
|
856
494
|
}
|
|
857
495
|
else {
|
|
858
|
-
// 新 park **或**同一行的投影内容变了(见 `prevFrames` 顶注:派生位会在行不变时翻面)⇒ upsert 一帧
|
|
859
496
|
for (const [k, frame] of curFrames)
|
|
860
497
|
if (prevFrames.get(k) !== frame)
|
|
861
498
|
res.write(`event: pending\ndata: ${frame}\n\n`);
|
|
862
499
|
for (const [k, p] of prev)
|
|
863
500
|
if (!cur.has(k))
|
|
864
|
-
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
|
|
501
|
+
res.write(`event: resolved\ndata: ${JSON.stringify({ type: "resolved", sessionId: p.sessionId, toolCallId: p.toolCallId })}\n\n`);
|
|
865
502
|
}
|
|
866
503
|
prev = cur;
|
|
867
504
|
prevFrames = curFrames;
|