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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist — 主库归属与数据目录解析(05 篇 §6.7 三级梯子 + §6.6 权限纪律)。
|
|
3
|
+
*
|
|
4
|
+
* 三级梯子:显式 BERRY_AGENT_DATA_DIR > 显式 BERRY_AGENT_DB_PATH > 缺省
|
|
5
|
+
* ~/.berry-agent/sessions.db。环境变量优先级与命名全表在 07 篇;本文件是
|
|
6
|
+
* 梯子的唯一实现(宿主各入口共用,禁止散落第二份解析)。
|
|
7
|
+
*/
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
/** 数据目录环境变量(整目录覆盖——多数据目录 = 独立实例的正门,05 §6.6) */
|
|
12
|
+
export const DATA_DIR_ENV = 'BERRY_AGENT_DATA_DIR';
|
|
13
|
+
/** 库文件路径环境变量(单文件级覆盖——优先于数据目录拼接位) */
|
|
14
|
+
export const DB_PATH_ENV = 'BERRY_AGENT_DB_PATH';
|
|
15
|
+
/** 缺省数据目录名(家目录下) */
|
|
16
|
+
export const DEFAULT_DATA_DIR_BASENAME = '.berry-agent';
|
|
17
|
+
/** 缺省主库文件名 */
|
|
18
|
+
export const DEFAULT_DB_BASENAME = 'sessions.db';
|
|
19
|
+
/** 内存库哨兵值(CLI 组合诊断形态;better-sqlite3 原生 :memory: 语义) */
|
|
20
|
+
export const MEMORY_DB_PATH = ':memory:';
|
|
21
|
+
/**
|
|
22
|
+
* 解析数据目录(梯子第 1/3 级):BERRY_AGENT_DATA_DIR 显式值 > 缺省 ~/.berry-agent。
|
|
23
|
+
* 注意:不解析 BERRY_AGENT_DB_PATH(那是库文件级覆盖,见 resolveDatabasePath)。
|
|
24
|
+
*/
|
|
25
|
+
export function resolveDataDir() {
|
|
26
|
+
const override = process.env[DATA_DIR_ENV];
|
|
27
|
+
if (override && override.trim() !== '')
|
|
28
|
+
return override;
|
|
29
|
+
return join(homedir(), DEFAULT_DATA_DIR_BASENAME);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 解析主库文件路径(三级梯子全序):
|
|
33
|
+
* BERRY_AGENT_DB_PATH(完整文件路径,含 ':memory:' 哨兵直通)
|
|
34
|
+
* > BERRY_AGENT_DATA_DIR 目录下 sessions.db
|
|
35
|
+
* > 缺省 ~/.berry-agent/sessions.db。
|
|
36
|
+
*/
|
|
37
|
+
export function resolveDatabasePath() {
|
|
38
|
+
const dbOverride = process.env[DB_PATH_ENV];
|
|
39
|
+
if (dbOverride && dbOverride.trim() !== '')
|
|
40
|
+
return dbOverride;
|
|
41
|
+
return join(resolveDataDir(), DEFAULT_DB_BASENAME);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析主库文件路径(锚定数据目录形——宿主装配根显式 dataDir 位的配套解析):
|
|
45
|
+
* BERRY_AGENT_DB_PATH 单文件级覆盖在场恒赢(tier-2 不被目录锚定吞掉);
|
|
46
|
+
* 否则库文件 = 传入目录下 sessions.db。
|
|
47
|
+
*
|
|
48
|
+
* 为什么需要本形(bug 回归锁的规范位):resolveDatabasePath() 只看 env 梯子,
|
|
49
|
+
* 完全无视调用方显式传入的 dataDir——宿主装配根(CLI --data-dir / 测试 rig)
|
|
50
|
+
* 传了 dataDir 时库仍开在 env/家目录位,secret.key 与库分家(1e3a299 修过
|
|
51
|
+
* CLI 直开库两处的同病,装配根主路径是漏网的第三处)。参数级语义:
|
|
52
|
+
* 显式 dataDir > env BERRY_AGENT_DATA_DIR(常识序——显式参数先于 env)。
|
|
53
|
+
*/
|
|
54
|
+
export function resolveDatabasePathIn(dataDir) {
|
|
55
|
+
const dbOverride = process.env[DB_PATH_ENV];
|
|
56
|
+
if (dbOverride && dbOverride.trim() !== '')
|
|
57
|
+
return dbOverride;
|
|
58
|
+
return join(dataDir, DEFAULT_DB_BASENAME);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 确保数据目录在场且权限 0700(05 §6.6 自检修复 + warn——凭证是操作者状态
|
|
62
|
+
* 五样之一,目录权限是第一道面)。幂等:在场且权限已符即静默通过。
|
|
63
|
+
* @param dir 数据目录路径
|
|
64
|
+
* @param warn 权限修复告警面(装配根接 logger.warn;缺省 console.error)
|
|
65
|
+
*/
|
|
66
|
+
export function ensureDataDir(dir, warn) {
|
|
67
|
+
if (!existsSync(dir)) {
|
|
68
|
+
// mkdir 递归建链(父目录缺省权限随 umask——只执法数据目录自身 0700)
|
|
69
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
70
|
+
}
|
|
71
|
+
repairMode(dir, 0o700, '数据目录', warn);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 单文件权限自检修复(0600 面——主库文件/secret.key 共用):不符即 chmod
|
|
75
|
+
* 修复 + warn(best-effort——chmod 失败也 warn 后继续,权限是加固面不是门禁)。
|
|
76
|
+
* @param target 目标路径(须已存在)
|
|
77
|
+
* @param name 告警文案里的目标名
|
|
78
|
+
*/
|
|
79
|
+
export function repairFileMode(target, name, warn) {
|
|
80
|
+
repairMode(target, 0o600, name, warn);
|
|
81
|
+
}
|
|
82
|
+
/** 权限位自检修复共用腿(目录 0700 / 文件 0600 同式) */
|
|
83
|
+
function repairMode(target, mode, name, warn) {
|
|
84
|
+
let bits;
|
|
85
|
+
try {
|
|
86
|
+
bits = statSync(target).mode & 0o777;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return; // 不存在/不可 stat——创建方负责,此处不越权
|
|
90
|
+
}
|
|
91
|
+
if (bits === mode)
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
chmodSync(target, mode);
|
|
95
|
+
warn(`[persist] ${name}权限 ${bits.toString(8)} ≠ ${mode.toString(8)},已修复(${target})`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
warn(`[persist] ${name}权限 ${bits.toString(8)} ≠ ${mode.toString(8)},修复失败:${String(err)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** 数据目录派生位(secret.key 等数据目录内文件的定位面) */
|
|
102
|
+
export function dataFilePath(dataDir, basename) {
|
|
103
|
+
return join(dataDir, basename);
|
|
104
|
+
}
|
|
105
|
+
/** 库文件路径的数据目录推算(备份文件命名等邻接文件定位面) */
|
|
106
|
+
export function databaseDir(dbPath) {
|
|
107
|
+
return dirname(dbPath);
|
|
108
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist — Persistence 门面(05 篇 §6 的进程级编排面)。
|
|
3
|
+
*
|
|
4
|
+
* 组合 Store(物理读写)与 WriteBehind(批落编舞),向 host 装配根暴露:
|
|
5
|
+
* 会话创建/装载/种子创建(§5 fork·导入的物理腿)/ flush 屏障 / close 退出序 /
|
|
6
|
+
* 跨会话查询透传。SessionLog 经 onAppend 回调接入(append 热路径零 I/O——
|
|
7
|
+
* 落库异步在写队列)。
|
|
8
|
+
*
|
|
9
|
+
* 退出钩子(exit 前强制 flush + 失败退出非零码)归 host 装配根(07 篇进程
|
|
10
|
+
* 模型批接线)——本文件只提供 flush/close 两个编舞原语。
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { BaseError } from '../contracts/index.js';
|
|
14
|
+
import { SessionLog } from '../session/index.js';
|
|
15
|
+
import { resolveDataDir, resolveDatabasePath } from './paths.js';
|
|
16
|
+
import { openStore, } from './store.js';
|
|
17
|
+
import { WriteBehind } from './write-behind.js';
|
|
18
|
+
/**
|
|
19
|
+
* Persistence——进程级单例门面(05 §6.1 律 2:进程内多会话共用)。
|
|
20
|
+
*
|
|
21
|
+
* 会话 id 发放纪律(§5.2「不返回幻影 id」的分层落地):
|
|
22
|
+
* - createSession:新开空会话——id 即刻发放但**零持久化承诺**(行随首事件
|
|
23
|
+
* 落库;无事件即无行——空会话本就无可读内容);
|
|
24
|
+
* - createSeededSession:fork/导入种子——**先同步落库再返回 id**(调用方
|
|
25
|
+
* 拿到的 id 必可读——半导入状态不可见,§5.1 flush 前置)。
|
|
26
|
+
*/
|
|
27
|
+
export class Persistence {
|
|
28
|
+
store;
|
|
29
|
+
writeBehind;
|
|
30
|
+
warn;
|
|
31
|
+
/** durable 事件活体镜像(缺省零回调——纯库形态/测试面) */
|
|
32
|
+
onDurableEvent;
|
|
33
|
+
/** 已打开会话的登记快照(onAppend 闭包捕获用——首登身份,与 sessions 行首登同源) */
|
|
34
|
+
registrations = new Map();
|
|
35
|
+
closed = false;
|
|
36
|
+
constructor(store, writeBehind, warn, onDurableEvent) {
|
|
37
|
+
this.store = store;
|
|
38
|
+
this.writeBehind = writeBehind;
|
|
39
|
+
this.warn = warn;
|
|
40
|
+
this.onDurableEvent = onDurableEvent;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 开库并装配(openStore 门禁序 + 写队列接线)。
|
|
44
|
+
* 失败面全部 fail-loud(TOO_NEW/UNRECOGNIZED/密钥不可读——半装配不留残)。
|
|
45
|
+
*/
|
|
46
|
+
static open(options = {}) {
|
|
47
|
+
const warn = options.warn ?? ((message) => console.error(message));
|
|
48
|
+
const store = openStore({
|
|
49
|
+
dbPath: options.dbPath ?? resolveDatabasePath(),
|
|
50
|
+
dataDir: options.dataDir ?? resolveDataDir(),
|
|
51
|
+
migrations: options.migrations,
|
|
52
|
+
warn,
|
|
53
|
+
clock: options.clock,
|
|
54
|
+
secretKey: options.secretKey,
|
|
55
|
+
});
|
|
56
|
+
const writeBehind = new WriteBehind({
|
|
57
|
+
target: store,
|
|
58
|
+
warn,
|
|
59
|
+
clock: options.clock,
|
|
60
|
+
...options.writeBehind,
|
|
61
|
+
});
|
|
62
|
+
return new Persistence(store, writeBehind, warn, options.onDurableEvent ?? (() => undefined));
|
|
63
|
+
}
|
|
64
|
+
ensureOpen() {
|
|
65
|
+
if (this.closed)
|
|
66
|
+
throw new Error('Persistence 已关闭(close 后不得再用——调用序 bug)');
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 新会话创建(零 I/O——行随首事件经写队列落库)。
|
|
70
|
+
* @returns 接好 onAppend 的 SessionLog(append 即入队)
|
|
71
|
+
*/
|
|
72
|
+
createSession(init) {
|
|
73
|
+
this.ensureOpen();
|
|
74
|
+
const sessionId = randomUUID();
|
|
75
|
+
const registration = {
|
|
76
|
+
origin: init.origin,
|
|
77
|
+
parentId: init.parentId,
|
|
78
|
+
seedLength: init.seedLength ?? 0,
|
|
79
|
+
workspaceRoot: init.workspaceRoot,
|
|
80
|
+
title: init.title,
|
|
81
|
+
};
|
|
82
|
+
return this.attachSession(sessionId, [], registration);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* 会话行在场判定(e-4 操控轴幽灵守卫的判据位——loadSession 的零副作用
|
|
86
|
+
* 读面:只查行不 attach、不合成 closer、不建驱动)。
|
|
87
|
+
*/
|
|
88
|
+
hasSession(sessionId) {
|
|
89
|
+
this.ensureOpen();
|
|
90
|
+
return this.store.getSessionRow(sessionId) !== undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 装载既有会话(恢复重放面):events 全量读(撕裂尾 heal 在 Store)→
|
|
94
|
+
* 种子前缀重建 SessionLog → onAppend 接写队列续写。
|
|
95
|
+
* closer 合成(孤儿 tool/未闭合 turn 等,§4)归调用方——在返回的 log 上
|
|
96
|
+
* appendSynthetic 补形即可(合成物同权入日志)。
|
|
97
|
+
* @throws 会话不存在(fail-loud——读未落库 id 属调用方 bug)
|
|
98
|
+
*/
|
|
99
|
+
loadSession(sessionId) {
|
|
100
|
+
this.ensureOpen();
|
|
101
|
+
const row = this.store.getSessionRow(sessionId);
|
|
102
|
+
if (!row) {
|
|
103
|
+
throw new BaseError('PERSIST_DATA_CORRUPT', `会话 ${sessionId} 不存在(读未落库 id 或已删除——调用序检视)`);
|
|
104
|
+
}
|
|
105
|
+
const events = this.store.loadEvents(sessionId);
|
|
106
|
+
const registration = {
|
|
107
|
+
origin: row.origin,
|
|
108
|
+
parentId: row.parentId,
|
|
109
|
+
seedLength: row.seedLength,
|
|
110
|
+
workspaceRoot: row.workspaceRoot,
|
|
111
|
+
title: row.title,
|
|
112
|
+
};
|
|
113
|
+
const log = this.attachSession(sessionId, events, registration);
|
|
114
|
+
return {
|
|
115
|
+
log,
|
|
116
|
+
lineage: row.parentId !== undefined || row.seedLength > 0 || row.origin !== 'conversation'
|
|
117
|
+
? { parentId: row.parentId, seedLength: row.seedLength, origin: row.origin }
|
|
118
|
+
: undefined,
|
|
119
|
+
row,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 种子会话创建(fork/导入的物理腿,§5.0/§5.1):**同步落库**(种子事件 +
|
|
124
|
+
* sessions 行单事务——flush 语义内联,返回 id 必可读)后发放接好续写的
|
|
125
|
+
* SessionLog。
|
|
126
|
+
* @param seed 种子前缀(须为合法种子——ensureSeeded 校验归 session 侧导入闸;
|
|
127
|
+
* 此处只保物理写:seq 从 0 连续)
|
|
128
|
+
* @throws 内存会话(:memory:)→ SESSION_PERSISTENCE_REQUIRED(05 §5.1:
|
|
129
|
+
* fork 语义需要物理新会话,静默降级为拷贝 = 语义谎言)
|
|
130
|
+
*/
|
|
131
|
+
createSeededSession(seed, registration) {
|
|
132
|
+
this.ensureOpen();
|
|
133
|
+
if (this.store.inMemory) {
|
|
134
|
+
throw new BaseError('SESSION_PERSISTENCE_REQUIRED', '内存会话(:memory:)不支持 fork/导入种子——fork 语义需要物理新会话(05 §5.1)');
|
|
135
|
+
}
|
|
136
|
+
const sessionId = randomUUID();
|
|
137
|
+
const full = {
|
|
138
|
+
origin: registration.origin,
|
|
139
|
+
parentId: registration.parentId,
|
|
140
|
+
seedLength: seed.length,
|
|
141
|
+
workspaceRoot: registration.workspaceRoot,
|
|
142
|
+
title: registration.title,
|
|
143
|
+
};
|
|
144
|
+
// 同步落库(绕过写队列——§5.2「不返回幻影 id」:行未提交前不发放 id)
|
|
145
|
+
if (seed.length > 0) {
|
|
146
|
+
this.store.writeEvents(seed.map((event) => ({ sessionId, event, registration: full })));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// 空种子(如 slice 出空段):登记行仍须落(无行 = 幻影 id)
|
|
150
|
+
this.store.registerSessionRow(sessionId, full);
|
|
151
|
+
}
|
|
152
|
+
return this.attachSession(sessionId, seed, full);
|
|
153
|
+
}
|
|
154
|
+
/** 统一装订:SessionLog 构造 + onAppend 写队列接线 + 登记快照入册 */
|
|
155
|
+
attachSession(sessionId, seed, registration) {
|
|
156
|
+
this.registrations.set(sessionId, registration);
|
|
157
|
+
const writeBehind = this.writeBehind;
|
|
158
|
+
const log = new SessionLog({
|
|
159
|
+
sessionId,
|
|
160
|
+
seed,
|
|
161
|
+
lineage: registration.parentId !== undefined || registration.seedLength > 0 || registration.origin !== 'conversation'
|
|
162
|
+
? { parentId: registration.parentId, seedLength: registration.seedLength, origin: registration.origin }
|
|
163
|
+
: undefined,
|
|
164
|
+
onAppend: (event) => {
|
|
165
|
+
writeBehind.enqueue({ sessionId, event, registration });
|
|
166
|
+
// 活体镜像(03 §146——写入后即发;观察者异常隔离不影响提交,append
|
|
167
|
+
// 热路径零 I/O 律不破坏:观察者是内存分派桥非 I/O 面)
|
|
168
|
+
try {
|
|
169
|
+
this.onDurableEvent({ sessionId, event });
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// 镜像观察者故障不波及会话流(双保险腿——dispatch 桥自身另有
|
|
173
|
+
// 监听器互隔离)
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
warn: this.warn,
|
|
177
|
+
});
|
|
178
|
+
return log;
|
|
179
|
+
}
|
|
180
|
+
/** 会话删除(flush 先行——在飞事件落定后再三删同事务,§2.5) */
|
|
181
|
+
async deleteSession(sessionId) {
|
|
182
|
+
this.ensureOpen();
|
|
183
|
+
await this.flush();
|
|
184
|
+
const gone = this.store.deleteSession(sessionId);
|
|
185
|
+
this.registrations.delete(sessionId);
|
|
186
|
+
return gone;
|
|
187
|
+
}
|
|
188
|
+
/** flush 屏障透传(05 §4——审批落账/压缩对/turn 收口等关键事务点调用) */
|
|
189
|
+
async flush() {
|
|
190
|
+
this.ensureOpen();
|
|
191
|
+
await this.writeBehind.flush();
|
|
192
|
+
}
|
|
193
|
+
/** 跨会话查询透传(05 §3.4——obs/goal/CLI 导出的底层原语) */
|
|
194
|
+
queryEvents(filter) {
|
|
195
|
+
this.ensureOpen();
|
|
196
|
+
return this.store.queryEvents(filter);
|
|
197
|
+
}
|
|
198
|
+
/** 标题更新透传(auto-title 面) */
|
|
199
|
+
updateSessionTitle(sessionId, title) {
|
|
200
|
+
this.ensureOpen();
|
|
201
|
+
return this.store.updateSessionTitle(sessionId, title);
|
|
202
|
+
}
|
|
203
|
+
/** 会话列表透传(「按 cwd 取最新会话」选取面) */
|
|
204
|
+
listSessions(options = {}) {
|
|
205
|
+
this.ensureOpen();
|
|
206
|
+
return this.store.listSessions(options);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 会话内全文检索透传(05 §9 首发口径:查询面限定 session_id——跨会话检索
|
|
210
|
+
* 非首发面)。**flush 先行归调用方编排**(write-behind 在飞事件不进 FTS
|
|
211
|
+
* 索引,检索前须屏障);命中返回 seq 升序列表(limit 缺省 50)。
|
|
212
|
+
*/
|
|
213
|
+
searchSessionFts(sessionId, pattern, limit) {
|
|
214
|
+
this.ensureOpen();
|
|
215
|
+
return this.store.searchSessionFts(sessionId, pattern, limit);
|
|
216
|
+
}
|
|
217
|
+
/** 关库退出序:flush(失败即抛——调用方转非零退出,05 §6.3#6)→ checkpoint → close */
|
|
218
|
+
async close() {
|
|
219
|
+
if (this.closed)
|
|
220
|
+
return;
|
|
221
|
+
await this.flush();
|
|
222
|
+
this.closed = true;
|
|
223
|
+
this.store.close();
|
|
224
|
+
}
|
|
225
|
+
/** 只读判别(诊断面) */
|
|
226
|
+
get inMemory() {
|
|
227
|
+
return this.store.inMemory;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist — v1 基线 schema(05 篇 §9 表清单的 DDL 真源)。
|
|
3
|
+
*
|
|
4
|
+
* 七对象:events / sessions / store_state / credentials / model_catalog /
|
|
5
|
+
* session_fts(FTS5 虚表)/ persist_incidents(毒丸 durable 标记,§6.3)。
|
|
6
|
+
* 列级以 05 §9 表为准绳;本文件是唯一 DDL 事实源(迁移链增量归 migrations
|
|
7
|
+
* 注册面,宿主聚合 core: 插件表族声明后并入)。
|
|
8
|
+
*
|
|
9
|
+
* 身份印记:application_id 写入恒定值(仅 forensics 用途——门禁走 user_version
|
|
10
|
+
* 版本链 + 空库判定,05 §6.4;不为它立第二道门)。
|
|
11
|
+
*/
|
|
12
|
+
/** 库身份印记('bage' ASCII——数据值位非标识符;门禁不消费,仅助外部 forensics) */
|
|
13
|
+
export const APPLICATION_ID = 0x6261_6765;
|
|
14
|
+
/** 基线 schema 版本(迁移链起点——v1 即首版六表 + 毒丸标记表,05 §6.4「从 1 起」) */
|
|
15
|
+
export const SCHEMA_VERSION = 1;
|
|
16
|
+
/**
|
|
17
|
+
* v1 基线 DDL(全新库单事务一次到位——冷启动原子性,05 §6.5)。
|
|
18
|
+
* 注释里的中文段说明不参与任何比对(本仓 v1 无指纹比对门禁,版本链即门禁)。
|
|
19
|
+
*/
|
|
20
|
+
export const CANONICAL_DDL = `
|
|
21
|
+
-- ── events:会话事件日志物理表(append-only;主键 = 会话内 seq 连续律载体)──
|
|
22
|
+
-- type 索引服务 eventsOfType 面(05 §9 表注);时间维查询 v1 不建索引
|
|
23
|
+
-- (单人量级顺序扫毫秒级,真实量级再加——承 berry 同款裁决)。
|
|
24
|
+
CREATE TABLE events (
|
|
25
|
+
session_id TEXT NOT NULL,
|
|
26
|
+
seq INTEGER NOT NULL,
|
|
27
|
+
type TEXT NOT NULL,
|
|
28
|
+
time INTEGER NOT NULL,
|
|
29
|
+
data TEXT NOT NULL, -- JSON 快照(写入侧已冻结的拷贝序列化)
|
|
30
|
+
ignorable INTEGER NOT NULL DEFAULT 0, -- 读侧向前兼容位(0/1)
|
|
31
|
+
surface_op TEXT, -- 遮蔽指令 JSON(仅改历史事件携带)
|
|
32
|
+
source_event_seqs TEXT, -- 溯源 seq 数组 JSON(仅遮蔽指令携带)
|
|
33
|
+
PRIMARY KEY (session_id, seq)
|
|
34
|
+
) STRICT;
|
|
35
|
+
CREATE INDEX idx_events_type ON events(type);
|
|
36
|
+
|
|
37
|
+
-- ── sessions:会话登记行(血缘三元组 + 工作区选取键;无归属列——会话直归
|
|
38
|
+
-- agent,05 §0;行由 write-behind 首写登记,血缘首登为准不回改)────────────
|
|
39
|
+
CREATE TABLE sessions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
title TEXT, -- 标题(缺省 NULL;会话列表呈现面消费)
|
|
42
|
+
origin TEXT NOT NULL, -- 'conversation' | 'delegation' | 'import' | 'fork' | 'trigger'
|
|
43
|
+
parent_id TEXT, -- 血缘父会话(根会话 NULL)
|
|
44
|
+
seed_length INTEGER NOT NULL DEFAULT 0, -- 种子前缀长度(fork/导入切片)
|
|
45
|
+
workspace_root TEXT, -- 工作区根(按 cwd 取最新会话的选取键)
|
|
46
|
+
created_at INTEGER NOT NULL, -- 毫秒时间戳
|
|
47
|
+
updated_at INTEGER NOT NULL, -- 最后写批时刻(首写登记、批写推进)
|
|
48
|
+
last_seq INTEGER NOT NULL DEFAULT 0 -- 已落库最大 seq(批写推进)
|
|
49
|
+
) STRICT;
|
|
50
|
+
CREATE INDEX idx_sessions_workspace ON sessions(workspace_root);
|
|
51
|
+
|
|
52
|
+
-- ── store_state:插件持久键值统一面(03 篇受理制写面——受理执法在装载层,
|
|
53
|
+
-- 物理层只供表与治理:LRU 256 帽 + ttl 过期清扫,05 §6.2)──────────────────
|
|
54
|
+
CREATE TABLE store_state (
|
|
55
|
+
key TEXT PRIMARY KEY,
|
|
56
|
+
value TEXT NOT NULL, -- JSON 值
|
|
57
|
+
expires_at INTEGER, -- ttl 到期时刻(NULL = 永不过期)
|
|
58
|
+
kind TEXT NOT NULL DEFAULT 'kv', -- 值类别标签(消费方自释义)
|
|
59
|
+
last_accessed_at INTEGER NOT NULL -- 读写触达即刷新(LRU 逐出依据)
|
|
60
|
+
) STRICT;
|
|
61
|
+
|
|
62
|
+
-- ── credentials:凭证表(api_key 列 AES-256-GCM 密文;0600 纪律见 openStore)──
|
|
63
|
+
CREATE TABLE credentials (
|
|
64
|
+
provider TEXT PRIMARY KEY,
|
|
65
|
+
api_key TEXT NOT NULL, -- v1:<base64(iv|tag|ct)>(secret-box 加密产物)
|
|
66
|
+
meta TEXT, -- JSON 附加元数据(可空)
|
|
67
|
+
updated_at INTEGER NOT NULL
|
|
68
|
+
) STRICT;
|
|
69
|
+
|
|
70
|
+
-- ── model_catalog:模型目录(06/07 篇消费;物理 CRUD 面在本模块)──────────────
|
|
71
|
+
CREATE TABLE model_catalog (
|
|
72
|
+
id TEXT PRIMARY KEY, -- 模型全名(provider/model 形)
|
|
73
|
+
provider TEXT NOT NULL,
|
|
74
|
+
label TEXT, -- 呈现名(可空)
|
|
75
|
+
meta TEXT, -- JSON 元数据(上下文窗/定价等,可空)
|
|
76
|
+
updated_at INTEGER NOT NULL
|
|
77
|
+
) STRICT;
|
|
78
|
+
CREATE INDEX idx_model_catalog_provider ON model_catalog(provider);
|
|
79
|
+
|
|
80
|
+
-- ── session_fts:全文搜索索引(FTS5;派生索引可整体重建——对账三档,05 §9)──
|
|
81
|
+
-- tokenize=trigram:中英混排 substring 检索;session_id/seq UNINDEXED——MATCH
|
|
82
|
+
-- 只打 body,命中行携带定位(会话内检索查询面限定 session_id 过滤,首发口径)。
|
|
83
|
+
CREATE VIRTUAL TABLE session_fts USING fts5(
|
|
84
|
+
session_id UNINDEXED,
|
|
85
|
+
seq UNINDEXED,
|
|
86
|
+
body,
|
|
87
|
+
tokenize='trigram'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
-- ── persist_incidents:毒丸隔离 durable 标记(§6.3——物理层自账,只增不删)──
|
|
91
|
+
CREATE TABLE persist_incidents (
|
|
92
|
+
id INTEGER PRIMARY KEY,
|
|
93
|
+
time INTEGER NOT NULL,
|
|
94
|
+
session_id TEXT, -- 被隔离事件所属会话(可空:非事件类故障)
|
|
95
|
+
seq INTEGER, -- 被隔离事件 seq
|
|
96
|
+
type TEXT, -- 被隔离事件类型
|
|
97
|
+
reason TEXT NOT NULL -- 违例原因(SQLite 码 + 消息)
|
|
98
|
+
) STRICT;
|
|
99
|
+
`;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* persist — 凭证密文盒(05 篇 §9 credentials 行加密细则,2026-09-05 persist
|
|
3
|
+
* 落码批补细则的落码面)。
|
|
4
|
+
*
|
|
5
|
+
* AES-256-GCM:
|
|
6
|
+
* - 密钥 = 数据目录 secret.key(32 字节随机,0600,缺失自动生成——首启自举);
|
|
7
|
+
* - 密文自描述 `v1:<base64(iv|tag|ct)>`(iv 12 字节随机 / tag 16 字节 / ct 明文等长);
|
|
8
|
+
* - 解密失败(密钥丢失/不匹配/密文损坏)→ fail-loud `PERSIST_SECRET_UNREADABLE`
|
|
9
|
+
* ——重录凭证即恢复(旧密文作废,不静默降级为明文存储)。
|
|
10
|
+
*/
|
|
11
|
+
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { BaseError } from '../contracts/index.js';
|
|
14
|
+
import { dataFilePath, repairFileMode } from './paths.js';
|
|
15
|
+
/** 密钥文件名(数据目录内) */
|
|
16
|
+
export const SECRET_KEY_BASENAME = 'secret.key';
|
|
17
|
+
/** 密钥长度(AES-256 位) */
|
|
18
|
+
const SECRET_KEY_LENGTH = 32;
|
|
19
|
+
/** GCM 推荐 iv 长度(12 字节) */
|
|
20
|
+
const IV_LENGTH = 12;
|
|
21
|
+
/** GCM 认证标签长度(16 字节) */
|
|
22
|
+
const TAG_LENGTH = 16;
|
|
23
|
+
/** 密文版本前缀(自描述格式——未来算法升级走 v2 前缀,v1 读侧永在) */
|
|
24
|
+
const BOX_PREFIX = 'v1:';
|
|
25
|
+
/**
|
|
26
|
+
* 加载或生成数据密钥(幂等自举):
|
|
27
|
+
* - 在场:读出并校验长度(长度异常 = 密钥文件损坏 → fail-loud,宁拒勿猜);
|
|
28
|
+
* - 缺席:randomBytes(32) 落盘(0600——写入即带权限,勿先宽后收留窗口)。
|
|
29
|
+
* @param dataDir 数据目录(须已存在——ensureDataDir 前置)
|
|
30
|
+
* @param warn 权限修复告警面(0600 自检修复 + warn,05 §6.6 同款纪律)
|
|
31
|
+
* @returns 32 字节密钥
|
|
32
|
+
*/
|
|
33
|
+
export function loadOrCreateSecretKey(dataDir, warn) {
|
|
34
|
+
const keyPath = dataFilePath(dataDir, SECRET_KEY_BASENAME);
|
|
35
|
+
if (existsSync(keyPath)) {
|
|
36
|
+
const key = readFileSync(keyPath);
|
|
37
|
+
if (key.length !== SECRET_KEY_LENGTH) {
|
|
38
|
+
throw new BaseError('PERSIST_SECRET_UNREADABLE', `密钥文件 ${keyPath} 长度 ${key.length} ≠ ${SECRET_KEY_LENGTH} 字节(损坏或手编)——移除该文件并重录凭证可恢复(旧密文作废)`);
|
|
39
|
+
}
|
|
40
|
+
repairFileMode(keyPath, '密钥文件', warn);
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
const key = randomBytes(SECRET_KEY_LENGTH);
|
|
44
|
+
// mode 0600 在创建时即生效(open(2) O_CREAT 权限位——umask 只会收紧不会放宽)
|
|
45
|
+
writeFileSync(keyPath, key, { mode: 0o600 });
|
|
46
|
+
return key;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 明文 → 自描述密文(`v1:<base64(iv|tag|ct)>`)。每次加密随机 iv——同明文
|
|
50
|
+
* 两次加密密文不同(GCM nonce 复用是灾难,结构性排除)。
|
|
51
|
+
* @param key 32 字节密钥(loadOrCreateSecretKey 产物)
|
|
52
|
+
* @param plaintext 凭证明文(api_key 等)
|
|
53
|
+
*/
|
|
54
|
+
export function encryptSecret(key, plaintext) {
|
|
55
|
+
const iv = randomBytes(IV_LENGTH);
|
|
56
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
57
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
58
|
+
const tag = cipher.getAuthTag();
|
|
59
|
+
return BOX_PREFIX + Buffer.concat([iv, tag, ciphertext]).toString('base64');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 自描述密文 → 明文。任何一步失败(前缀不认识/base64 损坏/认证标签不过)
|
|
63
|
+
* 统一抛 `PERSIST_SECRET_UNREADABLE`——调用方对用户的话术是「重录凭证」。
|
|
64
|
+
* @param key 32 字节密钥
|
|
65
|
+
* @param boxed encryptSecret 产物(或其存储态)
|
|
66
|
+
*/
|
|
67
|
+
export function decryptSecret(key, boxed) {
|
|
68
|
+
const fail = (detail) => new BaseError('PERSIST_SECRET_UNREADABLE', `凭证密文不可解读(${detail})——密钥丢失或不匹配;重录凭证即恢复`);
|
|
69
|
+
if (!boxed.startsWith(BOX_PREFIX))
|
|
70
|
+
throw fail('密文缺 v1: 版本前缀');
|
|
71
|
+
let packed;
|
|
72
|
+
try {
|
|
73
|
+
packed = Buffer.from(boxed.slice(BOX_PREFIX.length), 'base64');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw fail('base64 损坏');
|
|
77
|
+
}
|
|
78
|
+
if (packed.length < IV_LENGTH + TAG_LENGTH + 1)
|
|
79
|
+
throw fail('密文长度不足');
|
|
80
|
+
const iv = packed.subarray(0, IV_LENGTH);
|
|
81
|
+
const tag = packed.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
82
|
+
const ciphertext = packed.subarray(IV_LENGTH + TAG_LENGTH);
|
|
83
|
+
try {
|
|
84
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
85
|
+
decipher.setAuthTag(tag);
|
|
86
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
throw fail(`认证失败:${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** 测试/诊断面:生成临时内存密钥(:memory: 形态的凭证面用——库亡密亡) */
|
|
93
|
+
export function ephemeralSecretKey() {
|
|
94
|
+
return randomBytes(SECRET_KEY_LENGTH);
|
|
95
|
+
}
|