@armadra/agent 0.0.0-stage → 0.2.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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +426 -2
- package/dist/agent/agent.d.ts +56 -0
- package/dist/agent/agent.js +129 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +158 -0
- package/dist/agent/queue.d.ts +25 -0
- package/dist/agent/queue.js +55 -0
- package/dist/agent/retry.d.ts +22 -0
- package/dist/agent/retry.js +92 -0
- package/dist/agent/schema.d.ts +27 -0
- package/dist/agent/schema.js +186 -0
- package/dist/agent/session-cache.d.ts +100 -0
- package/dist/agent/session-cache.js +511 -0
- package/dist/agent/session-compaction.d.ts +49 -0
- package/dist/agent/session-compaction.js +251 -0
- package/dist/agent/session-core.d.ts +103 -0
- package/dist/agent/session-core.js +7 -0
- package/dist/agent/session-run.d.ts +55 -0
- package/dist/agent/session-run.js +246 -0
- package/dist/agent/session-state.d.ts +39 -0
- package/dist/agent/session-state.js +118 -0
- package/dist/agent/session-subagent.d.ts +54 -0
- package/dist/agent/session-subagent.js +157 -0
- package/dist/agent/session-sync.d.ts +20 -0
- package/dist/agent/session-sync.js +104 -0
- package/dist/agent/session-tools.d.ts +24 -0
- package/dist/agent/session-tools.js +277 -0
- package/dist/agent/session.d.ts +117 -0
- package/dist/agent/session.js +484 -0
- package/dist/agent/system-prompt.d.ts +49 -0
- package/dist/agent/system-prompt.js +121 -0
- package/dist/agent/tool-runner.d.ts +64 -0
- package/dist/agent/tool-runner.js +309 -0
- package/dist/agent/transform.d.ts +26 -0
- package/dist/agent/transform.js +171 -0
- package/dist/agent/types.d.ts +378 -0
- package/dist/agent/types.js +21 -0
- package/dist/ai/apis/anthropic-messages.d.ts +26 -0
- package/dist/ai/apis/anthropic-messages.js +214 -0
- package/dist/ai/apis/anthropic-request.d.ts +42 -0
- package/dist/ai/apis/anthropic-request.js +282 -0
- package/dist/ai/apis/api.d.ts +28 -0
- package/dist/ai/apis/api.js +102 -0
- package/dist/ai/apis/cache-params.d.ts +48 -0
- package/dist/ai/apis/cache-params.js +163 -0
- package/dist/ai/apis/google-generative-ai.d.ts +32 -0
- package/dist/ai/apis/google-generative-ai.js +249 -0
- package/dist/ai/apis/google-request.d.ts +40 -0
- package/dist/ai/apis/google-request.js +255 -0
- package/dist/ai/apis/openai-compat.d.ts +35 -0
- package/dist/ai/apis/openai-compat.js +142 -0
- package/dist/ai/apis/openai-completions.d.ts +28 -0
- package/dist/ai/apis/openai-completions.js +246 -0
- package/dist/ai/apis/openai-request.d.ts +35 -0
- package/dist/ai/apis/openai-request.js +303 -0
- package/dist/ai/apis/openai-responses-request.d.ts +52 -0
- package/dist/ai/apis/openai-responses-request.js +266 -0
- package/dist/ai/apis/openai-responses.d.ts +28 -0
- package/dist/ai/apis/openai-responses.js +303 -0
- package/dist/ai/apis/shared.d.ts +56 -0
- package/dist/ai/apis/shared.js +179 -0
- package/dist/ai/cache/economics.d.ts +24 -0
- package/dist/ai/cache/economics.js +69 -0
- package/dist/ai/cache/fingerprint.d.ts +17 -0
- package/dist/ai/cache/fingerprint.js +39 -0
- package/dist/ai/cache/miss.d.ts +49 -0
- package/dist/ai/cache/miss.js +96 -0
- package/dist/ai/cache/reporting.d.ts +39 -0
- package/dist/ai/cache/reporting.js +102 -0
- package/dist/ai/cache/types.d.ts +94 -0
- package/dist/ai/cache/types.js +19 -0
- package/dist/ai/cache/warmer.d.ts +94 -0
- package/dist/ai/cache/warmer.js +224 -0
- package/dist/ai/context.d.ts +35 -0
- package/dist/ai/context.js +114 -0
- package/dist/ai/cost.d.ts +21 -0
- package/dist/ai/cost.js +60 -0
- package/dist/ai/event-stream.d.ts +33 -0
- package/dist/ai/event-stream.js +101 -0
- package/dist/ai/fake/fake-provider.d.ts +68 -0
- package/dist/ai/fake/fake-provider.js +278 -0
- package/dist/ai/fake/fake-script.d.ts +69 -0
- package/dist/ai/fake/fake-script.js +142 -0
- package/dist/ai/http.d.ts +54 -0
- package/dist/ai/http.js +212 -0
- package/dist/ai/json-partial.d.ts +17 -0
- package/dist/ai/json-partial.js +243 -0
- package/dist/ai/overflow.d.ts +22 -0
- package/dist/ai/overflow.js +61 -0
- package/dist/ai/providers/auth.d.ts +71 -0
- package/dist/ai/providers/auth.js +202 -0
- package/dist/ai/providers/builtin.d.ts +23 -0
- package/dist/ai/providers/builtin.js +148 -0
- package/dist/ai/providers/catalog-data.d.ts +5 -0
- package/dist/ai/providers/catalog-data.js +19 -0
- package/dist/ai/providers/catalog.d.ts +28 -0
- package/dist/ai/providers/catalog.js +166 -0
- package/dist/ai/providers/registry.d.ts +83 -0
- package/dist/ai/providers/registry.js +305 -0
- package/dist/ai/sse.d.ts +32 -0
- package/dist/ai/sse.js +140 -0
- package/dist/ai/thinking.d.ts +42 -0
- package/dist/ai/thinking.js +101 -0
- package/dist/ai/types.d.ts +428 -0
- package/dist/ai/types.js +19 -0
- package/dist/bundle/ama-sandbox.cjs +324 -0
- package/dist/bundle/ama.cjs +25770 -0
- package/dist/bundle.d.ts +9 -0
- package/dist/bundle.js +15 -0
- package/dist/cli/args.d.ts +82 -0
- package/dist/cli/args.js +381 -0
- package/dist/cli/bootstrap.d.ts +18 -0
- package/dist/cli/bootstrap.js +398 -0
- package/dist/cli/compose-providers.d.ts +37 -0
- package/dist/cli/compose-providers.js +104 -0
- package/dist/cli/compose-session.d.ts +69 -0
- package/dist/cli/compose-session.js +370 -0
- package/dist/cli/compose-store.d.ts +24 -0
- package/dist/cli/compose-store.js +125 -0
- package/dist/cli/compose.d.ts +106 -0
- package/dist/cli/compose.js +206 -0
- package/dist/cli/default-model.d.ts +16 -0
- package/dist/cli/default-model.js +24 -0
- package/dist/cli/deps.d.ts +205 -0
- package/dist/cli/deps.js +12 -0
- package/dist/cli/exit-codes.d.ts +29 -0
- package/dist/cli/exit-codes.js +40 -0
- package/dist/cli/main.d.ts +26 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/runtime.d.ts +90 -0
- package/dist/cli/runtime.js +8 -0
- package/dist/cli/startup-screen.d.ts +15 -0
- package/dist/cli/startup-screen.js +56 -0
- package/dist/cli/startup-steps.d.ts +35 -0
- package/dist/cli/startup-steps.js +197 -0
- package/dist/cli/subcommands/auth.d.ts +12 -0
- package/dist/cli/subcommands/auth.js +91 -0
- package/dist/cli/subcommands/config.d.ts +44 -0
- package/dist/cli/subcommands/config.js +201 -0
- package/dist/cli/subcommands/context.d.ts +21 -0
- package/dist/cli/subcommands/context.js +41 -0
- package/dist/cli/subcommands/doctor.d.ts +11 -0
- package/dist/cli/subcommands/doctor.js +246 -0
- package/dist/cli/subcommands/models-cache-probe.d.ts +34 -0
- package/dist/cli/subcommands/models-cache-probe.js +190 -0
- package/dist/cli/subcommands/models-discover.d.ts +40 -0
- package/dist/cli/subcommands/models-discover.js +204 -0
- package/dist/cli/subcommands/models.d.ts +40 -0
- package/dist/cli/subcommands/models.js +144 -0
- package/dist/cli/subcommands/sessions.d.ts +11 -0
- package/dist/cli/subcommands/sessions.js +100 -0
- package/dist/codemode/capability.d.ts +37 -0
- package/dist/codemode/capability.js +55 -0
- package/dist/codemode/declarations.d.ts +44 -0
- package/dist/codemode/declarations.js +111 -0
- package/dist/codemode/host-side.d.ts +59 -0
- package/dist/codemode/host-side.js +253 -0
- package/dist/codemode/modes.d.ts +23 -0
- package/dist/codemode/modes.js +40 -0
- package/dist/codemode/protocol.d.ts +91 -0
- package/dist/codemode/protocol.js +114 -0
- package/dist/codemode/sandbox-entry.d.ts +25 -0
- package/dist/codemode/sandbox-entry.js +323 -0
- package/dist/codemode/store.d.ts +26 -0
- package/dist/codemode/store.js +53 -0
- package/dist/codemode/tool.d.ts +105 -0
- package/dist/codemode/tool.js +283 -0
- package/dist/compaction/branch-summary.d.ts +30 -0
- package/dist/compaction/branch-summary.js +79 -0
- package/dist/compaction/breaker.d.ts +32 -0
- package/dist/compaction/breaker.js +65 -0
- package/dist/compaction/cut-point.d.ts +23 -0
- package/dist/compaction/cut-point.js +70 -0
- package/dist/compaction/estimate.d.ts +30 -0
- package/dist/compaction/estimate.js +114 -0
- package/dist/compaction/prune-tier.d.ts +31 -0
- package/dist/compaction/prune-tier.js +83 -0
- package/dist/compaction/serialize.d.ts +20 -0
- package/dist/compaction/serialize.js +115 -0
- package/dist/compaction/summarize-tier.d.ts +111 -0
- package/dist/compaction/summarize-tier.js +305 -0
- package/dist/config/auth-file.d.ts +39 -0
- package/dist/config/auth-file.js +92 -0
- package/dist/config/context-files.d.ts +30 -0
- package/dist/config/context-files.js +77 -0
- package/dist/config/load.d.ts +45 -0
- package/dist/config/load.js +97 -0
- package/dist/config/merge.d.ts +73 -0
- package/dist/config/merge.js +248 -0
- package/dist/config/paths.d.ts +38 -0
- package/dist/config/paths.js +108 -0
- package/dist/config/profile.d.ts +27 -0
- package/dist/config/profile.js +65 -0
- package/dist/config/schema.d.ts +34 -0
- package/dist/config/schema.js +413 -0
- package/dist/config/trust.d.ts +42 -0
- package/dist/config/trust.js +122 -0
- package/dist/config/types.d.ts +172 -0
- package/dist/config/types.js +27 -0
- package/dist/config/write.d.ts +10 -0
- package/dist/config/write.js +25 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +30 -0
- package/dist/hooks/config.d.ts +49 -0
- package/dist/hooks/config.js +102 -0
- package/dist/hooks/dispatcher.d.ts +41 -0
- package/dist/hooks/dispatcher.js +104 -0
- package/dist/hooks/matcher.d.ts +22 -0
- package/dist/hooks/matcher.js +108 -0
- package/dist/hooks/protocol.d.ts +33 -0
- package/dist/hooks/protocol.js +240 -0
- package/dist/hooks/runner.d.ts +30 -0
- package/dist/hooks/runner.js +143 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +20 -0
- package/dist/host/api-impl.d.ts +75 -0
- package/dist/host/api-impl.js +173 -0
- package/dist/host/loader.d.ts +31 -0
- package/dist/host/loader.js +147 -0
- package/dist/host/types.d.ts +169 -0
- package/dist/host/types.js +14 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +28 -0
- package/dist/modes/commands-core.d.ts +44 -0
- package/dist/modes/commands-core.js +171 -0
- package/dist/modes/interactive/approval-dialog.d.ts +48 -0
- package/dist/modes/interactive/approval-dialog.js +202 -0
- package/dist/modes/interactive/commands.d.ts +41 -0
- package/dist/modes/interactive/commands.js +202 -0
- package/dist/modes/interactive/completion.d.ts +53 -0
- package/dist/modes/interactive/completion.js +157 -0
- package/dist/modes/interactive/interactive-mode.d.ts +46 -0
- package/dist/modes/interactive/interactive-mode.js +487 -0
- package/dist/modes/interactive/key-dispatch.d.ts +29 -0
- package/dist/modes/interactive/key-dispatch.js +107 -0
- package/dist/modes/interactive/line/line-editor.d.ts +50 -0
- package/dist/modes/interactive/line/line-editor.js +232 -0
- package/dist/modes/interactive/line/line-mode.d.ts +20 -0
- package/dist/modes/interactive/line/line-mode.js +216 -0
- package/dist/modes/interactive/line/line-render.d.ts +35 -0
- package/dist/modes/interactive/line/line-render.js +171 -0
- package/dist/modes/interactive/line/paste-state.d.ts +26 -0
- package/dist/modes/interactive/line/paste-state.js +72 -0
- package/dist/modes/interactive/message-view.d.ts +77 -0
- package/dist/modes/interactive/message-view.js +269 -0
- package/dist/modes/interactive/pickers.d.ts +42 -0
- package/dist/modes/interactive/pickers.js +115 -0
- package/dist/modes/interactive/startup-ui.d.ts +28 -0
- package/dist/modes/interactive/startup-ui.js +238 -0
- package/dist/modes/interactive/status-bar.d.ts +48 -0
- package/dist/modes/interactive/status-bar.js +139 -0
- package/dist/modes/interactive/tool-view.d.ts +95 -0
- package/dist/modes/interactive/tool-view.js +344 -0
- package/dist/modes/print/json-event.d.ts +15 -0
- package/dist/modes/print/json-event.js +39 -0
- package/dist/modes/print/print-mode.d.ts +15 -0
- package/dist/modes/print/print-mode.js +96 -0
- package/dist/modes/rpc/commands.d.ts +40 -0
- package/dist/modes/rpc/commands.js +258 -0
- package/dist/modes/rpc/jsonl.d.ts +16 -0
- package/dist/modes/rpc/jsonl.js +55 -0
- package/dist/modes/rpc/rpc-mode.d.ts +22 -0
- package/dist/modes/rpc/rpc-mode.js +153 -0
- package/dist/modes/session-report.d.ts +55 -0
- package/dist/modes/session-report.js +278 -0
- package/dist/modes/shared.d.ts +13 -0
- package/dist/modes/shared.js +52 -0
- package/dist/modes/startup-ui-text.d.ts +16 -0
- package/dist/modes/startup-ui-text.js +103 -0
- package/dist/permissions/broker.d.ts +47 -0
- package/dist/permissions/broker.js +107 -0
- package/dist/permissions/dangerous.d.ts +47 -0
- package/dist/permissions/dangerous.js +370 -0
- package/dist/permissions/pipeline.d.ts +51 -0
- package/dist/permissions/pipeline.js +181 -0
- package/dist/permissions/preview.d.ts +43 -0
- package/dist/permissions/preview.js +463 -0
- package/dist/permissions/rules.d.ts +58 -0
- package/dist/permissions/rules.js +249 -0
- package/dist/permissions/types.d.ts +100 -0
- package/dist/permissions/types.js +18 -0
- package/dist/rpc.d.ts +171 -0
- package/dist/rpc.js +13 -0
- package/dist/sdk.d.ts +98 -0
- package/dist/sdk.js +195 -0
- package/dist/session/manager.d.ts +70 -0
- package/dist/session/manager.js +278 -0
- package/dist/session/migrate.d.ts +19 -0
- package/dist/session/migrate.js +53 -0
- package/dist/session/projection.d.ts +50 -0
- package/dist/session/projection.js +180 -0
- package/dist/session/store.d.ts +47 -0
- package/dist/session/store.js +210 -0
- package/dist/session/tree.d.ts +21 -0
- package/dist/session/tree.js +88 -0
- package/dist/session/types.d.ts +199 -0
- package/dist/session/types.js +18 -0
- package/dist/skills/discover.d.ts +60 -0
- package/dist/skills/discover.js +160 -0
- package/dist/skills/expand.d.ts +35 -0
- package/dist/skills/expand.js +46 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +157 -0
- package/dist/skills/index-prompt.d.ts +13 -0
- package/dist/skills/index-prompt.js +42 -0
- package/dist/skills/templates.d.ts +52 -0
- package/dist/skills/templates.js +177 -0
- package/dist/tools/bash.d.ts +49 -0
- package/dist/tools/bash.js +222 -0
- package/dist/tools/edit-fuzzy.d.ts +36 -0
- package/dist/tools/edit-fuzzy.js +129 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +241 -0
- package/dist/tools/file-mutex.d.ts +8 -0
- package/dist/tools/file-mutex.js +29 -0
- package/dist/tools/glob.d.ts +44 -0
- package/dist/tools/glob.js +217 -0
- package/dist/tools/grep.d.ts +39 -0
- package/dist/tools/grep.js +179 -0
- package/dist/tools/ignore.d.ts +40 -0
- package/dist/tools/ignore.js +174 -0
- package/dist/tools/ls.d.ts +21 -0
- package/dist/tools/ls.js +96 -0
- package/dist/tools/output-accumulator.d.ts +48 -0
- package/dist/tools/output-accumulator.js +106 -0
- package/dist/tools/paths.d.ts +20 -0
- package/dist/tools/paths.js +58 -0
- package/dist/tools/presets.d.ts +71 -0
- package/dist/tools/presets.js +128 -0
- package/dist/tools/process-tree.d.ts +35 -0
- package/dist/tools/process-tree.js +125 -0
- package/dist/tools/read.d.ts +28 -0
- package/dist/tools/read.js +181 -0
- package/dist/tools/registry.d.ts +43 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/shell.d.ts +38 -0
- package/dist/tools/shell.js +91 -0
- package/dist/tools/task.d.ts +29 -0
- package/dist/tools/task.js +107 -0
- package/dist/tools/todo.d.ts +28 -0
- package/dist/tools/todo.js +107 -0
- package/dist/tools/truncate.d.ts +44 -0
- package/dist/tools/truncate.js +175 -0
- package/dist/tools/types.d.ts +135 -0
- package/dist/tools/types.js +19 -0
- package/dist/tools/write.d.ts +20 -0
- package/dist/tools/write.js +84 -0
- package/dist/tui/ansi.d.ts +29 -0
- package/dist/tui/ansi.js +420 -0
- package/dist/tui/component.d.ts +38 -0
- package/dist/tui/component.js +13 -0
- package/dist/tui/components/box.d.ts +26 -0
- package/dist/tui/components/box.js +65 -0
- package/dist/tui/components/container.d.ts +14 -0
- package/dist/tui/components/container.js +39 -0
- package/dist/tui/components/editor-buffer.d.ts +67 -0
- package/dist/tui/components/editor-buffer.js +300 -0
- package/dist/tui/components/editor-paste.d.ts +29 -0
- package/dist/tui/components/editor-paste.js +64 -0
- package/dist/tui/components/editor.d.ts +114 -0
- package/dist/tui/components/editor.js +530 -0
- package/dist/tui/components/key-value.d.ts +29 -0
- package/dist/tui/components/key-value.js +48 -0
- package/dist/tui/components/loader.d.ts +35 -0
- package/dist/tui/components/loader.js +71 -0
- package/dist/tui/components/markdown.d.ts +90 -0
- package/dist/tui/components/markdown.js +462 -0
- package/dist/tui/components/meter.d.ts +29 -0
- package/dist/tui/components/meter.js +47 -0
- package/dist/tui/components/overlay.d.ts +24 -0
- package/dist/tui/components/overlay.js +52 -0
- package/dist/tui/components/select-list.d.ts +57 -0
- package/dist/tui/components/select-list.js +179 -0
- package/dist/tui/components/spacer.d.ts +11 -0
- package/dist/tui/components/spacer.js +16 -0
- package/dist/tui/components/text.d.ts +30 -0
- package/dist/tui/components/text.js +65 -0
- package/dist/tui/keybindings.d.ts +66 -0
- package/dist/tui/keybindings.js +120 -0
- package/dist/tui/keys.d.ts +38 -0
- package/dist/tui/keys.js +275 -0
- package/dist/tui/stdin-buffer.d.ts +41 -0
- package/dist/tui/stdin-buffer.js +206 -0
- package/dist/tui/terminal.d.ts +79 -0
- package/dist/tui/terminal.js +153 -0
- package/dist/tui/theme.d.ts +31 -0
- package/dist/tui/theme.js +199 -0
- package/dist/tui/tui.d.ts +103 -0
- package/dist/tui/tui.js +363 -0
- package/dist/tui/vt-screen.d.ts +46 -0
- package/dist/tui/vt-screen.js +251 -0
- package/dist/tui.d.ts +30 -0
- package/dist/tui.js +31 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +24 -0
- package/docs/codemode.md +102 -0
- package/docs/hooks.md +196 -0
- package/docs/host-api.md +161 -0
- package/docs/providers.md +316 -0
- package/docs/rpc.md +253 -0
- package/docs/session-format.md +119 -0
- package/docs/tui.md +168 -0
- package/package.json +101 -4
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 危险命令表(设计 §7.2;v1 §7.1 的表 + `git branch -D`、`npm publish`、`docker system prune -a`、
|
|
3
|
+
* `shutdown/reboot`)。[B3]
|
|
4
|
+
*
|
|
5
|
+
* 命中 → 管线第 ② 步 ask(无人值守 deny),allow 规则不能越过。识别按段进行:命令先按
|
|
6
|
+
* `&&`、`||`、`;`、`|`、`&`、换行切段(引号内不切),每段剥掉前导的环境赋值与
|
|
7
|
+
* `sudo / command / exec / nohup / time / env` 等前缀后看命令名;`sh -c '…'`、`eval …`、`xargs …`、`find -exec … ;` 里的
|
|
8
|
+
* 命令递归识别,嵌套超过 {@link MAX_NESTING} 层按危险处理。整条命令另外做跨段检查
|
|
9
|
+
* (`curl … | sh`、fork 炸弹)。每条规则都有正例与反例测试(dangerous.test.ts)。
|
|
10
|
+
*/
|
|
11
|
+
import { splitShellSegments } from "./rules.js";
|
|
12
|
+
const WRAPPERS = new Set(["command", "exec", "nohup", "time", "env", "builtin", "nice"]);
|
|
13
|
+
/**
|
|
14
|
+
* 分词:按未加引号的空白切;单引号内原样,双引号内反斜杠只转义 `\`、`"`、`$`、反引号,引号外反斜杠转义下一个字符;
|
|
15
|
+
* 相邻的引号段与普通字符拼成一个词(`'a'"b"c` → `abc`)。不做变量与通配展开。
|
|
16
|
+
*/
|
|
17
|
+
export function shellWords(segment) {
|
|
18
|
+
const words = [];
|
|
19
|
+
let cur = "";
|
|
20
|
+
let inWord = false;
|
|
21
|
+
for (let i = 0; i < segment.length; i++) {
|
|
22
|
+
const ch = segment[i];
|
|
23
|
+
if (/\s/.test(ch)) {
|
|
24
|
+
if (inWord)
|
|
25
|
+
words.push(cur);
|
|
26
|
+
cur = "";
|
|
27
|
+
inWord = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
inWord = true;
|
|
31
|
+
if (ch === "'") {
|
|
32
|
+
const end = segment.indexOf("'", i + 1);
|
|
33
|
+
const stop = end === -1 ? segment.length : end;
|
|
34
|
+
cur += segment.slice(i + 1, stop);
|
|
35
|
+
i = stop;
|
|
36
|
+
}
|
|
37
|
+
else if (ch === '"') {
|
|
38
|
+
i++;
|
|
39
|
+
while (i < segment.length && segment[i] !== '"') {
|
|
40
|
+
const c = segment[i];
|
|
41
|
+
if (c === "\\" && i + 1 < segment.length && '\\"$`'.includes(segment[i + 1])) {
|
|
42
|
+
cur += segment[i + 1];
|
|
43
|
+
i += 2;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
cur += c;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (ch === "\\" && i + 1 < segment.length) {
|
|
52
|
+
cur += segment[++i];
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
cur += ch;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (inWord)
|
|
59
|
+
words.push(cur);
|
|
60
|
+
return words;
|
|
61
|
+
}
|
|
62
|
+
/** 剥前缀;`keepSudo` 为假时连 sudo 一并剥掉。 */
|
|
63
|
+
export function commandWords(segment, keepSudo = false) {
|
|
64
|
+
const words = shellWords(segment.replace(/^[({\s]+/, ""));
|
|
65
|
+
let i = 0;
|
|
66
|
+
while (i < words.length) {
|
|
67
|
+
const w = words[i];
|
|
68
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w))
|
|
69
|
+
i++;
|
|
70
|
+
else if (WRAPPERS.has(w))
|
|
71
|
+
i++;
|
|
72
|
+
else if (!keepSudo && (w === "sudo" || w === "doas")) {
|
|
73
|
+
i++;
|
|
74
|
+
while (i < words.length && words[i].startsWith("-"))
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
return words.slice(i);
|
|
81
|
+
}
|
|
82
|
+
function base(word) {
|
|
83
|
+
return (word ?? "").split("/").pop() ?? "";
|
|
84
|
+
}
|
|
85
|
+
function flags(argv) {
|
|
86
|
+
return argv
|
|
87
|
+
.filter((a) => /^-[A-Za-z]/.test(a))
|
|
88
|
+
.map((a) => a.slice(1))
|
|
89
|
+
.join("");
|
|
90
|
+
}
|
|
91
|
+
function hasFlag(argv, short, long) {
|
|
92
|
+
return argv.some((a) => (/^-[A-Za-z]+$/.test(a) && a.includes(short)) || (long !== undefined && a === long));
|
|
93
|
+
}
|
|
94
|
+
const RM_DANGEROUS_TARGETS = /^(\/|\/\*|~|~\/|~\/\*|\$HOME|\$HOME\/|\$HOME\/\*|\.|\.\/|\.\/\*|\*|\.\.|\.\.\/|\/\.\*)$/;
|
|
95
|
+
/** git 的全局选项(出现在子命令之前):取值的与不取值的。 */
|
|
96
|
+
const GIT_VALUE_OPTIONS = new Set([
|
|
97
|
+
"-C",
|
|
98
|
+
"-c",
|
|
99
|
+
"--git-dir",
|
|
100
|
+
"--work-tree",
|
|
101
|
+
"--namespace",
|
|
102
|
+
"--exec-path",
|
|
103
|
+
"--config-env",
|
|
104
|
+
]);
|
|
105
|
+
const GIT_FLAG_OPTIONS = /^(-p|-P|--paginate|--no-pager|--bare|--no-replace-objects|--literal-pathspecs|--glob-pathspecs|--noglob-pathspecs|--icase-pathspecs|--no-optional-locks|--(git-dir|work-tree|namespace|exec-path|config-env)=.*|-[cC].+)$/;
|
|
106
|
+
/**
|
|
107
|
+
* 去掉 git 子命令之前的全局选项:`git -C dir -c k=v reset --hard` → `git reset --hard`。
|
|
108
|
+
* 不是 git 时返回 undefined。不去掉的话 `git -C x reset --hard` 这类写法会漏判。
|
|
109
|
+
*/
|
|
110
|
+
function gitArgv(argv) {
|
|
111
|
+
if (base(argv[0]) !== "git")
|
|
112
|
+
return undefined;
|
|
113
|
+
let i = 1;
|
|
114
|
+
while (i < argv.length) {
|
|
115
|
+
const a = argv[i] ?? "";
|
|
116
|
+
if (GIT_VALUE_OPTIONS.has(a))
|
|
117
|
+
i += 2;
|
|
118
|
+
else if (GIT_FLAG_OPTIONS.test(a))
|
|
119
|
+
i += 1;
|
|
120
|
+
else
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
return [argv[0] ?? "git", ...argv.slice(i)];
|
|
124
|
+
}
|
|
125
|
+
export const DANGEROUS_RULES = [
|
|
126
|
+
{
|
|
127
|
+
id: "rm-rf-root",
|
|
128
|
+
description: "rm -rf on /, ~, ., .. or *",
|
|
129
|
+
segment: (argv) => {
|
|
130
|
+
if (base(argv[0]) !== "rm")
|
|
131
|
+
return false;
|
|
132
|
+
const f = flags(argv);
|
|
133
|
+
const recursive = /[rR]/.test(f) || argv.includes("--recursive");
|
|
134
|
+
const force = f.includes("f") || argv.includes("--force");
|
|
135
|
+
return recursive && force && argv.slice(1).some((a) => RM_DANGEROUS_TARGETS.test(a));
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "sudo",
|
|
140
|
+
description: "privilege escalation with sudo",
|
|
141
|
+
segment: (_argv, segment) => {
|
|
142
|
+
const words = commandWords(segment, true);
|
|
143
|
+
return base(words[0]) === "sudo" || base(words[0]) === "doas";
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{ id: "su", description: "switching user with su", segment: (argv) => base(argv[0]) === "su" },
|
|
147
|
+
{
|
|
148
|
+
id: "dd",
|
|
149
|
+
description: "raw disk copy with dd",
|
|
150
|
+
segment: (argv) => base(argv[0]) === "dd",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "mkfs",
|
|
154
|
+
description: "creating a filesystem (mkfs)",
|
|
155
|
+
segment: (argv) => /^mkfs(\.[a-z0-9]+)?$/.test(base(argv[0])),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "write-block-device",
|
|
159
|
+
description: "redirecting output to a block device",
|
|
160
|
+
whole: (cmd) => />\s*\/dev\/(sd[a-z]|hd[a-z]|nvme\d|disk\d|rdisk\d|mmcblk\d|xvd[a-z]|vd[a-z])/.test(cmd),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "git-push-force",
|
|
164
|
+
description: "git push --force",
|
|
165
|
+
segment: (raw) => {
|
|
166
|
+
const argv = gitArgv(raw) ?? [];
|
|
167
|
+
return (argv[1] === "push" &&
|
|
168
|
+
argv.some((a) => a === "--force" ||
|
|
169
|
+
a.startsWith("--force-with-lease") ||
|
|
170
|
+
a === "--force-if-includes" ||
|
|
171
|
+
(/^-[A-Za-z]+$/.test(a) && a.includes("f")) ||
|
|
172
|
+
/^\+/.test(a)));
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "git-reset-hard",
|
|
177
|
+
description: "git reset --hard",
|
|
178
|
+
segment: (raw) => {
|
|
179
|
+
const argv = gitArgv(raw) ?? [];
|
|
180
|
+
return argv[1] === "reset" && argv.includes("--hard");
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: "git-clean-force",
|
|
185
|
+
description: "git clean -f (deletes untracked files)",
|
|
186
|
+
segment: (raw) => {
|
|
187
|
+
const argv = gitArgv(raw) ?? [];
|
|
188
|
+
return argv[1] === "clean" && (hasFlag(argv, "f") || argv.includes("--force"));
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: "git-branch-force-delete",
|
|
193
|
+
description: "git branch -D",
|
|
194
|
+
segment: (raw) => {
|
|
195
|
+
const argv = gitArgv(raw) ?? [];
|
|
196
|
+
return (argv[1] === "branch" &&
|
|
197
|
+
(argv.some((a) => /^-[A-Za-z]*D[A-Za-z]*$/.test(a)) ||
|
|
198
|
+
(argv.includes("--delete") && argv.includes("--force"))));
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: "pipe-to-shell",
|
|
203
|
+
description: "piping a download (curl / wget) into a shell",
|
|
204
|
+
whole: (cmd) => /\b(curl|wget)\b[^|;&]*\|\s*(sudo\s+)?(env\s+)?(ba|z|da|k|fi)?sh\b/.test(cmd) ||
|
|
205
|
+
/\b(ba|z)?sh\s+(-c\s+)?["']?\$\(\s*(curl|wget)\b/.test(cmd) ||
|
|
206
|
+
/\b(ba|z)?sh\s+<\(\s*(curl|wget)\b/.test(cmd),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "chmod-777-recursive",
|
|
210
|
+
description: "chmod -R 777",
|
|
211
|
+
segment: (argv) => base(argv[0]) === "chmod" &&
|
|
212
|
+
(hasFlag(argv, "R") || argv.includes("--recursive")) &&
|
|
213
|
+
argv.some((a) => /^0?777$/.test(a) || a === "a+rwx"),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "kill-all",
|
|
217
|
+
description: "kill -9 -1 (kills every process you own)",
|
|
218
|
+
segment: (argv) => base(argv[0]) === "kill" && argv.slice(1).includes("-1"),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "fork-bomb",
|
|
222
|
+
description: "fork bomb",
|
|
223
|
+
whole: (cmd) => /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/.test(cmd),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "package-publish",
|
|
227
|
+
description: "publishing a package (npm / pnpm / yarn publish)",
|
|
228
|
+
segment: (argv) => ["npm", "pnpm", "yarn"].includes(base(argv[0])) && argv[1] === "publish",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "docker-system-prune-all",
|
|
232
|
+
description: "docker system prune -a",
|
|
233
|
+
segment: (argv) => base(argv[0]) === "docker" &&
|
|
234
|
+
argv[1] === "system" &&
|
|
235
|
+
argv[2] === "prune" &&
|
|
236
|
+
(hasFlag(argv, "a") || argv.includes("--all")),
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: "shutdown-reboot",
|
|
240
|
+
description: "shutting down or rebooting the machine",
|
|
241
|
+
segment: (argv) => ["shutdown", "reboot", "halt", "poweroff"].includes(base(argv[0])) ||
|
|
242
|
+
(base(argv[0]) === "systemctl" && ["poweroff", "reboot", "halt"].includes(argv[1] ?? "")) ||
|
|
243
|
+
(base(argv[0]) === "init" && (argv[1] === "0" || argv[1] === "6")),
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
247
|
+
/** 嵌套层数上限:`sh -c` / `eval` 等每包一层计一层,超过按危险处理。 */
|
|
248
|
+
export const MAX_NESTING = 3;
|
|
249
|
+
const TOO_DEEP = {
|
|
250
|
+
id: "nested-too-deep",
|
|
251
|
+
description: `shell command nested more than ${MAX_NESTING} levels deep`,
|
|
252
|
+
};
|
|
253
|
+
/** `sh -c 'cmd'` 的 cmd:选项簇里有 `c` 时取第一个非选项参数;`-o` / `+o` 吃掉下一个词。 */
|
|
254
|
+
function shellCommandString(argv) {
|
|
255
|
+
let hasC = false;
|
|
256
|
+
for (let i = 1; i < argv.length; i++) {
|
|
257
|
+
const a = argv[i];
|
|
258
|
+
if (a === "--")
|
|
259
|
+
return hasC ? argv[i + 1] : undefined;
|
|
260
|
+
if (a.startsWith("--"))
|
|
261
|
+
continue;
|
|
262
|
+
if (/^[-+][A-Za-z]+$/.test(a)) {
|
|
263
|
+
if (a[0] === "-" && a.includes("c"))
|
|
264
|
+
hasC = true;
|
|
265
|
+
if (a.endsWith("o"))
|
|
266
|
+
i++;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
return hasC ? a : undefined;
|
|
270
|
+
}
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
/** 把词重新拼回命令文本:含特殊字符的词加单引号。 */
|
|
274
|
+
function joinWords(words) {
|
|
275
|
+
return words
|
|
276
|
+
.map((w) => (/^[A-Za-z0-9_@%+=:,./{}~-]+$/.test(w) ? w : `'${w.replace(/'/g, `'\\''`)}'`))
|
|
277
|
+
.join(" ");
|
|
278
|
+
}
|
|
279
|
+
/** xargs 中带独立参数的短选项。 */
|
|
280
|
+
const XARGS_ARG_OPTS = new Set(["-I", "-L", "-n", "-P", "-s", "-E", "-d", "-a"]);
|
|
281
|
+
/** `xargs [选项] cmd …` 的 cmd 部分;没有命令(缺省 echo)返回 undefined。 */
|
|
282
|
+
function xargsCommand(argv) {
|
|
283
|
+
let i = 1;
|
|
284
|
+
while (i < argv.length) {
|
|
285
|
+
const a = argv[i];
|
|
286
|
+
if (a === "--") {
|
|
287
|
+
i++;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (!a.startsWith("-") || a === "-")
|
|
291
|
+
break;
|
|
292
|
+
i += XARGS_ARG_OPTS.has(a) ? 2 : 1;
|
|
293
|
+
}
|
|
294
|
+
return i < argv.length ? joinWords(argv.slice(i)) : undefined;
|
|
295
|
+
}
|
|
296
|
+
/** `find … -exec / -execdir / -ok / -okdir cmd … ;|+` 里的每条 cmd。 */
|
|
297
|
+
function findExecCommands(argv) {
|
|
298
|
+
const out = [];
|
|
299
|
+
for (let i = 1; i < argv.length; i++) {
|
|
300
|
+
if (!["-exec", "-execdir", "-ok", "-okdir"].includes(argv[i]))
|
|
301
|
+
continue;
|
|
302
|
+
let end = i + 1;
|
|
303
|
+
while (end < argv.length && argv[end] !== ";" && argv[end] !== "+")
|
|
304
|
+
end++;
|
|
305
|
+
if (end > i + 1)
|
|
306
|
+
out.push(joinWords(argv.slice(i + 1, end)));
|
|
307
|
+
i = end;
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* 段内嵌套的命令文本(剥过前缀的 argv):`sh / bash / zsh / dash / ksh -c '…'` 的字符串参数;
|
|
313
|
+
* `eval` 其余词以空格拼接;`xargs` 要执行的命令;`find -exec … ;` 里的命令。
|
|
314
|
+
*/
|
|
315
|
+
export function nestedCommands(argv) {
|
|
316
|
+
const name = base(argv[0]);
|
|
317
|
+
if (SHELLS.has(name)) {
|
|
318
|
+
const inner = shellCommandString(argv);
|
|
319
|
+
return inner === undefined ? [] : [inner];
|
|
320
|
+
}
|
|
321
|
+
if (name === "eval")
|
|
322
|
+
return argv.length > 1 ? [argv.slice(1).join(" ")] : [];
|
|
323
|
+
if (name === "xargs") {
|
|
324
|
+
const inner = xargsCommand(argv);
|
|
325
|
+
return inner === undefined ? [] : [inner];
|
|
326
|
+
}
|
|
327
|
+
if (name === "find")
|
|
328
|
+
return findExecCommands(argv);
|
|
329
|
+
return [];
|
|
330
|
+
}
|
|
331
|
+
/** 整条命令里所有嵌套的命令文本,供 allow / deny 规则与会话记忆逐层核对。 */
|
|
332
|
+
export function collectNestedCommands(command) {
|
|
333
|
+
const out = { commands: [], tooDeep: false };
|
|
334
|
+
const walk = (text, depth) => {
|
|
335
|
+
for (const segment of splitShellSegments(text)) {
|
|
336
|
+
for (const inner of nestedCommands(commandWords(segment))) {
|
|
337
|
+
if (depth >= MAX_NESTING) {
|
|
338
|
+
out.tooDeep = true;
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
out.commands.push(inner);
|
|
342
|
+
walk(inner, depth + 1);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
walk(command, 0);
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/** 第一条命中的危险规则;无则 undefined。嵌套命令({@link nestedCommands})递归识别。 */
|
|
350
|
+
export function matchDangerous(command, depth = 0) {
|
|
351
|
+
for (const rule of DANGEROUS_RULES) {
|
|
352
|
+
if (rule.whole?.(command))
|
|
353
|
+
return { id: rule.id, description: rule.description };
|
|
354
|
+
}
|
|
355
|
+
for (const segment of splitShellSegments(command)) {
|
|
356
|
+
const argv = commandWords(segment);
|
|
357
|
+
for (const rule of DANGEROUS_RULES) {
|
|
358
|
+
if (rule.segment?.(argv, segment))
|
|
359
|
+
return { id: rule.id, description: rule.description };
|
|
360
|
+
}
|
|
361
|
+
for (const inner of nestedCommands(argv)) {
|
|
362
|
+
if (depth >= MAX_NESTING)
|
|
363
|
+
return TOO_DEEP;
|
|
364
|
+
const hit = matchDangerous(inner, depth + 1);
|
|
365
|
+
if (hit)
|
|
366
|
+
return hit;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限管线(设计 §6.3 第 3 步、§7.1、D11)。[B3]
|
|
3
|
+
*
|
|
4
|
+
* ① deny 规则 ∪ Hook deny → deny
|
|
5
|
+
* ② 危险命令(仅 bash) → ask(无人值守 deny),之后各步不能放宽
|
|
6
|
+
* ③ 模式:plan read 允许,write / execute deny
|
|
7
|
+
* default read 允许,write / execute 问
|
|
8
|
+
* auto-edit read / write 允许,execute 问
|
|
9
|
+
* full-auto 全部允许
|
|
10
|
+
* ④ allow 规则 ∪ Hook allow ∪ allow_session 记忆:把 ③ 的 ask 变 allow(不越过 ①②,不改 plan 的 deny);
|
|
11
|
+
* Hook ask:随后把 allow 变 ask——净效果是 Hook ask 总得到 ask(reason "hook",对话框显示 Hook 的 reason)
|
|
12
|
+
* 无人值守:最终 ask → deny。
|
|
13
|
+
*
|
|
14
|
+
* allow_session 记忆(内存,不落盘):bash 记命令的前两个词(`npm test`),之后以它开头的命令
|
|
15
|
+
* 命中;带 path 的工具记文件所在目录,之后该目录下的路径命中;其它工具只记工具名。
|
|
16
|
+
* 记忆只作用于第 ③ 步模式产生的 ask。
|
|
17
|
+
*
|
|
18
|
+
* 包装里的命令(`sh -c '…'`、`eval`、`xargs`、`find -exec`,见 dangerous.ts)逐层核对:deny 规则命中
|
|
19
|
+
* 任一层即 deny;allow 规则与会话记忆要求外层与每一层嵌套命令都被覆盖,嵌套超深时不放行。
|
|
20
|
+
*/
|
|
21
|
+
import type { Decision, PermissionCheckInput, PermissionMode, PermissionPipelineApi, PermissionVerdict, Rule } from "./types.js";
|
|
22
|
+
import type { ToolPermission } from "../tools/types.js";
|
|
23
|
+
/** 第 ③ 步的模式真值表。 */
|
|
24
|
+
export declare function modeDecision(mode: PermissionMode, permission: ToolPermission): Decision;
|
|
25
|
+
export declare const UNATTENDED_MESSAGE = "This tool call requires approval, but no one is available to approve it (unattended mode).";
|
|
26
|
+
interface SessionGrant {
|
|
27
|
+
toolName: string;
|
|
28
|
+
kind: "command" | "dir" | "any";
|
|
29
|
+
prefix: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function sessionGrantFor(toolName: string, input: unknown, cwd: string): SessionGrant;
|
|
32
|
+
export interface PermissionPipelineOptions {
|
|
33
|
+
mode: PermissionMode;
|
|
34
|
+
rules: readonly Rule[];
|
|
35
|
+
cwd: string;
|
|
36
|
+
}
|
|
37
|
+
export declare class PermissionPipeline implements PermissionPipelineApi {
|
|
38
|
+
private currentMode;
|
|
39
|
+
private readonly ruleList;
|
|
40
|
+
private readonly grants;
|
|
41
|
+
readonly cwd: string;
|
|
42
|
+
constructor(options: PermissionPipelineOptions);
|
|
43
|
+
get mode(): PermissionMode;
|
|
44
|
+
setMode(mode: PermissionMode): void;
|
|
45
|
+
get rules(): readonly Rule[];
|
|
46
|
+
rememberForSession(toolName: string, input: unknown): void;
|
|
47
|
+
clearSessionGrants(): void;
|
|
48
|
+
check(input: PermissionCheckInput): PermissionVerdict;
|
|
49
|
+
private evaluate;
|
|
50
|
+
}
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限管线(设计 §6.3 第 3 步、§7.1、D11)。[B3]
|
|
3
|
+
*
|
|
4
|
+
* ① deny 规则 ∪ Hook deny → deny
|
|
5
|
+
* ② 危险命令(仅 bash) → ask(无人值守 deny),之后各步不能放宽
|
|
6
|
+
* ③ 模式:plan read 允许,write / execute deny
|
|
7
|
+
* default read 允许,write / execute 问
|
|
8
|
+
* auto-edit read / write 允许,execute 问
|
|
9
|
+
* full-auto 全部允许
|
|
10
|
+
* ④ allow 规则 ∪ Hook allow ∪ allow_session 记忆:把 ③ 的 ask 变 allow(不越过 ①②,不改 plan 的 deny);
|
|
11
|
+
* Hook ask:随后把 allow 变 ask——净效果是 Hook ask 总得到 ask(reason "hook",对话框显示 Hook 的 reason)
|
|
12
|
+
* 无人值守:最终 ask → deny。
|
|
13
|
+
*
|
|
14
|
+
* allow_session 记忆(内存,不落盘):bash 记命令的前两个词(`npm test`),之后以它开头的命令
|
|
15
|
+
* 命中;带 path 的工具记文件所在目录,之后该目录下的路径命中;其它工具只记工具名。
|
|
16
|
+
* 记忆只作用于第 ③ 步模式产生的 ask。
|
|
17
|
+
*
|
|
18
|
+
* 包装里的命令(`sh -c '…'`、`eval`、`xargs`、`find -exec`,见 dangerous.ts)逐层核对:deny 规则命中
|
|
19
|
+
* 任一层即 deny;allow 规则与会话记忆要求外层与每一层嵌套命令都被覆盖,嵌套超深时不放行。
|
|
20
|
+
*/
|
|
21
|
+
import { dirname, sep } from "node:path";
|
|
22
|
+
import { findAllowRule, findDenyRule, inputCommand, inputPath, normalizeCommand, splitShellSegments, } from "./rules.js";
|
|
23
|
+
import { collectNestedCommands, matchDangerous } from "./dangerous.js";
|
|
24
|
+
/** bash 调用拆成「外层 + 每层嵌套命令」各自的输入;嵌套超深时 tooDeep 为真。非 bash 原样返回。 */
|
|
25
|
+
function layeredInputs(toolName, input) {
|
|
26
|
+
const command = toolName === "bash" ? inputCommand(input) : undefined;
|
|
27
|
+
if (command === undefined)
|
|
28
|
+
return { inputs: [input], tooDeep: false };
|
|
29
|
+
const nested = collectNestedCommands(command);
|
|
30
|
+
return {
|
|
31
|
+
inputs: [input, ...nested.commands.map((c) => ({ ...input, command: c }))],
|
|
32
|
+
tooDeep: nested.tooDeep,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** 第 ③ 步的模式真值表。 */
|
|
36
|
+
export function modeDecision(mode, permission) {
|
|
37
|
+
if (permission === "read" || mode === "full-auto")
|
|
38
|
+
return "allow";
|
|
39
|
+
if (mode === "plan")
|
|
40
|
+
return "deny";
|
|
41
|
+
if (mode === "auto-edit")
|
|
42
|
+
return permission === "write" ? "allow" : "ask";
|
|
43
|
+
return "ask";
|
|
44
|
+
}
|
|
45
|
+
export const UNATTENDED_MESSAGE = "This tool call requires approval, but no one is available to approve it (unattended mode).";
|
|
46
|
+
export function sessionGrantFor(toolName, input, cwd) {
|
|
47
|
+
const command = inputCommand(input);
|
|
48
|
+
if (command !== undefined) {
|
|
49
|
+
const words = normalizeCommand(command).split(" ").slice(0, 2);
|
|
50
|
+
return { toolName, kind: "command", prefix: words.join(" ") };
|
|
51
|
+
}
|
|
52
|
+
const path = inputPath(toolName, input, cwd);
|
|
53
|
+
if (path !== undefined)
|
|
54
|
+
return { toolName, kind: "dir", prefix: dirname(path) };
|
|
55
|
+
return { toolName, kind: "any", prefix: "" };
|
|
56
|
+
}
|
|
57
|
+
function grantCovers(grant, toolName, input, cwd) {
|
|
58
|
+
if (grant.toolName !== toolName)
|
|
59
|
+
return false;
|
|
60
|
+
if (grant.kind === "any")
|
|
61
|
+
return true;
|
|
62
|
+
if (grant.kind === "command") {
|
|
63
|
+
const command = inputCommand(input);
|
|
64
|
+
if (command === undefined || /\$\(|`/.test(command))
|
|
65
|
+
return false;
|
|
66
|
+
const segments = splitShellSegments(command);
|
|
67
|
+
return (segments.length > 0 &&
|
|
68
|
+
segments.every((s) => s === grant.prefix || s.startsWith(`${grant.prefix} `)));
|
|
69
|
+
}
|
|
70
|
+
const path = inputPath(toolName, input, cwd);
|
|
71
|
+
return path !== undefined && (path === grant.prefix || path.startsWith(grant.prefix + sep));
|
|
72
|
+
}
|
|
73
|
+
export class PermissionPipeline {
|
|
74
|
+
currentMode;
|
|
75
|
+
ruleList;
|
|
76
|
+
grants = [];
|
|
77
|
+
cwd;
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.currentMode = options.mode;
|
|
80
|
+
this.ruleList = [...options.rules];
|
|
81
|
+
this.cwd = options.cwd;
|
|
82
|
+
}
|
|
83
|
+
get mode() {
|
|
84
|
+
return this.currentMode;
|
|
85
|
+
}
|
|
86
|
+
setMode(mode) {
|
|
87
|
+
this.currentMode = mode;
|
|
88
|
+
}
|
|
89
|
+
get rules() {
|
|
90
|
+
return this.ruleList;
|
|
91
|
+
}
|
|
92
|
+
rememberForSession(toolName, input) {
|
|
93
|
+
this.grants.push(sessionGrantFor(toolName, input, this.cwd));
|
|
94
|
+
}
|
|
95
|
+
clearSessionGrants() {
|
|
96
|
+
this.grants.length = 0;
|
|
97
|
+
}
|
|
98
|
+
check(input) {
|
|
99
|
+
const verdict = this.evaluate(input);
|
|
100
|
+
if (verdict.decision === "ask" && input.unattended) {
|
|
101
|
+
return { ...verdict, decision: "deny", message: UNATTENDED_MESSAGE };
|
|
102
|
+
}
|
|
103
|
+
return verdict;
|
|
104
|
+
}
|
|
105
|
+
evaluate(input) {
|
|
106
|
+
const { toolName, permission } = input;
|
|
107
|
+
// ① deny
|
|
108
|
+
const layers = layeredInputs(toolName, input.input);
|
|
109
|
+
let deny;
|
|
110
|
+
for (const layer of layers.inputs) {
|
|
111
|
+
deny = findDenyRule(this.ruleList, toolName, layer, this.cwd);
|
|
112
|
+
if (deny)
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (deny) {
|
|
116
|
+
return {
|
|
117
|
+
decision: "deny",
|
|
118
|
+
step: "deny-rule",
|
|
119
|
+
rule: deny,
|
|
120
|
+
message: `Denied by permission rule ${deny.raw} (${deny.source})`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (input.hookDecision === "deny") {
|
|
124
|
+
return {
|
|
125
|
+
decision: "deny",
|
|
126
|
+
step: "hook-deny",
|
|
127
|
+
message: input.hookReason ?? "Denied by a PreToolUse hook",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// ② 危险命令
|
|
131
|
+
const command = toolName === "bash" ? inputCommand(input.input) : undefined;
|
|
132
|
+
const danger = command !== undefined ? matchDangerous(command) : undefined;
|
|
133
|
+
if (danger) {
|
|
134
|
+
return {
|
|
135
|
+
decision: "ask",
|
|
136
|
+
step: "dangerous",
|
|
137
|
+
approvalReason: "dangerous",
|
|
138
|
+
message: `Potentially dangerous command: ${danger.description}`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// ③ 模式
|
|
142
|
+
const byMode = modeDecision(this.currentMode, permission);
|
|
143
|
+
if (byMode === "deny") {
|
|
144
|
+
return {
|
|
145
|
+
decision: "deny",
|
|
146
|
+
step: "mode",
|
|
147
|
+
message: `Permission mode "${this.currentMode}" allows only read-only tools`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
let verdict = byMode === "allow"
|
|
151
|
+
? { decision: "allow", step: "mode" }
|
|
152
|
+
: { decision: "ask", step: "mode", approvalReason: "mode" };
|
|
153
|
+
// ④ allow 规则 / Hook allow / 会话记忆
|
|
154
|
+
if (verdict.decision === "ask") {
|
|
155
|
+
const allow = layers.tooDeep
|
|
156
|
+
? undefined
|
|
157
|
+
: findAllowRule(this.ruleList, toolName, input.input, this.cwd);
|
|
158
|
+
const allowAll = allow !== undefined &&
|
|
159
|
+
layers.inputs.every((l) => findAllowRule(this.ruleList, toolName, l, this.cwd));
|
|
160
|
+
const granted = !layers.tooDeep &&
|
|
161
|
+
layers.inputs.every((l) => this.grants.some((g) => grantCovers(g, toolName, l, this.cwd)));
|
|
162
|
+
if (allowAll)
|
|
163
|
+
verdict = { decision: "allow", step: "allow-rule", rule: allow };
|
|
164
|
+
else if (input.hookDecision === "allow")
|
|
165
|
+
verdict = { decision: "allow", step: "hook" };
|
|
166
|
+
else if (granted) {
|
|
167
|
+
verdict = { decision: "allow", step: "session" };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (input.hookDecision === "ask") {
|
|
171
|
+
const reason = "hook";
|
|
172
|
+
verdict = {
|
|
173
|
+
decision: "ask",
|
|
174
|
+
step: "hook",
|
|
175
|
+
approvalReason: reason,
|
|
176
|
+
...(input.hookReason !== undefined ? { message: input.hookReason } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return verdict;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行前预览(第三波 §2.2):审批时列出「这一步会碰到什么」。[W3-B9a-2]
|
|
3
|
+
*
|
|
4
|
+
* - 只读:只用 `statSync / lstatSync / readdirSync / readFileSync`,不起子进程、不写盘;
|
|
5
|
+
* - 有上限:目录递归计数每个目标最多 `maxEntries`(缺省 2000)项,整次预览共用一个时间预算
|
|
6
|
+
* (缺省 200 ms,预算循环里逐项检查),超出只给提示;超时 / 超限不降低严重度;
|
|
7
|
+
* - bash:按段(含 `sh -c` / `eval` / `xargs` / `find -exec` 的嵌套命令)识别 `rm / rmdir / unlink`、
|
|
8
|
+
* `mv`、`git clean`、`git checkout -- <路径>`、`git reset --hard` 与 `>` / `>>` 重定向目标;
|
|
9
|
+
* 通配符、变量与 `{}` 不展开,原样显示并提示范围可能更大;段首的 `cd <路径>` 改变后续段的基准;
|
|
10
|
+
* - write:目标是否存在、现有行数 / 大小 → 新内容;覆盖本会话未 read 过的文件标 warn;
|
|
11
|
+
* - edit:读原文用 `planEdits` 干跑,成功给每处 −n/+m 行与总变化,失败提前说明原因;
|
|
12
|
+
* 超过 2 MiB 的文件只报大小,不读内容;
|
|
13
|
+
* - 其它工具:一行参数摘要。
|
|
14
|
+
*
|
|
15
|
+
* `lines` 不含颜色;`affected[].path` 与行里的路径一样是显示路径(cwd 内相对、`/` 分隔)。
|
|
16
|
+
*/
|
|
17
|
+
import type { ActionPreview, ApprovalRequest } from "./types.js";
|
|
18
|
+
export declare const PREVIEW_MAX_ENTRIES = 2000;
|
|
19
|
+
export declare const PREVIEW_BUDGET_MS = 200;
|
|
20
|
+
/** 超过它的文件不读内容,只报大小。 */
|
|
21
|
+
export declare const PREVIEW_MAX_READ_BYTES: number;
|
|
22
|
+
/** 一次预览最多列出的目标数。 */
|
|
23
|
+
export declare const PREVIEW_MAX_TARGETS = 20;
|
|
24
|
+
export interface PreviewOptions {
|
|
25
|
+
/** 相对路径的基准(会话 cwd)。 */
|
|
26
|
+
cwd: string;
|
|
27
|
+
maxEntries?: number;
|
|
28
|
+
budgetMs?: number;
|
|
29
|
+
now?: () => number;
|
|
30
|
+
}
|
|
31
|
+
export declare function formatBytes(bytes: number): string;
|
|
32
|
+
/** 段里的 `>` / `>>` 重定向:返回目标与去掉重定向后的文本。 */
|
|
33
|
+
export declare function splitRedirects(segment: string): {
|
|
34
|
+
rest: string;
|
|
35
|
+
targets: {
|
|
36
|
+
word: string;
|
|
37
|
+
append: boolean;
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
/** 审批请求的执行前预览;纯读,不抛错(内部错误返回空预览)。 */
|
|
41
|
+
export declare function previewAction(request: ApprovalRequest, options: PreviewOptions): ActionPreview;
|
|
42
|
+
/** 对话框 / 行式界面里显示的预览行(`other` 已由输入摘要覆盖,不重复显示)。 */
|
|
43
|
+
export declare function previewDisplayLines(preview: ActionPreview | undefined, max?: number): string[];
|