berry-agent 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +96 -0
- package/dist/.api-emit.stamp +1 -0
- package/dist/.build-meta.json +6 -0
- package/dist/agent/events.js +1 -0
- package/dist/agent/index.js +2 -0
- package/dist/agent/loop.js +139 -0
- package/dist/agent/queue.js +119 -0
- package/dist/agent/stream.js +62 -0
- package/dist/agent/tools-batch.js +221 -0
- package/dist/agent/types.js +8 -0
- package/dist/api/berry-agent-llm.d.ts +18 -0
- package/dist/api/berry-agent.d.ts +9 -0
- package/dist/api/surface.json +1632 -0
- package/dist/api/tsconfig.paths.json +12 -0
- package/dist/api/typebox-compile.d.ts +2 -0
- package/dist/api/typebox-value.d.ts +2 -0
- package/dist/api/typebox.d.ts +2 -0
- package/dist/browser/cdp.js +256 -0
- package/dist/browser/codes.js +31 -0
- package/dist/browser/discover.js +107 -0
- package/dist/browser/engine.js +169 -0
- package/dist/browser/index.js +17 -0
- package/dist/browser/install.js +342 -0
- package/dist/browser/page.js +321 -0
- package/dist/browser/service.js +158 -0
- package/dist/browser/tools.js +198 -0
- package/dist/browser/types.js +81 -0
- package/dist/channels/ask-queue.js +82 -0
- package/dist/channels/commands.js +129 -0
- package/dist/channels/engine/cell.js +143 -0
- package/dist/channels/engine/diff.js +143 -0
- package/dist/channels/engine/engine.js +416 -0
- package/dist/channels/engine/index.js +8 -0
- package/dist/channels/engine/input-keys.js +86 -0
- package/dist/channels/engine/input.js +596 -0
- package/dist/channels/engine/memory-io.js +71 -0
- package/dist/channels/engine/process-io.js +38 -0
- package/dist/channels/engine/types.js +17 -0
- package/dist/channels/engine/width.js +122 -0
- package/dist/channels/index.js +15 -0
- package/dist/channels/registry.js +56 -0
- package/dist/channels/sdk/admit.js +29 -0
- package/dist/channels/sdk/backend.js +102 -0
- package/dist/channels/sdk/cursor.js +56 -0
- package/dist/channels/sdk/index.js +20 -0
- package/dist/channels/sdk/jsonl.js +208 -0
- package/dist/channels/sdk/protocol.js +24 -0
- package/dist/channels/sdk/schema.js +91 -0
- package/dist/channels/sdk/wire-core.js +446 -0
- package/dist/channels/service.js +169 -0
- package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
- package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
- package/dist/channels/tui/autocomplete/popup.js +148 -0
- package/dist/channels/tui/autocomplete/provider.js +7 -0
- package/dist/channels/tui/autocomplete/token.js +68 -0
- package/dist/channels/tui/backend/ansi-rows.js +211 -0
- package/dist/channels/tui/backend/main-screen.js +207 -0
- package/dist/channels/tui/backend/osc.js +91 -0
- package/dist/channels/tui/backend/transcript.js +230 -0
- package/dist/channels/tui/backend/tui-backend.js +915 -0
- package/dist/channels/tui/editor/editor-model.js +543 -0
- package/dist/channels/tui/editor/editor-view.js +140 -0
- package/dist/channels/tui/editor/editor.js +275 -0
- package/dist/channels/tui/editor/undo-stack.js +35 -0
- package/dist/channels/tui/editor/visual-lines.js +126 -0
- package/dist/channels/tui/editor/word-nav.js +106 -0
- package/dist/channels/tui/history/history-viewer.js +403 -0
- package/dist/channels/tui/index.js +3 -0
- package/dist/channels/tui/layout.js +167 -0
- package/dist/channels/tui/markdown/blocks.js +111 -0
- package/dist/channels/tui/markdown/inline.js +75 -0
- package/dist/channels/tui/markdown/markdown.js +182 -0
- package/dist/channels/tui/memory/memory-viewer.js +540 -0
- package/dist/channels/tui/overlay/alt-screen.js +105 -0
- package/dist/channels/tui/overlay/overlay.js +57 -0
- package/dist/channels/tui/overlay/select-confirm.js +156 -0
- package/dist/channels/tui/panels/todo-panel.js +54 -0
- package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
- package/dist/channels/tui/scroll/scroll-view.js +254 -0
- package/dist/channels/tui/status/status-line.js +76 -0
- package/dist/channels/tui/text.js +38 -0
- package/dist/channels/tui/theme.js +41 -0
- package/dist/channels/types.js +11 -0
- package/dist/channels/ui-core.js +209 -0
- package/dist/checkpoint/capture.js +45 -0
- package/dist/checkpoint/codes.js +33 -0
- package/dist/checkpoint/command.js +82 -0
- package/dist/checkpoint/gate.js +48 -0
- package/dist/checkpoint/index.js +20 -0
- package/dist/checkpoint/restore.js +172 -0
- package/dist/checkpoint/store.js +247 -0
- package/dist/checkpoint/types.js +4 -0
- package/dist/checkpoint/walk.js +146 -0
- package/dist/compaction/ccr-tools.js +112 -0
- package/dist/compaction/ccr.js +84 -0
- package/dist/compaction/codes.js +21 -0
- package/dist/compaction/index.js +18 -0
- package/dist/compaction/policy.js +200 -0
- package/dist/compaction/service.js +422 -0
- package/dist/compaction/slots.js +121 -0
- package/dist/compaction/types.js +10 -0
- package/dist/context/codes.js +40 -0
- package/dist/context/events.js +160 -0
- package/dist/context/index.js +13 -0
- package/dist/context/logger.js +136 -0
- package/dist/context/scope.js +182 -0
- package/dist/context/workspace.js +104 -0
- package/dist/contracts/agent-events.d.ts +73 -0
- package/dist/contracts/agent-events.js +16 -0
- package/dist/contracts/api.d.ts +249 -0
- package/dist/contracts/api.js +299 -0
- package/dist/contracts/approval.d.ts +37 -0
- package/dist/contracts/approval.js +9 -0
- package/dist/contracts/env-ref.d.ts +41 -0
- package/dist/contracts/env-ref.js +32 -0
- package/dist/contracts/errors.d.ts +56 -0
- package/dist/contracts/errors.js +266 -0
- package/dist/contracts/events.d.ts +70 -0
- package/dist/contracts/events.js +329 -0
- package/dist/contracts/index.d.ts +32 -0
- package/dist/contracts/index.js +26 -0
- package/dist/contracts/llm.d.ts +275 -0
- package/dist/contracts/llm.js +12 -0
- package/dist/contracts/messages.d.ts +63 -0
- package/dist/contracts/messages.js +65 -0
- package/dist/contracts/redact.d.ts +46 -0
- package/dist/contracts/redact.js +223 -0
- package/dist/contracts/tools.d.ts +197 -0
- package/dist/contracts/tools.js +33 -0
- package/dist/contracts/types.d.ts +302 -0
- package/dist/contracts/types.js +50 -0
- package/dist/contracts/ui.d.ts +122 -0
- package/dist/contracts/ui.js +15 -0
- package/dist/conversation/agent-service.js +61 -0
- package/dist/conversation/approval-wiring.js +82 -0
- package/dist/conversation/backoff.js +38 -0
- package/dist/conversation/codes.js +47 -0
- package/dist/conversation/control-tools.js +102 -0
- package/dist/conversation/control.js +210 -0
- package/dist/conversation/driver.js +922 -0
- package/dist/conversation/index.js +12 -0
- package/dist/conversation/model-visible.js +169 -0
- package/dist/conversation/open-tools.js +126 -0
- package/dist/conversation/reseed.js +132 -0
- package/dist/conversation/sessions.js +215 -0
- package/dist/conversation/todo.js +203 -0
- package/dist/conversation/types.js +35 -0
- package/dist/conversation/wiring.js +204 -0
- package/dist/credentials/codes.js +59 -0
- package/dist/credentials/commands.js +186 -0
- package/dist/credentials/env-ref.js +34 -0
- package/dist/credentials/index.js +28 -0
- package/dist/credentials/migration.js +18 -0
- package/dist/credentials/oauth.js +266 -0
- package/dist/credentials/refresh.js +152 -0
- package/dist/credentials/secrets.js +118 -0
- package/dist/credentials/types.js +30 -0
- package/dist/exec/bash.js +279 -0
- package/dist/exec/codes.js +37 -0
- package/dist/exec/env.js +99 -0
- package/dist/exec/environment.js +39 -0
- package/dist/exec/git-guard.js +623 -0
- package/dist/exec/index.js +24 -0
- package/dist/exec/registry.js +120 -0
- package/dist/exec/spawn.js +281 -0
- package/dist/exec/tail.js +77 -0
- package/dist/exec/types.js +5 -0
- package/dist/goal/codes.js +33 -0
- package/dist/goal/command.js +175 -0
- package/dist/goal/fold.js +121 -0
- package/dist/goal/gates.js +122 -0
- package/dist/goal/index.js +22 -0
- package/dist/goal/migration.js +48 -0
- package/dist/goal/service.js +501 -0
- package/dist/goal/todo-tool.js +174 -0
- package/dist/goal/types.js +1 -0
- package/dist/goal/update-tool.js +46 -0
- package/dist/host/approval-cmd.js +276 -0
- package/dist/host/assembly.js +1146 -0
- package/dist/host/boot-failures.js +104 -0
- package/dist/host/budget-advisory.js +74 -0
- package/dist/host/budget-broadcast.js +50 -0
- package/dist/host/builtins.js +54 -0
- package/dist/host/cli.js +544 -0
- package/dist/host/codes.js +120 -0
- package/dist/host/config-schema.js +195 -0
- package/dist/host/conversation-stack.js +654 -0
- package/dist/host/core-plugins.js +1685 -0
- package/dist/host/credentials-cmd.js +42 -0
- package/dist/host/disclosure.js +30 -0
- package/dist/host/dispatch.js +105 -0
- package/dist/host/doors-cmd.js +154 -0
- package/dist/host/dump-config.js +44 -0
- package/dist/host/hook-dispatch-guard.js +27 -0
- package/dist/host/import-gate.js +153 -0
- package/dist/host/index.js +54 -0
- package/dist/host/issue-session.js +241 -0
- package/dist/host/loader.js +372 -0
- package/dist/host/main.js +161 -0
- package/dist/host/manifest.js +381 -0
- package/dist/host/mcp-entry.js +53 -0
- package/dist/host/plugin-boot.js +952 -0
- package/dist/host/plugin-context.js +672 -0
- package/dist/host/plugin-install.js +480 -0
- package/dist/host/plugin-reload.js +124 -0
- package/dist/host/plugin-store.js +462 -0
- package/dist/host/plugin-tools.js +359 -0
- package/dist/host/plugin-uninstall.js +257 -0
- package/dist/host/plugins-cmd.js +302 -0
- package/dist/host/plugins-command.js +130 -0
- package/dist/host/plugins-config.js +195 -0
- package/dist/host/prompt-sections.js +133 -0
- package/dist/host/run-entry.js +544 -0
- package/dist/host/runtime.js +209 -0
- package/dist/host/scheduler-tick.js +294 -0
- package/dist/host/serve-daemon.js +501 -0
- package/dist/host/serve-entry.js +279 -0
- package/dist/host/session-anchor.js +41 -0
- package/dist/host/sessions-cmd.js +219 -0
- package/dist/host/sessions-face.js +47 -0
- package/dist/host/settings-store.js +112 -0
- package/dist/host/signals.js +69 -0
- package/dist/host/single-instance.js +105 -0
- package/dist/host/subagent-factory.js +291 -0
- package/dist/host/testkit/harness.js +283 -0
- package/dist/host/testkit/index.js +14 -0
- package/dist/host/testkit/matrix.js +302 -0
- package/dist/host/tool-policy-store.js +177 -0
- package/dist/host/triggers.js +212 -0
- package/dist/host/tui-entry.js +216 -0
- package/dist/host/webui-bridge.js +165 -0
- package/dist/issue/codes.js +38 -0
- package/dist/issue/filter.js +146 -0
- package/dist/issue/github.js +116 -0
- package/dist/issue/index.js +17 -0
- package/dist/issue/mount.js +70 -0
- package/dist/issue/poll.js +58 -0
- package/dist/issue/service.js +341 -0
- package/dist/issue/tools.js +70 -0
- package/dist/issue/types.js +34 -0
- package/dist/issue/webhook.js +126 -0
- package/dist/llm/codes.js +77 -0
- package/dist/llm/complete.js +196 -0
- package/dist/llm/events.js +15 -0
- package/dist/llm/index.js +24 -0
- package/dist/llm/inflight.js +65 -0
- package/dist/llm/model-id.js +58 -0
- package/dist/llm/provider-face.d.ts +32 -0
- package/dist/llm/provider-face.js +34 -0
- package/dist/llm/recovery.js +146 -0
- package/dist/llm/runtime.js +46 -0
- package/dist/llm/stream-fn.js +147 -0
- package/dist/lsp/codes.js +31 -0
- package/dist/lsp/connection.js +147 -0
- package/dist/lsp/frame.js +110 -0
- package/dist/lsp/index.js +16 -0
- package/dist/lsp/inject.js +58 -0
- package/dist/lsp/instance.js +264 -0
- package/dist/lsp/service.js +522 -0
- package/dist/lsp/tools.js +53 -0
- package/dist/lsp/types.js +201 -0
- package/dist/mcp/bridge.js +222 -0
- package/dist/mcp/codes.js +21 -0
- package/dist/mcp/index.js +19 -0
- package/dist/mcp/jsonrpc.js +230 -0
- package/dist/mcp/service.js +125 -0
- package/dist/mcp/tools.js +145 -0
- package/dist/mcp/types.js +108 -0
- package/dist/memory/cite.js +131 -0
- package/dist/memory/codes.js +50 -0
- package/dist/memory/command.js +71 -0
- package/dist/memory/consolidate.js +219 -0
- package/dist/memory/cycle.js +109 -0
- package/dist/memory/dao.js +878 -0
- package/dist/memory/diff.js +238 -0
- package/dist/memory/extract.js +181 -0
- package/dist/memory/fts.js +58 -0
- package/dist/memory/index.js +42 -0
- package/dist/memory/inject.js +354 -0
- package/dist/memory/merge.js +178 -0
- package/dist/memory/migration.js +132 -0
- package/dist/memory/pollution.js +59 -0
- package/dist/memory/port.js +385 -0
- package/dist/memory/review.js +201 -0
- package/dist/memory/scan.js +89 -0
- package/dist/memory/tools.js +508 -0
- package/dist/memory/types.js +140 -0
- package/dist/obs/codes.js +38 -0
- package/dist/obs/db.js +115 -0
- package/dist/obs/index.js +20 -0
- package/dist/obs/rollup.js +61 -0
- package/dist/obs/service.js +363 -0
- package/dist/obs/session-tools.js +282 -0
- package/dist/obs/session-view.js +225 -0
- package/dist/obs/tool.js +97 -0
- package/dist/obs/types.js +1 -0
- package/dist/persist/audit.js +63 -0
- package/dist/persist/aux.js +39 -0
- package/dist/persist/codes.js +35 -0
- package/dist/persist/index.js +30 -0
- package/dist/persist/load-history.js +50 -0
- package/dist/persist/migrations.js +37 -0
- package/dist/persist/paths.js +108 -0
- package/dist/persist/persistence.js +229 -0
- package/dist/persist/schema.js +99 -0
- package/dist/persist/secret-box.js +95 -0
- package/dist/persist/store.js +706 -0
- package/dist/persist/write-behind.js +238 -0
- package/dist/safety/approval.js +171 -0
- package/dist/safety/bwrap.js +123 -0
- package/dist/safety/codes.js +57 -0
- package/dist/safety/danger.js +505 -0
- package/dist/safety/gate.js +226 -0
- package/dist/safety/index.js +24 -0
- package/dist/safety/presets.js +73 -0
- package/dist/safety/roots.js +139 -0
- package/dist/safety/sandbox.js +214 -0
- package/dist/safety/seatbelt.js +88 -0
- package/dist/safety/sensitive.js +34 -0
- package/dist/safety/tool-policy.js +203 -0
- package/dist/safety/types.js +13 -0
- package/dist/scheduler/codes.js +54 -0
- package/dist/scheduler/cron-backend.js +135 -0
- package/dist/scheduler/engine.js +280 -0
- package/dist/scheduler/gates.js +60 -0
- package/dist/scheduler/index.js +25 -0
- package/dist/scheduler/migration.js +22 -0
- package/dist/scheduler/runner.js +171 -0
- package/dist/scheduler/schedule.js +193 -0
- package/dist/scheduler/service.js +283 -0
- package/dist/scheduler/tick.js +139 -0
- package/dist/scheduler/types.js +1 -0
- package/dist/sdk/http.js +865 -0
- package/dist/sdk/index.js +22 -0
- package/dist/sdk/mcp.js +289 -0
- package/dist/sdk/plugin-route-registry.js +126 -0
- package/dist/sdk/plugin-routes.js +149 -0
- package/dist/sdk/security.js +116 -0
- package/dist/sdk/types.js +32 -0
- package/dist/session/budget.js +159 -0
- package/dist/session/codes.js +40 -0
- package/dist/session/derive.js +180 -0
- package/dist/session/event-data.js +1 -0
- package/dist/session/fork.js +72 -0
- package/dist/session/import-gates.js +148 -0
- package/dist/session/index.js +18 -0
- package/dist/session/recover.js +103 -0
- package/dist/session/session.js +279 -0
- package/dist/session/snapshot.js +107 -0
- package/dist/skills/agents.js +242 -0
- package/dist/skills/codes.js +51 -0
- package/dist/skills/discovery.js +266 -0
- package/dist/skills/frontmatter.js +215 -0
- package/dist/skills/index.js +28 -0
- package/dist/skills/load.js +118 -0
- package/dist/skills/manage.js +201 -0
- package/dist/skills/registry.js +138 -0
- package/dist/skills/render.js +124 -0
- package/dist/skills/sections.js +171 -0
- package/dist/skills/types.js +21 -0
- package/dist/subagent/codes.js +50 -0
- package/dist/subagent/declarative.js +55 -0
- package/dist/subagent/index.js +19 -0
- package/dist/subagent/notify.js +51 -0
- package/dist/subagent/provide.js +7 -0
- package/dist/subagent/registry.js +149 -0
- package/dist/subagent/service.js +338 -0
- package/dist/subagent/surface.js +44 -0
- package/dist/subagent/tool.js +153 -0
- package/dist/subagent/types.js +25 -0
- package/dist/tools/apply-patch.js +164 -0
- package/dist/tools/codes.js +109 -0
- package/dist/tools/fs.js +501 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/observed.js +99 -0
- package/dist/tools/pipeline.js +176 -0
- package/dist/tools/protected-read.js +54 -0
- package/dist/tools/registry.js +250 -0
- package/dist/tools/search.js +451 -0
- package/dist/tools/worktree.js +327 -0
- package/dist/web/codes.js +37 -0
- package/dist/web/gate.js +43 -0
- package/dist/web/hygiene.js +195 -0
- package/dist/web/index.js +16 -0
- package/dist/web/service.js +207 -0
- package/dist/web/ssrf-guard.js +44 -0
- package/dist/web/tool.js +76 -0
- package/dist/web/types.js +10 -0
- package/dist/webui/assets/index-B-t2O8gh.js +22 -0
- package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
- package/dist/webui/index.html +14 -0
- package/dist/webui/index.js +14 -0
- package/dist/webui/server.js +549 -0
- package/dist/webui/types.js +38 -0
- package/examples/README.md +26 -0
- package/examples/minimal-code-plugin/entry.js +43 -0
- package/examples/minimal-code-plugin/package.json +12 -0
- package/examples/pure-skill-pack/package.json +14 -0
- package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
- package/package.json +86 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 safety — 危险工具闸(04 §13:不可逆外部写的预授权执法)。
|
|
3
|
+
*
|
|
4
|
+
* 定位:审批对的无人值守对偶——§9 审批对管「模型发起动作、人在场实时裁决」;
|
|
5
|
+
* 本闸管「不可逆外部写」(git push / 开 PR / 发评论……)在无人值守编排中的
|
|
6
|
+
* 放行:无人值守场景无应答者(headless unavailable 拒),代之以**预授权契约
|
|
7
|
+
* + 逐动作过闸执法 + 全量审计**四要素(Vibe-Trading live 安全件同构借形):
|
|
8
|
+
* 1. **frozen 契约(mandate)**:量化授权闭包 `{ actions, targets, maxPerDay }`
|
|
9
|
+
* ——原料全部来自用户配置层(mount config,模型写不进配置面),装配期
|
|
10
|
+
* normalize 后冻结只读注入;闸对 mandate 取规范化哈希(canonical JSON:
|
|
11
|
+
* 递归键字典序、无空白——consent 绑定与门检两侧同函数单源)。targets
|
|
12
|
+
* **保序即身份**(同集异序 = 漂移即死,fail-closed 向假阳性——用户重签
|
|
13
|
+
* 一次即解)。
|
|
14
|
+
* 2. **consent token**:人面唯写(/danger approve)、哈希绑批、过期即死、
|
|
15
|
+
* 漂移即死;缺席 = fail-closed 缺省(闸落地零配置零行为变化——deny 路径
|
|
16
|
+
* 即原「阻塞转人审」语义)。
|
|
17
|
+
* 3. **HALT 文件哨兵**:<dataDir>/HALT 存在性判、内容不解析(损坏仍
|
|
18
|
+
* tripped、stat 不可达按在场)——touch 即全停、删即恢复,独立于 LLM 与
|
|
19
|
+
* 事件面;fired-once latch(HALT.fired)改造为审计观测位(曾拦过痕迹,
|
|
20
|
+
* 不自动删;latch 落盘失败 = warn 降级不阻断拒回执)。
|
|
21
|
+
* 4. **hash-chain 审计账本**:<dataDir>/danger-ledger.jsonl 每闸决策一 JSON
|
|
22
|
+
* 行(ALLOW 与 DENY 全记);recordHash = SHA-256(seq‖prevHash‖规范化载荷)、
|
|
23
|
+
* 首笔 prevHash = 64 个十六进制零字符;追加前自链首重算全链、链坏拒续写
|
|
24
|
+
* (DANGER_LEDGER_CORRUPT);逐写 fsync + 首建目录 fsync;日帽计数源 =
|
|
25
|
+
* 账本派生无第二状态(UTC 日界——03 §10.8「存储客观、呈现主观」同律);
|
|
26
|
+
* verdict 三值 allow-succeeded / allow-failed / deny(日计数只在确认成功
|
|
27
|
+
* 后递增——防「失败重试烧帽」与「虚计数」两向失真)。
|
|
28
|
+
*
|
|
29
|
+
* 闸序(fail-closed,任何一步不过零外联):0 账本链健康 → 1 HALT 不在场
|
|
30
|
+
* (kill switch 最优先呈报)→ 2 mandate 在场(结构保证——构造器必填)→
|
|
31
|
+
* 3 consent 三查 → 4 值域 → 5 日帽 → 6 执行 + 回执后记账。adjudication→
|
|
32
|
+
* 执行→append 整段经进程内互斥串行(promise 链——§7 写串行链同族先例,
|
|
33
|
+
* 防并发双过步 5 超帽的 fail-open 缝)。
|
|
34
|
+
*
|
|
35
|
+
* 归宿纪律:零新席零新边(07 §1.1 safety 行职能枚举第六项)——文件 IO 走
|
|
36
|
+
* node:fs(NODE_BUILTIN 全局放行不占边);dataDir 经装配必填注入
|
|
37
|
+
* (installSafetyGate dataDir 同款先例)。v1 唯一消费方 core:issue 经装配
|
|
38
|
+
* 窄面注入(IssueDangerFace 词面归 issue 侧 types.ts——issue 不 import
|
|
39
|
+
* safety、结构兼容编译期即验,GoalJobsFace 同句式)。
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { mkdir, open, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
43
|
+
import { dirname, join } from 'node:path';
|
|
44
|
+
import { BaseError } from '../contracts/index.js';
|
|
45
|
+
/* ---------------- 常量(04 §13 定形——数据目录文件族单源) ---------------- */
|
|
46
|
+
/** v1 动作闭集两值(扩值随真实消费件立题——闭集律,定形不扩) */
|
|
47
|
+
export const DANGER_V1_ACTIONS = ['push', 'create-pr'];
|
|
48
|
+
/** consent 文件名(map 形 `{ consumers: { [consumerId]: {...} } }`) */
|
|
49
|
+
export const DANGER_CONSENT_FILE = 'danger-consent.json';
|
|
50
|
+
/** HALT 哨兵文件名(存在性判、内容不解析) */
|
|
51
|
+
export const DANGER_HALT_FILE = 'HALT';
|
|
52
|
+
/** HALT fired-once latch 文件名(审计观测位——曾拦过痕迹,不自动删) */
|
|
53
|
+
export const DANGER_HALT_LATCH_FILE = 'HALT.fired';
|
|
54
|
+
/** hash-chain 审计账本文件名(每闸决策一 JSON 行) */
|
|
55
|
+
export const DANGER_LEDGER_FILE = 'danger-ledger.jsonl';
|
|
56
|
+
/** 链首 prevHash(64 个十六进制零字符) */
|
|
57
|
+
export const DANGER_GENESIS_HASH = '0'.repeat(64);
|
|
58
|
+
/** mandate 缺省日成功帽(04 §13:v1 = issue 件 maxDeliveriesPerDay 缺省 10) */
|
|
59
|
+
export const DANGER_MAX_PER_DAY_DEFAULT = 10;
|
|
60
|
+
/**
|
|
61
|
+
* 归一 mandate(装配期调用——坏形 {ok:false, message} 响亮拒由装配位折
|
|
62
|
+
* ISSUE_CONFIG_INVALID 行级失败)。normalize 细则 = 缺省填充 + 坏形拒,
|
|
63
|
+
* **无重排操作**:targets 数组保序即身份。
|
|
64
|
+
*/
|
|
65
|
+
export function normalizeDangerMandate(raw) {
|
|
66
|
+
if (raw === null || typeof raw !== 'object') {
|
|
67
|
+
return { ok: false, message: `danger mandate 须对象形(得 ${typeof raw})` };
|
|
68
|
+
}
|
|
69
|
+
const obj = raw;
|
|
70
|
+
// actions:缺省 v1 闭集两值;在场须闭集子集非空(预建空位 = 预发明闸)
|
|
71
|
+
let actions;
|
|
72
|
+
if (obj.actions === undefined) {
|
|
73
|
+
actions = [...DANGER_V1_ACTIONS];
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (!Array.isArray(obj.actions) || obj.actions.length === 0) {
|
|
77
|
+
return { ok: false, message: 'danger mandate actions 须非空数组(v1 闭集值域 push/create-pr)' };
|
|
78
|
+
}
|
|
79
|
+
actions = [];
|
|
80
|
+
for (const a of obj.actions) {
|
|
81
|
+
if (typeof a !== 'string' || !DANGER_V1_ACTIONS.includes(a)) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
message: `danger mandate actions 元素越闭集(得 ${JSON.stringify(a)}——v1 值域 push/create-pr,扩值随真实消费件立题)`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
actions.push(a);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// targets:必填非空字符串数组(保序——序即身份,无重排)
|
|
91
|
+
if (!Array.isArray(obj.targets) || obj.targets.length === 0) {
|
|
92
|
+
return { ok: false, message: 'danger mandate targets 须非空数组(目标值域)' };
|
|
93
|
+
}
|
|
94
|
+
const targets = [];
|
|
95
|
+
for (const t of obj.targets) {
|
|
96
|
+
if (typeof t !== 'string' || t === '') {
|
|
97
|
+
return { ok: false, message: `danger mandate targets 元素须非空串(得 ${JSON.stringify(t)})` };
|
|
98
|
+
}
|
|
99
|
+
targets.push(t);
|
|
100
|
+
}
|
|
101
|
+
// maxPerDay:缺省 10;正整数(空帽即坏形律——0/负数/坏形拒;彻底关停走
|
|
102
|
+
// HALT 哨兵或撤 consent,不走空帽)
|
|
103
|
+
let maxPerDay = DANGER_MAX_PER_DAY_DEFAULT;
|
|
104
|
+
if (obj.maxPerDay !== undefined) {
|
|
105
|
+
const n = obj.maxPerDay;
|
|
106
|
+
if (typeof n !== 'number' || !Number.isInteger(n) || n < 1 || n > 100_000) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
message: `danger mandate maxPerDay 须 1..100000 正整数(得 ${JSON.stringify(n)}——彻底关停走 HALT 哨兵或撤 consent,不走空帽)`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
maxPerDay = n;
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, mandate: { actions, targets, maxPerDay } };
|
|
115
|
+
}
|
|
116
|
+
/* ---------------- 规范化哈希(canonical JSON——单源两消费:consent 绑定与门检) ---------------- */
|
|
117
|
+
/**
|
|
118
|
+
* canonical JSON(04 §13 frozen 条):递归键字典序、无空白、undefined 键跳过
|
|
119
|
+
* (缺席即不在身份里)。数组保序(元素序是值的一部分——targets 保序即身份
|
|
120
|
+
* 的哈希面承载)。
|
|
121
|
+
*/
|
|
122
|
+
export function canonicalJson(value) {
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
return 'null';
|
|
125
|
+
if (value === null)
|
|
126
|
+
return 'null';
|
|
127
|
+
if (Array.isArray(value))
|
|
128
|
+
return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
|
|
129
|
+
if (typeof value === 'object') {
|
|
130
|
+
const entries = Object.entries(value)
|
|
131
|
+
.filter(([, v]) => v !== undefined)
|
|
132
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
133
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(',')}}`;
|
|
134
|
+
}
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
/** SHA-256 十六进制(账本与 mandate 哈希共用底座) */
|
|
138
|
+
function sha256Hex(input) {
|
|
139
|
+
return createHash('sha256').update(input, 'utf8').digest('hex');
|
|
140
|
+
}
|
|
141
|
+
/* ---------------- 目标值域匹配(与触发面同值同函数语义) ---------------- */
|
|
142
|
+
/**
|
|
143
|
+
* 单 pattern 判定:精确串全等 / 含 `*` 的 glob 段级通配锚定全配。
|
|
144
|
+
* 与 issue 件 repoMatchesGlob 同算法独立持有(词面独立律——safety 与 issue
|
|
145
|
+
* 零 DAG 边;漂移由对拍测试互证,webui/security 同款先例)。
|
|
146
|
+
*/
|
|
147
|
+
export function dangerTargetMatches(target, pattern) {
|
|
148
|
+
if (!pattern.includes('*'))
|
|
149
|
+
return pattern === target;
|
|
150
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
|
|
151
|
+
return new RegExp(`^${escaped}$`).test(target);
|
|
152
|
+
}
|
|
153
|
+
/** 组危险工具闸(进程内单实例——与 §1 单活跃机一致,单写者纪律) */
|
|
154
|
+
export function createDangerGate(opts) {
|
|
155
|
+
const warn = opts.warn ?? (() => undefined);
|
|
156
|
+
const now = opts.now ?? Date.now;
|
|
157
|
+
const consumerId = opts.consumerId;
|
|
158
|
+
const mandate = opts.mandate;
|
|
159
|
+
const consentPath = join(opts.dataDir, DANGER_CONSENT_FILE);
|
|
160
|
+
const haltPath = join(opts.dataDir, DANGER_HALT_FILE);
|
|
161
|
+
const latchPath = join(opts.dataDir, DANGER_HALT_LATCH_FILE);
|
|
162
|
+
const ledgerPath = join(opts.dataDir, DANGER_LEDGER_FILE);
|
|
163
|
+
const mandateHash = sha256Hex(canonicalJson(mandate));
|
|
164
|
+
/* ---- 互斥串行(promise 链:adjudication→执行→append 整段排队) ---- */
|
|
165
|
+
let tail = Promise.resolve();
|
|
166
|
+
function serialized(job) {
|
|
167
|
+
const run = tail.then(job, job);
|
|
168
|
+
tail = run.then(() => undefined, () => undefined);
|
|
169
|
+
return run;
|
|
170
|
+
}
|
|
171
|
+
/* ---- 原子写底座(tmp + rename + fsync——consent/latch 同族纪律) ---- */
|
|
172
|
+
/** 目录 fsync(崩溃撕裂防线——平台/文件系统不支持则尽力而为) */
|
|
173
|
+
async function fsyncDir(dir) {
|
|
174
|
+
try {
|
|
175
|
+
const dh = await open(dir, 'r');
|
|
176
|
+
try {
|
|
177
|
+
await dh.sync();
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await dh.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// 目录 fsync 平台差异面(不构成拒因——文件级 fsync 已足)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** tmp+rename 原子写 JSON(写前 fsync tmp、写后 fsync 目录) */
|
|
188
|
+
async function atomicWriteJson(path, value) {
|
|
189
|
+
await mkdir(dirname(path), { recursive: true });
|
|
190
|
+
const tmp = `${path}.tmp`;
|
|
191
|
+
const fh = await open(tmp, 'w');
|
|
192
|
+
try {
|
|
193
|
+
await writeFile(fh, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
194
|
+
await fh.sync();
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
await fh.close();
|
|
198
|
+
}
|
|
199
|
+
await rename(tmp, path);
|
|
200
|
+
await fsyncDir(dirname(path));
|
|
201
|
+
}
|
|
202
|
+
/* ---- 账本读写(追加前验证全链——账本完整性先于可用性) ---- */
|
|
203
|
+
/** 账本载荷(recordHash 输入——seq/prevHash/recordHash 三位不入自身哈希) */
|
|
204
|
+
function payloadOf(record) {
|
|
205
|
+
return {
|
|
206
|
+
at: record.at,
|
|
207
|
+
consumer: record.consumer,
|
|
208
|
+
action: record.action,
|
|
209
|
+
target: record.target,
|
|
210
|
+
verdict: record.verdict,
|
|
211
|
+
...(record.code !== undefined ? { code: record.code } : {}),
|
|
212
|
+
...(record.detail !== undefined ? { detail: record.detail } : {}),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** 读账本 + 自链首重算全链(v1 量级毫秒级合理——检查点/增量验证随规模立题) */
|
|
216
|
+
async function loadLedger() {
|
|
217
|
+
let text;
|
|
218
|
+
try {
|
|
219
|
+
text = await readFile(ledgerPath, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (err.code === 'ENOENT')
|
|
223
|
+
return []; // 未建 = 空链健康
|
|
224
|
+
throw new BaseError('DANGER_LEDGER_CORRUPT', `[DANGER_LEDGER_CORRUPT] 账本不可读(${err.message})——按链坏拒续写处理(fail-closed)`);
|
|
225
|
+
}
|
|
226
|
+
const records = [];
|
|
227
|
+
let prevHash = DANGER_GENESIS_HASH;
|
|
228
|
+
const lines = text.split('\n');
|
|
229
|
+
for (let i = 0; i < lines.length; i++) {
|
|
230
|
+
const line = lines[i].trim();
|
|
231
|
+
if (line === '')
|
|
232
|
+
continue; // 尾行空行(末笔 \n 产物)非记录
|
|
233
|
+
let parsed;
|
|
234
|
+
try {
|
|
235
|
+
parsed = JSON.parse(line);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
throw new BaseError('DANGER_LEDGER_CORRUPT', `[DANGER_LEDGER_CORRUPT] 账本第 ${i + 1} 行坏 JSON(${err.message})——链坏拒续写,修复归人手(截断/重建是人工决策)`);
|
|
239
|
+
}
|
|
240
|
+
const record = parsed;
|
|
241
|
+
const seq = records.length + 1;
|
|
242
|
+
if (record.seq !== seq ||
|
|
243
|
+
record.prevHash !== prevHash ||
|
|
244
|
+
typeof record.recordHash !== 'string' ||
|
|
245
|
+
record.recordHash !== sha256Hex(`${seq}|${prevHash}|${canonicalJson(payloadOf(record))}`)) {
|
|
246
|
+
throw new BaseError('DANGER_LEDGER_CORRUPT', `[DANGER_LEDGER_CORRUPT] 账本第 ${seq} 笔链验证红(seq/prevHash/recordHash 任一不符——改/删历史行则哈希传播破坏其后全部)——链坏拒续写,修复归人手(截断/重建是人工决策)`);
|
|
247
|
+
}
|
|
248
|
+
records.push(record);
|
|
249
|
+
prevHash = record.recordHash;
|
|
250
|
+
}
|
|
251
|
+
return records;
|
|
252
|
+
}
|
|
253
|
+
/** 追加一笔(调用方已在互斥段内持最新全链 records——「追加前验证」由 loadLedger 承担) */
|
|
254
|
+
async function appendRecord(records, entry, nowMs) {
|
|
255
|
+
const prevHash = records.length === 0 ? DANGER_GENESIS_HASH : records[records.length - 1].recordHash;
|
|
256
|
+
const seq = records.length + 1;
|
|
257
|
+
const payload = payloadOf({
|
|
258
|
+
at: new Date(nowMs).toISOString(),
|
|
259
|
+
consumer: consumerId,
|
|
260
|
+
action: entry.action,
|
|
261
|
+
target: entry.target,
|
|
262
|
+
verdict: entry.verdict,
|
|
263
|
+
...(entry.code !== undefined ? { code: entry.code } : {}),
|
|
264
|
+
...(entry.detail !== undefined ? { detail: entry.detail } : {}),
|
|
265
|
+
});
|
|
266
|
+
const recordHash = sha256Hex(`${seq}|${prevHash}|${canonicalJson(payload)}`);
|
|
267
|
+
const line = `${JSON.stringify({ seq, prevHash, recordHash, ...payload })}\n`;
|
|
268
|
+
await mkdir(opts.dataDir, { recursive: true });
|
|
269
|
+
const fh = await open(ledgerPath, 'a');
|
|
270
|
+
try {
|
|
271
|
+
await fh.write(line);
|
|
272
|
+
await fh.sync(); // 逐写 fsync(崩溃撕裂防线)
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await fh.close();
|
|
276
|
+
}
|
|
277
|
+
if (records.length === 0)
|
|
278
|
+
await fsyncDir(opts.dataDir); // 首建(或空链重建)目录 fsync
|
|
279
|
+
}
|
|
280
|
+
/** 当日 allow-succeeded 计数(UTC 日界——账本派生无第二状态) */
|
|
281
|
+
function countTodayAllowSucceeded(records, nowMs) {
|
|
282
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
283
|
+
return records.filter((r) => r.verdict === 'allow-succeeded' && r.consumer === consumerId && r.at.slice(0, 10) === day).length;
|
|
284
|
+
}
|
|
285
|
+
/* ---- HALT 哨兵与 latch ---- */
|
|
286
|
+
/** 哨兵在场判定(存在性判、内容不解析;stat 不可达按在场处理同向) */
|
|
287
|
+
async function haltTripped() {
|
|
288
|
+
try {
|
|
289
|
+
await stat(haltPath);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
return err.code !== 'ENOENT';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** latch 读(best-effort——坏形/缺席返回 undefined,不构成拒因) */
|
|
297
|
+
async function readLatch() {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(await readFile(latchPath, 'utf8'));
|
|
300
|
+
return typeof parsed?.firstFiredAt === 'string' ? parsed.firstFiredAt : undefined;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/** latch 落盘(首次因 HALT 拒时——审计观测位:不自动删、失败 = warn 降级) */
|
|
307
|
+
async function writeLatchOnce(nowMs) {
|
|
308
|
+
try {
|
|
309
|
+
await stat(latchPath);
|
|
310
|
+
return; // 已在场——firstFiredAt 保持首笔(观测位语义)
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// 不在场——落盘(其余 stat 错误也让位于下方写失败的 warn 降级)
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await atomicWriteJson(latchPath, { firstFiredAt: new Date(nowMs).toISOString() });
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
warn(`HALT.fired latch 落盘失败(warn 降级——拒已成立,观测位尽力而为):${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/* ---- consent 读写 ---- */
|
|
323
|
+
/** 读本 consumer 的 consent 条目(缺席/坏形/不可读 = undefined——fail-closed 向) */
|
|
324
|
+
async function readConsentEntry() {
|
|
325
|
+
let text;
|
|
326
|
+
try {
|
|
327
|
+
text = await readFile(consentPath, 'utf8');
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return undefined; // 缺席(ENOENT)或不可读——均按缺席(拒面同向)
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const parsed = JSON.parse(text);
|
|
334
|
+
const entry = parsed?.consumers?.[consumerId];
|
|
335
|
+
if (entry === null || typeof entry !== 'object')
|
|
336
|
+
return undefined;
|
|
337
|
+
const { mandateHash: hash, approvedAt, expiresAt } = entry;
|
|
338
|
+
if (typeof hash !== 'string' || typeof approvedAt !== 'number' || typeof expiresAt !== 'number') {
|
|
339
|
+
warn('danger-consent.json 本 consumer 条目坏形——按缺席处理(重签即解)');
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
return { mandateHash: hash, approvedAt, expiresAt };
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
warn('danger-consent.json 坏形——按缺席处理(用户资产可手改;重签即解)');
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/* ---- 闸序执行(0-6——serialized 段内) ---- */
|
|
350
|
+
/**
|
|
351
|
+
* deny 记账 + 上抛(决策已成立——记账失败 = warn 降级不吞拒:拒回执优先,
|
|
352
|
+
* 账本侧告警归人检)。HALT 分支先落 latch(观测位尽力而为)。
|
|
353
|
+
*/
|
|
354
|
+
async function denyAndThrow(records, req, code, message, nowMs, options) {
|
|
355
|
+
if (options?.latch === true)
|
|
356
|
+
await writeLatchOnce(nowMs);
|
|
357
|
+
try {
|
|
358
|
+
await appendRecord(records, { action: req.action, target: req.target, verdict: 'deny', code, detail: req.detail }, nowMs);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
warn(`危险闸 deny 记账失败(决策已成立——账本侧告警):${err.message}`);
|
|
362
|
+
}
|
|
363
|
+
throw new BaseError(code, message);
|
|
364
|
+
}
|
|
365
|
+
function runGuarded(req, execute) {
|
|
366
|
+
return serialized(async () => {
|
|
367
|
+
const nowMs = now();
|
|
368
|
+
const dataDirNote = opts.dataDir;
|
|
369
|
+
/* 步 0:账本链健康(前置不变式——链坏 = 全拒,后续判定一概不发生) */
|
|
370
|
+
const records = await loadLedger();
|
|
371
|
+
/* 步 1:HALT 哨兵不在场(kill switch 最优先呈报——全停理由先于一切门检) */
|
|
372
|
+
if (await haltTripped()) {
|
|
373
|
+
await denyAndThrow(records, req, 'DANGER_HALTED', `[DANGER_HALTED] 危险闸 HALT 哨兵在场——全部危险动作拒(存在性执法:删除 ${join(dataDirNote, DANGER_HALT_FILE)} 文件即恢复)`, nowMs, { latch: true });
|
|
374
|
+
}
|
|
375
|
+
/* 步 2:mandate 在场——结构保证(构造器必填冻结注入),无运行期判定 */
|
|
376
|
+
/* 步 3:consent 三查(在场 + 哈希绑批 + 未过期——任一不满足即拒) */
|
|
377
|
+
const consent = await readConsentEntry();
|
|
378
|
+
if (consent === undefined) {
|
|
379
|
+
await denyAndThrow(records, req, 'DANGER_CONSENT_ABSENT', '[DANGER_CONSENT_ABSENT] 危险闸 consent 缺席(fail-closed 缺省)——TUI 运行 /danger approve 签发后重试(「全自动」双层显式 opt-in:mode auto + consent 各自独立拒)', nowMs);
|
|
380
|
+
}
|
|
381
|
+
else if (consent.mandateHash !== mandateHash) {
|
|
382
|
+
await denyAndThrow(records, req, 'DANGER_CONSENT_INVALID', `[DANGER_CONSENT_INVALID] 危险闸 consent 漂移(mandate 哈希不符——批了 A 契约不等于批了 B 契约)——重跑 /danger approve 重签(活体哈希 ${mandateHash})`, nowMs);
|
|
383
|
+
}
|
|
384
|
+
else if (nowMs >= consent.expiresAt) {
|
|
385
|
+
await denyAndThrow(records, req, 'DANGER_CONSENT_INVALID', '[DANGER_CONSENT_INVALID] 危险闸 consent 过期(不自动续、不静默宽限——重签唯一出路)——重跑 /danger approve', nowMs);
|
|
386
|
+
}
|
|
387
|
+
/* 步 4:动作可解析且在值域(action ∈ actions、target 匹配 targets) */
|
|
388
|
+
if (!mandate.actions.includes(req.action)) {
|
|
389
|
+
await denyAndThrow(records, req, 'DANGER_TARGET_DENIED', `[DANGER_TARGET_DENIED] 动作不在 mandate 值域(action=${req.action}——v1 闭集 ${mandate.actions.join('/')})`, nowMs);
|
|
390
|
+
}
|
|
391
|
+
if (!mandate.targets.some((pattern) => dangerTargetMatches(req.target, pattern))) {
|
|
392
|
+
await denyAndThrow(records, req, 'DANGER_TARGET_DENIED', `[DANGER_TARGET_DENIED] 目标不在 mandate 值域(target=${req.target}——值域 ${mandate.targets.join(', ')})`, nowMs);
|
|
393
|
+
}
|
|
394
|
+
/* 步 5:日帽未超(当日 allow-succeeded 笔数 < maxPerDay——账本派生) */
|
|
395
|
+
const used = countTodayAllowSucceeded(records, nowMs);
|
|
396
|
+
if (used >= mandate.maxPerDay) {
|
|
397
|
+
await denyAndThrow(records, req, 'DANGER_CAP_EXCEEDED', `[DANGER_CAP_EXCEEDED] 当日成功帽已满(${used}/${mandate.maxPerDay},UTC 日界恢复)——次日重试或提帽后重签 consent`, nowMs);
|
|
398
|
+
}
|
|
399
|
+
/* 步 6:执行外部写 → 回执后记账(verdict 按成败落;失败上抛 = allow-failed 原错误直传)。
|
|
400
|
+
* 记账失败 = warn 降级(denyAndThrow 同律对称——决策与执行已成立,账本侧 IO
|
|
401
|
+
* 失败不改变回执语义:成功尾不折假失败〔外部写已发生〕、失败腿原错误不被
|
|
402
|
+
* 记账错误顶替;链完整性由下次 loadLedger 的追加前验证承担)。 */
|
|
403
|
+
let result;
|
|
404
|
+
try {
|
|
405
|
+
result = await execute();
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
try {
|
|
409
|
+
await appendRecord(records, {
|
|
410
|
+
action: req.action,
|
|
411
|
+
target: req.target,
|
|
412
|
+
verdict: 'allow-failed',
|
|
413
|
+
detail: err instanceof Error ? err.message.slice(0, 300) : String(err).slice(0, 300),
|
|
414
|
+
}, nowMs);
|
|
415
|
+
}
|
|
416
|
+
catch (accErr) {
|
|
417
|
+
warn(`危险闸 allow-failed 记账失败(原错误优先上报——账本侧告警):${accErr.message}`);
|
|
418
|
+
}
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await appendRecord(records, { action: req.action, target: req.target, verdict: 'allow-succeeded' }, nowMs);
|
|
423
|
+
}
|
|
424
|
+
catch (accErr) {
|
|
425
|
+
warn(`危险闸 allow-succeeded 记账失败(外部写已发生——回执不降级,账本侧告警):${accErr.message}`);
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const gate = {
|
|
431
|
+
consumerId,
|
|
432
|
+
mandateHash,
|
|
433
|
+
runGuarded,
|
|
434
|
+
async approve(ttlDays = 30) {
|
|
435
|
+
if (!Number.isInteger(ttlDays) || ttlDays < 1 || ttlDays > 3650) {
|
|
436
|
+
return { ok: false, message: `ttlDays 须 1..3650 正整数(得 ${JSON.stringify(ttlDays)})` };
|
|
437
|
+
}
|
|
438
|
+
const nowMs = now();
|
|
439
|
+
const approvedAt = nowMs;
|
|
440
|
+
const expiresAt = nowMs + ttlDays * 86_400_000;
|
|
441
|
+
// 读全图保留他 consumer 条目(map 形——多消费方形态随第三方面立题)
|
|
442
|
+
let map = { consumers: {} };
|
|
443
|
+
try {
|
|
444
|
+
const parsed = JSON.parse(await readFile(consentPath, 'utf8'));
|
|
445
|
+
if (parsed !== null &&
|
|
446
|
+
typeof parsed === 'object' &&
|
|
447
|
+
parsed.consumers !== null &&
|
|
448
|
+
typeof parsed.consumers === 'object') {
|
|
449
|
+
map = parsed;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// 缺席/坏形——重建(consent 是用户资产可手改;签发即覆写本条目)
|
|
454
|
+
}
|
|
455
|
+
map.consumers[consumerId] = { mandateHash, approvedAt, expiresAt };
|
|
456
|
+
await atomicWriteJson(consentPath, map);
|
|
457
|
+
return { ok: true, mandateHash, approvedAt, expiresAt };
|
|
458
|
+
},
|
|
459
|
+
async status() {
|
|
460
|
+
const nowMs = now();
|
|
461
|
+
let records = [];
|
|
462
|
+
let healthy = true;
|
|
463
|
+
try {
|
|
464
|
+
records = await loadLedger();
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
healthy = false; // 链坏——呈现诚实(deliver 步 0 恒拒)
|
|
468
|
+
}
|
|
469
|
+
const entry = await readConsentEntry();
|
|
470
|
+
let state = 'absent';
|
|
471
|
+
if (entry !== undefined) {
|
|
472
|
+
if (entry.mandateHash !== mandateHash)
|
|
473
|
+
state = 'drifted';
|
|
474
|
+
else if (nowMs >= entry.expiresAt)
|
|
475
|
+
state = 'expired';
|
|
476
|
+
else
|
|
477
|
+
state = 'valid';
|
|
478
|
+
}
|
|
479
|
+
const tripped = await haltTripped();
|
|
480
|
+
const firstFiredAt = await readLatch(); // 独立呈现——删 HALT 恢复后「曾拦过」痕迹仍在
|
|
481
|
+
return {
|
|
482
|
+
consumer: consumerId,
|
|
483
|
+
mandateHash,
|
|
484
|
+
mandate,
|
|
485
|
+
consent: {
|
|
486
|
+
state,
|
|
487
|
+
...(entry !== undefined
|
|
488
|
+
? {
|
|
489
|
+
approvedAt: new Date(entry.approvedAt).toISOString(),
|
|
490
|
+
expiresAt: new Date(entry.expiresAt).toISOString(),
|
|
491
|
+
}
|
|
492
|
+
: {}),
|
|
493
|
+
},
|
|
494
|
+
halt: { tripped, ...(firstFiredAt !== undefined ? { firstFiredAt } : {}) },
|
|
495
|
+
cap: {
|
|
496
|
+
used: healthy ? countTodayAllowSucceeded(records, nowMs) : null,
|
|
497
|
+
max: mandate.maxPerDay,
|
|
498
|
+
day: new Date(nowMs).toISOString().slice(0, 10),
|
|
499
|
+
},
|
|
500
|
+
ledger: { healthy, total: records.length },
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
return gate;
|
|
505
|
+
}
|