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,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* worktree 工具族(04 §7 worktree 条——fs 族扩展,批 16)。
|
|
3
|
+
*
|
|
4
|
+
* 三动词封闭面(路径由工具派生、非模型任意指定——封闭动词面则):
|
|
5
|
+
* - create:`git worktree add -b <name> <派生路径> [baseRef]`——名字即分支名
|
|
6
|
+
* 即目录段(三用同源),撞名(既存 worktree 目录 / 既存分支)拒
|
|
7
|
+
* `FS_WORKTREE_EXISTS`;
|
|
8
|
+
* - list:`git worktree list --porcelain` 枚举(只读、不登记观察态——ls 同判);
|
|
9
|
+
* - clean:`git worktree remove`——遇未提交变更拒 `FS_WORKTREE_DIRTY`(防误清,
|
|
10
|
+
* 宁拒勿删),`force: true` 显式覆盖;**不删分支**(留史——交付物引用)。
|
|
11
|
+
*
|
|
12
|
+
* 派生路径形:仓同级 `<repo 目录名>-worktrees/<name>`(worktree 不落仓内——
|
|
13
|
+
* 避免被仓自身遍历/快照误纳)。
|
|
14
|
+
*
|
|
15
|
+
* 树级写互斥(04 §7 补钉③):create/clean 以 worktree 根 canonical 路径入
|
|
16
|
+
* 写串行链(serializeTreeWrites)——树操作在飞时其域内文件写(serializeWrites)
|
|
17
|
+
* 排队其后,防 clean 拆掉他方正在写的根;两向握手见 fs.ts。
|
|
18
|
+
*
|
|
19
|
+
* 会话可写根授予(04 §7 补钉①):create 成功即自动将产物 canonical 路径记入
|
|
20
|
+
* 该会话的授予集(fence 组合归装配——writableRoots provider 并入
|
|
21
|
+
* grantedRoots(sessionId);数据目录 carve-out 在 safety 面恒优先,本件不管
|
|
22
|
+
* fence 执法只记账);releaseSession 随会话关闭 / Job 终态回收。
|
|
23
|
+
*
|
|
24
|
+
* git 元数据白名单(04 §8 桥条款——冷读挂账 #4 词面单源锚):本件自身的 git
|
|
25
|
+
* 调用是「工具内置受控路径」(封闭动词面则——不在 carve-out 拦截面);
|
|
26
|
+
* `GIT_METADATA_COMMANDS` 是经 bash 的 git 命令族受控放行的闭集动词表词汇
|
|
27
|
+
* 源(执法位 = exec bash 拦截面,随该面落码批接线——先有词后有法)。
|
|
28
|
+
*/
|
|
29
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
30
|
+
import { existsSync } from 'node:fs';
|
|
31
|
+
import { basename, dirname, join } from 'node:path';
|
|
32
|
+
import { promisify } from 'node:util';
|
|
33
|
+
import { Type } from 'typebox';
|
|
34
|
+
import { BaseError } from '../contracts/index.js';
|
|
35
|
+
import { canonicalize, serializeTreeWrites } from './fs.js';
|
|
36
|
+
const execFile = promisify(execFileCallback);
|
|
37
|
+
/**
|
|
38
|
+
* git 元数据命令闭集动词表(04 §8 carve-out 桥条款白名单形——词面单源锚)。
|
|
39
|
+
* 经 bash 执行的 git 命令族元数据操作(写 `.git/objects`、`.git/refs`——
|
|
40
|
+
* worktree 形态下含 `.git/worktrees/<name>/`)受控放行的动词全集;非本表
|
|
41
|
+
* 动词(如 `fast-import`/`update-ref` 直改版本史字节)不在放行面。
|
|
42
|
+
* 执法位 = exec bash 拦截面(该面落码批接线——本常量先锚词汇)。
|
|
43
|
+
*/
|
|
44
|
+
export const GIT_METADATA_COMMANDS = [
|
|
45
|
+
'add',
|
|
46
|
+
'commit',
|
|
47
|
+
'branch',
|
|
48
|
+
'checkout',
|
|
49
|
+
'switch',
|
|
50
|
+
'merge',
|
|
51
|
+
'rebase',
|
|
52
|
+
'stash',
|
|
53
|
+
'tag',
|
|
54
|
+
'push',
|
|
55
|
+
'pull',
|
|
56
|
+
'fetch',
|
|
57
|
+
'status',
|
|
58
|
+
'log',
|
|
59
|
+
'diff',
|
|
60
|
+
'show',
|
|
61
|
+
'config',
|
|
62
|
+
'worktree',
|
|
63
|
+
];
|
|
64
|
+
/** worktree 名字词法(名即分支名即目录段三用同源——禁分隔符,首字符字母数字) */
|
|
65
|
+
const WORKTREE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
66
|
+
/** 单 git 命令超时毫秒(缺省) */
|
|
67
|
+
const GIT_TIMEOUT_MS = 30_000;
|
|
68
|
+
/** git 输出缓冲上限字节(diff 可大——16 MiB 帽) */
|
|
69
|
+
const GIT_MAX_BUFFER = 16 * 1024 * 1024;
|
|
70
|
+
/** 名字词法校验(坏形 = 参数面问题——TOOL_INVALID_ARGS 语义域) */
|
|
71
|
+
function assertNameValid(name) {
|
|
72
|
+
if (!WORKTREE_NAME_RE.test(name)) {
|
|
73
|
+
throw new BaseError('TOOL_INVALID_ARGS', `[TOOL_INVALID_ARGS] worktree 名字坏形:${name}(词法 ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$——名即分支名即目录段,禁分隔符)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 组 worktree 服务。git 经 execFile 直调(node: 内建全局放行域——零外部
|
|
78
|
+
* 二进制依赖);全部 git 调用 cwd 锚主仓根。服务实例持会话授予表——同一
|
|
79
|
+
* 仓应共享单实例(授予集与树级互斥链同源)。
|
|
80
|
+
*/
|
|
81
|
+
export function createWorktreeService(opts) {
|
|
82
|
+
const repoRoot = opts.repoRoot;
|
|
83
|
+
const git = opts.gitCommand ?? 'git';
|
|
84
|
+
const timeoutMs = opts.timeoutMs ?? GIT_TIMEOUT_MS;
|
|
85
|
+
// 会话 → 授予路径集(canonical 形;fence 组合装配面读 grantedRoots 并入)
|
|
86
|
+
const grants = new Map();
|
|
87
|
+
/** 执行 git(cwd 锚主仓;非零退出折带 stderr 的 Error——语义码由调用方判形折码) */
|
|
88
|
+
const runGit = async (args) => {
|
|
89
|
+
try {
|
|
90
|
+
const { stdout } = await execFile(git, args, { cwd: repoRoot, timeout: timeoutMs, maxBuffer: GIT_MAX_BUFFER });
|
|
91
|
+
return stdout;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const e = err;
|
|
95
|
+
const detail = (e.stderr ?? e.message ?? String(err)).trim();
|
|
96
|
+
if (e.killed === true) {
|
|
97
|
+
throw new Error(`[git ${args[0]}] 超时(${timeoutMs}ms):${detail.slice(0, 500)}`);
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`[git ${args.join(' ')}] ${detail.slice(0, 500)}`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
/** 派生 worktree 路径:仓同级 `<repo 目录名>-worktrees/<name>`(工具派生非模型指定) */
|
|
103
|
+
const derivedPath = (name) => join(dirname(repoRoot), `${basename(repoRoot)}-worktrees`, name);
|
|
104
|
+
/** 分支在场判定(git branch --list 前缀匹配——须整行精确比对) */
|
|
105
|
+
const branchExists = async (name) => {
|
|
106
|
+
const out = await runGit(['branch', '--list', name]);
|
|
107
|
+
return out
|
|
108
|
+
.split('\n')
|
|
109
|
+
.map((l) => l
|
|
110
|
+
.trim()
|
|
111
|
+
.replace(/^\*\s+/, '')
|
|
112
|
+
.replace(/\s+$/, ''))
|
|
113
|
+
.some((l) => l === name);
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
async create(req) {
|
|
117
|
+
assertNameValid(req.name);
|
|
118
|
+
const path = derivedPath(req.name);
|
|
119
|
+
// 撞名两查:目录在场(前次 create 产物或同名异物)/ 分支在场
|
|
120
|
+
if (existsSync(path)) {
|
|
121
|
+
throw new BaseError('FS_WORKTREE_EXISTS', `[FS_WORKTREE_EXISTS] worktree 已存在:${req.name}(路径 ${path} 在场)`);
|
|
122
|
+
}
|
|
123
|
+
if (await branchExists(req.name)) {
|
|
124
|
+
throw new BaseError('FS_WORKTREE_EXISTS', `[FS_WORKTREE_EXISTS] 分支已存在:${req.name}(名字即分支名——分支与 worktree 同名域撞名)`);
|
|
125
|
+
}
|
|
126
|
+
// 树级入链(占位键取派生路径的最近在场祖先 canonical——建后路径才在场)
|
|
127
|
+
await serializeTreeWrites(await canonicalize(path), async () => {
|
|
128
|
+
try {
|
|
129
|
+
await runGit(req.baseRef === undefined
|
|
130
|
+
? ['worktree', 'add', '-b', req.name, path]
|
|
131
|
+
: ['worktree', 'add', '-b', req.name, path, req.baseRef]);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// 链内竞速撞名(并发 create 同名)折语义码;其余 git 失败原样上抛
|
|
135
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
136
|
+
if (msg.includes('already exists') || msg.includes('already checked out')) {
|
|
137
|
+
throw new BaseError('FS_WORKTREE_EXISTS', `[FS_WORKTREE_EXISTS] worktree/分支已存在:${req.name}(并发创建竞速)`);
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// 建成后路径在场——canonical 全程解析(授予集记 canonical 形,与 fence 比对同基)
|
|
143
|
+
const canonical = await canonicalize(path);
|
|
144
|
+
if (req.sessionId !== undefined) {
|
|
145
|
+
let set = grants.get(req.sessionId);
|
|
146
|
+
if (set === undefined) {
|
|
147
|
+
set = new Set();
|
|
148
|
+
grants.set(req.sessionId, set);
|
|
149
|
+
}
|
|
150
|
+
set.add(canonical);
|
|
151
|
+
}
|
|
152
|
+
return { name: req.name, path: canonical, branch: req.name };
|
|
153
|
+
},
|
|
154
|
+
async list() {
|
|
155
|
+
const out = await runGit(['worktree', 'list', '--porcelain']);
|
|
156
|
+
const entries = [];
|
|
157
|
+
let current = {};
|
|
158
|
+
for (const line of out.split('\n')) {
|
|
159
|
+
if (line === '') {
|
|
160
|
+
// 块尾——落项(worktree 行必有才有块)
|
|
161
|
+
if (current.path !== undefined) {
|
|
162
|
+
entries.push({
|
|
163
|
+
name: basename(current.path),
|
|
164
|
+
path: current.path,
|
|
165
|
+
branch: current.branch ?? '',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
current = {};
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const space = line.indexOf(' ');
|
|
172
|
+
const key = space === -1 ? line : line.slice(0, space);
|
|
173
|
+
const value = space === -1 ? '' : line.slice(space + 1);
|
|
174
|
+
if (key === 'worktree')
|
|
175
|
+
current.path = value;
|
|
176
|
+
else if (key === 'branch')
|
|
177
|
+
current.branch = value.replace(/^refs\/heads\//, '');
|
|
178
|
+
// HEAD/bare/detached/locked/prunable 行不进枚举面
|
|
179
|
+
}
|
|
180
|
+
if (current.path !== undefined) {
|
|
181
|
+
entries.push({ name: basename(current.path), path: current.path, branch: current.branch ?? '' });
|
|
182
|
+
}
|
|
183
|
+
return entries;
|
|
184
|
+
},
|
|
185
|
+
async clean(req) {
|
|
186
|
+
assertNameValid(req.name);
|
|
187
|
+
const path = derivedPath(req.name);
|
|
188
|
+
// 在册判定(缺席 = 目标不存在——FS_NOT_FOUND 同 read/ls 语义域)。
|
|
189
|
+
// 比对基 = canonical 双侧:git 记录 realpath(macOS /var → /private/var
|
|
190
|
+
// 符号链形下与派生串不同形——字符串直比必假阴)
|
|
191
|
+
const targetCanonical = await canonicalize(path);
|
|
192
|
+
const entries = await this.list();
|
|
193
|
+
let inList = false;
|
|
194
|
+
for (const e of entries) {
|
|
195
|
+
if ((await canonicalize(e.path)) === targetCanonical) {
|
|
196
|
+
inList = true;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!inList) {
|
|
201
|
+
throw new BaseError('FS_NOT_FOUND', `[FS_NOT_FOUND] worktree 不存在:${req.name}(路径 ${path})`);
|
|
202
|
+
}
|
|
203
|
+
const dirtyLines = async () => {
|
|
204
|
+
const status = await runGit(['-C', path, 'status', '--porcelain']);
|
|
205
|
+
return status
|
|
206
|
+
.split('\n')
|
|
207
|
+
.map((l) => l.trim())
|
|
208
|
+
.filter((l) => l !== '');
|
|
209
|
+
};
|
|
210
|
+
await serializeTreeWrites(await canonicalize(path), async () => {
|
|
211
|
+
// 未提交变更两查(force 显式覆盖):链外先查给出可读拒因;链内复验——
|
|
212
|
+
// 链上写者已排空,窗口收敛到链外写者(bash 重定向等——诚实边界)
|
|
213
|
+
if (req.force !== true) {
|
|
214
|
+
const lines = await dirtyLines();
|
|
215
|
+
if (lines.length > 0) {
|
|
216
|
+
throw new BaseError('FS_WORKTREE_DIRTY', `[FS_WORKTREE_DIRTY] worktree 有未提交变更拒拆除:${req.name}(${lines.length} 处,如:${lines
|
|
217
|
+
.slice(0, 3)
|
|
218
|
+
.join(';')})——提交后重试或显式 force`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await runGit(req.force === true ? ['worktree', 'remove', '--force', path] : ['worktree', 'remove', path]);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
// git 自身拒拆(本仓两查后的链外写者窗口)折同码——不裸抛
|
|
226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
227
|
+
if (msg.includes('dirty')) {
|
|
228
|
+
throw new BaseError('FS_WORKTREE_DIRTY', `[FS_WORKTREE_DIRTY] git 拒拆(未提交变更):${req.name}——提交后重试或显式 force`);
|
|
229
|
+
}
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
// 分支留史不删(交付物引用——04 §7 落码定形)
|
|
234
|
+
return { name: req.name, path };
|
|
235
|
+
},
|
|
236
|
+
async diffPatch(req) {
|
|
237
|
+
assertNameValid(req.name);
|
|
238
|
+
const path = derivedPath(req.name);
|
|
239
|
+
return runGit(['-C', path, 'diff', `${req.baseRef}...HEAD`]);
|
|
240
|
+
},
|
|
241
|
+
async grant(req) {
|
|
242
|
+
// canonical 化后记入(与 fence 比对同基;幂等——Set 语义)
|
|
243
|
+
const canonical = await canonicalize(req.path);
|
|
244
|
+
let set = grants.get(req.sessionId);
|
|
245
|
+
if (set === undefined) {
|
|
246
|
+
set = new Set();
|
|
247
|
+
grants.set(req.sessionId, set);
|
|
248
|
+
}
|
|
249
|
+
set.add(canonical);
|
|
250
|
+
},
|
|
251
|
+
grantedRoots(sessionId) {
|
|
252
|
+
return [...(grants.get(sessionId) ?? [])];
|
|
253
|
+
},
|
|
254
|
+
releaseSession(sessionId) {
|
|
255
|
+
const released = [...(grants.get(sessionId) ?? [])];
|
|
256
|
+
grants.delete(sessionId);
|
|
257
|
+
return released;
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 组 worktree 三工具(04 §7 worktree 条词面定名:worktree_create/list/clean)。
|
|
263
|
+
* sessionId 取 toolCtx(会话内模型调用形态——授予自动记该会话);件侧编排
|
|
264
|
+
* 直接调 service(同样携 owner 会话 id)。effect:create/clean = write(过
|
|
265
|
+
* 审批对),list 只读。
|
|
266
|
+
*/
|
|
267
|
+
export function createWorktreeTools(service) {
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
name: 'worktree_create',
|
|
271
|
+
effect: 'write',
|
|
272
|
+
description: '在当前仓旁建独立 git worktree + 同名新分支(名字即分支名)。产物路径由工具派生(仓同级 <仓目录名>-worktrees/<名字>),建成功即自动加入本会话可写根(后续 fs 写/bash 以该 worktree 为锚)。适合并行开一条独立工作线(如同时改两个特性互不踩踏)。',
|
|
273
|
+
parameters: Type.Object({
|
|
274
|
+
name: Type.String({ description: 'worktree 名(= 新分支名;字母数字开头,可含 ._-,禁斜杠,≤64 字符)' }),
|
|
275
|
+
baseRef: Type.Optional(Type.String({ description: '基线提交/分支(缺省 HEAD)' })),
|
|
276
|
+
}),
|
|
277
|
+
execute: async (args, toolCtx) => {
|
|
278
|
+
const created = await service.create({
|
|
279
|
+
name: args.name,
|
|
280
|
+
baseRef: args.baseRef,
|
|
281
|
+
sessionId: toolCtx.sessionId,
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: `worktree 已建:${created.name}\n路径:${created.path}\n分支:${created.branch}(此后在本会话内该路径可写)`,
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
details: { name: created.name, path: created.path, branch: created.branch },
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'worktree_list',
|
|
296
|
+
effect: 'read',
|
|
297
|
+
description: '枚举当前仓全部 git worktree(含主仓——路径/分支/名),只读。',
|
|
298
|
+
parameters: Type.Object({}),
|
|
299
|
+
execute: async () => {
|
|
300
|
+
const entries = await service.list();
|
|
301
|
+
const lines = entries.map((e) => `${e.name}\t分支 ${e.branch || '(detached)'}\t${e.path}`);
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: lines.length > 0 ? lines.join('\n') : '(无 worktree)' }],
|
|
304
|
+
details: { count: entries.length },
|
|
305
|
+
};
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'worktree_clean',
|
|
310
|
+
effect: 'write',
|
|
311
|
+
description: '拆除指定 worktree(目录与 git 登记一并移除;分支保留不删——交付物引用)。有未提交变更时拒绝(FS_WORKTREE_DIRTY)——先提交,或显式传 force=true 强拆(变更将丢失)。',
|
|
312
|
+
parameters: Type.Object({
|
|
313
|
+
name: Type.String({ description: 'worktree 名(create 时的名字)' }),
|
|
314
|
+
force: Type.Optional(Type.Boolean({ description: '强拆(丢弃未提交变更——破坏性动作)' })),
|
|
315
|
+
}),
|
|
316
|
+
execute: async (args) => {
|
|
317
|
+
const removed = await service.clean({ name: args.name, force: args.force === true });
|
|
318
|
+
return {
|
|
319
|
+
content: [
|
|
320
|
+
{ type: 'text', text: `worktree 已拆除:${removed.name}(${removed.path})——同名分支保留` },
|
|
321
|
+
],
|
|
322
|
+
details: { name: removed.name, path: removed.path },
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web 域错误码注册(02 §4.1 席 18 / 07 §1.1 core:web 行——SSRF 五卫生件拦截码族)。
|
|
3
|
+
*
|
|
4
|
+
* 前缀族明列见 contracts/errors.ts ERROR_CODE_PREFIXES(02 §5.3 #1——WEB_ 族
|
|
5
|
+
* 前缀已注册,具体码名规范未逐一定名,本批落码定名〔07 §1.1 core:web 行
|
|
6
|
+
* 「原样」指席职能〕);03 §10.3 browser 件章 L694 安全卫生条指认「拦截码
|
|
7
|
+
* 复用 WEB_ 族不自造」——browser 导航拦截消费本族。注册纪律:写入点文件
|
|
8
|
+
* 必须实际 import 本文件注册才发生(与 safety/tools 的 codes.ts 同款)。
|
|
9
|
+
*/
|
|
10
|
+
import { registerErrorCodes } from '../contracts/index.js';
|
|
11
|
+
registerErrorCodes([
|
|
12
|
+
{
|
|
13
|
+
code: 'WEB_URL_INVALID',
|
|
14
|
+
module: 'web',
|
|
15
|
+
description: '入口 URL 不可解析(new URL 红或空串)——SSRF 卫生件段 0 拒',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
code: 'WEB_PROTOCOL_REJECTED',
|
|
19
|
+
module: 'web',
|
|
20
|
+
description: '协议白名单外拒——仅 http/https(03 §10.3 browser 件章协议白名单同源;file/ftp/javascript 等一律拒)',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: 'WEB_PRIVATE_ADDRESS',
|
|
24
|
+
module: 'web',
|
|
25
|
+
description: '私网/环回/链路本地/未指定地址拒(SSRF 红线)——字面 IP 段与 DNS 解析结果双查(重定向每跳重查)',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: 'WEB_REDIRECT_LIMIT',
|
|
29
|
+
module: 'web',
|
|
30
|
+
description: '重定向跟随跳数触帽拒(缺省 5 跳)——每跳目标经同一卫生单源复检',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
code: 'WEB_RATE_LIMITED',
|
|
34
|
+
module: 'web',
|
|
35
|
+
description: '在飞门满拒(缺省并发 4)——fetch 与 browser 导航共享同一门实例(03 §10.3「同一在飞门第三消费位」);可重试档',
|
|
36
|
+
},
|
|
37
|
+
]);
|
package/dist/web/gate.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在飞门(限流卫生件——07 §1.1 core:web 行「限流」;03 §10.3 browser 件章
|
|
3
|
+
* 「导航限流与 fetch 共享同一在飞门实例」)。
|
|
4
|
+
*
|
|
5
|
+
* 形态:并发计数 + 满即拒(fail-fast 不排队——排队会在卫生件内积压 Promise,
|
|
6
|
+
* 与超时预算相互作用产生迟到风暴;拒是可重试档,调用方自决退避)。实例经
|
|
7
|
+
* 装配根共享给 browser 导航(第三消费位)——门身份即共享点,非全局单例。
|
|
8
|
+
*/
|
|
9
|
+
import { BaseError } from '../contracts/index.js';
|
|
10
|
+
/** 在飞门构造(容量 ≤0 视为 1——保守钳制,配置坏形不放大并发面) */
|
|
11
|
+
export function createInFlightGate(capacity) {
|
|
12
|
+
const effective = Math.max(1, Math.floor(capacity));
|
|
13
|
+
let inFlight = 0;
|
|
14
|
+
return {
|
|
15
|
+
get inFlight() {
|
|
16
|
+
return inFlight;
|
|
17
|
+
},
|
|
18
|
+
get capacity() {
|
|
19
|
+
return effective;
|
|
20
|
+
},
|
|
21
|
+
acquire() {
|
|
22
|
+
if (inFlight >= effective) {
|
|
23
|
+
throw new BaseError('WEB_RATE_LIMITED', `在飞门满(并发 ${inFlight}/${effective})——稍后重试或降低并发`);
|
|
24
|
+
}
|
|
25
|
+
inFlight += 1;
|
|
26
|
+
},
|
|
27
|
+
release() {
|
|
28
|
+
// 配对义务面:release 多于 acquire 不负计数(防御调用方 bug,不 fail-loud
|
|
29
|
+
// ——放门在 finally 路径,抛错会吞业务异常)
|
|
30
|
+
if (inFlight > 0)
|
|
31
|
+
inFlight -= 1;
|
|
32
|
+
},
|
|
33
|
+
async run(task) {
|
|
34
|
+
this.acquire();
|
|
35
|
+
try {
|
|
36
|
+
return await task();
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
this.release();
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF 卫生件纯函数(07 §1.1 core:web 行五卫生件之「私网拒绝」+ 协议白名单;
|
|
3
|
+
* 03 §10.3 browser 件章安全卫生条:navigate 入口 URL 经本单源复用——拦截码
|
|
4
|
+
* 复用 WEB_ 族不自造)。
|
|
5
|
+
*
|
|
6
|
+
* 判定序(execute 内编排序,本件只供原子判定):
|
|
7
|
+
* URL 解析(WEB_URL_INVALID)→ 协议白名单 http/https(WEB_PROTOCOL_REJECTED)
|
|
8
|
+
* → 主机私网判定(字面 IP 段查 + DNS 解析结果查——WEB_PRIVATE_ADDRESS)
|
|
9
|
+
* 重定向每跳目标重跑同一判定序。
|
|
10
|
+
*
|
|
11
|
+
* 已知边界(v1 注记):DNS 校验与实际连接之间存在解析漂移窗(rebinding 全防
|
|
12
|
+
* 需自定义连接级 lookup/undici dispatcher)——本件落「请求前解析全查」档,
|
|
13
|
+
* 连接级钉死挂账后续批。
|
|
14
|
+
*/
|
|
15
|
+
import { lookup } from 'node:dns/promises';
|
|
16
|
+
import { BaseError } from '../contracts/index.js';
|
|
17
|
+
/** 协议白名单(http(s) 两形——03 §10.3 browser 件章同源) */
|
|
18
|
+
export const WEB_PROTOCOLS = new Set(['http:', 'https:']);
|
|
19
|
+
/** 缺省 DNS 解析器:node:dns/promises lookup 全地址族(生产位;测试注入桩) */
|
|
20
|
+
export const defaultDnsResolver = async (hostname) => {
|
|
21
|
+
const results = await lookup(hostname, { all: true });
|
|
22
|
+
return results.map((result) => result.address);
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 入口 URL 解析 + 协议白名单(卫生件段 0/1)。
|
|
26
|
+
* @param raw 原始 URL 串
|
|
27
|
+
* @returns 解析后的 URL 对象(原样——不做补全/改写;无协议裸主机形属调用方
|
|
28
|
+
* 语义决定,本件保守拒——补全 https:// 的宽容归工具面参数预处理,不进卫生件)
|
|
29
|
+
* @throws WEB_URL_INVALID 不可解析;WEB_PROTOCOL_REJECTED 白名单外协议
|
|
30
|
+
*/
|
|
31
|
+
export function parseWebUrl(raw) {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new BaseError('WEB_URL_INVALID', `URL 不可解析:${raw}`);
|
|
38
|
+
}
|
|
39
|
+
if (!WEB_PROTOCOLS.has(url.protocol)) {
|
|
40
|
+
throw new BaseError('WEB_PROTOCOL_REJECTED', `协议 ${url.protocol} 不在白名单(仅 http/https):${raw}`);
|
|
41
|
+
}
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* IPv4 私网/保留段判定(点分十进制已拆四段)。
|
|
46
|
+
* 覆盖:环回 127/8、未指定 0/8、私网三段 10/8 + 172.16/12 + 192.168/16、
|
|
47
|
+
* 链路本地 169.254/16、CGNAT 100.64/10、组播 224/4、保留 240/4。
|
|
48
|
+
*/
|
|
49
|
+
function isPrivateIPv4(octets) {
|
|
50
|
+
// 解构带缺省窄化索引访问(noUncheckedIndexedAccess);缺省 -1 不落任何段
|
|
51
|
+
const [a = -1, b = -1] = octets;
|
|
52
|
+
return (a === 127 || // 环回
|
|
53
|
+
a === 0 || // 未指定
|
|
54
|
+
a === 10 || // 私网 A
|
|
55
|
+
(a === 172 && b >= 16 && b <= 31) || // 私网 B
|
|
56
|
+
(a === 192 && b === 168) || // 私网 C
|
|
57
|
+
(a === 169 && b === 254) || // 链路本地
|
|
58
|
+
(a === 100 && b >= 64 && b <= 127) || // CGNAT
|
|
59
|
+
a >= 224 // 组播 + 保留(224-255)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* IPv6 私网/保留段判定(入参 = expandIPv6 产出的 8 组 4 位补零十六进制)。
|
|
64
|
+
* 覆盖:未指定 ::、环回 ::1、唯一本地 fc00::/7、链路本地 fe80::/10、
|
|
65
|
+
* v4 映射 ::ffff:0:0/96 与 v4 兼容 ::/96(尾 32 位还原 v4 递归查)、
|
|
66
|
+
* v4 翻译 64:ff9b::/96(同递归)。
|
|
67
|
+
*/
|
|
68
|
+
function isPrivateIPv6(groups) {
|
|
69
|
+
// 显式索引取位(?? 收窄 noUncheckedIndexedAccess)——解构按位置赋值,
|
|
70
|
+
// 命名跳位不等于索引跳位(首版 const [g0,g5,g6,g7] 实取 groups[0..3],
|
|
71
|
+
// v4 映射/兼容段误判——回归锁:::ffff:8.8.8.8 → false)
|
|
72
|
+
const g0 = groups[0] ?? '';
|
|
73
|
+
const g5 = groups[5] ?? '';
|
|
74
|
+
const g6 = groups[6] ?? '0000';
|
|
75
|
+
const g7 = groups[7] ?? '0000';
|
|
76
|
+
// 未指定 ::(全零)
|
|
77
|
+
if (groups.every((group) => group === '0000'))
|
|
78
|
+
return true;
|
|
79
|
+
// 环回 ::1(前七组零 + 尾组 1)
|
|
80
|
+
if (groups.slice(0, 7).every((group) => group === '0000') && groups[7] === '0001')
|
|
81
|
+
return true;
|
|
82
|
+
// 唯一本地 fc00::/7(fc/fd 前缀)
|
|
83
|
+
if (g0.startsWith('fc') || g0.startsWith('fd'))
|
|
84
|
+
return true;
|
|
85
|
+
// 链路本地 fe80::/10(fe80–febf:第三位十六进制 8-b)
|
|
86
|
+
if (g0.startsWith('fe') && ['8', '9', 'a', 'b'].includes(g0[2] ?? ''))
|
|
87
|
+
return true;
|
|
88
|
+
// 尾 32 位还原 v4 四段(g6 高 16 位 / g7 低 16 位)
|
|
89
|
+
const decodeV4Tail = () => {
|
|
90
|
+
const high = Number.parseInt(g6, 16);
|
|
91
|
+
const low = Number.parseInt(g7, 16);
|
|
92
|
+
return [(high >> 8) & 0xff, high & 0xff, (low >> 8) & 0xff, low & 0xff];
|
|
93
|
+
};
|
|
94
|
+
// v4 映射 ::ffff:0:0/96(前 5 组零 + 第 6 组 ffff)与 v4 兼容 ::/96(前 6 组零)
|
|
95
|
+
if (groups.slice(0, 5).every((group) => group === '0000') && (g5 === 'ffff' || g5 === '0000')) {
|
|
96
|
+
return isPrivateIPv4(decodeV4Tail());
|
|
97
|
+
}
|
|
98
|
+
// v4 翻译 64:ff9b::/96(0064:ff9b 前缀 + 第 3-6 组零)
|
|
99
|
+
if (g0 === '0064' && groups[1] === 'ff9b' && groups.slice(2, 6).every((group) => group === '0000')) {
|
|
100
|
+
return isPrivateIPv4(decodeV4Tail());
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 主机名私网判定(字面腿——不做 DNS):字面 IP(v4/v6 含 [ ] 包裹形)与
|
|
106
|
+
* localhost 族(RFC 6761——*.localhost 恒解析环回)。
|
|
107
|
+
* @returns true = 私网/保留(拒);false = 字面面非私网或非字面形(交 DNS 腿)
|
|
108
|
+
*/
|
|
109
|
+
export function isPrivateHostLiteral(hostname) {
|
|
110
|
+
const host = hostname.toLowerCase();
|
|
111
|
+
// IPv6 包裹形 [::1]
|
|
112
|
+
const bare = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
113
|
+
// v4 字面:四段点分全数字
|
|
114
|
+
const v4Match = bare.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
115
|
+
if (v4Match) {
|
|
116
|
+
const octets = v4Match.slice(1).map(Number);
|
|
117
|
+
if (octets.every((octet) => octet >= 0 && octet <= 255)) {
|
|
118
|
+
return isPrivateIPv4(octets);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// IPv6 字面(含 %zone 尾——链路本地常用 zone 形,直接拒)
|
|
122
|
+
if (bare.includes(':')) {
|
|
123
|
+
const zoneless = bare.split('%')[0] ?? bare;
|
|
124
|
+
const expanded = expandIPv6(zoneless);
|
|
125
|
+
if (expanded)
|
|
126
|
+
return isPrivateIPv6(expanded);
|
|
127
|
+
}
|
|
128
|
+
// localhost 族:裸 localhost 与 *.localhost(点尾任意)
|
|
129
|
+
if (host === 'localhost' || host.endsWith('.localhost'))
|
|
130
|
+
return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* IPv6 展开辅助:把压缩形(::)展开为 8 组 4 位十六进制前 2 位小写数组;
|
|
135
|
+
* 非 8 组形(内嵌 v4 等)返回 null 交调用方原样处理。
|
|
136
|
+
* 返回形:每组保留完整 4 位十六进制(比较用)。
|
|
137
|
+
*/
|
|
138
|
+
function expandIPv6(input) {
|
|
139
|
+
// 内嵌 v4 尾(如 ::ffff:192.168.0.1)先转两组十六进制
|
|
140
|
+
let text = input;
|
|
141
|
+
const v4Tail = text.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);
|
|
142
|
+
if (v4Tail) {
|
|
143
|
+
const [a = -1, b = -1, c = -1, d = -1] = (v4Tail[2] ?? '').split('.').map(Number);
|
|
144
|
+
if ([a, b, c, d].some((octet) => octet < 0 || octet > 255))
|
|
145
|
+
return null;
|
|
146
|
+
const high = ((a << 8) | b).toString(16);
|
|
147
|
+
const low = ((c << 8) | d).toString(16);
|
|
148
|
+
text = `${v4Tail[1] ?? ''}${high}:${low}`;
|
|
149
|
+
}
|
|
150
|
+
const halves = text.split('::');
|
|
151
|
+
if (halves.length > 2)
|
|
152
|
+
return null; // 双 :: 非法
|
|
153
|
+
const parseGroups = (part) => (part === '' ? [] : part.split(':'));
|
|
154
|
+
const head = parseGroups(halves[0] ?? '');
|
|
155
|
+
const tail = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];
|
|
156
|
+
if (halves.length === 1 && head.length !== 8)
|
|
157
|
+
return null;
|
|
158
|
+
if (halves.length === 2 && head.length + tail.length > 7)
|
|
159
|
+
return null;
|
|
160
|
+
const fill = 8 - head.length - tail.length;
|
|
161
|
+
const groups = [...head, ...Array.from({ length: halves.length === 2 ? fill : 0 }, () => '0'), ...tail];
|
|
162
|
+
if (groups.length !== 8)
|
|
163
|
+
return null;
|
|
164
|
+
if (groups.some((group) => !/^[0-9a-f]{1,4}$/.test(group)))
|
|
165
|
+
return null;
|
|
166
|
+
return groups.map((group) => group.padStart(4, '0'));
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 主机私网判定全腿(字面 + DNS):字面腿命中即拒;非字面主机名经解析器
|
|
170
|
+
* 全地址查——任一解析结果落私网段即拒(DNS rebinding 首跳防御档)。
|
|
171
|
+
* @throws WEB_PRIVATE_ADDRESS 私网/保留段命中
|
|
172
|
+
* @throws 原样上抛解析失败(非卫生拦截——网络不可达属普通失败,不携 WEB_ 码)
|
|
173
|
+
*/
|
|
174
|
+
export async function assertPublicHost(url, resolveDns) {
|
|
175
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, ''); // URL 已剥 [],防御双保险
|
|
176
|
+
if (isPrivateHostLiteral(hostname)) {
|
|
177
|
+
throw new BaseError('WEB_PRIVATE_ADDRESS', `目标主机属私网/保留段(SSRF 红线拒):${hostname}`);
|
|
178
|
+
}
|
|
179
|
+
const addresses = await resolveDns(hostname);
|
|
180
|
+
const hit = addresses.find((address) => {
|
|
181
|
+
const bare = address.split('%')[0] ?? address;
|
|
182
|
+
if (bare.includes(':')) {
|
|
183
|
+
const expanded = expandIPv6(bare);
|
|
184
|
+
return expanded ? isPrivateIPv6(expanded) : false;
|
|
185
|
+
}
|
|
186
|
+
const v4Match = bare.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
187
|
+
if (!v4Match)
|
|
188
|
+
return false;
|
|
189
|
+
const octets = v4Match.slice(1).map(Number);
|
|
190
|
+
return octets.every((octet) => octet >= 0 && octet <= 255) && isPrivateIPv4(octets);
|
|
191
|
+
});
|
|
192
|
+
if (hit) {
|
|
193
|
+
throw new BaseError('WEB_PRIVATE_ADDRESS', `目标主机解析命中私网/保留段地址(SSRF 红线拒):${hostname} → ${hit}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web 模块公开面(L3 core: 官方插件 15 之 #6——02 §4.1 席 18 / 07 §1.1
|
|
3
|
+
* core:web 行:fetch 工具 + ctx.fetch 服务 + SSRF 五卫生件〔两消费面同一
|
|
4
|
+
* execute〕)。
|
|
5
|
+
*
|
|
6
|
+
* 本批(14c)纯逻辑腿:卫生件纯函数 + 在飞门 + 服务(唯一 execute 编排)+
|
|
7
|
+
* fetch 工具定义。装载态集成(core:web 官方引用形 apply——注册工具 + provide
|
|
8
|
+
* ctx.fetch 服务 + 归因 sink 接 durable 落账面 + 与 browser 共享同一在飞门
|
|
9
|
+
* 实例)归批 12 装载面后装配批(03 §1.4 官方引用形;批序档装载面前置律)。
|
|
10
|
+
*/
|
|
11
|
+
import './codes.js';
|
|
12
|
+
export { createInFlightGate } from './gate.js';
|
|
13
|
+
export { WEB_PROTOCOLS, defaultDnsResolver, parseWebUrl, isPrivateHostLiteral, assertPublicHost } from './hygiene.js';
|
|
14
|
+
export { createSsrfGuardedFetch } from './ssrf-guard.js';
|
|
15
|
+
export { createWebFetchService, DEFAULT_WEB_LIMITS } from './service.js';
|
|
16
|
+
export { createFetchTool } from './tool.js';
|