@namzu/sdk 51.0.0 → 52.0.0
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 +110 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/agents/examples/SupervisorAgent.d.ts +42 -0
- package/dist/agents/examples/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/examples/SupervisorAgent.js +80 -4
- package/dist/agents/examples/SupervisorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +36 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -0
- package/dist/authorization/protected-paths.js +107 -0
- package/dist/authorization/protected-paths.js.map +1 -0
- package/dist/authorization/readonly-commands.d.ts +39 -0
- package/dist/authorization/readonly-commands.d.ts.map +1 -0
- package/dist/authorization/readonly-commands.js +797 -0
- package/dist/authorization/readonly-commands.js.map +1 -0
- package/dist/authorization/readonly-flags.d.ts +19 -0
- package/dist/authorization/readonly-flags.d.ts.map +1 -0
- package/dist/authorization/readonly-flags.js +178 -0
- package/dist/authorization/readonly-flags.js.map +1 -0
- package/dist/authorization/removal.d.ts +52 -0
- package/dist/authorization/removal.d.ts.map +1 -0
- package/dist/authorization/removal.js +283 -0
- package/dist/authorization/removal.js.map +1 -0
- package/dist/authorization/shell-paths.d.ts +66 -0
- package/dist/authorization/shell-paths.d.ts.map +1 -0
- package/dist/authorization/shell-paths.js +697 -0
- package/dist/authorization/shell-paths.js.map +1 -0
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/compaction/context-window.d.ts +24 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +34 -0
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +10 -2
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/plan.d.ts +4 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +6 -2
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/verifier.d.ts +11 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +19 -7
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +55 -7
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +23 -2
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/web/guarded-fetch.d.ts.map +1 -1
- package/dist/connector/web/guarded-fetch.js +24 -0
- package/dist/connector/web/guarded-fetch.js.map +1 -1
- package/dist/connector/web/index.d.ts +1 -1
- package/dist/connector/web/index.d.ts.map +1 -1
- package/dist/connector/web/page.d.ts +59 -0
- package/dist/connector/web/page.d.ts.map +1 -0
- package/dist/connector/web/page.js +112 -0
- package/dist/connector/web/page.js.map +1 -0
- package/dist/connector/web/types.d.ts +47 -0
- package/dist/connector/web/types.d.ts.map +1 -1
- package/dist/connector/web/types.js.map +1 -1
- package/dist/constants/continuation.d.ts +8 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +8 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +1 -1
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +1 -1
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +7 -0
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +11 -2
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +4 -2
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/index.js +1 -1
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/lifecycle.d.ts +6 -0
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +98 -114
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook.d.ts +90 -22
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +434 -41
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/provider/errors.d.ts +10 -0
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +16 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/retry.d.ts +27 -0
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +19 -3
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +2 -2
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +15 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.d.ts +10 -0
- package/dist/runtime/query/executor/tool-call-admission.d.ts.map +1 -1
- package/dist/runtime/query/executor/tool-call-admission.js +74 -13
- package/dist/runtime/query/executor/tool-call-admission.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +41 -2
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +190 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/finalize-turn.d.ts +6 -0
- package/dist/runtime/query/finalize-turn.d.ts.map +1 -1
- package/dist/runtime/query/finalize-turn.js +1 -1
- package/dist/runtime/query/finalize-turn.js.map +1 -1
- package/dist/runtime/query/index.d.ts +29 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +15 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +31 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +216 -7
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +58 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +91 -0
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -0
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +89 -7
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +165 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +24 -1
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +36 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -0
- package/dist/runtime/query/path-escalation.js +184 -0
- package/dist/runtime/query/path-escalation.js.map +1 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +8 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/reminders.d.ts +35 -0
- package/dist/runtime/query/reminders.d.ts.map +1 -0
- package/dist/runtime/query/reminders.js +7 -0
- package/dist/runtime/query/reminders.js.map +1 -0
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +26 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +26 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +79 -5
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tool-output-budget.d.ts +46 -0
- package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
- package/dist/runtime/query/tool-output-budget.js +85 -1
- package/dist/runtime/query/tool-output-budget.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/session/paths.d.ts +5 -1
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +8 -0
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +2 -2
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +2 -2
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/naming.d.ts +9 -0
- package/dist/store/memory/naming.d.ts.map +1 -1
- package/dist/store/memory/naming.js +12 -0
- package/dist/store/memory/naming.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +30 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +170 -26
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +2 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +4 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +60 -18
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +8 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +152 -33
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +44 -5
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +52 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -1
- package/dist/tools/builtins/read-render.js +143 -0
- package/dist/tools/builtins/read-render.js.map +1 -1
- package/dist/tools/builtins/web.d.ts +13 -2
- package/dist/tools/builtins/web.d.ts.map +1 -1
- package/dist/tools/builtins/web.js +225 -32
- package/dist/tools/builtins/web.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +2 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +256 -92
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/question-options.d.ts +1 -0
- package/dist/tools/coordinator/question-options.d.ts.map +1 -1
- package/dist/tools/coordinator/question-options.js +6 -1
- package/dist/tools/coordinator/question-options.js.map +1 -1
- package/dist/tools/defineTool.d.ts +4 -1
- package/dist/tools/defineTool.d.ts.map +1 -1
- package/dist/tools/defineTool.js +6 -1
- package/dist/tools/defineTool.js.map +1 -1
- package/dist/tools/paths.d.ts +6 -0
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +1 -1
- package/dist/tools/paths.js.map +1 -1
- package/dist/types/agent/query.d.ts +3 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +1 -1
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/hitl/index.d.ts +104 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +93 -19
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +3 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/error.d.ts +6 -0
- package/dist/types/provider/error.d.ts.map +1 -1
- package/dist/types/provider/errors.d.ts +19 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +39 -1
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +15 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +9 -9
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +1 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +5 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/types/tool/index.d.ts +52 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/backoff.d.ts +12 -0
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +15 -0
- package/dist/utils/backoff.js.map +1 -1
- package/package.json +4 -2
- package/src/agents/QueryAgent.ts +5 -1
- package/src/agents/SupervisorAgent.ts +1 -1
- package/src/agents/examples/SupervisorAgent.ts +217 -106
- package/src/agents/index.ts +1 -1
- package/src/authorization/protected-paths.ts +110 -0
- package/src/authorization/readonly-commands.ts +771 -0
- package/src/authorization/readonly-flags.ts +196 -0
- package/src/authorization/removal.ts +326 -0
- package/src/authorization/shell-paths.ts +927 -0
- package/src/bridge/acp/update.ts +1 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/compaction/context-window.ts +41 -0
- package/src/compaction/manual.ts +10 -2
- package/src/compaction/plan.ts +11 -2
- package/src/compaction/verifier.ts +28 -3
- package/src/config/runtime.ts +23 -2
- package/src/connector/web/guarded-fetch.ts +26 -0
- package/src/connector/web/index.ts +3 -0
- package/src/connector/web/page.ts +130 -0
- package/src/connector/web/types.ts +47 -0
- package/src/constants/continuation.ts +10 -0
- package/src/constants/plugin/index.ts +1 -1
- package/src/manager/agent/lifecycle.ts +12 -4
- package/src/manager/session/turn-recorder.ts +10 -2
- package/src/plugin/index.ts +1 -0
- package/src/plugin/lifecycle.ts +107 -119
- package/src/plugin/shell-hook.ts +477 -43
- package/src/provider/errors.ts +18 -0
- package/src/provider/retry.ts +53 -3
- package/src/public-runtime.ts +8 -0
- package/src/public-types.ts +4 -0
- package/src/runtime/jobs/registry.ts +39 -2
- package/src/runtime/query/executor/tool-call-admission.ts +79 -13
- package/src/runtime/query/executor.ts +242 -8
- package/src/runtime/query/finalize-turn.ts +7 -1
- package/src/runtime/query/index.ts +45 -3
- package/src/runtime/query/iteration/index.ts +251 -12
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +106 -0
- package/src/runtime/query/iteration/phases/compaction.ts +99 -12
- package/src/runtime/query/iteration/phases/context.ts +11 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +177 -2
- package/src/runtime/query/iteration/stream-turn.ts +24 -1
- package/src/runtime/query/path-escalation.ts +204 -0
- package/src/runtime/query/plugin-hooks.ts +8 -0
- package/src/runtime/query/reminders.ts +45 -0
- package/src/runtime/query/resume-pending.ts +27 -0
- package/src/runtime/query/review-policy.ts +106 -5
- package/src/runtime/query/tool-output-budget.ts +115 -1
- package/src/runtime/query/tooling.ts +6 -0
- package/src/session/paths.ts +11 -1
- package/src/store/memory/disk.ts +2 -1
- package/src/store/memory/markdown.ts +2 -1
- package/src/store/memory/naming.ts +13 -0
- package/src/tools/builtins/bash.ts +203 -34
- package/src/tools/builtins/content-fingerprint.ts +5 -0
- package/src/tools/builtins/edit.ts +69 -19
- package/src/tools/builtins/grep.ts +159 -38
- package/src/tools/builtins/read-file.ts +48 -5
- package/src/tools/builtins/read-render.ts +193 -0
- package/src/tools/builtins/web.ts +258 -33
- package/src/tools/coordinator/ask-user-question.ts +309 -103
- package/src/tools/coordinator/question-options.ts +8 -1
- package/src/tools/defineTool.ts +11 -2
- package/src/tools/paths.ts +1 -1
- package/src/types/agent/query.ts +3 -0
- package/src/types/errors/catalog.ts +1 -1
- package/src/types/hitl/index.ts +104 -0
- package/src/types/message/index.ts +2 -0
- package/src/types/plugin/index.ts +101 -18
- package/src/types/provider/error.ts +6 -0
- package/src/types/provider/errors.ts +49 -1
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +15 -2
- package/src/types/session/records.ts +1 -0
- package/src/types/session/stop-reason.ts +5 -0
- package/src/types/tool/index.ts +53 -2
- package/src/utils/backoff.ts +21 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a call writes or removes that no mode may approve in advance.
|
|
3
|
+
*
|
|
4
|
+
* Three questions the kernel answers about a prepared call before it runs, all
|
|
5
|
+
* of them about the call's own arguments and none about the model's intent:
|
|
6
|
+
*
|
|
7
|
+
* - does it write a protected path (a git hook, a shell rc file, editor or
|
|
8
|
+
* tool configuration, the project's `.namzu` directory)?
|
|
9
|
+
* - does its shell line remove something catastrophic?
|
|
10
|
+
* - does its shell line reach a path outside the turn's roots, through a
|
|
11
|
+
* command whose operands are paths or through a redirection?
|
|
12
|
+
*
|
|
13
|
+
* The first two are floors: they hold in every mode, the way a sandbox escape
|
|
14
|
+
* does. The third is the existing outside-root boundary, extended from the
|
|
15
|
+
* file tools' one declared argument to the paths a shell line names.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { isAbsolute, relative, resolve } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { isProtectedPath } from '../../authorization/protected-paths.js'
|
|
22
|
+
import { dangerousRemoval } from '../../authorization/removal.js'
|
|
23
|
+
import type { ShellDialect } from '../../authorization/shell-lexer.js'
|
|
24
|
+
import { readShellLine, shellPathAccess } from '../../authorization/shell-paths.js'
|
|
25
|
+
import { canonicalize, pathOutsideRoots } from '../../tools/paths.js'
|
|
26
|
+
|
|
27
|
+
/** What the escalation records carry. */
|
|
28
|
+
export interface PathEscalations {
|
|
29
|
+
readonly outsidePaths: readonly string[]
|
|
30
|
+
readonly protectedPaths: readonly string[]
|
|
31
|
+
readonly dangerousRemoval?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PathEscalationContext {
|
|
35
|
+
/** The working directory first, then the added directories. */
|
|
36
|
+
readonly roots: readonly string[]
|
|
37
|
+
/** Whether to report paths outside the roots (`outsideRootAccess: 'review'`, no sandbox). */
|
|
38
|
+
readonly reviewOutside: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The part of `absolute` that protected names are looked for in.
|
|
43
|
+
*
|
|
44
|
+
* Inside a root (the working directory or an added directory) that is the part
|
|
45
|
+
* below the root, not the whole path: the root is where the turn was put, and
|
|
46
|
+
* a root that itself sits under a protected name is not a reason to ask about
|
|
47
|
+
* every file in it. The case that matters is the managed worktree a sub-agent
|
|
48
|
+
* runs in, `~/.namzu/projects/<slug>/worktrees/<name>`, which lies under
|
|
49
|
+
* `.namzu` by construction; the reference terminal makes the same exception
|
|
50
|
+
* for its own worktrees directory. A path in no root is looked at whole.
|
|
51
|
+
*/
|
|
52
|
+
function protectedScope(absolute: string, roots: readonly string[]): string {
|
|
53
|
+
let best: string | undefined
|
|
54
|
+
for (const root of roots) {
|
|
55
|
+
const base = resolve(root)
|
|
56
|
+
const below = relative(base, absolute)
|
|
57
|
+
if (below === '' || below.startsWith('..') || isAbsolute(below)) continue
|
|
58
|
+
if (best === undefined || below.length < best.length) best = below
|
|
59
|
+
}
|
|
60
|
+
return best ?? absolute
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A write is protected when its spelling, or the place a link leads to, is. */
|
|
64
|
+
async function isProtectedWrite(
|
|
65
|
+
spelled: string,
|
|
66
|
+
absolute: string,
|
|
67
|
+
roots: readonly string[],
|
|
68
|
+
): Promise<boolean> {
|
|
69
|
+
if (isProtectedPath(isAbsolute(spelled) ? protectedScope(absolute, roots) : spelled)) return true
|
|
70
|
+
if (isProtectedPath(protectedScope(absolute, roots))) return true
|
|
71
|
+
const real = await canonicalize(absolute)
|
|
72
|
+
const realRoots = await Promise.all(roots.map((root) => canonicalize(resolve(root))))
|
|
73
|
+
return isProtectedPath(protectedScope(real, [...roots, ...realRoots]))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `{a,b}` with plain comma lists, one level at a time, bounded. */
|
|
77
|
+
export function expandBraces(value: string, limit = 16): string[] {
|
|
78
|
+
const open = value.indexOf('{')
|
|
79
|
+
if (open < 0) return [value]
|
|
80
|
+
let depth = 0
|
|
81
|
+
let close = -1
|
|
82
|
+
const commas: number[] = []
|
|
83
|
+
for (let i = open; i < value.length; i += 1) {
|
|
84
|
+
const c = value[i]
|
|
85
|
+
if (c === '{') depth += 1
|
|
86
|
+
else if (c === '}') {
|
|
87
|
+
depth -= 1
|
|
88
|
+
if (depth === 0) {
|
|
89
|
+
close = i
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
} else if (c === ',' && depth === 1) commas.push(i)
|
|
93
|
+
}
|
|
94
|
+
if (close < 0 || commas.length === 0) return [value]
|
|
95
|
+
const head = value.slice(0, open)
|
|
96
|
+
const tail = value.slice(close + 1)
|
|
97
|
+
const parts: string[] = []
|
|
98
|
+
let start = open + 1
|
|
99
|
+
for (const comma of [...commas, close]) {
|
|
100
|
+
parts.push(value.slice(start, comma))
|
|
101
|
+
start = comma + 1
|
|
102
|
+
}
|
|
103
|
+
const out: string[] = []
|
|
104
|
+
for (const part of parts) {
|
|
105
|
+
for (const rest of expandBraces(head + part + tail, limit)) {
|
|
106
|
+
out.push(rest)
|
|
107
|
+
if (out.length >= limit) return out
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The path a use names with `~` and `$HOME` read as the home directory, or why it cannot be. */
|
|
114
|
+
function settle(
|
|
115
|
+
value: string,
|
|
116
|
+
home: string,
|
|
117
|
+
): { readonly path: string } | { readonly unresolved: true } {
|
|
118
|
+
if (value === '~') return { path: home }
|
|
119
|
+
if (value.startsWith('~/')) return { path: resolve(home, value.slice(2)) }
|
|
120
|
+
if (value.startsWith('~')) return { unresolved: true }
|
|
121
|
+
const variable = /^\$(?:\{HOME\}|HOME)(?=\/|$)/.exec(value)
|
|
122
|
+
if (variable !== null) return { path: resolve(home, `.${value.slice(variable[0].length)}`) }
|
|
123
|
+
if (/[$`]/.test(value)) return { unresolved: true }
|
|
124
|
+
return { path: value }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The part of a glob path before the first segment that holds a glob character. */
|
|
128
|
+
function beforeGlob(path: string): string {
|
|
129
|
+
const at = path.search(/[*?[]/)
|
|
130
|
+
if (at < 0) return path
|
|
131
|
+
const cut = path.lastIndexOf('/', at)
|
|
132
|
+
return cut <= 0 ? '/' : path.slice(0, cut)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A write the file tool's own declared path argument names. */
|
|
136
|
+
export async function declaredWriteEscalation(
|
|
137
|
+
roots: readonly string[],
|
|
138
|
+
value: string,
|
|
139
|
+
): Promise<string | undefined> {
|
|
140
|
+
const [first] = roots
|
|
141
|
+
if (first === undefined || value === '') return undefined
|
|
142
|
+
const absolute = resolve(resolve(first), value)
|
|
143
|
+
return (await isProtectedWrite(value, absolute, roots)) ? absolute : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** What a shell line reaches. */
|
|
147
|
+
export async function shellEscalations(
|
|
148
|
+
line: string,
|
|
149
|
+
dialect: ShellDialect,
|
|
150
|
+
context: PathEscalationContext,
|
|
151
|
+
): Promise<PathEscalations> {
|
|
152
|
+
const reading = readShellLine(line, dialect)
|
|
153
|
+
const access = shellPathAccess(reading)
|
|
154
|
+
const [cwd, ...added] = context.roots
|
|
155
|
+
const outside = new Set<string>()
|
|
156
|
+
const protectedPaths = new Set<string>()
|
|
157
|
+
const home = homedir()
|
|
158
|
+
if (cwd !== undefined) {
|
|
159
|
+
for (const use of access.uses) {
|
|
160
|
+
for (const spelled of expandBraces(use.path)) {
|
|
161
|
+
const settled = settle(spelled, home)
|
|
162
|
+
const unresolved = use.unresolved || 'unresolved' in settled
|
|
163
|
+
const base = 'path' in settled ? settled.path : spelled
|
|
164
|
+
const absolute = isAbsolute(base) ? resolve(base) : resolve(cwd, base)
|
|
165
|
+
if (use.access === 'write' && (await isProtectedWrite(spelled, absolute, context.roots))) {
|
|
166
|
+
protectedPaths.add(absolute)
|
|
167
|
+
}
|
|
168
|
+
if (!context.reviewOutside) continue
|
|
169
|
+
if (unresolved) {
|
|
170
|
+
// A write whose target is decided at runtime cannot be checked; a
|
|
171
|
+
// read of one is left, as the reference leaves it.
|
|
172
|
+
if (use.access === 'write') outside.add(spelled)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
access.changesDirectory &&
|
|
177
|
+
use.access === 'write' &&
|
|
178
|
+
!isAbsolute(base) &&
|
|
179
|
+
!spelled.startsWith('~')
|
|
180
|
+
) {
|
|
181
|
+
// A `cd` earlier in the line moves a relative target somewhere the
|
|
182
|
+
// roots cannot be compared with.
|
|
183
|
+
outside.add(spelled)
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
const reached = await pathOutsideRoots(context.roots, beforeGlob(absolute))
|
|
187
|
+
if (reached !== undefined) outside.add(reached)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const removal =
|
|
192
|
+
cwd === undefined
|
|
193
|
+
? undefined
|
|
194
|
+
: dangerousRemoval(reading, access.changesDirectory, {
|
|
195
|
+
cwd,
|
|
196
|
+
additionalDirectories: added,
|
|
197
|
+
home,
|
|
198
|
+
})
|
|
199
|
+
return {
|
|
200
|
+
outsidePaths: [...outside],
|
|
201
|
+
protectedPaths: [...protectedPaths],
|
|
202
|
+
...(removal !== undefined ? { dangerousRemoval: removal } : {}),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -27,6 +27,14 @@ export function applyLifecycleHookResults(
|
|
|
27
27
|
if (result.text.trim().length > 0) annotations.push(result.text.trim())
|
|
28
28
|
continue
|
|
29
29
|
case 'skip':
|
|
30
|
+
// `turn_end`, `subagent_stop` and `pre_compact` answer with `skip`
|
|
31
|
+
// to keep the turn going or to cancel a pass. The sites that can act
|
|
32
|
+
// on that read the results themselves; a site that cannot (the
|
|
33
|
+
// final `turn_end` of a turn that already ended) has nothing to
|
|
34
|
+
// refuse and carries on.
|
|
35
|
+
if (event === 'turn_end' || event === 'subagent_stop' || event === 'pre_compact') {
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
30
38
|
// The one lifecycle event with a verdict: a hook may refuse the
|
|
31
39
|
// prompt before the model sees it, and the turn ends there.
|
|
32
40
|
if (event === 'user_prompt_submit') {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SessionId, TurnId } from '../../types/ids/index.js'
|
|
2
|
+
import type { Message } from '../../types/message/index.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a reminder source is told when the kernel asks it for hidden reminders.
|
|
6
|
+
*/
|
|
7
|
+
export interface ReminderContext {
|
|
8
|
+
readonly sessionId: SessionId
|
|
9
|
+
readonly turnId: TurnId
|
|
10
|
+
/** The model request about to be made; 1 for the first of the turn. */
|
|
11
|
+
readonly iteration: number
|
|
12
|
+
/**
|
|
13
|
+
* The conversation as it stands, reminders included. Read-only: a source
|
|
14
|
+
* derives its cadence from this (how many assistant turns since it last
|
|
15
|
+
* spoke), so a resumed or compacted conversation needs no counter of its own.
|
|
16
|
+
*/
|
|
17
|
+
readonly messages: readonly Message[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A host's hidden reminders for the next model request.
|
|
22
|
+
*
|
|
23
|
+
* The kernel calls it once at the start of every model request of a turn
|
|
24
|
+
* (never the closing one) and appends each returned text to the conversation
|
|
25
|
+
* as a user-role message with provenance `{ type: 'runtime-context', kind:
|
|
26
|
+
* 'reminder' }`. The message is part of the durable history, so it is not
|
|
27
|
+
* operator input, never counts as the latest user request, and is not shown as
|
|
28
|
+
* something the operator typed. The channel is opt-in
|
|
29
|
+
* (`QueryParams.reminders`): a turn without a source behaves as before.
|
|
30
|
+
*
|
|
31
|
+
* A source that throws is treated as having nothing to say; a reminder is
|
|
32
|
+
* context, and a fault in one must not end a turn.
|
|
33
|
+
*/
|
|
34
|
+
export type ReminderSource = (
|
|
35
|
+
context: ReminderContext,
|
|
36
|
+
) => readonly string[] | undefined | Promise<readonly string[] | undefined>
|
|
37
|
+
|
|
38
|
+
/** Whether a message is a reminder the kernel appended for a {@link ReminderSource}. */
|
|
39
|
+
export function isReminderMessage(message: Message): boolean {
|
|
40
|
+
return (
|
|
41
|
+
message.role === 'user' &&
|
|
42
|
+
message.source?.type === 'runtime-context' &&
|
|
43
|
+
message.source.kind === 'reminder'
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -517,6 +517,24 @@ export async function applyPendingResume(
|
|
|
517
517
|
reason: "the turn's review approved this call, answered across a restart",
|
|
518
518
|
})
|
|
519
519
|
}
|
|
520
|
+
for (const path of call.escalation?.protectedPaths ?? []) {
|
|
521
|
+
await recorder.recordAudit({
|
|
522
|
+
what: { action: 'protected_path_write', tool: call.name, resource: path },
|
|
523
|
+
outcome: 'approved',
|
|
524
|
+
reason: "the turn's review approved this call, answered across a restart",
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
if (call.escalation?.dangerousRemoval !== undefined) {
|
|
528
|
+
await recorder.recordAudit({
|
|
529
|
+
what: {
|
|
530
|
+
action: 'dangerous_removal',
|
|
531
|
+
tool: call.name,
|
|
532
|
+
resource: call.escalation.dangerousRemoval,
|
|
533
|
+
},
|
|
534
|
+
outcome: 'approved',
|
|
535
|
+
reason: "the turn's review approved this call, answered across a restart",
|
|
536
|
+
})
|
|
537
|
+
}
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
// The parked assistant message is already in the log: it goes back into
|
|
@@ -562,6 +580,15 @@ export function escalationRefusal(
|
|
|
562
580
|
for (const path of escalation.outsidePaths ?? []) {
|
|
563
581
|
if (!shown?.outsidePaths?.includes(path)) return refusal
|
|
564
582
|
}
|
|
583
|
+
for (const path of escalation.protectedPaths ?? []) {
|
|
584
|
+
if (!shown?.protectedPaths?.includes(path)) return refusal
|
|
585
|
+
}
|
|
586
|
+
if (
|
|
587
|
+
escalation.dangerousRemoval !== undefined &&
|
|
588
|
+
shown?.dangerousRemoval !== escalation.dangerousRemoval
|
|
589
|
+
) {
|
|
590
|
+
return refusal
|
|
591
|
+
}
|
|
565
592
|
if (escalation.sandboxEscape && (!shown?.sandboxEscape || !confirmed?.includes(id))) {
|
|
566
593
|
return 'Refused after resume: running this command outside the sandbox needs a person to confirm it for this call, and the durable approval did not.'
|
|
567
594
|
}
|
|
@@ -228,6 +228,26 @@ export const REQUIRES_APPROVAL_UNATTENDED_REFUSAL =
|
|
|
228
228
|
export const UNKNOWN_PROGRAM_UNATTENDED_REFUSAL =
|
|
229
229
|
'Refused: a call in this batch runs a command whose own program name is decided at runtime (a substitution or a variable stands for it), which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Write the program name literally; compute what needs computing in the arguments instead.'
|
|
230
230
|
|
|
231
|
+
/**
|
|
232
|
+
* What the model is told when a batch writes a protected path (a git hook, a
|
|
233
|
+
* shell rc file, editor or tool configuration, the project's `.namzu`
|
|
234
|
+
* directory) and nobody can be asked.
|
|
235
|
+
*
|
|
236
|
+
* Refused in every mode, `auto` included: a write there runs later, outside
|
|
237
|
+
* the turn, with the operator's rights, so an answer given before the call
|
|
238
|
+
* existed (a mode, a rule, "approve all") is not an answer about it.
|
|
239
|
+
*/
|
|
240
|
+
export const PROTECTED_PATH_UNATTENDED_REFUSAL =
|
|
241
|
+
"Refused: a call in this batch writes a protected path (a git hook or config, a shell startup file, editor or tool configuration, or the project's .namzu directory), which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Leave those files alone, or tell the user what change you need so they can make it."
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* What the model is told when a batch removes something catastrophic (the
|
|
245
|
+
* root, the home directory, the working directory or a parent of it, a glob
|
|
246
|
+
* across directories, a target a variable may empty) and nobody can be asked.
|
|
247
|
+
*/
|
|
248
|
+
export const DANGEROUS_REMOVAL_UNATTENDED_REFUSAL =
|
|
249
|
+
'Refused: a call in this batch removes something this check treats as catastrophic (the file system root, a top-level or home directory, the working directory or a parent of it, a glob across directories, or a target a variable or substitution may leave empty), which needs a person to approve it each time, and nobody can be asked in this session. Nothing in this batch ran. Name the exact directory to remove with a literal path.'
|
|
250
|
+
|
|
231
251
|
/**
|
|
232
252
|
* What the model is told when a batch would show it the operator's screen for
|
|
233
253
|
* the first time in a session and nobody can be asked.
|
|
@@ -250,6 +270,21 @@ export interface ScreenConsentRecord {
|
|
|
250
270
|
readonly sessions: Set<string>
|
|
251
271
|
}
|
|
252
272
|
|
|
273
|
+
/** Whether the call writes a protected path or removes something catastrophic. */
|
|
274
|
+
function reachesAFloor(tc: ToolCallSummary): boolean {
|
|
275
|
+
return (
|
|
276
|
+
(tc.escalation?.protectedPaths?.length ?? 0) > 0 ||
|
|
277
|
+
tc.escalation?.dangerousRemoval !== undefined
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The refusal for a batch that reaches a floor and cannot be asked about it. */
|
|
282
|
+
function floorRefusal(toolCalls: readonly ToolCallSummary[]): string {
|
|
283
|
+
return toolCalls.some((tc) => (tc.escalation?.protectedPaths?.length ?? 0) > 0)
|
|
284
|
+
? PROTECTED_PATH_UNATTENDED_REFUSAL
|
|
285
|
+
: DANGEROUS_REMOVAL_UNATTENDED_REFUSAL
|
|
286
|
+
}
|
|
287
|
+
|
|
253
288
|
/** The batch a person is asked about. */
|
|
254
289
|
export interface ToolReviewRequest {
|
|
255
290
|
/** Originating session, preserved by createReviewHandler for host attribution. */
|
|
@@ -339,6 +374,16 @@ export interface ReviewPolicyOptions {
|
|
|
339
374
|
* `capturesScreen` declaration, read from `registry`; nothing without one.
|
|
340
375
|
*/
|
|
341
376
|
readonly capturesScreen?: (name: string, input: unknown) => boolean
|
|
377
|
+
/**
|
|
378
|
+
* The one write `plan` mode lets through: a host that gives plan mode a plan
|
|
379
|
+
* file says here which call writes it. In `plan`, a call this accepts is not
|
|
380
|
+
* a change — it is neither refused nor asked about — unless a rule asked to
|
|
381
|
+
* review it, in which case it is put to the person like a read outside the
|
|
382
|
+
* roots. Every other write stays refused. Omitted (the default), `plan`
|
|
383
|
+
* refuses every change, as it always has; the option does nothing in any
|
|
384
|
+
* other mode.
|
|
385
|
+
*/
|
|
386
|
+
readonly planModeWrite?: (call: ToolCallSummary) => boolean
|
|
342
387
|
}
|
|
343
388
|
|
|
344
389
|
/**
|
|
@@ -358,6 +403,23 @@ function isSkillGranted(tc: ToolCallSummary): boolean {
|
|
|
358
403
|
)
|
|
359
404
|
}
|
|
360
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Whether a `pre_tool_use` hook's `allow` may stand in for a person on this
|
|
408
|
+
* call. The same bar as a skill's grant, repeated here so that a mark on a
|
|
409
|
+
* persisted or host-built summary cannot carry more than the kernel gave it.
|
|
410
|
+
*/
|
|
411
|
+
function isHookGranted(tc: ToolCallSummary): boolean {
|
|
412
|
+
return (
|
|
413
|
+
tc.hookGrant !== undefined &&
|
|
414
|
+
tc.hookPermission?.decision !== 'ask' &&
|
|
415
|
+
!tc.authorization?.explicitReview &&
|
|
416
|
+
tc.authorization?.decision !== 'deny' &&
|
|
417
|
+
!tc.isDestructive &&
|
|
418
|
+
tc.escalation === undefined &&
|
|
419
|
+
tc.requiresApproval !== true
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
|
|
361
423
|
/** The handler behind `createReviewPolicy`, for a host that wants only the function. */
|
|
362
424
|
export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHandler {
|
|
363
425
|
const { prompt, registry } = options
|
|
@@ -422,7 +484,16 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
422
484
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
423
485
|
answered = answer
|
|
424
486
|
}
|
|
425
|
-
|
|
487
|
+
// The plan file, in `plan`: the one write that is not a change. A rule
|
|
488
|
+
// that asked for review keeps the call in the batch, so it is asked.
|
|
489
|
+
const isPlanFileWrite = (tc: ToolCallSummary): boolean =>
|
|
490
|
+
mode === 'plan' &&
|
|
491
|
+
options.planModeWrite?.(tc) === true &&
|
|
492
|
+
tc.authorization?.decision !== 'deny'
|
|
493
|
+
const reviewable = request.toolCalls.filter(
|
|
494
|
+
(tc) => !(isPlanFileWrite(tc) && !tc.authorization?.explicitReview),
|
|
495
|
+
)
|
|
496
|
+
if (!batchNeedsReview(reviewable, exempt)) {
|
|
426
497
|
return { action: 'approve_tools' }
|
|
427
498
|
}
|
|
428
499
|
// A batch of nothing but non-destructive file edits is the case
|
|
@@ -450,7 +521,13 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
450
521
|
// to do, and the question such a path gets is the same in every mode that
|
|
451
522
|
// lets reads run. It goes on to that question below; `strict` still
|
|
452
523
|
// refuses it, since no rule can approve a path outside the roots.
|
|
453
|
-
if (
|
|
524
|
+
if (
|
|
525
|
+
mode === 'plan' &&
|
|
526
|
+
!onlyReadsOutsideRoots(
|
|
527
|
+
reviewable.filter((tc) => !isPlanFileWrite(tc)),
|
|
528
|
+
exempt,
|
|
529
|
+
)
|
|
530
|
+
) {
|
|
454
531
|
return { action: 'reject_tools', feedback: PLAN_MODE_REFUSAL }
|
|
455
532
|
}
|
|
456
533
|
if (mode === 'strict') return { action: 'reject_tools', feedback: STRICT_MODE_REFUSAL }
|
|
@@ -471,6 +548,9 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
471
548
|
// This opt-in confirms only the sandbox escape. A different call in
|
|
472
549
|
// the batch, or another boundary on this same call, still needs a
|
|
473
550
|
// person; the early return must not approve that boundary too.
|
|
551
|
+
if (request.toolCalls.some(reachesAFloor)) {
|
|
552
|
+
return { action: 'reject_tools', feedback: floorRefusal(request.toolCalls) }
|
|
553
|
+
}
|
|
474
554
|
if (request.toolCalls.some((tc) => (tc.escalation?.outsidePaths?.length ?? 0) > 0)) {
|
|
475
555
|
return { action: 'reject_tools', feedback: OUTSIDE_ROOTS_UNATTENDED_REFUSAL }
|
|
476
556
|
}
|
|
@@ -495,6 +575,24 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
495
575
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
496
576
|
return { action: 'approve_tools', confirmedEscalations: escapes }
|
|
497
577
|
}
|
|
578
|
+
// A write to a protected path, or a removal of something catastrophic,
|
|
579
|
+
// is a question for a person, every time, in every mode that got this
|
|
580
|
+
// far: `auto` and a remembered "approve all" were answers given before
|
|
581
|
+
// the call existed. With nobody to ask it is refused, not approved.
|
|
582
|
+
if (request.toolCalls.some(reachesAFloor)) {
|
|
583
|
+
if (!prompt) return { action: 'reject_tools', feedback: floorRefusal(request.toolCalls) }
|
|
584
|
+
const answer = await ask()
|
|
585
|
+
if (answer.kind === 'reject') {
|
|
586
|
+
return {
|
|
587
|
+
action: 'reject_tools',
|
|
588
|
+
feedback: answer.feedback ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
589
|
+
...(answer.declined ? { declined: answer.declined } : {}),
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
// Latches for the ordinary calls that follow, never for the next floor.
|
|
593
|
+
if (answer.kind === 'approve-all') remembered.all = true
|
|
594
|
+
return { action: 'approve_tools' }
|
|
595
|
+
}
|
|
498
596
|
// A path outside the roots is a question for a person, every time,
|
|
499
597
|
// like an escape: `auto` and a remembered "approve all" were answers
|
|
500
598
|
// given about tools, before this path was named, and neither saw it.
|
|
@@ -571,15 +669,18 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
571
669
|
(mode === 'accept-edits' && ACCEPT_EDITS_TOOLS.has(tc.name))
|
|
572
670
|
),
|
|
573
671
|
)
|
|
672
|
+
const skillsHonoured = options.skillGrants !== 'ignore'
|
|
574
673
|
if (
|
|
575
674
|
answered === undefined &&
|
|
576
|
-
options.skillGrants !== 'ignore' &&
|
|
577
675
|
needsPerson.length > 0 &&
|
|
578
|
-
needsPerson.every(isSkillGranted)
|
|
676
|
+
needsPerson.every((tc) => isHookGranted(tc) || (skillsHonoured && isSkillGranted(tc)))
|
|
579
677
|
) {
|
|
678
|
+
const byHook = needsPerson.filter(isHookGranted).map((tc) => tc.id)
|
|
679
|
+
const bySkill = needsPerson.filter((tc) => !isHookGranted(tc)).map((tc) => tc.id)
|
|
580
680
|
return {
|
|
581
681
|
action: 'approve_tools',
|
|
582
|
-
skillGranted:
|
|
682
|
+
...(bySkill.length > 0 ? { skillGranted: bySkill } : {}),
|
|
683
|
+
...(byHook.length > 0 ? { hookGranted: byHook } : {}),
|
|
583
684
|
}
|
|
584
685
|
}
|
|
585
686
|
const answer = await ask()
|
|
@@ -18,6 +18,19 @@ import { digest, spillManifest } from '../../store/evidence/format.js'
|
|
|
18
18
|
*/
|
|
19
19
|
export const DEFAULT_MAX_TOOL_OUTPUT_CHARS = 40_000
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Cap on the COMBINED model-visible text of every tool result one assistant
|
|
23
|
+
* message produced.
|
|
24
|
+
*
|
|
25
|
+
* The per-result cap alone lets eight parallel calls put 320,000 characters in
|
|
26
|
+
* front of the model at once. This is the bound on the whole batch; see
|
|
27
|
+
* {@link fitResultsToMessageBudget}.
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_MAX_TOOL_OUTPUT_CHARS_PER_MESSAGE = 200_000
|
|
30
|
+
|
|
31
|
+
/** Size of the preview that stands in for a result replaced to fit the message budget. */
|
|
32
|
+
export const MESSAGE_BUDGET_PREVIEW_CHARS = 2_000
|
|
33
|
+
|
|
21
34
|
/**
|
|
22
35
|
* Opening of the line that points at a spilled output.
|
|
23
36
|
*
|
|
@@ -298,7 +311,10 @@ function spill(
|
|
|
298
311
|
writeFileSync(path, bytes, { flag: 'wx', mode: 0o600 })
|
|
299
312
|
try {
|
|
300
313
|
const manifest = spillManifest(bytes)
|
|
301
|
-
writeFileSync(`${path}.manifest.json`, manifest, {
|
|
314
|
+
writeFileSync(`${path}.manifest.json`, manifest, {
|
|
315
|
+
flag: 'wx',
|
|
316
|
+
mode: 0o600,
|
|
317
|
+
})
|
|
302
318
|
return { path, integrity: digest(manifest) }
|
|
303
319
|
} catch {
|
|
304
320
|
onError?.('The full output was retained, but its integrity manifest could not be written.')
|
|
@@ -324,3 +340,101 @@ function spill(
|
|
|
324
340
|
return undefined
|
|
325
341
|
}
|
|
326
342
|
}
|
|
343
|
+
|
|
344
|
+
export interface MessageBudgetItem {
|
|
345
|
+
readonly toolCallId: string
|
|
346
|
+
readonly toolName: string
|
|
347
|
+
readonly output: string
|
|
348
|
+
/** False for a result that must stay as the tool produced it. */
|
|
349
|
+
readonly replaceable: boolean
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export interface MessageBudgetReplacement {
|
|
353
|
+
readonly output: string
|
|
354
|
+
readonly originalLength: number
|
|
355
|
+
readonly spillPath?: string
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export interface FitResultsToMessageBudgetOptions {
|
|
359
|
+
readonly maxChars: number
|
|
360
|
+
readonly spillDir?: string | undefined
|
|
361
|
+
readonly onError?: (message: string) => void
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const SPILL_LINE = new RegExp(`^${SPILL_MARKER} .+$`, 'm')
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Hold one assistant message's combined tool results under a cap.
|
|
368
|
+
*
|
|
369
|
+
* Replaceable results are replaced largest first, each by a short preview that
|
|
370
|
+
* names where its full text is kept, until the total is within `maxChars`. A
|
|
371
|
+
* result already spilled keeps the path it was given; one that was not is
|
|
372
|
+
* spilled now. Results that are not replaceable are left alone and are not
|
|
373
|
+
* counted, because nothing here could reduce them.
|
|
374
|
+
*
|
|
375
|
+
* The caller writes the replacement into the result BEFORE the result reaches
|
|
376
|
+
* the history. From then on it is just the message's content, so it is the
|
|
377
|
+
* same on every later request and a prompt-cache prefix over it stays valid;
|
|
378
|
+
* nothing is decided again on a later turn.
|
|
379
|
+
*
|
|
380
|
+
* Returns only the results that changed, keyed by `toolCallId`. Ties keep the
|
|
381
|
+
* earlier call, so the same batch always yields the same replacements.
|
|
382
|
+
*/
|
|
383
|
+
export function fitResultsToMessageBudget(
|
|
384
|
+
items: readonly MessageBudgetItem[],
|
|
385
|
+
opts: FitResultsToMessageBudgetOptions,
|
|
386
|
+
): Map<string, MessageBudgetReplacement> {
|
|
387
|
+
const replaced = new Map<string, MessageBudgetReplacement>()
|
|
388
|
+
if (!Number.isFinite(opts.maxChars) || opts.maxChars <= 0) return replaced
|
|
389
|
+
|
|
390
|
+
const candidates = items.filter((item) => item.replaceable)
|
|
391
|
+
let total = candidates.reduce((sum, item) => sum + item.output.length, 0)
|
|
392
|
+
if (total <= opts.maxChars) return replaced
|
|
393
|
+
|
|
394
|
+
const bySize = candidates
|
|
395
|
+
.map((item, index) => ({ item, index }))
|
|
396
|
+
.sort((a, b) => b.item.output.length - a.item.output.length || a.index - b.index)
|
|
397
|
+
|
|
398
|
+
for (const { item } of bySize) {
|
|
399
|
+
if (total <= opts.maxChars) break
|
|
400
|
+
const replacement = previewForMessageBudget(item, opts)
|
|
401
|
+
if (replacement.output.length >= item.output.length) continue
|
|
402
|
+
total -= item.output.length - replacement.output.length
|
|
403
|
+
replaced.set(item.toolCallId, replacement)
|
|
404
|
+
}
|
|
405
|
+
return replaced
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function previewForMessageBudget(
|
|
409
|
+
item: MessageBudgetItem,
|
|
410
|
+
opts: FitResultsToMessageBudgetOptions,
|
|
411
|
+
): MessageBudgetReplacement {
|
|
412
|
+
const spilledAt = SPILL_LINE.exec(item.output)?.[0]
|
|
413
|
+
if (spilledAt !== undefined) {
|
|
414
|
+
const output = [
|
|
415
|
+
safeHead(item.output, MESSAGE_BUDGET_PREVIEW_CHARS / 2),
|
|
416
|
+
'',
|
|
417
|
+
`[... omitted: this message's tool results together were over the ${opts.maxChars.toLocaleString()}-character budget ...]`,
|
|
418
|
+
spilledAt,
|
|
419
|
+
].join('\n')
|
|
420
|
+
return {
|
|
421
|
+
output,
|
|
422
|
+
originalLength: item.output.length,
|
|
423
|
+
spillPath: spilledAt.slice(SPILL_MARKER.length + 1),
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const budgeted = applyToolOutputBudget({
|
|
427
|
+
toolName: item.toolName,
|
|
428
|
+
// A distinct name: the spill file is keyed by it and opened exclusively.
|
|
429
|
+
toolUseId: `${item.toolCallId}:message-budget`,
|
|
430
|
+
output: item.output,
|
|
431
|
+
maxChars: MESSAGE_BUDGET_PREVIEW_CHARS,
|
|
432
|
+
spillDir: opts.spillDir,
|
|
433
|
+
...(opts.onError ? { onError: opts.onError } : {}),
|
|
434
|
+
})
|
|
435
|
+
return {
|
|
436
|
+
output: budgeted.output,
|
|
437
|
+
originalLength: item.output.length,
|
|
438
|
+
...(budgeted.spillPath ? { spillPath: budgeted.spillPath } : {}),
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -54,6 +54,7 @@ export interface ToolingBootstrapConfig {
|
|
|
54
54
|
maxToolCalls?: number
|
|
55
55
|
readToolCallBudgetRecords?: () => Promise<readonly SessionRecord[]>
|
|
56
56
|
maxToolOutputChars?: number
|
|
57
|
+
maxToolOutputCharsPerMessage?: number
|
|
57
58
|
/** See `QueryParams.toolResultGuardrails`. Absent installs the shipped default; a registry's own win. */
|
|
58
59
|
toolResultGuardrails?: readonly import('../../types/guardrail/index.js').ToolResultGuardrailSpec[]
|
|
59
60
|
retainedToolPreviewChars?: number
|
|
@@ -119,6 +120,11 @@ export class ToolingBootstrap {
|
|
|
119
120
|
...(config.maxToolOutputChars !== undefined
|
|
120
121
|
? { maxToolOutputChars: config.maxToolOutputChars }
|
|
121
122
|
: {}),
|
|
123
|
+
...(config.maxToolOutputCharsPerMessage !== undefined
|
|
124
|
+
? {
|
|
125
|
+
maxToolOutputCharsPerMessage: config.maxToolOutputCharsPerMessage,
|
|
126
|
+
}
|
|
127
|
+
: {}),
|
|
122
128
|
...(config.toolResultGuardrails !== undefined
|
|
123
129
|
? { toolResultGuardrails: config.toolResultGuardrails }
|
|
124
130
|
: {}),
|
package/src/session/paths.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { uuidv7 } from '../utils/uuidv7.js'
|
|
|
28
28
|
* <session-id>.jsonl the session log (source of truth)
|
|
29
29
|
* <session-id>/
|
|
30
30
|
* subagents/<child-id>.jsonl, <child-id>.meta.json, <child-id>/subagents/…
|
|
31
|
-
* tool-results/ checkpoints/ budgets/ tasks/ feedback/ goals/
|
|
31
|
+
* tool-results/ checkpoints/ budgets/ tasks/ plans/ feedback/ goals/
|
|
32
32
|
* file-history/ lease.json
|
|
33
33
|
* $TMPDIR/namzu-<user>/<slug>/<session-id>/scratchpad/
|
|
34
34
|
* ```
|
|
@@ -412,6 +412,16 @@ export class SessionPaths {
|
|
|
412
412
|
return join(this.sessionDir(locator), 'tasks')
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/** The session's plan directory; a host that has a plan mode keeps its one plan file here. */
|
|
416
|
+
plans(locator: SessionLocator): string {
|
|
417
|
+
return join(this.sessionDir(locator), 'plans')
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** The one file a session's plan mode may write: `<session-id>/plans/plan.md`. */
|
|
421
|
+
planFile(locator: SessionLocator): string {
|
|
422
|
+
return join(this.plans(locator), 'plan.md')
|
|
423
|
+
}
|
|
424
|
+
|
|
415
425
|
taskFile(locator: SessionLocator, taskId: TaskId): string {
|
|
416
426
|
return join(this.tasks(locator), `${checkId(taskId, 'task')}.json`)
|
|
417
427
|
}
|
package/src/store/memory/disk.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { InMemoryMemoryIndex, searchMemoryEntries, selectMemorySearchCandidates
|
|
|
23
23
|
import {
|
|
24
24
|
MemoryNameConflictError,
|
|
25
25
|
assertOptionalMemoryFields,
|
|
26
|
+
entryPath,
|
|
26
27
|
isMemoryName,
|
|
27
28
|
nameHolder,
|
|
28
29
|
withOptionalFields,
|
|
@@ -228,7 +229,7 @@ export class DiskMemoryStore implements ConditionalMemoryStore {
|
|
|
228
229
|
|
|
229
230
|
private contentPath(location: StorageLocation, id: MemoryId): string {
|
|
230
231
|
assertStorageMemoryId(id, (reason) => invalidContent(id, reason, { field: 'id' }))
|
|
231
|
-
const path = resolve(location.contentDir,
|
|
232
|
+
const path = resolve(entryPath(location.contentDir, id, '.json'))
|
|
232
233
|
if (dirname(path) !== location.contentDir) {
|
|
233
234
|
invalidContent(id, 'resolved path escapes the content directory', { field: 'id' })
|
|
234
235
|
}
|