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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 tools — 三段 waterfall 管道(04 §7:schema → 守门 → 执行,工具执行唯一合法路径)。
|
|
3
|
+
*
|
|
4
|
+
* 结构(前三步是硬编舞、钩子是同词 waterfall 监听者——safety 守门行与插件
|
|
5
|
+
* 钩子同链,监听者可追加;守门段由 safety 件占首位):
|
|
6
|
+
*
|
|
7
|
+
* 0. schema 段(前置步,非钩子段)——typebox Value.Check 不过即拒
|
|
8
|
+
* TOOL_INVALID_ARGS(可纠正错误回执),不进守门/执行段;
|
|
9
|
+
* 1. tools_pre_execute 守门段——决策词汇 allow / block{reason}(短路)/
|
|
10
|
+
* mutate{args}(就地改参);监听器抛错 = fail-closed 视同 block
|
|
11
|
+
* (TOOL_GATE_FAILED——先落 gate/decision 再拒,审计链不断头);
|
|
12
|
+
* 2. tools_execute 执行段——around-dispatch(载荷 = 执行闭包,监听者包
|
|
13
|
+
* 装后经 next 传播);链尾缺省实现 = timeoutMs 预算竞速 + 调 execute
|
|
14
|
+
* (超时 TOOL_TIMEOUT——取消传播靠 signal,不强杀 promise);
|
|
15
|
+
* 3. tools_post_execute 后处理段——可就地改写 result(裁剪/isError 改写);
|
|
16
|
+
* 链尾固定步 = 输出护栏(64KiB 保尾截断 + 全文外溢临时文件——04 §7
|
|
17
|
+
* 执行段细则:护栏是管道属性,全部工具受益;只钳文本 content)。
|
|
18
|
+
*
|
|
19
|
+
* 失败统一抛 BaseError(message 首缀 `[CODE]`——02 §5.3 #4:错误信息 =
|
|
20
|
+
* code + 人读 message,durable 结果里码可见可序列化);loop 工具批的
|
|
21
|
+
* executeOne try/catch 把异常包装成 isError 结果(错误是数据面)。
|
|
22
|
+
*
|
|
23
|
+
* gate/decision durable 事件经注入 sink 落日志(装配根接 session.append;
|
|
24
|
+
* tools 不依赖 session——DAG 单向,04 §7 段 2)。
|
|
25
|
+
*/
|
|
26
|
+
import { Value } from 'typebox/value';
|
|
27
|
+
import { writeFile } from 'node:fs/promises';
|
|
28
|
+
import { tmpdir } from 'node:os';
|
|
29
|
+
import { randomBytes } from 'node:crypto';
|
|
30
|
+
import { BaseError } from '../contracts/index.js';
|
|
31
|
+
import { redactToolResultExit } from '../contracts/index.js';
|
|
32
|
+
import { TOOL_EXECUTE_EVENT, TOOL_POST_EXECUTE_EVENT, TOOL_PRE_EXECUTE_EVENT } from '../contracts/index.js';
|
|
33
|
+
/** 组装 `[CODE] message` 形态的错误文本(码随 message 进入工具结果——02 §5.3 #4) */
|
|
34
|
+
function codedMessage(code, message) {
|
|
35
|
+
return `[${code}] ${message}`;
|
|
36
|
+
}
|
|
37
|
+
/** 缺省输出护栏预算(64KiB——04 §7 执行段细则:输出护栏是管道属性) */
|
|
38
|
+
export const OUTPUT_GUARD_BYTES = 64 * 1024;
|
|
39
|
+
/** spill 文件序号(模块级自增——辅助段;唯一性主载体是随机前缀) */
|
|
40
|
+
let spillSeq = 0;
|
|
41
|
+
/**
|
|
42
|
+
* 取字节缓冲尾部至多 maxBytes 字节(UTF-8 安全:起点落在多字节字符中间则
|
|
43
|
+
* 前移过续字节——不产 U+FFFD 乱码尾巴)。
|
|
44
|
+
*/
|
|
45
|
+
function tailBytes(buf, maxBytes) {
|
|
46
|
+
if (maxBytes <= 0)
|
|
47
|
+
return '';
|
|
48
|
+
let start = Math.max(0, buf.length - maxBytes);
|
|
49
|
+
while (start > 0 && start < buf.length && (buf[start] & 0xc0) === 0x80)
|
|
50
|
+
start++;
|
|
51
|
+
return buf.subarray(start).toString('utf8');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 输出护栏(固定链尾步):文本 content 合计超帽保尾截断 + 全文外溢写系统
|
|
55
|
+
* 临时目录(spill 路径进截断注记——模型可循路径自取全文)。
|
|
56
|
+
* - 只钳文本 content;图片等其余 content 原样保留(自有界);
|
|
57
|
+
* - spill 写失败降级为仅截断(不抛——护栏自身故障不该炸掉正常结果);
|
|
58
|
+
* - 后处理监听器若已改写 result,护栏对改写后结果同样执法(监听器先于固定步)。
|
|
59
|
+
*/
|
|
60
|
+
async function applyOutputGuard(result, toolCallId, guardBytes) {
|
|
61
|
+
const textParts = result.content.filter((part) => part.type === 'text');
|
|
62
|
+
const full = textParts.map((part) => part.text).join('\n');
|
|
63
|
+
const totalBytes = Buffer.byteLength(full, 'utf8');
|
|
64
|
+
if (totalBytes <= guardBytes)
|
|
65
|
+
return;
|
|
66
|
+
// spill 全文(尽力而为):文件名只留安全字符,防 callId 形态未知的路径注入;
|
|
67
|
+
// 铸名 = 随机前缀 + 独占写 wx + 0600(04 §7 2026-09-14 spill 安全硬化三件)——
|
|
68
|
+
// 随机前缀使种植者不可预测文件名,wx 即便撞名也不跟随既有 symlink(种植位
|
|
69
|
+
// 结构性不可用),0600 独占读(工具输出可能含敏感面,世界可读即泄漏面)
|
|
70
|
+
spillSeq += 1;
|
|
71
|
+
const safeCallId = toolCallId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
72
|
+
const nonce = randomBytes(6).toString('hex');
|
|
73
|
+
const spillPath = `${tmpdir()}/tool-output-${nonce}-${safeCallId}-${spillSeq}.txt`;
|
|
74
|
+
let spilled = true;
|
|
75
|
+
try {
|
|
76
|
+
await writeFile(spillPath, full, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
spilled = false; // tmp 满/EEXIST 撞名等故障:截断照做,路径不承诺
|
|
80
|
+
}
|
|
81
|
+
const tail = tailBytes(Buffer.from(full, 'utf8'), guardBytes);
|
|
82
|
+
const note = `\n\n[输出 ${totalBytes} 字节超 ${guardBytes} 字节上限,已保尾截断${spilled ? `;全文外溢至 ${spillPath}(可用 read/grep 从外溢文件取段)` : ''}]`;
|
|
83
|
+
result.content = [...result.content.filter((part) => part.type !== 'text'), { type: 'text', text: `${tail}${note}` }];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 创建工具执行管道(装配根一次性创建;注册表把每个 ToolDefinition 的执行
|
|
87
|
+
* 接到本管道——toAgentTool 包装)。守门段 mutate 语义依赖「可变入参就地
|
|
88
|
+
* 改写」:gateInput 对象对整条链固定,守门者改 gateInput.args 即改执行段
|
|
89
|
+
* 所见参数(mutated 旗由改参者维护,落账 decision 判据)。
|
|
90
|
+
*/
|
|
91
|
+
export function createToolPipeline(dispatch, opts = {}) {
|
|
92
|
+
const defaultTimeoutMs = opts.defaultTimeoutMs ?? 60_000;
|
|
93
|
+
const guardBytes = opts.outputGuardBytes ?? OUTPUT_GUARD_BYTES;
|
|
94
|
+
const recordGate = opts.onGateDecision ?? (() => { });
|
|
95
|
+
const sensitiveValues = opts.sensitiveValues ?? (() => []);
|
|
96
|
+
return async function runToolPipeline(def, toolCallId, args, signal, onUpdate, sessionId) {
|
|
97
|
+
/* ---- 前置步:参数 schema 校验(typebox Value;不合法不进守门) ---- */
|
|
98
|
+
if (!Value.Check(def.parameters, args)) {
|
|
99
|
+
const problems = [...Value.Errors(def.parameters, args)]
|
|
100
|
+
.slice(0, 5)
|
|
101
|
+
.map((e) => `${e.instancePath || '(root)'} ${e.message}`)
|
|
102
|
+
.join(';');
|
|
103
|
+
throw new BaseError('TOOL_INVALID_ARGS', codedMessage('TOOL_INVALID_ARGS', `工具 ${def.name} 参数校验失败:${problems}`));
|
|
104
|
+
}
|
|
105
|
+
/* ---- 第一段:守门(fail-closed;block 短路不进执行段) ---- */
|
|
106
|
+
// sessionId 透传进守门载荷(04 §7 批 15d 补注——checkpoint 按会话判 per-run)
|
|
107
|
+
const gateInput = {
|
|
108
|
+
tool: def,
|
|
109
|
+
args,
|
|
110
|
+
toolCallId,
|
|
111
|
+
mutated: false,
|
|
112
|
+
...(signal !== undefined ? { signal } : {}),
|
|
113
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
114
|
+
};
|
|
115
|
+
let gated;
|
|
116
|
+
try {
|
|
117
|
+
gated = await dispatch.waterfall(TOOL_PRE_EXECUTE_EVENT, gateInput);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
// fail-closed:守门监听器自身异常 = 视为 block,绝不放行;先落决策再抛
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
recordGate({ toolCallId, decision: 'block', reason: codedMessage('TOOL_GATE_FAILED', message) });
|
|
123
|
+
throw new BaseError('TOOL_GATE_FAILED', codedMessage('TOOL_GATE_FAILED', `守门检查失败(fail-closed 拒绝):${message}`));
|
|
124
|
+
}
|
|
125
|
+
if (gated.outcome?.action === 'block') {
|
|
126
|
+
// block 短路:结构化拒绝(含 reason)经 throw 交 loop 编码 isError 结果返回模型
|
|
127
|
+
recordGate({ toolCallId, decision: 'block', reason: gated.outcome.reason });
|
|
128
|
+
throw new BaseError('TOOL_BLOCKED', codedMessage('TOOL_BLOCKED', gated.outcome.reason));
|
|
129
|
+
}
|
|
130
|
+
// 放行/改参:mutated 旗由改参的守门者维护,汇总进 durable 决策;allowReason
|
|
131
|
+
// = 放行来源标注(策略表 allow 免问命中置 policy-allow:<条目序>——04 §9 命中审计)
|
|
132
|
+
recordGate({ toolCallId, decision: gated.mutated ? 'mutate' : 'allow', reason: gated.allowReason ?? 'ok' });
|
|
133
|
+
/* ---- 第二段:执行(around-dispatch;链尾缺省实现 = 超时预算 + execute) ---- */
|
|
134
|
+
// 载荷 = 执行闭包:监听者包装闭包经 next 传播(超时预算/重试/指标类包装位)
|
|
135
|
+
const timedExecute = async () => {
|
|
136
|
+
const timeoutMs = def.timeoutMs ?? defaultTimeoutMs;
|
|
137
|
+
const toolCtx = {
|
|
138
|
+
toolCallId,
|
|
139
|
+
...(signal !== undefined ? { signal } : {}),
|
|
140
|
+
...(onUpdate !== undefined ? { onUpdate } : {}),
|
|
141
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
142
|
+
};
|
|
143
|
+
const execOwned = () => def.execute(gated.args, toolCtx);
|
|
144
|
+
// 预算竞速:超时先到即抛 TOOL_TIMEOUT(原 execute 继续跑但结果弃置——
|
|
145
|
+
// loop 侧 accepting 护栏忽略其迟到 onUpdate;取消传播靠 signal,不强杀)
|
|
146
|
+
let timer;
|
|
147
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
148
|
+
timer = setTimeout(() => reject(new BaseError('TOOL_TIMEOUT', codedMessage('TOOL_TIMEOUT', `工具 ${def.name} 执行超时(>${timeoutMs}ms)`))), timeoutMs);
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
return await Promise.race([execOwned(), timeoutPromise]);
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const wrapped = await dispatch.waterfall(TOOL_EXECUTE_EVENT, timedExecute);
|
|
158
|
+
const result = await wrapped();
|
|
159
|
+
/* ---- 第三段:后处理(可就地改写 result)+ 出口消毒 + 固定链尾输出护栏 ---- */
|
|
160
|
+
const postInput = { tool: def, args: gated.args, toolCallId, result };
|
|
161
|
+
const posted = await dispatch.waterfall(TOOL_POST_EXECUTE_EVENT, postInput);
|
|
162
|
+
// 出口治理③ 凭据消毒(04 §7 定形①:护栏之前一步——消毒先于截断,截断边界
|
|
163
|
+
// 不劈秘密对;spill 外溢文件同持消毒产物)。provider 故障只降级值基腿
|
|
164
|
+
// (模式腿恒在场——消毒是出口护栏非执法门,库坏不该炸掉正常结果)。
|
|
165
|
+
let secretValues = [];
|
|
166
|
+
try {
|
|
167
|
+
secretValues = sensitiveValues();
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* provider 读取故障 → 空值表 = 纯模式执法(降级诚实) */
|
|
171
|
+
}
|
|
172
|
+
redactToolResultExit(posted.result, secretValues);
|
|
173
|
+
await applyOutputGuard(posted.result, toolCallId, guardBytes);
|
|
174
|
+
return posted.result;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 tools — 敏感件读侧 carve-out 判定件(04 §7 读侧条 + 2026-09-08 P0① 落码定形③④)。
|
|
3
|
+
*
|
|
4
|
+
* 独立小文件、不 import fs.ts(防 fs → protected-read → fs 环——模块内文件
|
|
5
|
+
* 级依赖只许单向;search.ts 对 fs.ts 的 canonicalize 引用同理合法)。两判据:
|
|
6
|
+
* - 路径判(open 前):目标 canonical 路径命中敏感件集 = FS_READ_PROTECTED
|
|
7
|
+
* 硬拒——fail-closed、无审批出路、不登记观察(拒读不构成「看过」);deny
|
|
8
|
+
* 检查先于存在性检查(缺席同拒同文案——不暴露敏感件存在性差异);
|
|
9
|
+
* - inode 判(open 后):对已打开句柄 fstat 的 (dev, ino) 与敏感件集 stat
|
|
10
|
+
* 比对,命中 = 同硬拒——一判收口两攻击面(硬链别名:canonical 路径不同而
|
|
11
|
+
* inode 相同;TOCTOU 换靶:canonicalize 与 open 之间路径组件被换)。
|
|
12
|
+
*
|
|
13
|
+
* 敏感集 = 装配层注入 provider(() => canonical 绝对路径数组;缺省空集 =
|
|
14
|
+
* 判定 no-op)。tools 不 import safety(DAG 边表),与 fence 的
|
|
15
|
+
* writableRoots 同注入形——数据在装配层,判定语义在工具件。
|
|
16
|
+
*/
|
|
17
|
+
import { stat } from 'node:fs/promises';
|
|
18
|
+
import { BaseError } from '../contracts/index.js';
|
|
19
|
+
/** 保护面硬拒(两判据共用回执——同码同文案,不区分命中路径判还是 inode 判) */
|
|
20
|
+
function rejectProtected(abs) {
|
|
21
|
+
throw new BaseError('FS_READ_PROTECTED', `[FS_READ_PROTECTED] 读目标属敏感件保护面:${abs}(04 §7 读侧 carve-out——密钥与免问面恒不可读,fail-closed 无审批出路)`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* open 前路径判:canonical 目标命中敏感集 = 硬拒(不登记观察)。比对语义 =
|
|
25
|
+
* canonical 路径相等(敏感件是具体文件不是目录域——无前缀匹配面)。
|
|
26
|
+
*/
|
|
27
|
+
export function rejectProtectedReadPath(canonical, protectedReadFiles) {
|
|
28
|
+
for (const p of protectedReadFiles()) {
|
|
29
|
+
if (canonical === p)
|
|
30
|
+
rejectProtected(canonical);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* open 后 inode 判:已打开句柄的 fstat 产物与敏感件集逐件 stat 比对,
|
|
35
|
+
* (dev, ino) 双等 = 同硬拒。敏感件自身缺席(stat ENOENT)= 跳过(无可比对
|
|
36
|
+
* 象;保护面不虚构)。空敏感集零 stat(缺省装配 no-op 无开销)。
|
|
37
|
+
*
|
|
38
|
+
* @param abs 用户可见的请求路径(回执文案用——真实判据是 inode 不是路径)
|
|
39
|
+
* @param opened 打开句柄的 fstat 产物(`open` 已把路径解析钉死到具体 inode
|
|
40
|
+
* ——比对的正是「真正打开的那个」,而非再次路径解析的快照)
|
|
41
|
+
*/
|
|
42
|
+
export async function assertInodeNotProtected(abs, opened, protectedReadFiles) {
|
|
43
|
+
const paths = protectedReadFiles();
|
|
44
|
+
if (paths.length === 0)
|
|
45
|
+
return; // 敏感集缺席(诊断形/未注入)——判定 no-op
|
|
46
|
+
for (const p of paths) {
|
|
47
|
+
// 敏感件 stat:读失败(含缺席/权限)一律跳过——保护面只按在场可比对象执法
|
|
48
|
+
const st = await stat(p).catch(() => undefined);
|
|
49
|
+
if (st === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (st.dev === opened.dev && st.ino === opened.ino)
|
|
52
|
+
rejectProtected(abs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L2 tools — 工具注册表两层(04 §7:全局层 + 驱动层;03 §2.2 ctx.tools.register 落形)。
|
|
3
|
+
*
|
|
4
|
+
* 两层(2026-09-04 立项改造裁决:承 berry 三层删应用域分片——插件工具进
|
|
5
|
+
* 全局层,无第三层):
|
|
6
|
+
* - 全局层:进程单例 Map(宿主固定件工具 + 插件注册的工具——全部会话可见);
|
|
7
|
+
* - 驱动层:sessionId → Map(会话态工具面——存在理由是会话态状态〔fs 观察
|
|
8
|
+
* 态 per-driver / bash 升权闭包〕,不是域隔离)。
|
|
9
|
+
* - 工具面按会话解析:listFor(sessionId) = 全局层 ∪ 驱动层[sessionId]。
|
|
10
|
+
*
|
|
11
|
+
* 查重碰撞域(「任何单一组合面内不得双名」):全局层注册查「全局 ∪ 全部
|
|
12
|
+
* 驱动层」(全局工具进一切面——与任何驱动层条目在任何面都共现);驱动层[s]
|
|
13
|
+
* 注册查「全局 ∪ 驱动层[s]」;跨驱动层同名合法(永不同面——fs 四名每会话
|
|
14
|
+
* 一套即靠此)。03 §2.7「无论哪层哪来源双向对称」按此碰撞域执法。
|
|
15
|
+
*
|
|
16
|
+
* 注册面防线四道(任何来源同一执法——官方件同受管,防线在树上不在审查上):
|
|
17
|
+
* ①描述注入模式扫描(03 §2.8)→ TOOL_DESCRIPTION_REJECTED;
|
|
18
|
+
* ②parameters 根 object 断言 → TOOL_SCHEMA_INVALID;
|
|
19
|
+
* ③timeoutMs <= 0 拒(正数过小钳至下限——微小预算 = 变相自杀钟,钳而非拒);
|
|
20
|
+
* ④双帽(03 §3.4):两层合计 10^3 总量帽 TOOL_REGISTRY_CAPACITY +
|
|
21
|
+
* register/unregister 令牌桶(容量 240、回填 600/分钟,全局键)
|
|
22
|
+
* TOOL_CHANGE_RATE_LIMITED——可用性防线非安全边界。
|
|
23
|
+
*
|
|
24
|
+
* tools_change 事件(03 §2.4 主表 emit 面):每次注册/注销发射
|
|
25
|
+
* {kind: 'register'|'unregister', name, driver?}——loop 工具快照活数组原位
|
|
26
|
+
* 替换的接线源(04 §4 装配接线义务,消费在装配根)。
|
|
27
|
+
*/
|
|
28
|
+
import { BaseError } from '../contracts/index.js';
|
|
29
|
+
import { TOOLS_CHANGE_EVENT } from '../contracts/index.js';
|
|
30
|
+
/** 注册表两层合计件数帽(03 §3.4 总量帽——良性行为距阈值两个数量级,超限 = 失控或泄漏) */
|
|
31
|
+
const REGISTRY_TOTAL_LIMIT = 1_000;
|
|
32
|
+
/**
|
|
33
|
+
* register/unregister 变更频率桶参数(03 §3.4):容量 240 吃下 boot + 两连
|
|
34
|
+
* 重载的合法重注册序列;回填 600/分钟 = 10 op/s 持续供给,热迭代不触顶、
|
|
35
|
+
* 武器化持续注册耗尽容量即拦。全局键——registry 无 scope 概念。
|
|
36
|
+
*/
|
|
37
|
+
const REGISTRY_RATE = { capacity: 240, perMinute: 600 };
|
|
38
|
+
/** timeoutMs 下限(正数过小钳至此——500ms 类微小预算 = 变相自杀钟,钳而非拒) */
|
|
39
|
+
export const TOOL_TIMEOUT_FLOOR_MS = 100;
|
|
40
|
+
/**
|
|
41
|
+
* v1 注入模式最小词表(03 §2.8 描述扫描)。描述是进模型上下文的文本:
|
|
42
|
+
* `curl … | sh` 形态 = 让模型照描述执行任意下载,是描述面执行漏洞。词表随
|
|
43
|
+
* 真实生态扩充——规范只钉「注册面扫描」这个位置,不在此堆正则。
|
|
44
|
+
*/
|
|
45
|
+
const DESCRIPTION_INJECTION_PATTERNS = [/\b(curl|wget)\b[^\n|]*\|[^\n]*\b(ba|z|da)?sh\b/i];
|
|
46
|
+
/**
|
|
47
|
+
* 扫描工具描述是否命中注入模式(注册面统一防线——任何来源的工具同一执法)。
|
|
48
|
+
* @returns 命中的模式串(错误归因用);干净描述返回 undefined
|
|
49
|
+
*/
|
|
50
|
+
export function scanToolDescription(description) {
|
|
51
|
+
for (const pattern of DESCRIPTION_INJECTION_PATTERNS) {
|
|
52
|
+
if (pattern.test(description))
|
|
53
|
+
return pattern.source;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 变更频率令牌桶(03 §3.4 双帽之二):容量 240、每分钟回填 600——按流逝
|
|
59
|
+
* 时间连续回填(600/60000 per ms),take 不足即拒。桶状态进程内单份(全
|
|
60
|
+
* 局键),随注册表实例生命周期。
|
|
61
|
+
*/
|
|
62
|
+
class RateLimiter {
|
|
63
|
+
capacity;
|
|
64
|
+
perMinute;
|
|
65
|
+
/** 当前令牌数(初始满桶——boot 期的合法批量注册不受阻) */
|
|
66
|
+
tokens;
|
|
67
|
+
/** 上次回填时间戳(ms——按流逝时间折算回填量) */
|
|
68
|
+
lastRefill;
|
|
69
|
+
constructor(capacity, perMinute) {
|
|
70
|
+
this.capacity = capacity;
|
|
71
|
+
this.perMinute = perMinute;
|
|
72
|
+
this.tokens = capacity;
|
|
73
|
+
this.lastRefill = Date.now();
|
|
74
|
+
}
|
|
75
|
+
/** 取一枚令牌:充足即取、不足即拒(false——调用方抛 TOOL_CHANGE_RATE_LIMITED) */
|
|
76
|
+
take() {
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
const elapsed = now - this.lastRefill;
|
|
79
|
+
if (elapsed > 0) {
|
|
80
|
+
this.tokens = Math.min(this.capacity, this.tokens + (elapsed * this.perMinute) / 60_000);
|
|
81
|
+
this.lastRefill = now;
|
|
82
|
+
}
|
|
83
|
+
if (this.tokens < 1)
|
|
84
|
+
return false;
|
|
85
|
+
this.tokens -= 1;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 把 ToolDefinition 适配成 loop 面的 AgentTool(execute = 三段管道——管道是
|
|
91
|
+
* 唯一执行路径的结构保证位)。驱动层条目绑 sessionId(管道内 ToolContext
|
|
92
|
+
* 路由 per-session 状态的语境键)。
|
|
93
|
+
*/
|
|
94
|
+
export function toAgentTool(def, executor, sessionId) {
|
|
95
|
+
return {
|
|
96
|
+
name: def.name,
|
|
97
|
+
description: def.description,
|
|
98
|
+
parameters: def.parameters,
|
|
99
|
+
// 调度语义位透传(一位两用——批消费序按 effect 分段:read 段并发、write 屏障)
|
|
100
|
+
effect: def.effect,
|
|
101
|
+
execute: (toolCallId, args, signal, onUpdate) => executor(def, toolCallId, args, signal, onUpdate, sessionId),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 组装工具注册表(装配根调用一次;tools_change 经注入的 dispatch 发射)。
|
|
106
|
+
* 注册面归一:effect 缺省 'exec'(**未知缺省最危律**——2026-09-11 审批分档
|
|
107
|
+
* 批缺省反转,04 §9 定形块②:未声明效果面的工具按最高危档归一走全审批链,
|
|
108
|
+
* 「缺省放行」的静默豁口不立;03 §2.3 契约缺省同笔更文)、repeatable 缺省
|
|
109
|
+
* true;timeoutMs 正数过小钳至下限(存归一副本——对调用方原对象零改动)。
|
|
110
|
+
*/
|
|
111
|
+
export function createToolRegistry(dispatch, opts = {}) {
|
|
112
|
+
const executor = opts.pipeline;
|
|
113
|
+
const totalLimit = opts.registryTotalLimit ?? REGISTRY_TOTAL_LIMIT;
|
|
114
|
+
const rate = opts.changeRate ?? REGISTRY_RATE;
|
|
115
|
+
/** 全局层:name → 定义(Map 保注册序) */
|
|
116
|
+
const globalLayer = new Map();
|
|
117
|
+
/** 驱动层:sessionId → (name → 定义)(Map 保注册序) */
|
|
118
|
+
const driverLayers = new Map();
|
|
119
|
+
/** 变更频率桶(register 侧 fail-loud 先于变更;unregister 侧在注销器内) */
|
|
120
|
+
const changeRate = new RateLimiter(rate.capacity, rate.perMinute);
|
|
121
|
+
/** 撞名查重(碰撞域见文件头注)——命中返回既有来源描述(错误信息归因用) */
|
|
122
|
+
const findConflict = (name, driver) => {
|
|
123
|
+
const global = globalLayer.get(name);
|
|
124
|
+
if (global !== undefined)
|
|
125
|
+
return `全局层 ${global.name}`;
|
|
126
|
+
if (driver !== undefined) {
|
|
127
|
+
const inDriver = driverLayers.get(driver)?.get(name);
|
|
128
|
+
if (inDriver !== undefined)
|
|
129
|
+
return `驱动层[${driver}] ${inDriver.name}`;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
// 全局层注册与全部驱动层查撞(全局进一切面)
|
|
133
|
+
for (const [sessionId, layer] of driverLayers) {
|
|
134
|
+
const hit = layer.get(name);
|
|
135
|
+
if (hit !== undefined)
|
|
136
|
+
return `驱动层[${sessionId}] ${hit.name}`;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
};
|
|
140
|
+
/** 两层合计件数(总量帽读数) */
|
|
141
|
+
const totalSize = () => {
|
|
142
|
+
let n = globalLayer.size;
|
|
143
|
+
for (const layer of driverLayers.values())
|
|
144
|
+
n += layer.size;
|
|
145
|
+
return n;
|
|
146
|
+
};
|
|
147
|
+
/** tools_change 发射(emit 模式——观察面;发射失败不阻注册〔emit 已隔离〕) */
|
|
148
|
+
const announce = (kind, name, driver) => {
|
|
149
|
+
void dispatch.emit(TOOLS_CHANGE_EVENT, { kind, name, ...(driver !== undefined ? { driver } : {}) });
|
|
150
|
+
};
|
|
151
|
+
/** 注销器(幂等——disposer 重复调用零次侧效应;频率桶同扣〔unregister 也算变更〕) */
|
|
152
|
+
const makeDisposer = (name, driver, normalized) => {
|
|
153
|
+
let disposed = false;
|
|
154
|
+
return () => {
|
|
155
|
+
if (disposed)
|
|
156
|
+
return;
|
|
157
|
+
disposed = true;
|
|
158
|
+
const layer = driver === undefined ? globalLayer : driverLayers.get(driver);
|
|
159
|
+
// 身份护栏:仅当仍是本定义时移除(防误摘后来胜出者——03 §2.7 与 disposer 释放对齐三律②)
|
|
160
|
+
if (layer === undefined || layer.get(name) !== normalized)
|
|
161
|
+
return;
|
|
162
|
+
if (!changeRate.take()) {
|
|
163
|
+
throw new BaseError('TOOL_CHANGE_RATE_LIMITED', `工具变更频率桶空(容量 ${rate.capacity}、回填 ${rate.perMinute}/分钟)——注销被拦:${name}`);
|
|
164
|
+
}
|
|
165
|
+
layer.delete(name);
|
|
166
|
+
if (driver !== undefined && layer.size === 0)
|
|
167
|
+
driverLayers.delete(driver);
|
|
168
|
+
announce('unregister', name, driver);
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
const registry = {
|
|
172
|
+
executor,
|
|
173
|
+
definitions() {
|
|
174
|
+
// 全局层只读副本(快照与注册表解耦——后续注册不进旧快照)
|
|
175
|
+
return [...globalLayer.values()];
|
|
176
|
+
},
|
|
177
|
+
register(def, registerOpts) {
|
|
178
|
+
// ① 描述注入扫描(03 §2.8——任何来源同一防线,官方件同受管)
|
|
179
|
+
const injectionHit = scanToolDescription(def.description);
|
|
180
|
+
if (injectionHit !== undefined) {
|
|
181
|
+
throw new BaseError('TOOL_DESCRIPTION_REJECTED', `工具描述命中注入模式(/${injectionHit}/):${def.name}——描述是进模型上下文的文本,拒绝注册`);
|
|
182
|
+
}
|
|
183
|
+
// ② parameters 根 object 断言:顶层 union(无 type 字段)会被宽容网关剥
|
|
184
|
+
// 成空声明面(模型以空参数调用、宿主 root 级拒绝)——注册即炸前移到装配期
|
|
185
|
+
const schemaRoot = def.parameters;
|
|
186
|
+
if (schemaRoot !== undefined && schemaRoot.type !== 'object') {
|
|
187
|
+
throw new BaseError('TOOL_SCHEMA_INVALID', `工具 ${def.name} parameters 根节点非 object(${schemaRoot.type ?? '(无 type 字段——顶层 union 形)'})——` +
|
|
188
|
+
`声明契约只认根 object;互斥多形工具请用扁平 object(判别字段可选 + 字段级 enum)`);
|
|
189
|
+
}
|
|
190
|
+
// ③ timeoutMs 正数校验(<= 0 拒——拒绝式而非钳 0:钳制会静默改变行为)
|
|
191
|
+
if (def.timeoutMs !== undefined && def.timeoutMs <= 0) {
|
|
192
|
+
throw new BaseError('TOOL_INVALID_ARGS', `工具 ${def.name} timeoutMs <= 0(${def.timeoutMs})——不设预算请省略该字段(走管道缺省 60s)`);
|
|
193
|
+
}
|
|
194
|
+
const driver = registerOpts?.driver;
|
|
195
|
+
// ④ 双帽:变更频率桶先扣(fail-loud 先于变更)→ 撞名 → 总量帽
|
|
196
|
+
if (!changeRate.take()) {
|
|
197
|
+
throw new BaseError('TOOL_CHANGE_RATE_LIMITED', `工具变更频率桶空(容量 ${rate.capacity}、回填 ${rate.perMinute}/分钟)——注册被拦:${def.name}`);
|
|
198
|
+
}
|
|
199
|
+
const conflict = findConflict(def.name, driver);
|
|
200
|
+
if (conflict !== undefined) {
|
|
201
|
+
throw new BaseError('TOOL_NAME_CONFLICT', `工具名 ${def.name} 已被占用(${conflict})——碰撞域内双向对称拒绝(03 §2.7)`);
|
|
202
|
+
}
|
|
203
|
+
if (totalSize() >= totalLimit) {
|
|
204
|
+
throw new BaseError('TOOL_REGISTRY_CAPACITY', `注册表两层合计达总量帽 ${totalLimit}(当前 ${totalSize()})——超限拒新注册:${def.name}`);
|
|
205
|
+
}
|
|
206
|
+
// 归一副本:effect 缺省 exec(未知缺省最危律——04 §9 定形块②反转:
|
|
207
|
+
// 未声明 = 最高危档走全审批链,修前形 read 直达的静默豁口封死)、
|
|
208
|
+
// repeatable 缺省 true(03 §2.3 契约缺省);timeoutMs 钳下限
|
|
209
|
+
const normalized = {
|
|
210
|
+
...def,
|
|
211
|
+
effect: def.effect ?? 'exec',
|
|
212
|
+
repeatable: def.repeatable ?? true,
|
|
213
|
+
...(def.timeoutMs !== undefined ? { timeoutMs: Math.max(def.timeoutMs, TOOL_TIMEOUT_FLOOR_MS) } : {}),
|
|
214
|
+
};
|
|
215
|
+
if (driver === undefined) {
|
|
216
|
+
globalLayer.set(normalized.name, normalized);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const existing = driverLayers.get(driver);
|
|
220
|
+
if (existing !== undefined) {
|
|
221
|
+
existing.set(normalized.name, normalized);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const layer = new Map();
|
|
225
|
+
layer.set(normalized.name, normalized);
|
|
226
|
+
driverLayers.set(driver, layer);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
announce('register', normalized.name, driver);
|
|
230
|
+
return makeDisposer(normalized.name, driver, normalized);
|
|
231
|
+
},
|
|
232
|
+
listFor(sessionId) {
|
|
233
|
+
const driverLayer = driverLayers.get(sessionId);
|
|
234
|
+
if (driverLayer === undefined)
|
|
235
|
+
return [...globalLayer.values()];
|
|
236
|
+
// 驱动层条目在后(同面重名不可能——碰撞域已拒,序 = 全局先驱动后)
|
|
237
|
+
return [...globalLayer.values(), ...driverLayer.values()];
|
|
238
|
+
},
|
|
239
|
+
agentToolsFor(sessionId) {
|
|
240
|
+
if (executor === undefined) {
|
|
241
|
+
throw new BaseError('CONTEXT_SERVICE_MISSING', `工具注册表未接三段管道(pipeline 缺席)——agentToolsFor 不可用(装配缺陷)`);
|
|
242
|
+
}
|
|
243
|
+
return registry.listFor(sessionId).map((def) => toAgentTool(def, executor, sessionId));
|
|
244
|
+
},
|
|
245
|
+
get size() {
|
|
246
|
+
return totalSize();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
return registry;
|
|
250
|
+
}
|