@sema-agent/server 7.43.0 → 7.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +18 -0
- package/dist/adoption/plan.js +31 -192
- package/dist/adoption/quiesce.js +3 -86
- package/dist/adoption/runner.js +3 -140
- package/dist/adoption/sql.js +0 -74
- package/dist/adoption/wire.js +0 -80
- package/dist/approval-ask-machine.js +0 -75
- package/dist/approval-card.js +0 -323
- package/dist/approval-deny-reasons.js +0 -42
- package/dist/approval-hmac.js +0 -35
- package/dist/approval-reconciler.js +8 -166
- package/dist/approval.js +4 -67
- package/dist/audit.js +1 -44
- package/dist/auth-bridge.js +4 -47
- package/dist/auth-keys.js +0 -23
- package/dist/bake-runner/main.js +4 -65
- package/dist/bake-runner/protocol.js +3 -58
- package/dist/bake-runner/runner.js +5 -91
- package/dist/bench/l8/artifact.js +2 -61
- package/dist/bench/l8/escape.js +0 -25
- package/dist/bench/l8/index.js +0 -14
- package/dist/bench/l8/probes.js +0 -60
- package/dist/bench/l8/run-probes.js +2 -45
- package/dist/bench/s1/arms.js +14 -189
- package/dist/bench/s1/live-deps.js +11 -253
- package/dist/bench/s1/oracle.js +1 -36
- package/dist/bench/s1/repair-oracle-adapter.js +0 -30
- package/dist/bench/s1/reviewer.js +0 -34
- package/dist/bench/s1/row.js +0 -62
- package/dist/bench/s1/run-firm.js +4 -70
- package/dist/bench/s1/runner-ctx.js +0 -40
- package/dist/bench/s1/tasks.js +0 -115
- package/dist/boot/adoption.js +0 -20
- package/dist/boot/budget-tracing.js +3 -55
- package/dist/boot/config-center.js +43 -551
- package/dist/boot/coordinators.js +1 -69
- package/dist/boot/crash-last.js +0 -18
- package/dist/boot/deferred-sandbox-path-env.js +3 -119
- package/dist/boot/execution-env.js +5 -171
- package/dist/boot/governance-seams.js +0 -92
- package/dist/boot/leader.js +0 -69
- package/dist/boot/limit-sync.js +0 -2
- package/dist/boot/memory-boundary.js +3 -91
- package/dist/boot/org-memory.js +1 -25
- package/dist/boot/parked-revive-gate.js +0 -150
- package/dist/boot/permission-rules-audit.js +1 -56
- package/dist/boot/reapers.js +15 -273
- package/dist/boot/resolve-spec.js +9 -768
- package/dist/boot/retention-lane.js +4 -87
- package/dist/boot/runner-deps.js +3 -297
- package/dist/boot/runtime-caps.js +0 -40
- package/dist/boot/session-faces.js +2 -133
- package/dist/boot/shutdown.js +9 -83
- package/dist/boot/side-query-lane.js +2 -137
- package/dist/boot/stores.js +15 -215
- package/dist/boot/task-list-lane.js +0 -18
- package/dist/boot/webfetch-summarize-lane.js +0 -47
- package/dist/boot/workflow-orchestration.js +3 -99
- package/dist/boot-reclaim.js +0 -20
- package/dist/bounded-session-map.js +0 -19
- package/dist/brain.js +2 -139
- package/dist/budget.js +9 -244
- package/dist/capabilities/builtin-tools.js +0 -3
- package/dist/capabilities/center-plugins.js +6 -51
- package/dist/capabilities/center-prompts.js +6 -71
- package/dist/capabilities/code-review-council.js +7 -40
- package/dist/capabilities/collab-workflows.js +1 -44
- package/dist/capabilities/hands-lane.js +0 -65
- package/dist/capabilities/memory-notice.js +0 -70
- package/dist/capabilities/prompt.js +0 -14
- package/dist/capabilities/prompts/code-review.js +0 -14
- package/dist/capabilities/prompts/identity.js +0 -6
- package/dist/capabilities/prompts/team.js +0 -4
- package/dist/capabilities/repo-tools.js +0 -34
- package/dist/capabilities/sandbox-file-send.js +3 -92
- package/dist/capabilities/scenarios.d.ts +0 -1
- package/dist/capabilities/scenarios.js +18 -279
- package/dist/capabilities/select-environment-tool.js +0 -32
- package/dist/capabilities/send-user-file-tool.js +2 -66
- package/dist/capabilities/skills.d.ts +1 -2
- package/dist/capabilities/skills.js +3 -21
- package/dist/capabilities/team.d.ts +3 -11
- package/dist/capabilities/team.js +3 -51
- package/dist/capabilities/tool-defer.js +0 -3
- package/dist/config-center/apply-effective.js +15 -330
- package/dist/config-center/apply-ledger.js +5 -38
- package/dist/config-center/facade.js +0 -41
- package/dist/config-center/hot-keys-registry.js +0 -20
- package/dist/config-center/http-client.js +2 -125
- package/dist/config-center/mcp-revocation.js +2 -34
- package/dist/config-center/read-face.js +0 -59
- package/dist/config-center/restart-signal.js +1 -85
- package/dist/config-center/skills-mcp.d.ts +1 -1
- package/dist/config-center/skills-mcp.js +9 -76
- package/dist/config-center/stage-limits.js +8 -35
- package/dist/config-invariants.js +0 -16
- package/dist/config-lkg.js +0 -42
- package/dist/config-provider.js +3 -186
- package/dist/config-types.js +0 -5
- package/dist/config.js +107 -1145
- package/dist/degenerate-instrument.js +3 -67
- package/dist/deployment-governance.js +0 -124
- package/dist/digest-form.js +0 -12
- package/dist/elicitation.js +3 -86
- package/dist/env-facts.js +7 -75
- package/dist/fleet/fleet-bus.js +34 -507
- package/dist/fleet/fleet-reconciler.js +9 -149
- package/dist/fleet/fleet-terminal-window.js +10 -178
- package/dist/fleet/subagent-tail-bus.js +3 -72
- package/dist/fleet-client.js +10 -70
- package/dist/fleet-lease.js +5 -79
- package/dist/git-api-kind.js +0 -3
- package/dist/governance-ask-marks.js +2 -78
- package/dist/hooks/branch-transcript.js +0 -74
- package/dist/hooks/cc-agent-hook-prompt.js +0 -29
- package/dist/hooks/cc-stop-prompt.js +1 -46
- package/dist/hooks/hook-llm.js +1 -53
- package/dist/hooks/hook-runner.js +20 -414
- package/dist/http/active-run-conflict.js +4 -129
- package/dist/http/cursor-fingerprint.d.ts +5 -0
- package/dist/http/cursor-fingerprint.js +5 -0
- package/dist/http/idempotency.js +0 -37
- package/dist/http/principal-gate.js +3 -40
- package/dist/http/route-ctx.js +0 -9
- package/dist/http/routes/a2a-serve.js +7 -319
- package/dist/http/routes/admin-config-refresh.js +0 -4
- package/dist/http/routes/admin-drain.js +0 -10
- package/dist/http/routes/adoption.js +1 -29
- package/dist/http/routes/agents-roster.js +1 -47
- package/dist/http/routes/approvals-assistant.js +32 -395
- package/dist/http/routes/attachments.js +4 -22
- package/dist/http/routes/capabilities.js +3 -420
- package/dist/http/routes/diagnostics.js +1 -81
- package/dist/http/routes/fleet.js +9 -185
- package/dist/http/routes/images.js +14 -239
- package/dist/http/routes/leader.js +0 -13
- package/dist/http/routes/memory-bundle.js +1 -61
- package/dist/http/routes/memory-policy.js +9 -99
- package/dist/http/routes/notify-wake.js +3 -37
- package/dist/http/routes/observability.js +2 -19
- package/dist/http/routes/retention-ops.js +2 -34
- package/dist/http/routes/rules.js +1 -93
- package/dist/http/routes/runs.js +64 -873
- package/dist/http/routes/session-sync.js +19 -258
- package/dist/http/routes/sessions-list.js +17 -43
- package/dist/http/routes/sessions.js +34 -226
- package/dist/http/routes/shared-memory.js +5 -36
- package/dist/http/routes/side-query.js +1 -87
- package/dist/http/routes/tasks.js +52 -722
- package/dist/http/routes/trace-usage.js +32 -195
- package/dist/http/routes/workflows.js +19 -195
- package/dist/http/run-meta.js +0 -6
- package/dist/http/send.js +0 -32
- package/dist/http/server.js +92 -1597
- package/dist/http/sse-lifecycle.js +2 -13
- package/dist/http/sse-log.js +3 -48
- package/dist/http/tar.js +5 -21
- package/dist/http/verify-rounds.js +0 -5
- package/dist/http/wire-gate.js +0 -9
- package/dist/http/workspace-content.js +0 -10
- package/dist/images/bake-validate.js +1 -70
- package/dist/images/manifest.js +1 -6
- package/dist/index.js +0 -21
- package/dist/key-resolver.js +2 -17
- package/dist/leader/diffout.js +1 -20
- package/dist/leader/diffup.js +0 -47
- package/dist/leader/endpoint.js +2 -57
- package/dist/leader/fanout.js +3 -45
- package/dist/leader/grader-env-factory.js +3 -72
- package/dist/leader/leader.js +5 -156
- package/dist/leader/merge.js +8 -115
- package/dist/leader/planner.js +3 -54
- package/dist/leader/repair-oracle.js +1 -60
- package/dist/leader/repair-wire.js +2 -79
- package/dist/leader/wire.js +8 -307
- package/dist/lsp/e2b-bridge.js +4 -64
- package/dist/lsp/e2b-manager.js +6 -94
- package/dist/lsp/lsp-frames.js +0 -12
- package/dist/lsp/manager.js +4 -96
- package/dist/lsp/ws-transport.js +5 -55
- package/dist/lsp-evict.js +1 -15
- package/dist/main.js +57 -765
- package/dist/memory-bundle-engine.js +0 -55
- package/dist/memory-export.js +0 -4
- package/dist/memory-posture.js +1 -15
- package/dist/memory-scope.js +11 -148
- package/dist/memory-sync-client.js +2 -44
- package/dist/memory-sync.js +1 -80
- package/dist/model-select.js +3 -80
- package/dist/observability/cost-quota.js +1 -17
- package/dist/observability/cost-taxonomy.js +0 -34
- package/dist/observability/fail-open.js +7 -86
- package/dist/observability/logger.js +0 -6
- package/dist/observability/metrics.js +0 -94
- package/dist/observability/otel-exporter.js +3 -13
- package/dist/observability/principal-context.js +0 -9
- package/dist/observability/prompt-manifest.js +1 -37
- package/dist/observability/rate-limit.js +0 -4
- package/dist/observability/secret-env-scrub.js +2 -56
- package/dist/observability/tool-trace.js +1 -70
- package/dist/orchestration/hardened-vm-runner.js +4 -118
- package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
- package/dist/orchestration/hardened-vm-worker.js +0 -27
- package/dist/orchestration/subagent-steer.js +1 -45
- package/dist/orchestration/workflow-agent-steer.js +1 -80
- package/dist/orchestration/workflow-completion-inbox.js +32 -285
- package/dist/orchestration/workflow-notify-journal.js +16 -259
- package/dist/org-memory-admission.js +3 -47
- package/dist/parent-watch.js +2 -48
- package/dist/parked-decide.js +1 -109
- package/dist/per-task-image.js +0 -57
- package/dist/plan-cache-probe.js +3 -27
- package/dist/plugins/adoption-log-sql.js +2 -119
- package/dist/plugins/approval-ask-store-memory.js +3 -38
- package/dist/plugins/approval-ask-store-sql.js +6 -188
- package/dist/plugins/approval-exemption-store.js +2 -28
- package/dist/plugins/background-agent-store-sql.js +4 -105
- package/dist/plugins/background-shell-support.js +14 -122
- package/dist/plugins/blob-backend.js +6 -169
- package/dist/plugins/breaker-state-sql.js +8 -46
- package/dist/plugins/caching-session-store.js +4 -106
- package/dist/plugins/checkpoint-store-sql.js +18 -505
- package/dist/plugins/e2b-orphan-reclaim.js +0 -45
- package/dist/plugins/file-outcome-sink.js +0 -9
- package/dist/plugins/file-resume-anchor-store.js +4 -43
- package/dist/plugins/file-run-store.js +26 -364
- package/dist/plugins/file-snapshot-store-sql.js +10 -181
- package/dist/plugins/fork-routing-session-store.js +8 -111
- package/dist/plugins/host-platform.js +2 -91
- package/dist/plugins/image-bake-store-sql.js +7 -250
- package/dist/plugins/image-index-sql.js +4 -123
- package/dist/plugins/k8s-bg-scripts.js +4 -88
- package/dist/plugins/k8s-exec-protocol.js +0 -41
- package/dist/plugins/leader-run-store-sql.js +0 -101
- package/dist/plugins/local-checkpoint-store.js +8 -128
- package/dist/plugins/local-session-store.js +34 -296
- package/dist/plugins/local-task-attachment-store.js +2 -16
- package/dist/plugins/mailbox-store-sql.js +8 -65
- package/dist/plugins/memory-embedder-fingerprint.js +5 -166
- package/dist/plugins/memory-embedder.js +4 -70
- package/dist/plugins/memory-engine-pg.js +6 -170
- package/dist/plugins/memory-engine-tidb.js +7 -155
- package/dist/plugins/memory-engine-vector-util.js +0 -10
- package/dist/plugins/memory-key-guards.js +0 -34
- package/dist/plugins/memory-origin-law.js +0 -187
- package/dist/plugins/memory-resume-anchor-store.js +0 -17
- package/dist/plugins/memory-run-store.js +12 -89
- package/dist/plugins/memory-session-policy-store.js +0 -17
- package/dist/plugins/memory-sync-store-pg.js +4 -49
- package/dist/plugins/memory-sync-store-tidb.js +3 -35
- package/dist/plugins/outcome-ledger-sql.js +3 -97
- package/dist/plugins/permission-rule-store-file.js +5 -133
- package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
- package/dist/plugins/permission-rule-store-sql.js +50 -307
- package/dist/plugins/pg-cost-quota.js +0 -7
- package/dist/plugins/pg-pool.js +0 -92
- package/dist/plugins/pg-rate-limiter.js +2 -13
- package/dist/plugins/pg-safe-json.js +4 -40
- package/dist/plugins/pg-session-storage.js +25 -189
- package/dist/plugins/posix-shell-fs.js +1 -31
- package/dist/plugins/remote-env-adb.js +12 -101
- package/dist/plugins/remote-env-e2b.js +40 -371
- package/dist/plugins/remote-env-file-error.js +0 -37
- package/dist/plugins/remote-env-host.js +63 -483
- package/dist/plugins/remote-env-k8s.js +32 -326
- package/dist/plugins/remote-env-local-docker.js +24 -157
- package/dist/plugins/remote-env-ssh.js +20 -128
- package/dist/plugins/remote-scratchpad.js +2 -32
- package/dist/plugins/remote-shell.js +1 -32
- package/dist/plugins/resume-anchor-store-sql.js +0 -11
- package/dist/plugins/retention-lane-store-sql.js +0 -108
- package/dist/plugins/retention-store-sql.js +4 -383
- package/dist/plugins/roster-store-sql.js +0 -55
- package/dist/plugins/run-store-sql.js +14 -251
- package/dist/plugins/s3-presign.js +2 -49
- package/dist/plugins/scheduler-support.js +3 -80
- package/dist/plugins/send-file-ledger.js +4 -53
- package/dist/plugins/send-user-file.js +4 -94
- package/dist/plugins/session-placement.js +1 -89
- package/dist/plugins/session-policy-store-sql.js +3 -81
- package/dist/plugins/session-store.js +0 -57
- package/dist/plugins/shared-memory-store-sql.js +4 -167
- package/dist/plugins/sql-driver.js +0 -17
- package/dist/plugins/sql-errors.js +0 -7
- package/dist/plugins/sql-escape.js +0 -8
- package/dist/plugins/sql-row-helpers.js +0 -25
- package/dist/plugins/store-backend.js +42 -222
- package/dist/plugins/store-contracts.js +2 -46
- package/dist/plugins/task-attachment-store.js +3 -49
- package/dist/plugins/task-list-store-sql.js +0 -79
- package/dist/plugins/tidb-cost-quota.js +1 -4
- package/dist/plugins/tidb-pool.js +1 -207
- package/dist/plugins/tidb-rate-limiter.js +3 -9
- package/dist/plugins/tidb-session-storage.js +4 -70
- package/dist/plugins/tidb-session-store.js +19 -341
- package/dist/plugins/tool-result-store-sql.js +4 -179
- package/dist/plugins/usage-window-store-sql.js +0 -8
- package/dist/plugins/web-search.js +10 -132
- package/dist/plugins/workflow-journal-store-sql.js +2 -58
- package/dist/plugins/workflow-run-store-sql.js +6 -91
- package/dist/plugins/worktree-isolation.js +6 -126
- package/dist/plugins/write-behind-counter.js +16 -75
- package/dist/principal-jwt.js +5 -60
- package/dist/project-memory.js +15 -146
- package/dist/prompts-domain-validate.js +1 -51
- package/dist/question.js +2 -111
- package/dist/resource-suspend.js +0 -18
- package/dist/router/route-orchestration.js +0 -77
- package/dist/rules-consent.d.ts +44 -5
- package/dist/rules-consent.js +75 -215
- package/dist/run-local.js +14 -381
- package/dist/runs.js +29 -617
- package/dist/runtime-caps-resolver.js +6 -132
- package/dist/runtime-governance.js +1 -232
- package/dist/sandbox-pkg-source.js +0 -37
- package/dist/sealed-key.js +3 -68
- package/dist/security.js +6 -291
- package/dist/session-leaf-bus.js +0 -32
- package/dist/session-sync-content.js +1 -69
- package/dist/session-sync-kernel.js +3 -60
- package/dist/session-sync.js +3 -66
- package/dist/session-titler.js +7 -41
- package/dist/session-watch.js +9 -73
- package/dist/shared-memory-scope-authorizer.js +0 -18
- package/dist/sighup-idle.js +1 -12
- package/dist/spec-fields.js +4 -143
- package/dist/store-live-probe.js +3 -49
- package/dist/task-a2a.js +1 -126
- package/dist/task-cwd.js +1 -103
- package/dist/task-mcp.js +1 -89
- package/dist/task-settings.js +10 -300
- package/dist/task-workflow.js +8 -75
- package/dist/tool-approval.d.ts +28 -1
- package/dist/tool-approval.js +70 -1430
- package/dist/trace/artifacts.js +5 -20
- package/dist/trace/engine-notice-wire.js +6 -138
- package/dist/trace/ledger-sink.js +7 -96
- package/dist/trace/project.js +4 -448
- package/dist/trace/redact.js +9 -77
- package/dist/turn-activity.js +1 -27
- package/dist/usage-analytics.js +4 -34
- package/dist/wall-clock-jump-guard.js +1 -68
- package/package.json +3 -3
- package/dist/capabilities/scenario-alias.d.ts +0 -27
- package/dist/capabilities/scenario-alias.js +0 -61
package/dist/rules-consent.js
CHANGED
|
@@ -1,134 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #154 车二 —— **同意车道的 server 半场**(core 5.18.0 design/179 `permission-rule-consent.ts` 的宿主侧)。
|
|
3
|
-
*
|
|
4
|
-
* core 把一条规则进店的路钉成三步:`durable approval record (pending)` → `authenticated confirmation
|
|
5
|
-
* transfer (approved)` → `principal-bound redemption (redeemed)`,而且**只有第三步**够得着写面。core 明写
|
|
6
|
-
* 「The confirmation transfer is an in-process call. **A deployment must reach it only from its
|
|
7
|
-
* authenticated approval channel**」—— 那句话是把一段责任**交给宿主**,不是一段已经完成的检查。本文件就是
|
|
8
|
-
* 那段责任的落点:
|
|
9
|
-
*
|
|
10
|
-
* · 卡道(兑付口):一次 ask 决议携「不再询问」时,由**这里**去 prepare→confirm→redeem。人是谁 =
|
|
11
|
-
* 回决端点已验明的 principal(不是请求体里的任何字段);规则文本 = **引擎**从被裁决的命令铸的候选
|
|
12
|
-
* (`prepareCardApproval` 不收调用方候选,core 在那条入口上已经把「写自己的规则」这件事变成不可拼写),
|
|
13
|
-
* 客户端只能说「我选第几个」。
|
|
14
|
-
*
|
|
15
|
-
* · 导入道(CC settings):`prepare` 读用户交上来的 settings 层、铸 pending 记录 + **server 自铸的票**;
|
|
16
|
-
* `redeem` 原子消费票、confirm、`redeemRuleBatch`。
|
|
17
|
-
*
|
|
18
|
-
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
* 🔴 票的信任边界四条,**全部是 server 附加的**(不是 core 的语义)
|
|
20
|
-
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
* 亲读 core 的 `mintRuleTicket`(dist,5.22.0):`return \`rt.${candidateIndex}.${recordId}\`` —— 它**没有**
|
|
22
|
-
* TTL、没有签名、没有消费位、没有载荷绑定。core 的票在**进程内**是够的(它的相邻语义是「重放同一次兑付是
|
|
23
|
-
* 幂等的」),但一条跨网络、由客户端保管、可被复制的票需要另外四样:
|
|
24
|
-
* ① **principal 绑定** —— redeem 的已验明 principal 必须 = prepare 铸票的那一位。跨 principal redeem 与
|
|
25
|
-
* unknown ticket **同形 404**:两者返回不同的码就是一台存在性 oracle(拿别人的票撞库能探到「它存在」)。
|
|
26
|
-
* ② **过期** —— `RULE_IMPORT_TICKET_TTL_MS`;过期 redeem 拒。
|
|
27
|
-
* ③ **一次性原子消费** —— 同票二次 redeem 拒,并发双 redeem **恰一成功**。判据由 SQL 的单条条件
|
|
28
|
-
* `UPDATE … WHERE consumed_at_ms IS NULL` 回答,**禁 read-then-write**(见 store 那侧的 🔴 注)。
|
|
29
|
-
* ④ **载荷绑定** —— redeem 应用的规则集 = prepare 时刻的快照。core 已经结构上保证了「规则文本来自记录」,
|
|
30
|
-
* 本层再加一道:票上钉着 prepare 时刻候选集的摘要,redeem 时与记录上的候选集重算比对,不符即拒 ——
|
|
31
|
-
* 于是「篡改记录再拿旧票兑付」这条路上多一道等式,而不是只靠「记录只有我们能写」这一条信念。
|
|
32
|
-
*
|
|
33
|
-
* 命名(CLAUDE.md 工厂律):`build*` = 纯数据;`create*` = 活对象。本文件的 {@link createRuleConsentLane}
|
|
34
|
-
* 返回一个带方法的束 ⇒ `create*`。
|
|
35
|
-
*/
|
|
36
1
|
import { randomUUID } from "node:crypto";
|
|
37
2
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
3
|
+
import { createLogger } from "./observability/logger.js";
|
|
38
4
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
39
|
-
import { confirmRuleApproval, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, } from "@sema-agent/core";
|
|
5
|
+
import { confirmRuleApproval, parseAllowRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, } from "@sema-agent/core";
|
|
40
6
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
41
|
-
/** 导入票的存活窗。人从「看到预览」到「按下确认」是一次交互,不是一段会话——十分钟宽到不会误伤,
|
|
42
|
-
* 窄到一张泄漏的票不会常驻。运维旋钮暂不开(没有部署形要求它可调;要开时走 config.ts 的既有姿势)。 */
|
|
43
7
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
44
|
-
/** 可重试那一支通告给客户端的等待秒数(`state.rule_import_retry` 的 `retryAfterSec` / `Retry-After`)。
|
|
45
|
-
* 短 —— 它对应的是一次瞬时 store 抖动,不是排队。**单一真源在本文件**:放回认领时要用它判「这张票
|
|
46
|
-
* 还能不能撑过这段窗」,HTTP 层要用它填响应,两处用同一个数才谈得上「承诺兑得出来」。 */
|
|
47
8
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
48
|
-
/**
|
|
49
|
-
* **全部层合计**的候选上限(codex 交叉复审 round7 [high] 二,验真后修)。
|
|
50
|
-
*
|
|
51
|
-
* 单层 16 KiB 只管住了**请求体**,管不住**下游工作量**:紧凑的合法规则(`Bash(ls)` 十个字符)能让三层
|
|
52
|
-
* 塞进上千条候选,而 core 的 `redeemRuleBatch` 是**逐候选**跑一遍「读店 + CAS 写」的串行循环 —— 一个
|
|
53
|
-
* 已鉴权的请求就能把连接池占满、把响应撑到兆字节。全局限流器默认是关的,拦不住它(一次准入就够了)。
|
|
54
|
-
*
|
|
55
|
-
* 帽数的是 **core 已经解析好的候选**(不是我们自己再数一遍 JSON):零重复解析,而且数的正好是那条串行
|
|
56
|
-
* 循环的**真实**长度。位置在**铸票之前** —— 超限就没有票,那条昂贵的循环一次都跑不起来(代价只剩一条
|
|
57
|
-
* 已铸的 pending 记录,与任何一次没去兑付的 prepare 留下的残余同形)。
|
|
58
|
-
* 200 的取值:一份人手写的 CC settings 极少超过几十条;200 给了一个数量级的余量,又把最坏工作量钉在
|
|
59
|
-
* 「几百次 SQL」而不是「几万次」。
|
|
60
|
-
*/
|
|
61
9
|
export const MAX_IMPORT_CANDIDATES = 200;
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
63
|
-
// #203 §2 —— 撤销面(`GET /v1/rules` + `DELETE /v1/rules`)的车道半场
|
|
64
|
-
// ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
65
|
-
/**
|
|
66
|
-
* scope 的 **wire 形**:`global` | `project:<root>` 的判别式串(design/203 v2 §6 F5「显式成文」)。
|
|
67
|
-
*
|
|
68
|
-
* 🔴 为什么是一个串而不是把 core 的 `{kind, root}` 对象直接上 wire:这一份表示要同时当**列举结果里的
|
|
69
|
-
* 一列**、**查询参数**、**删除请求体的一个字段**和**分页游标的一部分**。四处若各用各的形,「删掉我刚才
|
|
70
|
-
* 列出来的那一行」就变成一次跨形状翻译 —— 而那正是最容易漂的地方。判别式串在四处逐字相同,而且
|
|
71
|
-
* `project:` 前缀让「这条规则是项目内的」在一眼扫日志时就成立。
|
|
72
|
-
*
|
|
73
|
-
* `root` 里可以含冒号(路径合法),所以只切**第一个**冒号 —— 用 `split(":")` 取 [1] 会把
|
|
74
|
-
* `project:/a:b` 悄悄截成 `/a`,那是一条**放宽面**上的静默改写(更短的 root 覆盖更多 cwd)。
|
|
75
|
-
*/
|
|
76
10
|
export function serializeRuleScope(scope) {
|
|
77
11
|
return scope.kind === "global" ? "global" : `project:${scope.root}`;
|
|
78
12
|
}
|
|
79
|
-
/**
|
|
80
|
-
* 🔴 判别式串的**上限,由铸侧的尺派生**(A-054.24,2026-08-19 合并重扫 confirmed —— 修的是「两个数字
|
|
81
|
-
* 各挑各的」这件事本身,不是把某个数字调大一点)。
|
|
82
|
-
*
|
|
83
|
-
* 病:铸侧 `cardRuleScopeRoot` 对 root 长度零把关(入口尺 = `isValidCwd` 的 {@link MAX_CWD_CHARS}),
|
|
84
|
-
* core 与两个店都只校验 `root.min(1)` ⇒ 写面全程无阻;而 `DELETE /v1/rules` 的体 schema 此前独立写死
|
|
85
|
-
* `scope: max(1088)`,且 safeParse 排在 `parseRuleScope`/权限判据**之前** ⇒ 超限 root 的规则
|
|
86
|
-
* `GET /v1/rules` **列得出**、按同一串 DELETE **恒 400**。全仓只有一个 `lane.removeRule` 调用点(直删
|
|
87
|
-
* 店行是无墓碑硬删、会被 sync 的 join 复活),所以那是**一条已列出的常驻放行规则在受支持接口上不可撤**;
|
|
88
|
-
* 唯一的钝器补偿是 `PERMISSION_RULES_ENABLED=false`(关掉全部规则,两口自身随之 501)。
|
|
89
|
-
*
|
|
90
|
-
* ⚠️ 可达性在 A-054.F-A 之后**变严重**:那批把铸侧出口从 `realpath` 改成 `path.resolve`,而 realpath
|
|
91
|
-
* 要求目录真实存在(macOS PATH_MAX=1024 ⇒ 宿主上 root 恒 ≤1023,结构性免疫);`path.resolve` 纯词法、
|
|
92
|
-
* 不碰盘 ⇒ **任何宿主上** 1081..4096 字符的注册 cwd 都能铸出这样一条规则。
|
|
93
|
-
*
|
|
94
|
-
* 取值 = `"project:"` 前缀 + 铸侧 root 的入口尺。`path.resolve` 对「已绝对、无 `..` 段」的路径不加长
|
|
95
|
-
* (`isValidCwd` 两条都要求),故 {@link MAX_CWD_CHARS} 是 root 的真上界。**两侧从此同源**:谁改铸侧
|
|
96
|
-
* 的尺,撤侧自动跟。
|
|
97
|
-
*/
|
|
98
13
|
export const MAX_RULE_SCOPE_CHARS = "project:".length + MAX_CWD_CHARS;
|
|
99
|
-
/** {@link serializeRuleScope} 的逆。读不出形 ⇒ `undefined`(调用方 400,绝不猜一个 global 出来 ——
|
|
100
|
-
* 猜 global 会把一次「删项目内规则」的请求变成一次删不掉任何东西的 no-op,或者更糟)。 */
|
|
101
14
|
export function parseRuleScope(text) {
|
|
102
15
|
if (text === "global")
|
|
103
16
|
return { kind: "global" };
|
|
104
17
|
if (!text.startsWith("project:"))
|
|
105
18
|
return undefined;
|
|
106
19
|
const root = text.slice("project:".length);
|
|
107
|
-
// 空 root 硬拒:`{kind:"project", root:""}` 在 core 的 `pathWithinRoot` 下**包含每一个 cwd**
|
|
108
|
-
// (空前缀包含一切)——一条项目内规则就此悄悄升成全局规则(cc-import 那侧同判据,同一条 codex 发现)。
|
|
109
20
|
return root === "" ? undefined : { kind: "project", root };
|
|
110
21
|
}
|
|
111
|
-
/**
|
|
112
|
-
* 桶引用 → 具体的规则店。
|
|
113
|
-
*
|
|
114
|
-
* 🔴 **穷举判别,未知形一律抛**(CLAUDE.md 词表闭集律 + codex 对抗复审 R3-[medium],验真后改)。
|
|
115
|
-
* 第一稿写的是 `kind === "principal" ? 甲 : 乙` 的二分:TS 那侧是穷举的,**运行期不是** —— 本车道是
|
|
116
|
-
* 一个**导出**的束,调用方可以是任何一层(HTTP 路由今天只递裸串,但这条口正是为了将来递结构形才开的)。
|
|
117
|
-
* 于是 `{kind:"principle"}`(拼错)或 `{}` 会静默落进 local-owner 那支 = 一次**跨桶读**;更坏的是
|
|
118
|
-
* 同一个坏值递给 core 的 `removePersistedRule` 时它按 `kind === "local-owner"` 反着判,落到
|
|
119
|
-
* `forPrincipal(undefined)`(空店)—— 列举和撤销**指向两只不同的桶**,而两边都不报错。
|
|
120
|
-
* 身份面上「不认识的形」只有一个正确答案:响亮拒。
|
|
121
|
-
*
|
|
122
|
-
* 🔴 local-owner 桶而 provider 没有 `forLocalOwner` ⇒ 同样**抛**,不静默回落到别的桶。回落到
|
|
123
|
-
* `forPrincipal(…)` 会把「读/删身份缺席桶」变成「读/删某个 principal 的桶」——那是一次跨桶的读或写,
|
|
124
|
-
* 是本车道最不该出的错。报文与 core 的 `removePersistedRule` 同支同措辞(那侧回 `failed` 带
|
|
125
|
-
* `forLocalOwner` 字样),两侧一起读日志时对得上。
|
|
126
|
-
*/
|
|
127
22
|
function storeForBucket(provider, ref) {
|
|
128
|
-
// 裸串 = principal 简写(core `removePersistedRule` 的同款归一,两侧对同一个输入判同一只桶)。
|
|
129
23
|
if (typeof ref === "string")
|
|
130
24
|
return provider.forPrincipal(ref);
|
|
131
|
-
// `ref.kind` 在类型上是闭集;`switch` 的 default 是**运行期**的那道门(不是死码)。
|
|
132
25
|
switch (ref.kind) {
|
|
133
26
|
case "principal":
|
|
134
27
|
return provider.forPrincipal(ref.principal);
|
|
@@ -138,44 +31,52 @@ function storeForBucket(provider, ref) {
|
|
|
138
31
|
}
|
|
139
32
|
return provider.forLocalOwner();
|
|
140
33
|
default: {
|
|
141
|
-
// core 给 `RuleOwner` 加员时这里是**编译**红(`never` 赋值失败),不是静默继承某一支。
|
|
142
34
|
const unknown = ref;
|
|
143
35
|
throw new Error(`unrecognised permission-rule bucket reference (${JSON.stringify(unknown)}) — refusing to guess which bucket it names`);
|
|
144
36
|
}
|
|
145
37
|
}
|
|
146
38
|
}
|
|
39
|
+
function isSupportedRuleMatch(match) {
|
|
40
|
+
switch (match) {
|
|
41
|
+
case "exact":
|
|
42
|
+
case "prefix":
|
|
43
|
+
return true;
|
|
44
|
+
default: {
|
|
45
|
+
const unknown = match;
|
|
46
|
+
void unknown;
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const unknownReasonLogger = createLogger();
|
|
52
|
+
function confirmRefusalWord(reason) {
|
|
53
|
+
switch (reason) {
|
|
54
|
+
case "edit_disabled":
|
|
55
|
+
case "edit_binding_mismatch":
|
|
56
|
+
return "edit-unsupported";
|
|
57
|
+
case "edit_rejected":
|
|
58
|
+
return "edit-rejected";
|
|
59
|
+
case "record_not_found":
|
|
60
|
+
case "selection_missing":
|
|
61
|
+
case "selection_invalid":
|
|
62
|
+
case "selection_mismatch":
|
|
63
|
+
case "batch_takes_no_selection":
|
|
64
|
+
case "not_pending":
|
|
65
|
+
case "conflict":
|
|
66
|
+
return "confirm-refused";
|
|
67
|
+
default: {
|
|
68
|
+
const unknown = reason;
|
|
69
|
+
unknownReasonLogger.warn("confirmRefusalWord: unknown ConfirmRefusalReason from core (dist/core version skew?) — folded to confirm-refused", { reason: String(unknown) });
|
|
70
|
+
return "confirm-refused";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
147
74
|
export function createRuleConsentLane(stores, opts) {
|
|
148
|
-
const deps = { provider: stores.provider, approvals: stores.approvals };
|
|
75
|
+
const deps = { provider: stores.provider, approvals: stores.approvals, cardEdits: true };
|
|
149
76
|
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
150
77
|
const newTicketId = opts?.newTicketId ?? (() => randomUUID());
|
|
151
78
|
const monotonic = opts?.monotonic ?? (() => performance.now());
|
|
152
|
-
/**
|
|
153
|
-
* [4232]③/[4235]③ —— **本进程铸出的票**的单调锚台账。
|
|
154
|
-
*
|
|
155
|
-
* 病灶(test 真机两条读数):`expiresAtMs = 铸票时刻的墙钟 + TTL`。墙钟在铸票与兑付之间被拨动 ⇒
|
|
156
|
-
* ·(前拨)真实只过 3 秒的票被判过期;
|
|
157
|
-
* ·(铸票时钟本身慢了 11 分钟,随后被 NTP 纠正)票**出生即过期** —— `expiresAtMs` 换算出的时刻
|
|
158
|
-
* 比这张票真实铸出的时刻还早一分钟。
|
|
159
|
-
* core [4235]③ 的判词「从票据内部无解」是对的:`mintedAt` 与 `expiresAt` 同源同错,行里没有第二个基准。
|
|
160
|
-
* 但**铸票与兑付发生在同一个进程**时,进程自己有第二个基准 —— 单调钟。于是判定钟改成
|
|
161
|
-
* `判定 now = 铸票时刻的墙钟 + (单调钟现在 − 单调钟铸票时)`
|
|
162
|
-
* 即「签发与判定同源单调锚」。它**给不了票额外寿命**:投影严格按单调钟前进,真实走满 TTL 就必然越过
|
|
163
|
-
* `expiresAtMs`(负控钉在 db-integration 里)。记账列(`consumed_at_ms`)一律仍是真墙钟。
|
|
164
|
-
*
|
|
165
|
-
* 🔴 **射程边界 —— 未闭合的残余,如实登记**(codex 对抗复审 R1 [high] 四,验真后**接受**):本锚只活在
|
|
166
|
-
* 当前进程的 Map 里。prepare 与 redeem 被负载均衡分到**不同副本**时(不需要故障,常规轮询就够),
|
|
167
|
-
* `decisionClock` 返回 undefined、store 退回该副本的墙钟 —— 那台副本若有钟偏,有效票**仍会**出生即过期。
|
|
168
|
-
* 也就是说本修把缺陷从「所有兑付」缩小到「跨副本兑付」,**没有消灭它**;正确性因此隐含依赖了一条
|
|
169
|
-
* 没人声明过的会话粘滞。不要把这条注读成「已解」。
|
|
170
|
-
*
|
|
171
|
-
* 收口方向(下一批,不在本小件批射程):把判定权威换成**两副本共享的那口钟 —— 数据库自己的钟**:
|
|
172
|
-
* 铸票时 `expires_at_ms = <db now> + ttl`、兑付时 `WHERE expires_at_ms > <db now>`
|
|
173
|
-
* (MySQL `CAST(UNIX_TIMESTAMP(NOW(3))*1000 AS SIGNED)` / PG `(EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint`)。
|
|
174
|
-
* 那一形对**所有**副本同时成立,还能把本台账整个删掉。记账列仍走进程墙钟(两钟分家的语义不变)。
|
|
175
|
-
*/
|
|
176
79
|
const anchors = new Map();
|
|
177
|
-
/** 台账只服务「本进程铸、本进程兑」的窗,所以按单调钟自清:超过一个 TTL 的锚已经不可能改变任何判定。
|
|
178
|
-
* 另加一个硬帽,免得一条只 prepare 不 redeem 的路径把 Map 撑大(每条锚两个 number,帽住即够)。 */
|
|
179
80
|
const ANCHOR_CAP = 4096;
|
|
180
81
|
const pruneAnchors = (nowMono) => {
|
|
181
82
|
for (const [id, a] of anchors) {
|
|
@@ -184,7 +85,6 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
184
85
|
}
|
|
185
86
|
if (anchors.size <= ANCHOR_CAP)
|
|
186
87
|
return;
|
|
187
|
-
// 仍然超帽 ⇒ 丢最老的(Map 迭代序 = 插入序 = 铸票序)。丢掉只是退回墙钟判定,不产生错判方向。
|
|
188
88
|
const excess = anchors.size - ANCHOR_CAP;
|
|
189
89
|
let dropped = 0;
|
|
190
90
|
for (const id of anchors.keys()) {
|
|
@@ -193,22 +93,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
193
93
|
anchors.delete(id);
|
|
194
94
|
}
|
|
195
95
|
};
|
|
196
|
-
/**
|
|
197
|
-
* 本进程铸的票 ⇒ 单调投影;别处铸的 ⇒ `undefined`(store 退回墙钟,既有行为逐字不变)。
|
|
198
|
-
*
|
|
199
|
-
* 🔴 `Math.ceil` 不是装饰:①`performance.now()` 是**小数**毫秒,而 `expires_at_ms` 是 BIGINT 列 ——
|
|
200
|
-
* 小数直接绑进 PG 会 `invalid input syntax for type bigint`(真双库当场逮到,mysql2 那侧则会静默接受,
|
|
201
|
-
* 正是「一个方言绿一个方言红」的经典形);②取整方向必须**保守**:判定式是 `expires_at_ms > 判定 now`,
|
|
202
|
-
* 判定 now 取小 = 票更晚过期,所以向上取整才不会白送半毫秒寿命。
|
|
203
|
-
*/
|
|
204
96
|
const decisionClock = (ticketId) => {
|
|
205
97
|
const a = anchors.get(ticketId);
|
|
206
98
|
return a === undefined ? undefined : { decisionNowMs: Math.ceil(a.mintedAtWall + (monotonic() - a.mintedAtMono)) };
|
|
207
99
|
};
|
|
208
100
|
return {
|
|
209
101
|
async persistCardRule(input) {
|
|
210
|
-
// 🔴 候选**由引擎铸**(core `prepareCardApproval` 不收调用方候选;那条入口上「写自己的规则」
|
|
211
|
-
// 结构上不可拼写)。本层只把「人选了哪一个」翻译成下标。
|
|
212
102
|
const prepared = await prepareCardApproval({
|
|
213
103
|
principal: input.principal,
|
|
214
104
|
toolName: input.toolName,
|
|
@@ -221,22 +111,50 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
221
111
|
if (prepared === undefined) {
|
|
222
112
|
return { ok: false, reason: "no-candidates", detail: "the rule lane cannot speak for this command (compound / redirection / unsupported tool)" };
|
|
223
113
|
}
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
if (
|
|
227
|
-
|
|
114
|
+
const redemption = input.redemption;
|
|
115
|
+
let ticket;
|
|
116
|
+
if (redemption.kind === "offered") {
|
|
117
|
+
const index = prepared.candidates.findIndex((c) => c.rule === redemption.ruleText);
|
|
118
|
+
const offered = index < 0 ? undefined : prepared.tickets[index];
|
|
119
|
+
if (index < 0 || offered === undefined) {
|
|
120
|
+
return { ok: false, reason: "unknown-candidate", detail: "the chosen rule text is not one the engine minted for this command" };
|
|
121
|
+
}
|
|
122
|
+
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedCandidate: index, deps });
|
|
123
|
+
if (!confirmed.ok)
|
|
124
|
+
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
125
|
+
ticket = offered;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (input.boundInputHash === undefined) {
|
|
129
|
+
return { ok: false, reason: "edit-unsupported", detail: "this approval card carries no bound-input digest — an edited rule has nothing to bind to" };
|
|
130
|
+
}
|
|
131
|
+
const confirmed = await confirmRuleApproval({
|
|
132
|
+
approvalId: prepared.approvalId,
|
|
133
|
+
principal: input.principal,
|
|
134
|
+
editedCandidate: { text: redemption.text, boundInputHash: input.boundInputHash },
|
|
135
|
+
deps,
|
|
136
|
+
});
|
|
137
|
+
if (!confirmed.ok)
|
|
138
|
+
return { ok: false, reason: confirmRefusalWord(confirmed.reason), detail: confirmed.detail?.message ?? confirmed.reason };
|
|
139
|
+
const minted = confirmed.mintedCandidate;
|
|
140
|
+
if (minted === undefined) {
|
|
141
|
+
return { ok: false, reason: "confirm-refused", detail: "the edited confirmation landed without a minted candidate — nothing to redeem" };
|
|
142
|
+
}
|
|
143
|
+
const canonical = parseAllowRuleText(minted.rule);
|
|
144
|
+
if ("reject" in canonical) {
|
|
145
|
+
return { ok: false, reason: "edit-rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
|
|
146
|
+
}
|
|
147
|
+
if (!isSupportedRuleMatch(canonical.rule.match)) {
|
|
148
|
+
return { ok: false, reason: "edit-rejected", detail: `the edited rule resolves to an unsupported match form (${String(canonical.rule.match)})` };
|
|
149
|
+
}
|
|
150
|
+
ticket = minted.ticket;
|
|
228
151
|
}
|
|
229
|
-
const confirmed = await confirmRuleApproval({ approvalId: prepared.approvalId, principal: input.principal, selectedCandidate: index, deps });
|
|
230
|
-
if (!confirmed.ok)
|
|
231
|
-
return { ok: false, reason: "confirm-refused", detail: confirmed.reason };
|
|
232
152
|
const redeemed = await redeemRuleTicket({ ticket, principal: input.principal, deps });
|
|
233
153
|
if (redeemed.status !== "redeemed")
|
|
234
154
|
return { ok: false, reason: "redeem-refused", detail: redeemed.reason };
|
|
235
155
|
return { ok: true, rule: redeemed.rule, rev: redeemed.rev, alreadyRedeemed: redeemed.alreadyRedeemed };
|
|
236
156
|
},
|
|
237
157
|
async prepareImport(principal, layers) {
|
|
238
|
-
// core 的 `CcImportLayer.readFile` 是一条**注入的**读口 —— 云端部署没有用户的文件系统,内容由
|
|
239
|
-
// 调用方交上来(这是本仓的部署形,不是绕过:读的仍然只是 allow 桶,拒/问桶是收紧方向、另有通道)。
|
|
240
158
|
const ccLayers = layers.map((l) => ({
|
|
241
159
|
layer: l.layer,
|
|
242
160
|
path: l.path,
|
|
@@ -244,11 +162,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
244
162
|
readFile: async () => l.content,
|
|
245
163
|
}));
|
|
246
164
|
const { preview, approvalId } = await prepareCcImport({ layers: ccLayers, principal, deps });
|
|
247
|
-
// 帽在**铸票之前**(见 {@link MAX_IMPORT_CANDIDATES}):没有票 ⇒ 那条逐候选的串行 SQL 循环跑不起来。
|
|
248
165
|
if (preview.candidates.length > MAX_IMPORT_CANDIDATES) {
|
|
249
|
-
// core 在**返回预览之前**就把 pending 记录落了盘 ⇒ 这次不做的导入会留一条永远没人要的行。
|
|
250
|
-
// 收掉它(codex round8 [medium]:被拒的 prepare 不该在共享库里长东西)。best-effort:收不掉
|
|
251
|
-
// 也只是留一条 pending 孤儿,不影响任何判决 —— 但**绝不**因此把 413 变成 500。
|
|
252
166
|
try {
|
|
253
167
|
await stores.approvals.discardPendingRecord?.(approvalId);
|
|
254
168
|
}
|
|
@@ -257,8 +171,6 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
257
171
|
}
|
|
258
172
|
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
259
173
|
}
|
|
260
|
-
// [4232]③:铸票**当拍**同时钉下两只钟 —— 墙钟锚取 `minted.expiresAtMs - ttlMs`(= store 自己用的
|
|
261
|
-
// 那个铸票时刻,不另取一次 `Date.now()`:两处各取一次会让锚与行差出一跳的量),单调锚取本进程单调钟。
|
|
262
174
|
const mintedAtMono = monotonic();
|
|
263
175
|
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
264
176
|
pruneAnchors(mintedAtMono);
|
|
@@ -266,65 +178,33 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
266
178
|
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
267
179
|
},
|
|
268
180
|
async redeemImport(principal, ticket) {
|
|
269
|
-
// ①②③ 一次原子**认领**同时回答(principal 绑定 / 未过期 / 未认领);四类拒绝在 wire 面同形。
|
|
270
|
-
// [4232]③:判定钟走本进程的单调锚(本进程没铸过这张票 ⇒ undefined ⇒ store 退回墙钟)。
|
|
271
181
|
const clock = decisionClock(ticket);
|
|
272
182
|
const consumed = await stores.tickets.consume(ticket, principal, clock);
|
|
273
183
|
if (!consumed.ok)
|
|
274
184
|
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
275
|
-
// 🔴 codex round1 [high] 一(验真后修):认领**之后**的每一步都可能失败,而旧形把认领当终局 ⇒
|
|
276
|
-
// 一次 store 抖动就永久烧掉这张票,客户端拿到与「票不存在」同形的 404,重试无门。
|
|
277
|
-
// 处置:裁定性拒绝(载荷被篡改 / 记录不存在)**留着**认领(重试不改判);不确定/瞬时失败**放回**
|
|
278
|
-
// 认领并标 `retryable`。放回不打开双兑付的门(认领仍是单持有者),兑付腿本身按 core 的设计幂等。
|
|
279
|
-
// 🔴 codex 交叉复审 round3 [high] 一(验真后修):返回值是**「认领确实放回去了吗」**,不是 void。
|
|
280
|
-
// 旧形无条件回 `retryable: true`,而释放本身也可能失败(同一场 store 故障往往两跳一起挂)——
|
|
281
|
-
// 那就变成对客户端**撒谎**:它拿着一张其实已经烧掉的票去重试,只会收到 404,人的确认永久丢失。
|
|
282
|
-
// 判据必须是「释放**确认**成功」;没确认就不许承诺重试(如实按不可重试报,四类同形 404)。
|
|
283
185
|
const releaseClaim = async () => {
|
|
284
186
|
try {
|
|
285
|
-
// 🔴 判定钟**当场重取**,不复用 `consume` 那次的读数(F-B,2026-08-17 合并码扫描)。同锚(同一张票的
|
|
286
|
-
// `mintedAtWall`/`mintedAtMono`)、同尺(同一只单调钟),但**读数必须是现在**:
|
|
287
|
-
// · `consume` 问的是向后看的问题(此刻窗还开着吗);`release` 问的是向前看的问题(这张票能不能
|
|
288
|
-
// 活过我即将通告的 Retry-After)。第二问只有在**释放这一刻**问才有意义。
|
|
289
|
-
// · capture 与这里之间隔着 consume 两往返 + approvals.get + confirmRuleApproval 的 CAS/OCC +
|
|
290
|
-
// 最多 MAX_IMPORT_CANDIDATES 条候选的串行兑付循环,而本闭包**只在店退化时**才被走到 ——
|
|
291
|
-
// 缺陷发作时这段恰恰最长(秒级 vs 2s 的窗),旧注那句「重取一次投影只会多走几微秒」说的是
|
|
292
|
-
// 重取的**成本**,却被当成了流逝量的上界,那是两件事。
|
|
293
|
-
// · 单调钟不回退 ⇒ 冻结读数恒 ≤ 新读数 ⇒ 误差全部落在 **permissive** 一侧,而 store 侧 `release`
|
|
294
|
-
// 的头注把这道门定义成保守门(「承诺一次必然兑现不了的重试比不承诺更坏」)。
|
|
295
|
-
// 本进程没铸过这张票时两处都回 undefined(store 退回墙钟),语义逐字不变。
|
|
296
187
|
return await stores.tickets.release(ticket, principal, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
|
|
297
188
|
}
|
|
298
189
|
catch (err) {
|
|
299
|
-
// 放不回去 = 回到「认领即烧票」的旧形(属主这一轮不能重试,票随 TTL 消失)。**不**把一次
|
|
300
|
-
// 已经失败的请求再变成一次抛错;但也**不**静默 —— 登记的 F 类兜底,理由逐字见 tag 词表。
|
|
301
190
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
302
191
|
return false;
|
|
303
192
|
}
|
|
304
193
|
};
|
|
305
|
-
/** 「不确定」那一支的统一收口:**释放确认成功**才承诺可重试。 */
|
|
306
194
|
const indeterminate = async (detail) => {
|
|
307
|
-
// 放回之后这张票至少要能撑过我们通告的等待窗 —— 撑不过就不承诺重试(见 store 侧 `release` 头注)。
|
|
308
195
|
const released = await releaseClaim();
|
|
309
196
|
return { ok: false, reason: "record-unusable", detail, ...(released ? { retryable: true } : {}) };
|
|
310
197
|
};
|
|
311
198
|
try {
|
|
312
|
-
// ④ 载荷绑定:记录上的候选集必须与铸票时刻的快照同摘要。
|
|
313
199
|
const record = await stores.approvals.get(consumed.approvalId);
|
|
314
200
|
if (record === undefined)
|
|
315
201
|
return { ok: false, reason: "record-unusable", detail: "the approval record behind this ticket is gone" };
|
|
316
202
|
if (buildRulePayloadHash(record.candidates) !== consumed.payloadHash) {
|
|
317
203
|
return { ok: false, reason: "payload-mismatch", detail: "the approval record's candidates changed after the ticket was minted" };
|
|
318
204
|
}
|
|
319
|
-
// 🔴 确认是**一次性状态跃迁**,不是每次兑付都要过的门:重试一张**放回过认领**的票时,记录
|
|
320
|
-
// 早已不是 `pending` 了(core 的崩溃序:`redeemRuleTicket` 在调用写面**之前**就把记录推到
|
|
321
|
-
// `redeemed`,所以哪怕一个候选都没落地,状态也已经变了)。对已确认的记录再调一次
|
|
322
|
-
// `confirmRuleApproval` 会拿到 `not_pending` —— 把它当拒绝就等于「放回了认领却永远兑不动」,
|
|
323
|
-
// round1/round3/round5 那一串修复到此全部白做。判据用记录**自己的状态**,不是猜错误串。
|
|
324
205
|
if (record.state === "pending") {
|
|
325
206
|
const confirmed = await confirmRuleApproval({ approvalId: consumed.approvalId, principal, deps });
|
|
326
207
|
if (!confirmed.ok) {
|
|
327
|
-
// `conflict` = CAS 输给了一个并发写者(瞬时);其余是裁定(记录不存在/状态不对)。
|
|
328
208
|
if (confirmed.reason === "conflict")
|
|
329
209
|
return await indeterminate(confirmed.reason);
|
|
330
210
|
return { ok: false, reason: "record-unusable", detail: confirmed.reason };
|
|
@@ -332,19 +212,8 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
332
212
|
}
|
|
333
213
|
const result = await redeemRuleBatch({ approvalId: consumed.approvalId, principal, deps });
|
|
334
214
|
if ("status" in result) {
|
|
335
|
-
// core 的 refused 里既有裁定也有瞬时(OCC 用尽 / 店读不出来)——分不清就按**可重试**放回:
|
|
336
|
-
// 兑付是幂等的,多放一次认领最坏是多跑一遍同样的重放;烧掉一张真票是不可逆的。
|
|
337
215
|
return await indeterminate(result.reason);
|
|
338
216
|
}
|
|
339
|
-
// 🔴 codex 交叉复审 round5 [high] 二(验真后修):core 的 `redeemRuleBatch` 把**逐候选**的失败
|
|
340
|
-
// 记在 `skippedAtRedeem` 里,顶层照样返回一个 ImportResult —— 只判顶层 `status` 会把「一半没落地」
|
|
341
|
-
// 当成功回 200,而认领已经烧掉,那一半再也补不回来。
|
|
342
|
-
// 为什么这条车道可以把「非空 skippedAtRedeem」一律当**不确定**:候选在 `prepareCcImport` 那一步
|
|
343
|
-
// 就已经全过了同一个验证器(过不了的进 `preview.skipped`,压根不进记录),所以兑付期的逐候选拒绝
|
|
344
|
-
// 实际只剩「店读不出/写不进 / OCC 用尽 / 隔离栅栏」这类**瞬时或异常**成因。重放是幂等的
|
|
345
|
-
// (记录里存着每个候选已铸的 dot),所以放回认领让人重试,代价上界只是再跑一遍同样的重放。
|
|
346
|
-
// 残留(如实登记):真出现一条**永久**被拒的候选时,客户端会一直拿到 503 直到票到期(10 分钟)——
|
|
347
|
-
// 有界、且不破坏任何已落地的规则;要更好得让 core 把逐候选拒绝分成瞬时/裁定两类,已列后续件。
|
|
348
217
|
if (result.skippedAtRedeem.length > 0) {
|
|
349
218
|
return await indeterminate(`partial import: ${result.skippedAtRedeem.length} candidate(s) did not land (${result.skippedAtRedeem.map((x) => x.reason).join("; ")})`);
|
|
350
219
|
}
|
|
@@ -355,21 +224,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
355
224
|
}
|
|
356
225
|
},
|
|
357
226
|
async listRules(bucket) {
|
|
358
|
-
// `list()` 已经把墓碑折算掉(`applyTombstones`)⇒ 这里拿到的就是**活着**的规则,治理面看到的
|
|
359
|
-
// 与引擎放行时看到的是同一份。空桶 = 空表 + rev 0,不是错误(「这个人没有规则」是合法状态)。
|
|
360
227
|
const stored = await storeForBucket(stores.provider, bucket).list();
|
|
361
228
|
const rows = stored.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds }));
|
|
362
229
|
rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
363
230
|
return { rows, rev: stored.rev };
|
|
364
231
|
},
|
|
365
232
|
async removeRule(input) {
|
|
366
|
-
// 🔴 一行也不自己写:OCC 重试、墓碑铸造、`stillLive` 的读回全在 core 那条原语里,而它的语义
|
|
367
|
-
// (add-wins、observed-remove、失败必须由读回确认「什么都没写」)是 core 的单一属主。
|
|
368
|
-
// 桶引用**原样**递下去(裸串走 core 的 principal 简写臂,结构形走 owner 臂)——我方不在这里
|
|
369
|
-
// 归一成 owner 对象:core 5.24.0 的 `removePersistedRule` 自己就做那次归一(`typeof === "string"`),
|
|
370
|
-
// 多归一一次等于把上游的简写语义抄一份到下游,抄件会漂。local-owner 而 provider 无 `forLocalOwner`
|
|
371
|
-
// 的那支也归 core:它回的是 `failed` + 报文含 `forLocalOwner`(C13 判据),比抛错更适合这条
|
|
372
|
-
// 三态返回的口。
|
|
373
233
|
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
374
234
|
},
|
|
375
235
|
};
|