@aitne/daemon 0.1.4 → 0.1.7
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/dist/adapters/notification-manager.d.ts +12 -0
- package/dist/adapters/notification-manager.d.ts.map +1 -1
- package/dist/adapters/notification-manager.js +39 -1
- package/dist/adapters/notification-manager.js.map +1 -1
- package/dist/api/routes/agent.d.ts.map +1 -1
- package/dist/api/routes/agent.js +7 -0
- package/dist/api/routes/agent.js.map +1 -1
- package/dist/api/routes/commands.d.ts.map +1 -1
- package/dist/api/routes/commands.js +16 -13
- package/dist/api/routes/commands.js.map +1 -1
- package/dist/api/routes/context.d.ts.map +1 -1
- package/dist/api/routes/context.js +13 -2
- package/dist/api/routes/context.js.map +1 -1
- package/dist/api/routes/dashboard.d.ts.map +1 -1
- package/dist/api/routes/dashboard.js +28 -0
- package/dist/api/routes/dashboard.js.map +1 -1
- package/dist/api/routes/fs.d.ts +23 -0
- package/dist/api/routes/fs.d.ts.map +1 -0
- package/dist/api/routes/fs.js +156 -0
- package/dist/api/routes/fs.js.map +1 -0
- package/dist/api/routes/fs.logic.d.ts +62 -0
- package/dist/api/routes/fs.logic.d.ts.map +1 -0
- package/dist/api/routes/fs.logic.js +137 -0
- package/dist/api/routes/fs.logic.js.map +1 -0
- package/dist/api/routes/health.d.ts.map +1 -1
- package/dist/api/routes/health.js +4 -2
- package/dist/api/routes/health.js.map +1 -1
- package/dist/api/routes/integrations.d.ts.map +1 -1
- package/dist/api/routes/integrations.js +8 -6
- package/dist/api/routes/integrations.js.map +1 -1
- package/dist/api/routes/metrics.d.ts +1 -0
- package/dist/api/routes/metrics.d.ts.map +1 -1
- package/dist/api/routes/metrics.js +24 -0
- package/dist/api/routes/metrics.js.map +1 -1
- package/dist/api/routes/observations.d.ts.map +1 -1
- package/dist/api/routes/observations.js +538 -25
- package/dist/api/routes/observations.js.map +1 -1
- package/dist/api/routes/skills.d.ts +9 -1
- package/dist/api/routes/skills.d.ts.map +1 -1
- package/dist/api/routes/skills.js +38 -16
- package/dist/api/routes/skills.js.map +1 -1
- package/dist/api/routes/wiki.d.ts +4 -0
- package/dist/api/routes/wiki.d.ts.map +1 -0
- package/dist/api/routes/wiki.js +1075 -0
- package/dist/api/routes/wiki.js.map +1 -0
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +27 -1
- package/dist/api/server.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-core.d.ts +25 -0
- package/dist/core/agent-core.d.ts.map +1 -1
- package/dist/core/agent-core.js.map +1 -1
- package/dist/core/backends/backend-router.d.ts +5 -1
- package/dist/core/backends/backend-router.d.ts.map +1 -1
- package/dist/core/backends/backend-router.js +10 -1
- package/dist/core/backends/backend-router.js.map +1 -1
- package/dist/core/backends/claude-code-core.d.ts.map +1 -1
- package/dist/core/backends/claude-code-core.js +62 -4
- package/dist/core/backends/claude-code-core.js.map +1 -1
- package/dist/core/backends/claude-tool-collection.d.ts +1 -1
- package/dist/core/backends/claude-tool-collection.d.ts.map +1 -1
- package/dist/core/backends/claude-tool-collection.js +327 -65
- package/dist/core/backends/claude-tool-collection.js.map +1 -1
- package/dist/core/backends/codex-core.d.ts.map +1 -1
- package/dist/core/backends/codex-core.js +36 -0
- package/dist/core/backends/codex-core.js.map +1 -1
- package/dist/core/backends/gemini-cli-core.d.ts +24 -5
- package/dist/core/backends/gemini-cli-core.d.ts.map +1 -1
- package/dist/core/backends/gemini-cli-core.js +62 -30
- package/dist/core/backends/gemini-cli-core.js.map +1 -1
- package/dist/core/backends/plan-presets.d.ts +3 -1
- package/dist/core/backends/plan-presets.d.ts.map +1 -1
- package/dist/core/backends/plan-presets.js +42 -2
- package/dist/core/backends/plan-presets.js.map +1 -1
- package/dist/core/bang-commands/commands-help.d.ts +5 -0
- package/dist/core/bang-commands/commands-help.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-help.js +69 -0
- package/dist/core/bang-commands/commands-help.js.map +1 -0
- package/dist/core/bang-commands/commands-wiki.d.ts +75 -0
- package/dist/core/bang-commands/commands-wiki.d.ts.map +1 -0
- package/dist/core/bang-commands/commands-wiki.js +574 -0
- package/dist/core/bang-commands/commands-wiki.js.map +1 -0
- package/dist/core/bang-commands/index.d.ts +4 -2
- package/dist/core/bang-commands/index.d.ts.map +1 -1
- package/dist/core/bang-commands/index.js +15 -1
- package/dist/core/bang-commands/index.js.map +1 -1
- package/dist/core/bang-commands/registry.d.ts +47 -4
- package/dist/core/bang-commands/registry.d.ts.map +1 -1
- package/dist/core/bang-commands/registry.js +85 -15
- package/dist/core/bang-commands/registry.js.map +1 -1
- package/dist/core/context-builder.d.ts +17 -0
- package/dist/core/context-builder.d.ts.map +1 -1
- package/dist/core/context-builder.js +64 -6
- package/dist/core/context-builder.js.map +1 -1
- package/dist/core/daemon-api-cli.d.ts.map +1 -1
- package/dist/core/daemon-api-cli.js +50 -2
- package/dist/core/daemon-api-cli.js.map +1 -1
- package/dist/core/dispatcher-message-handler.d.ts.map +1 -1
- package/dist/core/dispatcher-message-handler.js +10 -0
- package/dist/core/dispatcher-message-handler.js.map +1 -1
- package/dist/core/dispatcher-morning-routine.d.ts.map +1 -1
- package/dist/core/dispatcher-morning-routine.js +17 -2
- package/dist/core/dispatcher-morning-routine.js.map +1 -1
- package/dist/core/dispatcher-result-processor.d.ts +23 -0
- package/dist/core/dispatcher-result-processor.d.ts.map +1 -1
- package/dist/core/dispatcher-result-processor.js +124 -5
- package/dist/core/dispatcher-result-processor.js.map +1 -1
- package/dist/core/dispatcher-scheduled-tasks.d.ts.map +1 -1
- package/dist/core/dispatcher-scheduled-tasks.js +114 -80
- package/dist/core/dispatcher-scheduled-tasks.js.map +1 -1
- package/dist/core/dispatcher-types.d.ts +116 -1
- package/dist/core/dispatcher-types.d.ts.map +1 -1
- package/dist/core/dispatcher-types.js.map +1 -1
- package/dist/core/dispatcher.d.ts +36 -0
- package/dist/core/dispatcher.d.ts.map +1 -1
- package/dist/core/dispatcher.js +94 -1
- package/dist/core/dispatcher.js.map +1 -1
- package/dist/core/integration-lifecycle.d.ts.map +1 -1
- package/dist/core/integration-lifecycle.js +6 -8
- package/dist/core/integration-lifecycle.js.map +1 -1
- package/dist/core/metrics.d.ts +127 -0
- package/dist/core/metrics.d.ts.map +1 -1
- package/dist/core/metrics.js +256 -1
- package/dist/core/metrics.js.map +1 -1
- package/dist/core/prompts.d.ts +2 -1
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/prompts.js +40 -0
- package/dist/core/prompts.js.map +1 -1
- package/dist/core/roadmap-validate.js +13 -1
- package/dist/core/roadmap-validate.js.map +1 -1
- package/dist/core/routine-acquisition-plan.d.ts +51 -0
- package/dist/core/routine-acquisition-plan.d.ts.map +1 -1
- package/dist/core/routine-acquisition-plan.js +111 -12
- package/dist/core/routine-acquisition-plan.js.map +1 -1
- package/dist/core/routine-fetch-window-retry.d.ts +109 -0
- package/dist/core/routine-fetch-window-retry.d.ts.map +1 -0
- package/dist/core/routine-fetch-window-retry.js +210 -0
- package/dist/core/routine-fetch-window-retry.js.map +1 -0
- package/dist/core/routine-fetch-window-runner.d.ts +258 -32
- package/dist/core/routine-fetch-window-runner.d.ts.map +1 -1
- package/dist/core/routine-fetch-window-runner.js +1115 -185
- package/dist/core/routine-fetch-window-runner.js.map +1 -1
- package/dist/core/routine-windows.d.ts +19 -4
- package/dist/core/routine-windows.d.ts.map +1 -1
- package/dist/core/routine-windows.js +47 -0
- package/dist/core/routine-windows.js.map +1 -1
- package/dist/core/scheduler.d.ts +50 -2
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +88 -7
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/skill-curation/declarations.d.ts.map +1 -1
- package/dist/core/skill-curation/declarations.js +11 -12
- package/dist/core/skill-curation/declarations.js.map +1 -1
- package/dist/core/skill-source-paths.d.ts +14 -0
- package/dist/core/skill-source-paths.d.ts.map +1 -0
- package/dist/core/skill-source-paths.js +82 -0
- package/dist/core/skill-source-paths.js.map +1 -0
- package/dist/core/skills-compiler.d.ts +18 -0
- package/dist/core/skills-compiler.d.ts.map +1 -1
- package/dist/core/skills-compiler.js +65 -18
- package/dist/core/skills-compiler.js.map +1 -1
- package/dist/core/skills-manifest.d.ts.map +1 -1
- package/dist/core/skills-manifest.js +46 -0
- package/dist/core/skills-manifest.js.map +1 -1
- package/dist/core/system-reset.d.ts +25 -0
- package/dist/core/system-reset.d.ts.map +1 -1
- package/dist/core/system-reset.js +47 -0
- package/dist/core/system-reset.js.map +1 -1
- package/dist/core/wiki/approval-queue.d.ts +31 -0
- package/dist/core/wiki/approval-queue.d.ts.map +1 -0
- package/dist/core/wiki/approval-queue.js +44 -0
- package/dist/core/wiki/approval-queue.js.map +1 -0
- package/dist/core/wiki/bridge.d.ts +74 -0
- package/dist/core/wiki/bridge.d.ts.map +1 -0
- package/dist/core/wiki/bridge.js +405 -0
- package/dist/core/wiki/bridge.js.map +1 -0
- package/dist/core/wiki/compile-lock.d.ts +42 -0
- package/dist/core/wiki/compile-lock.d.ts.map +1 -0
- package/dist/core/wiki/compile-lock.js +55 -0
- package/dist/core/wiki/compile-lock.js.map +1 -0
- package/dist/core/wiki/compile-preview.d.ts +8 -0
- package/dist/core/wiki/compile-preview.d.ts.map +1 -0
- package/dist/core/wiki/compile-preview.js +200 -0
- package/dist/core/wiki/compile-preview.js.map +1 -0
- package/dist/core/wiki/cost-estimate.d.ts +30 -0
- package/dist/core/wiki/cost-estimate.d.ts.map +1 -0
- package/dist/core/wiki/cost-estimate.js +243 -0
- package/dist/core/wiki/cost-estimate.js.map +1 -0
- package/dist/core/wiki/dispatcher.d.ts +48 -0
- package/dist/core/wiki/dispatcher.d.ts.map +1 -0
- package/dist/core/wiki/dispatcher.js +92 -0
- package/dist/core/wiki/dispatcher.js.map +1 -0
- package/dist/core/wiki/git-precompile.d.ts +86 -0
- package/dist/core/wiki/git-precompile.d.ts.map +1 -0
- package/dist/core/wiki/git-precompile.js +96 -0
- package/dist/core/wiki/git-precompile.js.map +1 -0
- package/dist/core/wiki/import-migrate.d.ts +38 -0
- package/dist/core/wiki/import-migrate.d.ts.map +1 -0
- package/dist/core/wiki/import-migrate.js +310 -0
- package/dist/core/wiki/import-migrate.js.map +1 -0
- package/dist/core/wiki/import-probe.d.ts +76 -0
- package/dist/core/wiki/import-probe.d.ts.map +1 -0
- package/dist/core/wiki/import-probe.js +245 -0
- package/dist/core/wiki/import-probe.js.map +1 -0
- package/dist/core/wiki/index-cache.d.ts +39 -0
- package/dist/core/wiki/index-cache.d.ts.map +1 -0
- package/dist/core/wiki/index-cache.js +152 -0
- package/dist/core/wiki/index-cache.js.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts +52 -0
- package/dist/core/wiki/multi-url-dispatch.d.ts.map +1 -0
- package/dist/core/wiki/multi-url-dispatch.js +72 -0
- package/dist/core/wiki/multi-url-dispatch.js.map +1 -0
- package/dist/core/wiki/wiki-fts.d.ts +75 -0
- package/dist/core/wiki/wiki-fts.d.ts.map +1 -0
- package/dist/core/wiki/wiki-fts.js +265 -0
- package/dist/core/wiki/wiki-fts.js.map +1 -0
- package/dist/core/wiki/workspaces.d.ts +101 -0
- package/dist/core/wiki/workspaces.d.ts.map +1 -0
- package/dist/core/wiki/workspaces.js +352 -0
- package/dist/core/wiki/workspaces.js.map +1 -0
- package/dist/core/wiki/write-strategy.d.ts +70 -0
- package/dist/core/wiki/write-strategy.d.ts.map +1 -0
- package/dist/core/wiki/write-strategy.js +112 -0
- package/dist/core/wiki/write-strategy.js.map +1 -0
- package/dist/core/workdir.d.ts +8 -1
- package/dist/core/workdir.d.ts.map +1 -1
- package/dist/core/workdir.js +4 -1
- package/dist/core/workdir.js.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +122 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/wiki-store.d.ts +3 -0
- package/dist/db/wiki-store.d.ts.map +1 -0
- package/dist/db/wiki-store.js +7 -0
- package/dist/db/wiki-store.js.map +1 -0
- package/dist/index.js +87 -4
- package/dist/index.js.map +1 -1
- package/dist/messaging/setup-welcome-dm.d.ts +30 -0
- package/dist/messaging/setup-welcome-dm.d.ts.map +1 -0
- package/dist/messaging/setup-welcome-dm.js +86 -0
- package/dist/messaging/setup-welcome-dm.js.map +1 -0
- package/dist/messaging/url-extract.d.ts +8 -0
- package/dist/messaging/url-extract.d.ts.map +1 -0
- package/dist/messaging/url-extract.js +41 -0
- package/dist/messaging/url-extract.js.map +1 -0
- package/dist/observers/delegated-sync-worker.d.ts +33 -25
- package/dist/observers/delegated-sync-worker.d.ts.map +1 -1
- package/dist/observers/delegated-sync-worker.js +38 -31
- package/dist/observers/delegated-sync-worker.js.map +1 -1
- package/dist/observers/imminent-event-scheduler.d.ts +20 -7
- package/dist/observers/imminent-event-scheduler.d.ts.map +1 -1
- package/dist/observers/imminent-event-scheduler.js +134 -29
- package/dist/observers/imminent-event-scheduler.js.map +1 -1
- package/dist/safety/always-disallowed.d.ts +65 -0
- package/dist/safety/always-disallowed.d.ts.map +1 -1
- package/dist/safety/always-disallowed.js +106 -10
- package/dist/safety/always-disallowed.js.map +1 -1
- package/dist/safety/audit.d.ts +46 -1
- package/dist/safety/audit.d.ts.map +1 -1
- package/dist/safety/audit.js +79 -16
- package/dist/safety/audit.js.map +1 -1
- package/dist/safety/risk-classifier.d.ts.map +1 -1
- package/dist/safety/risk-classifier.js +29 -0
- package/dist/safety/risk-classifier.js.map +1 -1
- package/dist/settings/runtime-settings.d.ts +12 -1
- package/dist/settings/runtime-settings.d.ts.map +1 -1
- package/dist/settings/runtime-settings.js +59 -1
- package/dist/settings/runtime-settings.js.map +1 -1
- package/package.json +2 -2
|
@@ -31,7 +31,7 @@ import { dirname, resolve as resolvePath, isAbsolute } from "node:path";
|
|
|
31
31
|
import { getContextDir } from "../../config.js";
|
|
32
32
|
import { readIntegrations } from "../../db/integrations-store.js";
|
|
33
33
|
import { recordAbsoluteBlockAudit } from "../../safety/absolute-block-audit.js";
|
|
34
|
-
import { classifyAbsoluteBlock } from "../../safety/always-disallowed.js";
|
|
34
|
+
import { classifyAbsoluteBlock, stripBashHeredocs, stripBashStringContent, } from "../../safety/always-disallowed.js";
|
|
35
35
|
import { createLogger } from "../../logging.js";
|
|
36
36
|
import { computeDelegatedClaudeTools, computeNativeClaudeTools } from "./claude-probe.js";
|
|
37
37
|
import { isPathInsideOrEqual, shellPathForms } from "../path-compat.js";
|
|
@@ -107,6 +107,66 @@ function expandHomeForms(token, home) {
|
|
|
107
107
|
return home;
|
|
108
108
|
return token;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Decide whether a shell token (after `expandHomeForms` normalisation)
|
|
112
|
+
* resembles a filesystem path argument. Replaces the older "contains
|
|
113
|
+
* `/` or `\`" filter, which false-positived on quoted JSON bodies and
|
|
114
|
+
* HTTP header values whenever the session cwd was inside the data dir
|
|
115
|
+
* (production cwd is `<dataDir>/agent-sessions/<id>`):
|
|
116
|
+
*
|
|
117
|
+
* - `Content-Type: application/json` → `/` inside `application/json`
|
|
118
|
+
* was treated as a path separator, the token was resolved relative
|
|
119
|
+
* to the (data-dir-internal) cwd, the resulting candidate landed
|
|
120
|
+
* inside `absDataDir`, and the hook blocked an otherwise benign
|
|
121
|
+
* `curl -X PATCH -H '...'` invocation.
|
|
122
|
+
* - `'{"content":"line1\nline2"}'` → the literal `\` in `\n` triggered
|
|
123
|
+
* the same data-dir resolution path even though the token is a JSON
|
|
124
|
+
* payload, not a filename.
|
|
125
|
+
*
|
|
126
|
+
* The shape rules below are deliberately positive (a token must look
|
|
127
|
+
* like a path) rather than negative (skip if it contains JSON chars):
|
|
128
|
+
*
|
|
129
|
+
* 1. Absolute on POSIX (`/foo`) — also catches tokens that
|
|
130
|
+
* `expandHomeForms` rewrote from `~` / `$HOME` / `${HOME}` forms.
|
|
131
|
+
* 2. Explicit relative anchor (`./foo`, `../foo`, exactly `.` / `..`).
|
|
132
|
+
* 3. Unresolved home / env-var prefix that survived `expandHomeForms`
|
|
133
|
+
* (e.g. `~user/foo`, `$OTHER/foo` when the variable is unknown).
|
|
134
|
+
* Treating these as path candidates is a defensive belt — the
|
|
135
|
+
* static analysis can't know what they expand to at runtime, so
|
|
136
|
+
* err on the side of forwarding them through the data-dir check.
|
|
137
|
+
* 4. Bare multi-segment path made of filename-safe characters
|
|
138
|
+
* (`context/today.md`, `agent-sessions/foo/bar`). The character
|
|
139
|
+
* class deliberately excludes whitespace, `:`, `=`, `{`, `}`,
|
|
140
|
+
* `"`, `'`, `` ` ``, `?`, `*`, `<`, `>` — all of which appear in
|
|
141
|
+
* header values, JSON bodies, and query strings but never in
|
|
142
|
+
* well-formed filename segments.
|
|
143
|
+
*
|
|
144
|
+
* URL-shaped tokens (`http://...`) are filtered by the caller before
|
|
145
|
+
* this helper runs, so rule 1 / rule 4 cannot misfire on them.
|
|
146
|
+
*/
|
|
147
|
+
function looksLikePathArg(token) {
|
|
148
|
+
if (token.length === 0)
|
|
149
|
+
return false;
|
|
150
|
+
// Rules 1 + 2 — POSIX-absolute or anchored-relative.
|
|
151
|
+
if (token.startsWith("/"))
|
|
152
|
+
return true;
|
|
153
|
+
if (token === "." || token === ".." || token.startsWith("./") || token.startsWith("../")) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
// Rule 3 — unresolved home / env-var prefix.
|
|
157
|
+
if (token.startsWith("~") || token.startsWith("$"))
|
|
158
|
+
return true;
|
|
159
|
+
// Rule 4 — bare relative path with filename-safe segments only.
|
|
160
|
+
// `[A-Za-z0-9_.\-+@]` is the segment alphabet — broad enough to
|
|
161
|
+
// cover typical project filenames (dashes, underscores, dots,
|
|
162
|
+
// version suffixes, `@scope/pkg` style) without admitting tokens
|
|
163
|
+
// that came from a JSON body or header value. The trailing `/?`
|
|
164
|
+
// tolerates a directory-shape suffix (`context/`).
|
|
165
|
+
if (/^[A-Za-z0-9_.\-+@]+(?:\/[A-Za-z0-9_.\-+@]+)+\/?$/.test(token)) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
110
170
|
const logger = createLogger("claude-tool-collection");
|
|
111
171
|
/** Default allowed-tools list when the dashboard override is unset. */
|
|
112
172
|
export const CLAUDE_DEFAULT_ALLOWED_TOOLS = [
|
|
@@ -141,16 +201,56 @@ export const CLAUDE_DEFAULT_ALLOWED_TOOLS = [
|
|
|
141
201
|
* `delegatedToolCount` and `nativeToolCount` makes a misconfigured flip
|
|
142
202
|
* diagnosable without re-running the resolver.
|
|
143
203
|
*/
|
|
144
|
-
export function getAllowedTools(config, webSearchEnabled, delegatedTools = [], nativeTools = []
|
|
204
|
+
export function getAllowedTools(config, webSearchEnabled, delegatedTools = [], nativeTools = [],
|
|
205
|
+
// WIKI_BUILDER_DESIGN.md §4.3 — wiki.ingest_url turns need WebFetch on
|
|
206
|
+
// top of the default surface to read external pages (the `Bash(curl *)`
|
|
207
|
+
// PreToolUse hook keeps curl restricted to localhost). Gated on the
|
|
208
|
+
// same `!allowedToolsOverride` clause as `webSearchEnabled` so a user
|
|
209
|
+
// who configured a custom override gets the override verbatim — they
|
|
210
|
+
// are expected to add `WebFetch` themselves if they need it (matches
|
|
211
|
+
// the WebSearch contract; documented in /settings/wiki).
|
|
212
|
+
wikiUrlFetchEnabled = false,
|
|
213
|
+
// Wiki sessions must write only through the daemon Wiki API
|
|
214
|
+
// (`POST /api/wiki/<ws>/files/...`) — every wiki.* process key has a
|
|
215
|
+
// skill body and the wiki-agent profile both stating "no `Write` /
|
|
216
|
+
// `Edit` against the vault." Skill frontmatter `allowed-tools` is
|
|
217
|
+
// human-facing metadata and does NOT propagate into the SDK's
|
|
218
|
+
// session-level allowlist, so without this hard strip a wiki turn
|
|
219
|
+
// can bypass the API path-classifier, the agent_actions audit row,
|
|
220
|
+
// and the result-processor's write-verifier by Writing a vault
|
|
221
|
+
// path directly. Pass true for any `processKey.startsWith("wiki.")`.
|
|
222
|
+
wikiApiOnlyWrites = false) {
|
|
145
223
|
const base = config.allowedToolsOverride ?? [...CLAUDE_DEFAULT_ALLOWED_TOOLS];
|
|
146
224
|
const merged = new Set(base);
|
|
147
225
|
if (!config.allowedToolsOverride && webSearchEnabled) {
|
|
148
226
|
merged.add("WebSearch");
|
|
149
227
|
}
|
|
228
|
+
if (!config.allowedToolsOverride && wikiUrlFetchEnabled) {
|
|
229
|
+
merged.add("WebFetch");
|
|
230
|
+
}
|
|
150
231
|
for (const tool of delegatedTools)
|
|
151
232
|
merged.add(tool);
|
|
152
233
|
for (const tool of nativeTools)
|
|
153
234
|
merged.add(tool);
|
|
235
|
+
// Claude Code 2.1+ defers large MCP manifests (`mcp__claude_ai_*`) behind
|
|
236
|
+
// `ToolSearch` — the tools appear by name but their schemas are not
|
|
237
|
+
// loaded until the agent calls `ToolSearch select:<name>`. Without
|
|
238
|
+
// ToolSearch allowed, the model cannot invoke any unioned MCP tool and
|
|
239
|
+
// silently falls back to denied surfaces (raw Bash, WebFetch), surfacing
|
|
240
|
+
// as "Bash and WebFetch denied" failure DMs from native/delegated-same
|
|
241
|
+
// routines. Mirrors the same widening already applied by
|
|
242
|
+
// `composePrePassAllowedTools` (pre-pass), `CLAUDE_PROBE_TOOLS_PROMPT`
|
|
243
|
+
// (probe), and `claude-delegated.ts` (cross-backend proxy). Unioned even
|
|
244
|
+
// under `allowedToolsOverride` for the same orthogonality reason the
|
|
245
|
+
// MCP tools themselves bypass the override above — silently dropping
|
|
246
|
+
// ToolSearch while keeping the MCP names defeats the widening.
|
|
247
|
+
if (delegatedTools.length > 0 || nativeTools.length > 0) {
|
|
248
|
+
merged.add("ToolSearch");
|
|
249
|
+
}
|
|
250
|
+
if (wikiApiOnlyWrites) {
|
|
251
|
+
merged.delete("Write");
|
|
252
|
+
merged.delete("Edit");
|
|
253
|
+
}
|
|
154
254
|
return Array.from(merged);
|
|
155
255
|
}
|
|
156
256
|
/**
|
|
@@ -239,9 +339,60 @@ export function getSessionDeniedTools(mcpContext) {
|
|
|
239
339
|
*/
|
|
240
340
|
export function buildSecurityHooks(deps, allowMode = false) {
|
|
241
341
|
const { config, writeTracker, getMcpContext } = deps;
|
|
342
|
+
// Per-Bash-hook block logging. The SDK's `dontAsk` mode silently
|
|
343
|
+
// denies any Bash command that doesn't match an allowed prefix —
|
|
344
|
+
// no tool_result, no error feedback — and PreToolUse hooks that
|
|
345
|
+
// return `block` emit a generic reason that the agent often
|
|
346
|
+
// misinterprets as "Bash is blocked entirely." Without this log,
|
|
347
|
+
// diagnosing a failed wiki / context update means guessing at the
|
|
348
|
+
// command the model produced. The line is logged at warn level
|
|
349
|
+
// (one per actual block, not per call) so steady-state cost is
|
|
350
|
+
// negligible; the cmd is truncated to 400 chars to keep secrets
|
|
351
|
+
// out of logs and the entry parseable.
|
|
352
|
+
const wrapBashHook = (hookName, inner) => async (input) => {
|
|
353
|
+
const result = await inner(input);
|
|
354
|
+
if (result && result.decision === "block") {
|
|
355
|
+
const toolInput = input.tool_input;
|
|
356
|
+
const cmd = toolInput?.command ?? "";
|
|
357
|
+
logger.warn({
|
|
358
|
+
hook: hookName,
|
|
359
|
+
reason: result.reason,
|
|
360
|
+
cmd: cmd.slice(0, 400),
|
|
361
|
+
}, "Bash hook block");
|
|
362
|
+
}
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
242
365
|
const bashCurlHook = async (input) => {
|
|
243
366
|
const toolInput = input.tool_input;
|
|
244
367
|
const cmd = toolInput?.command ?? "";
|
|
368
|
+
// Three views of the command, each used by a different class of check:
|
|
369
|
+
//
|
|
370
|
+
// - `cmd` (raw) — the initial `\bcurl\b` keyword presence test.
|
|
371
|
+
// Must see literal token text so a `-d
|
|
372
|
+
// '{"text":"see curl docs"}'` body doesn't
|
|
373
|
+
// suppress the hook entirely.
|
|
374
|
+
// - `scan` — substring scans for flag PRESENCE (chained
|
|
375
|
+
// curl, --next, --proxy, -L, -o, -c, -b, etc.).
|
|
376
|
+
// Strips single-quoted strings AND heredoc
|
|
377
|
+
// bodies so prose inside a JSON payload like
|
|
378
|
+
// "set -o pipefail in scripts" cannot trip
|
|
379
|
+
// the flag detectors.
|
|
380
|
+
// - `tokenizable` — tokenizer walks and value extractors
|
|
381
|
+
// (top-level URL collection, `-d @file` arg
|
|
382
|
+
// walker, `-o <file>` path capture). Strips
|
|
383
|
+
// ONLY heredoc bodies (which are stdin
|
|
384
|
+
// payload, never shell argv) and PRESERVES
|
|
385
|
+
// quoted strings so the value extractors can
|
|
386
|
+
// still recognise quoted URL targets and
|
|
387
|
+
// quoted file paths.
|
|
388
|
+
//
|
|
389
|
+
// The wiki.ingest_url skill is the canonical case where this matters:
|
|
390
|
+
// it POSTs an article body via `-d @- <<'JSON' … JSON`, and the body
|
|
391
|
+
// routinely contains the source URL ("Source: https://news.example.com/…").
|
|
392
|
+
// Before this layered design the URL extractor scanned `cmd`, found
|
|
393
|
+
// the body URL, and falsely blocked with "Multiple URL targets".
|
|
394
|
+
const scan = stripBashStringContent(cmd);
|
|
395
|
+
const tokenizable = stripBashHeredocs(cmd);
|
|
245
396
|
if (/\bcurl\b/.test(cmd)) {
|
|
246
397
|
// ── Multi-request defenses (run BEFORE host/port loop) ─────────
|
|
247
398
|
// The SDK `allowedTools` glob is a prefix match against the full
|
|
@@ -260,7 +411,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
260
411
|
// pipeline counts as ONE curl (only the `curl` token itself
|
|
261
412
|
// is matched; the leading `jq` is not). Two or more anchored
|
|
262
413
|
// `curl` tokens → chained invocation → block.
|
|
263
|
-
const chainedCurlMatches =
|
|
414
|
+
const chainedCurlMatches = scan.match(/(?:^|[;&|`\n]|\$\()\s*curl\b/g) ?? [];
|
|
264
415
|
if (chainedCurlMatches.length > 1) {
|
|
265
416
|
return {
|
|
266
417
|
decision: "block",
|
|
@@ -275,8 +426,8 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
275
426
|
// because both URLs hit the same host:port, but curl issues
|
|
276
427
|
// one HTTP request per `--next` separator. Same exfil shape
|
|
277
428
|
// as chained curl, different syntax.
|
|
278
|
-
if (/(?:^|\s)--next(?:[\s=]|$)/.test(
|
|
279
|
-
|| /(?:^|\s)-:(?:\s|$)/.test(
|
|
429
|
+
if (/(?:^|\s)--next(?:[\s=]|$)/.test(scan)
|
|
430
|
+
|| /(?:^|\s)-:(?:\s|$)/.test(scan)) {
|
|
280
431
|
return {
|
|
281
432
|
decision: "block",
|
|
282
433
|
reason: "curl --next / -: (URL multiplexing) is not allowed "
|
|
@@ -285,29 +436,40 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
285
436
|
}
|
|
286
437
|
// 3. Multi-positional URL targets — `curl URL1 URL2 -X PUT -d
|
|
287
438
|
// @body` sends the same options to BOTH URLs sequentially,
|
|
288
|
-
// which `--next` blocking above does not catch. Tokenize
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
439
|
+
// which `--next` blocking above does not catch. Tokenize the
|
|
440
|
+
// heredoc-stripped command and collect tokens that are URLs:
|
|
441
|
+
//
|
|
442
|
+
// - Bare URL token: `curl http://localhost:8321/api/x`
|
|
443
|
+
// - Fully single-quoted URL: `curl 'http://localhost:8321/api/x'`
|
|
444
|
+
// - Fully double-quoted URL: `curl "http://localhost:8321/api/x"`
|
|
445
|
+
//
|
|
446
|
+
// URLs that appear INSIDE a quoted body / header value
|
|
447
|
+
// (e.g. `-d '{"link":"https://example.com"}'` or
|
|
448
|
+
// `-H "X-Source: https://example.com"`) are NOT counted: the
|
|
449
|
+
// surrounding quoted token carries other characters, so the
|
|
450
|
+
// "entire content is the URL" patterns below do not match.
|
|
294
451
|
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
// `cmd.match(/https?:\/\/[^\s'"]+/g)` would reject such
|
|
301
|
-
// requests because the body URL was non-localhost. Heuristic
|
|
302
|
-
// limitation: URL strings unquoted as data values are a rare
|
|
303
|
-
// false-positive surface and would be host-checked; the
|
|
304
|
-
// alternative is full shell tokenization (out of scope).
|
|
452
|
+
// Heredoc bodies (`<<'JSON' … JSON`) are stripped from
|
|
453
|
+
// `tokenizable` above because they are stdin payload, never
|
|
454
|
+
// shell argv — without that strip, the routine wiki.ingest_url
|
|
455
|
+
// shape of "store an article body that mentions other URLs"
|
|
456
|
+
// would trip this multi-URL rule on the body URL.
|
|
305
457
|
const topLevelTokenRe = /'[^']*'|"[^"]*"|[^'"\s]+/g;
|
|
306
458
|
const topLevelUrls = [];
|
|
307
459
|
let tokenMatch;
|
|
308
|
-
while ((tokenMatch = topLevelTokenRe.exec(
|
|
309
|
-
|
|
310
|
-
|
|
460
|
+
while ((tokenMatch = topLevelTokenRe.exec(tokenizable)) !== null) {
|
|
461
|
+
const token = tokenMatch[0];
|
|
462
|
+
if (/^https?:\/\//.test(token)) {
|
|
463
|
+
topLevelUrls.push(token);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
// Fully-quoted URL token: the WHOLE content between matching
|
|
467
|
+
// single or double quotes must be the URL — anything else
|
|
468
|
+
// (JSON body, header value) starts with non-URL characters
|
|
469
|
+
// after the quote.
|
|
470
|
+
const quoted = /^(['"])(https?:\/\/[^'"\s]+)\1$/.exec(token);
|
|
471
|
+
if (quoted) {
|
|
472
|
+
topLevelUrls.push(quoted[2]);
|
|
311
473
|
}
|
|
312
474
|
}
|
|
313
475
|
if (topLevelUrls.length > 1) {
|
|
@@ -351,7 +513,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
351
513
|
// Connection-override flags — host/proxy/socket redirection that
|
|
352
514
|
// would let curl reach something other than the configured loopback
|
|
353
515
|
// HTTP endpoint.
|
|
354
|
-
if (/--connect-to|--resolve|--config\b|(?:^|\s)-[a-zA-Z]*K|--proxy\b|(?:^|\s)-[a-zA-Z]*x|--socks|--unix-socket|--abstract-unix-socket|--interface\b|--local-port\b/.test(
|
|
516
|
+
if (/--connect-to|--resolve|--config\b|(?:^|\s)-[a-zA-Z]*K|--proxy\b|(?:^|\s)-[a-zA-Z]*x|--socks|--unix-socket|--abstract-unix-socket|--interface\b|--local-port\b/.test(scan)) {
|
|
355
517
|
return {
|
|
356
518
|
decision: "block",
|
|
357
519
|
reason: "curl connection override flags not allowed " +
|
|
@@ -381,7 +543,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
381
543
|
// `-L`. Same shape applied to every short-flag below — without it
|
|
382
544
|
// an attacker can stuff the dangerous letter into a benign-looking
|
|
383
545
|
// flag bundle like `-fs<X>` and bypass the deny rule entirely.
|
|
384
|
-
if (/(?:^|\s)(?:--upload-file\b|-[a-zA-Z]*T(?:\s|=|$))/.test(
|
|
546
|
+
if (/(?:^|\s)(?:--upload-file\b|-[a-zA-Z]*T(?:\s|=|$))/.test(scan)) {
|
|
385
547
|
return {
|
|
386
548
|
decision: "block",
|
|
387
549
|
reason: "curl --upload-file / -T not allowed — would read arbitrary files",
|
|
@@ -391,24 +553,89 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
391
553
|
// from stdin, used by pipelines like `echo $body | curl ... -d @-`).
|
|
392
554
|
// Block `@<anything-other-than-stdin-marker>`. The lookahead
|
|
393
555
|
// `(?!-["']?(?:\s|$))` lets `@-`, `@-"`, `@-'`, `@- ` through.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
556
|
+
// `-d / --data* / -F / --form` value-content checks. The previous
|
|
557
|
+
// regex `(?:^|\s)…\s+["']?@(?!-…)` matched the @-file syntax with
|
|
558
|
+
// the value attached, which meant a JSON BODY containing literal
|
|
559
|
+
// text like ` -d @<chars>` (an agent journal entry that quotes a
|
|
560
|
+
// shell example) also tripped it. Conversely, switching to the
|
|
561
|
+
// `scan` form alone loses single-quoted attack content (the
|
|
562
|
+
// legitimate `-d '@/etc/passwd'` form): scan strips the body and
|
|
563
|
+
// the regex no longer sees the `@`.
|
|
564
|
+
//
|
|
565
|
+
// The walker below is value-aware: tokenize the command (already
|
|
566
|
+
// quote-aware via the same regex used for URL extraction), find
|
|
567
|
+
// every `-d` / `--data*` / `-F` / `--form` flag token, recover the
|
|
568
|
+
// unquoted value (either after `=` in the same token or in the
|
|
569
|
+
// adjacent token), and reject if the value's FIRST CHARACTER is
|
|
570
|
+
// `@` (with the canonical stdin marker `@-` excluded). That
|
|
571
|
+
// discriminates:
|
|
572
|
+
// - `-d '@/etc/passwd'` → value starts with `@` and is not `@-`
|
|
573
|
+
// → block (matches the original protection).
|
|
574
|
+
// - `-d '{"content":"a -d @x b"}'` → value starts with `{` →
|
|
575
|
+
// allow (the body contains @ but is not an @-file argument).
|
|
576
|
+
//
|
|
577
|
+
// For `-F`/`--form`, the file-read syntax is `name=@file` /
|
|
578
|
+
// `name=<file` (first `=` in the value followed by `@` or `<`),
|
|
579
|
+
// which the same walker can test in the value once recovered.
|
|
580
|
+
// Adjacent-token merge: bash treats `-d='value'` as the SINGLE
|
|
581
|
+
// argument `-d=value` (the quote is stripped, the bare prefix and
|
|
582
|
+
// the quoted body are joined when there is no whitespace between
|
|
583
|
+
// them). The regex pass below splits the two pieces — track each
|
|
584
|
+
// match's start vs. the previous match's end and concatenate any
|
|
585
|
+
// pair with no whitespace gap. A composite token is treated as
|
|
586
|
+
// "effectively bare" if either constituent was bare, so the flag
|
|
587
|
+
// walker still recognises `-d='@/path'` as a `-d=` flag carrying
|
|
588
|
+
// the value `@/path` (which the regex form `["']?@` used to catch).
|
|
589
|
+
const argRe = /'([^']*)'|"([^"]*)"|`([^`]*)`|([^\s'"`]+)/g;
|
|
590
|
+
const argList = [];
|
|
591
|
+
let am;
|
|
592
|
+
let lastEnd = -1;
|
|
593
|
+
// Walks `tokenizable` (heredoc-stripped) so a body line like
|
|
594
|
+
// `prose mentioning -d @/etc/passwd` cannot be parsed as a real
|
|
595
|
+
// `-d` flag carrying an `@file` value. Single / double quotes are
|
|
596
|
+
// preserved so the `quoted` discriminator still tracks user intent
|
|
597
|
+
// correctly for the dataFlag / formFlag checks below.
|
|
598
|
+
while ((am = argRe.exec(tokenizable)) !== null) {
|
|
599
|
+
const value = am[1] ?? am[2] ?? am[3] ?? am[4] ?? "";
|
|
600
|
+
const quoted = am[4] === undefined;
|
|
601
|
+
if (am.index === lastEnd && argList.length > 0) {
|
|
602
|
+
const prev = argList[argList.length - 1];
|
|
603
|
+
prev.value = prev.value + value;
|
|
604
|
+
prev.quoted = prev.quoted && quoted;
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
argList.push({ value, quoted });
|
|
608
|
+
}
|
|
609
|
+
lastEnd = argRe.lastIndex;
|
|
406
610
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
611
|
+
const dataFlag = /^(?:--data(?:-binary|-raw|-urlencode|-ascii)?|--data|-d)(?:=(.*))?$/;
|
|
612
|
+
const formFlag = /^(?:--form|-F)(?:=(.*))?$/;
|
|
613
|
+
for (let i = 0; i < argList.length; i++) {
|
|
614
|
+
const tok = argList[i];
|
|
615
|
+
if (!tok || tok.quoted)
|
|
616
|
+
continue;
|
|
617
|
+
const dm = tok.value.match(dataFlag);
|
|
618
|
+
if (dm) {
|
|
619
|
+
const value = dm[1] ?? argList[i + 1]?.value ?? "";
|
|
620
|
+
if (value.length > 0 && value[0] === "@" && value !== "@-") {
|
|
621
|
+
return {
|
|
622
|
+
decision: "block",
|
|
623
|
+
reason: "curl -d/--data with `@file` syntax not allowed — reads local files",
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const fm = tok.value.match(formFlag);
|
|
629
|
+
if (fm) {
|
|
630
|
+
const value = fm[1] ?? argList[i + 1]?.value ?? "";
|
|
631
|
+
// `name=@path` / `name=<path`: first `=` then `@` or `<`.
|
|
632
|
+
if (/^[^=\s]*=[@<]/.test(value)) {
|
|
633
|
+
return {
|
|
634
|
+
decision: "block",
|
|
635
|
+
reason: "curl -F/--form with `=@file` or `=<file` syntax not allowed — reads local files",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
412
639
|
}
|
|
413
640
|
// File-write flags. The agent can land bytes anywhere on disk —
|
|
414
641
|
// overwriting shims, ssh keys, shell rc files, etc. Daemon API is
|
|
@@ -433,11 +660,18 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
433
660
|
// containment. Quoted paths with spaces (`-o "my file"`) are
|
|
434
661
|
// ALSO rejected so a denylist regex that stops at the space
|
|
435
662
|
// inside the quotes cannot be smuggled past.
|
|
436
|
-
|
|
663
|
+
// Flag PRESENCE detection runs on the scan (quote-stripped) command
|
|
664
|
+
// so a body containing prose like "set -o pipefail" does not falsely
|
|
665
|
+
// claim there is an output flag. The subsequent VALUE extraction
|
|
666
|
+
// reads `tokenizable` (heredoc-stripped, quotes preserved) so an
|
|
667
|
+
// earlier heredoc-body occurrence of `-o /etc/passwd` cannot be
|
|
668
|
+
// captured ahead of the real flag — while quoted paths like
|
|
669
|
+
// `-o "my file.pdf"` are still readable.
|
|
670
|
+
const hasOutputFlag = /(?:^|\s)(?:--output(?:\b|=)|-[a-zA-Z]*o(?:\s|=|$))/.test(scan);
|
|
437
671
|
if (hasOutputFlag) {
|
|
438
672
|
// Three capture-group alternatives so quoted paths with spaces
|
|
439
673
|
// are caught — `[^\s'"]+` alone fails on `"my file"`.
|
|
440
|
-
const valueMatch =
|
|
674
|
+
const valueMatch = tokenizable.match(/(?:^|\s)(?:--output(?:\s+|=)|-o(?:\s+|=))(?:"([^"]*)"|'([^']*)'|([^\s'"]+))/);
|
|
441
675
|
const target = valueMatch?.[1] ?? valueMatch?.[2] ?? valueMatch?.[3] ?? "";
|
|
442
676
|
const isSafeRelative = target.length > 0 &&
|
|
443
677
|
!target.startsWith("/") &&
|
|
@@ -454,19 +688,19 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
454
688
|
};
|
|
455
689
|
}
|
|
456
690
|
}
|
|
457
|
-
if (/(?:^|\s)(?:--remote-name(?:-all)?\b|-[a-zA-Z]*O(?:\s|=|$))/.test(
|
|
691
|
+
if (/(?:^|\s)(?:--remote-name(?:-all)?\b|-[a-zA-Z]*O(?:\s|=|$))/.test(scan)) {
|
|
458
692
|
return {
|
|
459
693
|
decision: "block",
|
|
460
694
|
reason: "curl --remote-name/-O not allowed — would write to URL-derived path",
|
|
461
695
|
};
|
|
462
696
|
}
|
|
463
|
-
if (/(?:^|\s)(?:--dump-header\b|-[a-zA-Z]*D(?:\s|=|$))/.test(
|
|
697
|
+
if (/(?:^|\s)(?:--dump-header\b|-[a-zA-Z]*D(?:\s|=|$))/.test(scan)) {
|
|
464
698
|
return {
|
|
465
699
|
decision: "block",
|
|
466
700
|
reason: "curl --dump-header/-D not allowed — writes response headers to disk",
|
|
467
701
|
};
|
|
468
702
|
}
|
|
469
|
-
if (/(?:^|\s)(?:--cookie-jar\b|-[a-zA-Z]*c(?:\s|=|$))/.test(
|
|
703
|
+
if (/(?:^|\s)(?:--cookie-jar\b|-[a-zA-Z]*c(?:\s|=|$))/.test(scan)) {
|
|
470
704
|
return {
|
|
471
705
|
decision: "block",
|
|
472
706
|
reason: "curl --cookie-jar/-c not allowed — writes cookie state to disk",
|
|
@@ -479,20 +713,20 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
479
713
|
// `-b name=value` would require parsing the value; the simpler
|
|
480
714
|
// safe stance is to refuse the flag outright since the daemon
|
|
481
715
|
// API uses bearer tokens, not cookies.
|
|
482
|
-
if (/(?:^|\s)(?:--cookie\b|-[a-zA-Z]*b(?:\s|=|$))/.test(
|
|
716
|
+
if (/(?:^|\s)(?:--cookie\b|-[a-zA-Z]*b(?:\s|=|$))/.test(scan)) {
|
|
483
717
|
return {
|
|
484
718
|
decision: "block",
|
|
485
719
|
reason: "curl --cookie/-b not allowed — when the value is a path, " +
|
|
486
720
|
"the file contents are sent as the Cookie header (file read).",
|
|
487
721
|
};
|
|
488
722
|
}
|
|
489
|
-
if (/(?:^|\s)--trace(?:-ascii)?\b/.test(
|
|
723
|
+
if (/(?:^|\s)--trace(?:-ascii)?\b/.test(scan)) {
|
|
490
724
|
return {
|
|
491
725
|
decision: "block",
|
|
492
726
|
reason: "curl --trace / --trace-ascii not allowed — writes protocol trace to disk",
|
|
493
727
|
};
|
|
494
728
|
}
|
|
495
|
-
if (/(?:^|\s)(?:--write-out\b|-[a-zA-Z]*w(?:\s|=|$))/.test(
|
|
729
|
+
if (/(?:^|\s)(?:--write-out\b|-[a-zA-Z]*w(?:\s|=|$))/.test(scan)) {
|
|
496
730
|
return {
|
|
497
731
|
decision: "block",
|
|
498
732
|
reason: "curl --write-out/-w not allowed — format strings include file/stderr sinks",
|
|
@@ -501,7 +735,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
501
735
|
// Cert / key file references. The daemon API is plain HTTP on
|
|
502
736
|
// loopback; none of these flags are needed for legitimate
|
|
503
737
|
// operation and they all read arbitrary files from disk.
|
|
504
|
-
if (/(?:^|\s)(?:--cert\b|--key\b|--cacert\b|--capath\b|-[a-zA-Z]*E(?:\s|=|$))/.test(
|
|
738
|
+
if (/(?:^|\s)(?:--cert\b|--key\b|--cacert\b|--capath\b|-[a-zA-Z]*E(?:\s|=|$))/.test(scan)) {
|
|
505
739
|
return {
|
|
506
740
|
decision: "block",
|
|
507
741
|
reason: "curl --cert/--key/--cacert/--capath/-E not allowed — read arbitrary files",
|
|
@@ -514,7 +748,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
514
748
|
// Combined-short-flag forms (`-fsSL`, `-vL`) are caught by the
|
|
515
749
|
// `[a-zA-Z]*L` alternation; the literal `--location` and
|
|
516
750
|
// `--location-trusted` long forms are matched explicitly.
|
|
517
|
-
if (/(?:^|\s)(?:-[a-zA-Z]*L(?:\s|=|$)|--location(?:-trusted)?\b)/.test(
|
|
751
|
+
if (/(?:^|\s)(?:-[a-zA-Z]*L(?:\s|=|$)|--location(?:-trusted)?\b)/.test(scan)) {
|
|
518
752
|
return {
|
|
519
753
|
decision: "block",
|
|
520
754
|
reason: "curl -L / --location not allowed — would follow redirects off localhost",
|
|
@@ -531,6 +765,14 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
531
765
|
// Narrow to THIS jq invocation's own args (up to the next pipe / chain op)
|
|
532
766
|
// so that later pipeline stages are not inspected by the jq rules.
|
|
533
767
|
//
|
|
768
|
+
// The match runs against `stripBashHeredocs(cmd)` so that prose inside
|
|
769
|
+
// a heredoc body (e.g. a wiki article that mentions "the jq env
|
|
770
|
+
// filter") cannot trip the env / -L / --slurpfile checks below.
|
|
771
|
+
// Quoted strings remain intact because the env-filter detector
|
|
772
|
+
// intentionally peers inside the single-quoted jq filter argument
|
|
773
|
+
// (jq syntax lives inside shell quotes, so blanket quote-stripping
|
|
774
|
+
// would lose the very thing we need to inspect).
|
|
775
|
+
//
|
|
534
776
|
// Known approximation: `[^|;&]*` does not respect shell quoting, so a
|
|
535
777
|
// jq filter with a `|` INSIDE a quoted expression (e.g. `jq 'env | keys'`)
|
|
536
778
|
// will truncate `jqPart` at the first `|` regardless of whether that `|`
|
|
@@ -540,7 +782,7 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
540
782
|
// payloads are still blocked. The downside is slightly reduced precision
|
|
541
783
|
// on benign expressions containing the jq `|` operator — those get
|
|
542
784
|
// scanned only up to the first pipe, not their full extent.
|
|
543
|
-
const jqMatch = cmd.match(/\bjq\b([^|;&]*)/);
|
|
785
|
+
const jqMatch = stripBashHeredocs(cmd).match(/\bjq\b([^|;&]*)/);
|
|
544
786
|
if (!jqMatch)
|
|
545
787
|
return { continue: true };
|
|
546
788
|
const jqPart = jqMatch[0];
|
|
@@ -629,10 +871,17 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
629
871
|
// API is the sanctioned write channel.
|
|
630
872
|
const absDataDir = resolvePath(config.dataDir);
|
|
631
873
|
const realDataDir = realpathLenient(absDataDir);
|
|
874
|
+
// Use the quote/heredoc-stripped form for Layer 1 (substring) and
|
|
875
|
+
// Layer 3 (interpreter regex) so a JSON body or heredoc payload that
|
|
876
|
+
// legitimately contains the absolute context-dir path string, or the
|
|
877
|
+
// literal text `bash -c …`, does not trip these layers. Layer 2
|
|
878
|
+
// still uses `cmd` because its tokenizer is already quote-aware via
|
|
879
|
+
// `looksLikePathArg`.
|
|
880
|
+
const scan = stripBashStringContent(cmd);
|
|
632
881
|
// ── Layer 1: substring match against well-known path forms ──
|
|
633
882
|
const pathForms = shellPathForms(absContextDir, home);
|
|
634
883
|
for (const form of pathForms) {
|
|
635
|
-
if (
|
|
884
|
+
if (scan.includes(form)) {
|
|
636
885
|
return blockContextWrite(absContextDir, `substring match: ${form}`);
|
|
637
886
|
}
|
|
638
887
|
}
|
|
@@ -645,11 +894,21 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
645
894
|
const tokens = tokenizeShellCommand(cmd);
|
|
646
895
|
for (const rawTok of tokens) {
|
|
647
896
|
const tok = expandHomeForms(rawTok, home);
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
//
|
|
897
|
+
// Skip URL-shaped tokens; they are not filesystem paths. Must
|
|
898
|
+
// come before `looksLikePathArg` because `http://localhost/...`
|
|
899
|
+
// satisfies the "starts with `/`" rule once the scheme prefix
|
|
900
|
+
// is removed — and the bare-path rule too — but is never a
|
|
901
|
+
// filesystem reference.
|
|
651
902
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(tok))
|
|
652
903
|
continue;
|
|
904
|
+
// The old filter (`!tok.includes("/") && !tok.includes("\\")`)
|
|
905
|
+
// forwarded any quoted token with a `/` or `\` into the data-dir
|
|
906
|
+
// resolution branch, which produced false positives on JSON
|
|
907
|
+
// bodies (`{"content":"a\nb"}`) and header values (`Content-Type:
|
|
908
|
+
// application/json`) whenever cwd lived under the data dir. See
|
|
909
|
+
// `looksLikePathArg` for the replacement rules.
|
|
910
|
+
if (!looksLikePathArg(tok))
|
|
911
|
+
continue;
|
|
653
912
|
const candidate = isAbsolute(tok) ? tok : resolvePath(cwd, tok);
|
|
654
913
|
const real = realpathLenient(candidate);
|
|
655
914
|
const landsInsideContext = isPathInsideOrEqual(absContextDir, candidate) ||
|
|
@@ -671,8 +930,8 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
671
930
|
// SDK Write/Edit tools and the daemon API cover legitimate
|
|
672
931
|
// file-touching use cases. Blocking the patterns themselves is the
|
|
673
932
|
// only way to keep this hook's guarantees meaningful.
|
|
674
|
-
if (/(?:^|[\s|;&])(?:bash|sh|zsh|ksh|dash|busybox)\s+-c\b/.test(
|
|
675
|
-
/(?:^|[\s|;&])(?:python3?|node|ruby|perl|php|deno|bun)\s+-[ce]\b/.test(
|
|
933
|
+
if (/(?:^|[\s|;&])(?:bash|sh|zsh|ksh|dash|busybox)\s+-c\b/.test(scan) ||
|
|
934
|
+
/(?:^|[\s|;&])(?:python3?|node|ruby|perl|php|deno|bun)\s+-[ce]\b/.test(scan)) {
|
|
676
935
|
return {
|
|
677
936
|
decision: "block",
|
|
678
937
|
reason: `Bash commands that invoke an interpreter with -c / -e are not ` +
|
|
@@ -814,12 +1073,15 @@ export function buildSecurityHooks(deps, allowMode = false) {
|
|
|
814
1073
|
{
|
|
815
1074
|
matcher: "Bash",
|
|
816
1075
|
hooks: allowMode
|
|
817
|
-
? [
|
|
1076
|
+
? [
|
|
1077
|
+
wrapBashHook("bashContextWriteHook", bashContextWriteHook),
|
|
1078
|
+
wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
|
|
1079
|
+
]
|
|
818
1080
|
: [
|
|
819
|
-
bashCurlHook,
|
|
820
|
-
bashJqHook,
|
|
821
|
-
bashContextWriteHook,
|
|
822
|
-
bashAbsoluteBlockHook,
|
|
1081
|
+
wrapBashHook("bashCurlHook", bashCurlHook),
|
|
1082
|
+
wrapBashHook("bashJqHook", bashJqHook),
|
|
1083
|
+
wrapBashHook("bashContextWriteHook", bashContextWriteHook),
|
|
1084
|
+
wrapBashHook("bashAbsoluteBlockHook", bashAbsoluteBlockHook),
|
|
823
1085
|
],
|
|
824
1086
|
},
|
|
825
1087
|
{ matcher: "Write", hooks: [fileWriteHook, writeAbsoluteBlockHook] },
|