@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
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
MemoryContentRejectedError,
|
|
39
39
|
MemoryNameConflictError,
|
|
40
40
|
assertOptionalMemoryFields,
|
|
41
|
+
entryPath,
|
|
41
42
|
isMemoryName,
|
|
42
43
|
nameHolder,
|
|
43
44
|
slugifyMemoryName,
|
|
@@ -327,7 +328,7 @@ export class MarkdownMemoryStore implements ConditionalMemoryStore {
|
|
|
327
328
|
|
|
328
329
|
private memoryPath(dir: string, name: string): string {
|
|
329
330
|
if (!isMemoryName(name)) invalidFile(join(dir, `${name}.md`), 'name is not a memory name')
|
|
330
|
-
const path = resolve(dir,
|
|
331
|
+
const path = resolve(entryPath(dir, name, '.md'))
|
|
331
332
|
if (dirname(path) !== dir) invalidFile(path, 'resolved path escapes the memory directory')
|
|
332
333
|
return path
|
|
333
334
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sep } from 'node:path'
|
|
1
2
|
import { NamzuError } from '../../types/errors/index.js'
|
|
2
3
|
import type { MemoryId } from '../../types/ids/index.js'
|
|
3
4
|
import type {
|
|
@@ -193,3 +194,15 @@ export function withOptionalFields(
|
|
|
193
194
|
...(type !== undefined ? { type } : {}),
|
|
194
195
|
}
|
|
195
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The path of one entry file: `dir`, a separator, `stem`, `extension`, before
|
|
200
|
+
* the caller resolves it. Written as plain concatenation on purpose. A static
|
|
201
|
+
* file tracer (the kind standalone-output builds use) evaluates
|
|
202
|
+
* `path.resolve(dir, `${stem}.ext`)` with an unknown `dir` as a wildcard and
|
|
203
|
+
* lists every `.ext` file under the repository as a runtime asset; it does not
|
|
204
|
+
* evaluate string concatenation, so this form leaves it nothing to expand.
|
|
205
|
+
*/
|
|
206
|
+
export function entryPath(dir: string, stem: string, extension: string): string {
|
|
207
|
+
return dir + sep + stem + extension
|
|
208
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
+
import { isReadOnlyCommandLine } from '../../authorization/readonly-commands.js'
|
|
2
3
|
import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
3
4
|
import { DANGEROUS_PATTERNS } from '../../constants/tools/index.js'
|
|
4
5
|
import { killTree } from '../../process/kill-tree.js'
|
|
@@ -51,12 +52,47 @@ const DEFAULT_BASH_MAX_BUFFER_BYTES = readPositiveIntEnv(
|
|
|
51
52
|
*
|
|
52
53
|
* So the tool declares a ceiling and the executor is given a deadline above
|
|
53
54
|
* it (see `timeoutMs` on the definition), which makes this the only clock
|
|
54
|
-
* that can fire in practice. A request past the ceiling is
|
|
55
|
-
*
|
|
56
|
-
*
|
|
55
|
+
* that can fire in practice. A request past the ceiling is CLAMPED to it, as
|
|
56
|
+
* the reference terminal does: the command still runs, and a timeout reports
|
|
57
|
+
* the deadline that was actually applied, so the number the model sees on a
|
|
58
|
+
* timeout is the one that fired.
|
|
57
59
|
*/
|
|
58
60
|
const MAX_BASH_TIMEOUT_MS = readPositiveIntEnv('NAMZU_BASH_MAX_TIMEOUT_MS', 10 * 60 * 1000)
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Background work switched off for this process: a command that outruns its
|
|
64
|
+
* timeout is killed, as it was before it could be moved to the background, and
|
|
65
|
+
* `run_in_background` is refused. Read at each call so a host can flip it.
|
|
66
|
+
*/
|
|
67
|
+
export const DISABLE_BACKGROUND_TASKS_ENV = 'NAMZU_DISABLE_BACKGROUND_TASKS'
|
|
68
|
+
|
|
69
|
+
function backgroundTasksDisabled(): boolean {
|
|
70
|
+
const value = process.env[DISABLE_BACKGROUND_TASKS_ENV]?.trim().toLowerCase()
|
|
71
|
+
return value !== undefined && value !== '' && value !== '0' && value !== 'false'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A command that is only waiting (`sleep 30`, also as the head of a chain)
|
|
76
|
+
* has nothing to keep running when its time is up, so it is still stopped.
|
|
77
|
+
*/
|
|
78
|
+
function startsWithSleep(command: string): boolean {
|
|
79
|
+
const head = command
|
|
80
|
+
.split(/&&|\|\||;|\||\n/)[0]
|
|
81
|
+
?.trim()
|
|
82
|
+
.split(/\s+/)[0]
|
|
83
|
+
return head === 'sleep'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What the model is told when a foreground command outran its timeout and was
|
|
88
|
+
* moved to the background instead of being stopped. Exported so the test
|
|
89
|
+
* states the same words.
|
|
90
|
+
*/
|
|
91
|
+
export function movedToBackgroundMessage(jobId: string, timeoutMs: number): string {
|
|
92
|
+
const seconds = Math.max(1, Math.round(timeoutMs / 1000))
|
|
93
|
+
return `Command did not complete within its ${seconds}s timeout and was moved to the background as job ${jobId}; it was not stopped. You will be told when it ends. Await it with wait_for_job: {"id":"${jobId}"}, read its output with job action "read" (id "${jobId}"), or stop it with job action "kill". Its output so far is above.`
|
|
94
|
+
}
|
|
95
|
+
|
|
60
96
|
const inputSchema = z.object({
|
|
61
97
|
command: z
|
|
62
98
|
.string()
|
|
@@ -73,10 +109,14 @@ const inputSchema = z.object({
|
|
|
73
109
|
timeout: z
|
|
74
110
|
.preprocess(
|
|
75
111
|
(v) => (typeof v === 'string' ? Number(v) : v),
|
|
76
|
-
z
|
|
112
|
+
z
|
|
113
|
+
.number()
|
|
114
|
+
.positive()
|
|
115
|
+
.default(DEFAULT_BASH_TIMEOUT_MS)
|
|
116
|
+
.transform((ms) => Math.min(ms, MAX_BASH_TIMEOUT_MS)),
|
|
77
117
|
)
|
|
78
118
|
.describe(
|
|
79
|
-
`Command timeout in milliseconds. Default: ${DEFAULT_BASH_TIMEOUT_MS}, maximum: ${MAX_BASH_TIMEOUT_MS}. For work that legitimately runs longer than the maximum, set run_in_background and await it with \`wait_for_job\`, rather than holding the turn open.`,
|
|
119
|
+
`Command timeout in milliseconds. Default: ${DEFAULT_BASH_TIMEOUT_MS}, maximum: ${MAX_BASH_TIMEOUT_MS}. For work that legitimately runs longer than the maximum, set run_in_background and await it with \`wait_for_job\`, rather than holding the turn open. A foreground command still running when the timeout elapses is moved to a background job (and reported as such) instead of being stopped, where the host has background jobs.`,
|
|
80
120
|
),
|
|
81
121
|
run_in_background: z
|
|
82
122
|
.boolean()
|
|
@@ -94,6 +134,21 @@ const inputSchema = z.object({
|
|
|
94
134
|
|
|
95
135
|
type BashInput = z.infer<typeof inputSchema>
|
|
96
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Whether this exact call only reads. A background job, or a command that
|
|
139
|
+
* leaves the sandbox, is never read-only whatever its line says.
|
|
140
|
+
*/
|
|
141
|
+
function bashInputIsReadOnly(input: BashInput | undefined): boolean {
|
|
142
|
+
// Asked with no input to describe the tool in general (a roster, an export).
|
|
143
|
+
if (typeof input?.command !== 'string') return false
|
|
144
|
+
if (input.run_in_background === true || input.dangerously_disable_sandbox === true) return false
|
|
145
|
+
try {
|
|
146
|
+
return isReadOnlyCommandLine(input.command, hostCommandShell().dialect)
|
|
147
|
+
} catch {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
97
152
|
const MAX_SHELL_PROGRESS_CHARS = 160
|
|
98
153
|
|
|
99
154
|
/** Keep a bounded tail even when a process never writes a newline. */
|
|
@@ -147,7 +202,10 @@ function shellProgress(report?: (message: string) => void) {
|
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
/** A progress callback for {@link execHostShell}: one line, per stream, coalesced. */
|
|
150
|
-
export type ExecHostShellProgress = (report: {
|
|
205
|
+
export type ExecHostShellProgress = (report: {
|
|
206
|
+
stream: 'stdout' | 'stderr'
|
|
207
|
+
data: string
|
|
208
|
+
}) => void
|
|
151
209
|
|
|
152
210
|
/**
|
|
153
211
|
* Keep the shell's process group until inherited pipes close. Node's exec
|
|
@@ -170,8 +228,22 @@ export function execHostShell(
|
|
|
170
228
|
shell?: CommandShell
|
|
171
229
|
signal?: AbortSignal
|
|
172
230
|
onOutput?: ExecHostShellProgress
|
|
231
|
+
/**
|
|
232
|
+
* Asked once, at the deadline, instead of killing the command. Given the
|
|
233
|
+
* LIVE child and what it has written so far, it may take the process over
|
|
234
|
+
* and return a job id; the command then keeps running, this call settles
|
|
235
|
+
* with `backgroundedJobId` set, and nothing here touches the process
|
|
236
|
+
* again (not on a later Stop, not on its exit). Returning `undefined`, or
|
|
237
|
+
* throwing, leaves the deadline to kill the command as it always did.
|
|
238
|
+
*/
|
|
239
|
+
onTimeout?: (running: {
|
|
240
|
+
child: ReturnType<typeof spawnHostShell>
|
|
241
|
+
initialOutput: readonly { stream: 'stdout' | 'stderr'; text: string }[]
|
|
242
|
+
}) => string | undefined
|
|
173
243
|
},
|
|
174
244
|
): Promise<{
|
|
245
|
+
/** Set when `onTimeout` took the process over; the command was not stopped. */
|
|
246
|
+
backgroundedJobId?: string
|
|
175
247
|
stdout: string
|
|
176
248
|
stderr: string
|
|
177
249
|
/** Bytes captured on each stream, before decoding; absent from a runner that does not count them. */
|
|
@@ -249,19 +321,31 @@ export function execHostShell(
|
|
|
249
321
|
child.stdout?.on('data', onStdout)
|
|
250
322
|
child.stderr?.on('data', onStderr)
|
|
251
323
|
let spawned = false
|
|
252
|
-
|
|
324
|
+
const onSpawn = () => {
|
|
253
325
|
spawned = true
|
|
254
|
-
}
|
|
255
|
-
child.once('
|
|
326
|
+
}
|
|
327
|
+
child.once('spawn', onSpawn)
|
|
328
|
+
const onError = (error: NodeJS.ErrnoException) => {
|
|
256
329
|
// Same words as a background job that cannot start.
|
|
257
330
|
failure ??= spawned
|
|
258
331
|
? error
|
|
259
332
|
: Object.assign(
|
|
260
|
-
new Error(describeSpawnFailure(error, options.cwd), {
|
|
333
|
+
new Error(describeSpawnFailure(error, options.cwd), {
|
|
334
|
+
cause: error,
|
|
335
|
+
}),
|
|
261
336
|
error.code === undefined ? {} : { code: error.code },
|
|
262
337
|
)
|
|
263
|
-
}
|
|
264
|
-
child.once('
|
|
338
|
+
}
|
|
339
|
+
child.once('error', onError)
|
|
340
|
+
const decodeCapture = (stream: 'stdout' | 'stderr') => {
|
|
341
|
+
const state = captures[stream]
|
|
342
|
+
const bytes = Buffer.concat(state.chunks, state.bytes)
|
|
343
|
+
// Do not flush an incomplete character cut by our byte cap.
|
|
344
|
+
const decoder = createHostOutputDecoder()
|
|
345
|
+
const text = decoder.write(bytes)
|
|
346
|
+
return state.truncated ? text : text + decoder.end()
|
|
347
|
+
}
|
|
348
|
+
const onClose = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
265
349
|
closed = true
|
|
266
350
|
disposeAbort?.()
|
|
267
351
|
clearTimeout(deadline)
|
|
@@ -277,14 +361,6 @@ export function execHostShell(
|
|
|
277
361
|
const data = captures[stream].decoder.end()
|
|
278
362
|
if (data) options.onOutput?.({ stream, data })
|
|
279
363
|
}
|
|
280
|
-
const decodeCapture = (stream: 'stdout' | 'stderr') => {
|
|
281
|
-
const state = captures[stream]
|
|
282
|
-
const bytes = Buffer.concat(state.chunks, state.bytes)
|
|
283
|
-
// Do not flush an incomplete character cut by our byte cap.
|
|
284
|
-
const decoder = createHostOutputDecoder()
|
|
285
|
-
const text = decoder.write(bytes)
|
|
286
|
-
return state.truncated ? text : text + decoder.end()
|
|
287
|
-
}
|
|
288
364
|
const stdout = decodeCapture('stdout')
|
|
289
365
|
const stderr = decodeCapture('stderr')
|
|
290
366
|
if (!failure && cause === undefined && code === 0 && signal === null) {
|
|
@@ -300,7 +376,9 @@ export function execHostShell(
|
|
|
300
376
|
failure ??
|
|
301
377
|
(cause === 'caller'
|
|
302
378
|
? Object.assign(
|
|
303
|
-
new Error('The operation was aborted', {
|
|
379
|
+
new Error('The operation was aborted', {
|
|
380
|
+
cause: options.signal?.reason,
|
|
381
|
+
}),
|
|
304
382
|
{
|
|
305
383
|
name: 'AbortError',
|
|
306
384
|
code: 'ABORT_ERR',
|
|
@@ -321,8 +399,41 @@ export function execHostShell(
|
|
|
321
399
|
timedOut: cause === 'timeout',
|
|
322
400
|
}),
|
|
323
401
|
)
|
|
324
|
-
}
|
|
325
|
-
|
|
402
|
+
}
|
|
403
|
+
child.once('close', onClose)
|
|
404
|
+
const deadline = setTimeout(() => {
|
|
405
|
+
if (options.onTimeout && !closed && cause === undefined) {
|
|
406
|
+
let jobId: string | undefined
|
|
407
|
+
try {
|
|
408
|
+
jobId = options.onTimeout({
|
|
409
|
+
child,
|
|
410
|
+
initialOutput: (['stdout', 'stderr'] as const)
|
|
411
|
+
.map((stream) => ({ stream, text: decodeCapture(stream) }))
|
|
412
|
+
.filter((part) => part.text !== ''),
|
|
413
|
+
})
|
|
414
|
+
} catch {
|
|
415
|
+
// A host that cannot take the process over leaves the kill in place.
|
|
416
|
+
}
|
|
417
|
+
if (jobId !== undefined) {
|
|
418
|
+
// The registry owns the process from here: stop listening to it,
|
|
419
|
+
// so a later Stop or its exit is not this call's business.
|
|
420
|
+
closed = true
|
|
421
|
+
disposeAbort?.()
|
|
422
|
+
child.stdout?.off('data', onStdout)
|
|
423
|
+
child.stderr?.off('data', onStderr)
|
|
424
|
+
child.off('close', onClose)
|
|
425
|
+
child.off('error', onError)
|
|
426
|
+
child.off('spawn', onSpawn)
|
|
427
|
+
resolve({
|
|
428
|
+
backgroundedJobId: jobId,
|
|
429
|
+
stdout: decodeCapture('stdout'),
|
|
430
|
+
stderr: decodeCapture('stderr'),
|
|
431
|
+
})
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
cancel('timeout')
|
|
436
|
+
}, options.timeout)
|
|
326
437
|
deadline.unref?.()
|
|
327
438
|
if (options.signal) {
|
|
328
439
|
disposeAbort = subscribeToAbort(options.signal, () => cancel('caller'))
|
|
@@ -372,9 +483,12 @@ const BashToolBase = defineTool({
|
|
|
372
483
|
// confirms it only by id; see `ToolDefinition.sandboxEscapeArgument`.
|
|
373
484
|
sandboxEscapeArgument: 'dangerously_disable_sandbox',
|
|
374
485
|
permissions: ['shell_execute'],
|
|
375
|
-
|
|
486
|
+
// A line made only of vetted read-only commands (`ls`, `cat`, `git status`,
|
|
487
|
+
// `git diff`, ...) needs no prompt and may run beside other reads; see
|
|
488
|
+
// `authorization/readonly-commands.ts`. Anything else is not read-only.
|
|
489
|
+
readOnly: (input: BashInput) => bashInputIsReadOnly(input),
|
|
376
490
|
destructive: (input: BashInput) => isDangerousCommand(input.command),
|
|
377
|
-
concurrencySafe:
|
|
491
|
+
concurrencySafe: (input: BashInput) => bashInputIsReadOnly(input),
|
|
378
492
|
// Above the ceiling the input schema accepts, so the executor's deadline
|
|
379
493
|
// is a backstop rather than a second clock racing this tool's own. It used
|
|
380
494
|
// to be undefined, which meant the executor's generic default applied —
|
|
@@ -424,6 +538,14 @@ const BashToolBase = defineTool({
|
|
|
424
538
|
}
|
|
425
539
|
}
|
|
426
540
|
|
|
541
|
+
if (input.run_in_background && backgroundTasksDisabled()) {
|
|
542
|
+
return {
|
|
543
|
+
success: false,
|
|
544
|
+
output: '',
|
|
545
|
+
error: `Background jobs are switched off here (${DISABLE_BACKGROUND_TASKS_ENV}). Run the command in the foreground, or raise \`timeout\` up to the tool maximum.`,
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
427
549
|
if (input.run_in_background) {
|
|
428
550
|
// A sandboxed turn gets a registry only when its sandbox can start a
|
|
429
551
|
// detached process inside the boundary (`Sandbox.spawnDetached`);
|
|
@@ -578,19 +700,66 @@ const BashToolBase = defineTool({
|
|
|
578
700
|
// the sandbox, and for what it therefore does not catch.
|
|
579
701
|
const inherited = scrubInheritedEnv()
|
|
580
702
|
|
|
703
|
+
// Where a job registry can take the process over, running out of time
|
|
704
|
+
// moves the command to the background instead of killing it: a build
|
|
705
|
+
// stopped at two minutes loses everything it did. Nowhere else (no
|
|
706
|
+
// registry, a sandboxed turn, a one-off sandbox escape, a plain wait)
|
|
707
|
+
// is there anything to hand the process to, so those keep the kill.
|
|
708
|
+
const adopt = context.backgroundJobs?.adopt?.bind(context.backgroundJobs)
|
|
709
|
+
const canMoveToBackground =
|
|
710
|
+
adopt !== undefined &&
|
|
711
|
+
context.sandbox === undefined &&
|
|
712
|
+
!backgroundTasksDisabled() &&
|
|
713
|
+
!startsWithSleep(input.command)
|
|
714
|
+
|
|
581
715
|
try {
|
|
582
|
-
const { stdout, stderr, stdoutBytes, stderrBytes } = await execHostShell(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
716
|
+
const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId } = await execHostShell(
|
|
717
|
+
input.command,
|
|
718
|
+
{
|
|
719
|
+
cwd: context.workingDirectory,
|
|
720
|
+
timeout: input.timeout,
|
|
721
|
+
env: { ...inherited.env, ...context.env },
|
|
722
|
+
maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
|
|
723
|
+
signal: context.abortSignal,
|
|
724
|
+
onOutput,
|
|
725
|
+
...(canMoveToBackground
|
|
726
|
+
? {
|
|
727
|
+
onTimeout: ({ child, initialOutput }) =>
|
|
728
|
+
adopt({
|
|
729
|
+
command: input.command,
|
|
730
|
+
child,
|
|
731
|
+
workingDirectory: context.workingDirectory,
|
|
732
|
+
initialOutput,
|
|
733
|
+
}).id,
|
|
734
|
+
}
|
|
735
|
+
: {}),
|
|
736
|
+
},
|
|
737
|
+
)
|
|
738
|
+
if (backgroundedJobId !== undefined) {
|
|
739
|
+
return {
|
|
740
|
+
success: true,
|
|
741
|
+
output: [
|
|
742
|
+
formatShellOutput(stdout, stderr),
|
|
743
|
+
movedToBackgroundMessage(backgroundedJobId, input.timeout),
|
|
744
|
+
]
|
|
745
|
+
.filter(Boolean)
|
|
746
|
+
.join('\n\n'),
|
|
747
|
+
data: {
|
|
748
|
+
jobId: backgroundedJobId,
|
|
749
|
+
background: true,
|
|
750
|
+
timedOut: true,
|
|
751
|
+
movedToBackground: true,
|
|
752
|
+
},
|
|
753
|
+
facts: { kind: 'command', command: input.command, background: true },
|
|
754
|
+
}
|
|
755
|
+
}
|
|
590
756
|
return {
|
|
591
757
|
success: true,
|
|
592
758
|
output: formatShellOutput(stdout, stderr) || '(no output)',
|
|
593
|
-
data: {
|
|
759
|
+
data: {
|
|
760
|
+
exitCode: 0,
|
|
761
|
+
...(leaveSandbox ? { sandboxed: false, sandboxEscape: true } : {}),
|
|
762
|
+
},
|
|
594
763
|
facts: commandFacts({
|
|
595
764
|
command: input.command,
|
|
596
765
|
exitCode: 0,
|
|
@@ -28,3 +28,8 @@ export function fingerprintContent(content: string): string {
|
|
|
28
28
|
export function staleFileError(path: string, operation: 'edit' | 'write' = 'edit'): string {
|
|
29
29
|
return `${path} changed on disk after you read it, so this ${operation} was based on a stale copy and was not applied. Read the file again and redo the ${operation} against its current contents. Nothing was written.`
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/** What a mutation tool tells the agent when the session never read the file. */
|
|
33
|
+
export function unreadFileError(path: string, operation: 'edit' | 'write' = 'edit'): string {
|
|
34
|
+
return `${path} has not been read yet. Read it first, then redo the ${operation}. Nothing was written.`
|
|
35
|
+
}
|
|
@@ -5,8 +5,8 @@ import type { ToolContext, ToolResult } from '../../types/tool/index.js'
|
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
6
|
import { resolveWithinAnyReal, toolRoots } from '../paths.js'
|
|
7
7
|
import { atomicWriteFile } from './atomic-write-file.js'
|
|
8
|
-
import { fingerprintContent, staleFileError } from './content-fingerprint.js'
|
|
9
|
-
import { applyEdit, normalizeEditInput } from './edit-apply.js'
|
|
8
|
+
import { fingerprintContent, staleFileError, unreadFileError } from './content-fingerprint.js'
|
|
9
|
+
import { type NormalizedEditInput, applyEdit, normalizeEditInput } from './edit-apply.js'
|
|
10
10
|
import { withFileMutationLock } from './file-mutation-lock.js'
|
|
11
11
|
import { jsonStringEscapes } from './json-string-hint.js'
|
|
12
12
|
|
|
@@ -151,7 +151,10 @@ export function dryRunEdit(
|
|
|
151
151
|
): { success: true; content: string } | { success: false; error: string } {
|
|
152
152
|
const parsed = inputSchema.safeParse(input)
|
|
153
153
|
if (!parsed.success) {
|
|
154
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
success: false,
|
|
156
|
+
error: parsed.error.issues.map((issue) => issue.message).join('; '),
|
|
157
|
+
}
|
|
155
158
|
}
|
|
156
159
|
const normalized = normalizeEditInput(parsed.data)
|
|
157
160
|
if (!normalized.success) return normalized
|
|
@@ -161,7 +164,10 @@ export function dryRunEdit(
|
|
|
161
164
|
operation.operation === 'replace' && operation.oldString === operation.newString,
|
|
162
165
|
)
|
|
163
166
|
) {
|
|
164
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: 'old_string and new_string are identical — no change needed',
|
|
170
|
+
}
|
|
165
171
|
}
|
|
166
172
|
const result = applyEdit(content, normalized.operations)
|
|
167
173
|
return result.success ? { success: true, content: result.content } : result
|
|
@@ -249,7 +255,10 @@ const modelInputSchema: Record<string, unknown> = {
|
|
|
249
255
|
type: 'string',
|
|
250
256
|
description: 'Exact text from the file at this point in the sequence.',
|
|
251
257
|
},
|
|
252
|
-
new_string: {
|
|
258
|
+
new_string: {
|
|
259
|
+
type: 'string',
|
|
260
|
+
description: 'Exact replacement text.',
|
|
261
|
+
},
|
|
253
262
|
replace_all: {
|
|
254
263
|
type: 'boolean',
|
|
255
264
|
description: 'Replace every occurrence of this old_string.',
|
|
@@ -322,6 +331,46 @@ function recordDrift(context: ToolContext, key: string): void {
|
|
|
322
331
|
context.fileReadTracker?.recordDriftObserved?.(key)
|
|
323
332
|
}
|
|
324
333
|
|
|
334
|
+
type EditAdmission =
|
|
335
|
+
| { success: false; output: ''; error: string }
|
|
336
|
+
| { success: true; result: ReturnType<typeof applyEdit> }
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Decide whether this edit may run against the body now on disk, and apply it.
|
|
340
|
+
*
|
|
341
|
+
* Two rules, in this order:
|
|
342
|
+
*
|
|
343
|
+
* 1. A file the session never read is refused. The model is about to rewrite
|
|
344
|
+
* text it has not seen, which is the commonest source of a wrong patch. A
|
|
345
|
+
* host that supplies no tracker has no ledger to ask and is not refused.
|
|
346
|
+
* 2. A file that moved since the read is refused UNLESS every operation is a
|
|
347
|
+
* text replacement that still applies to the current body — uniquely,
|
|
348
|
+
* unless `replace_all` was asked for. A formatter run between the read and
|
|
349
|
+
* the edit then costs nothing. An insertion by line number is never
|
|
350
|
+
* recovered: the line it named may now be a different line.
|
|
351
|
+
*/
|
|
352
|
+
function admitAgainstRead(
|
|
353
|
+
context: ToolContext,
|
|
354
|
+
key: string,
|
|
355
|
+
content: string,
|
|
356
|
+
operations: readonly NormalizedEditInput[],
|
|
357
|
+
): EditAdmission {
|
|
358
|
+
const tracker = context.fileReadTracker
|
|
359
|
+
if (tracker && !tracker.hasRead(key)) {
|
|
360
|
+
return { success: false, output: '', error: unreadFileError(key, 'edit') }
|
|
361
|
+
}
|
|
362
|
+
const seen = tracker?.fingerprint?.(key)
|
|
363
|
+
const applied = applyEdit(content, operations)
|
|
364
|
+
if (seen !== undefined && seen !== fingerprintContent(content)) {
|
|
365
|
+
const recoverable = applied.success && operations.every((o) => o.operation === 'replace')
|
|
366
|
+
if (!recoverable) {
|
|
367
|
+
recordDrift(context, key)
|
|
368
|
+
return { success: false, output: '', error: staleFileError(key) }
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { success: true, result: applied }
|
|
372
|
+
}
|
|
373
|
+
|
|
325
374
|
const EDIT_SHAPES =
|
|
326
375
|
'Three shapes. Replace: {"path":"file.md","old_string":"exact unique text","new_string":"replacement text"} (optional "replace_all": true). Insert: {"path":"file.md","insertLine":"end","new_string":"text to add"} where insertLine is a non-negative line number or "end". Batch: {"path":"file.md","edits":[{"old_string":"a","new_string":"b"},{"old_string":"c","new_string":"d"}]} applied in order, all or nothing. Exactly one of old_string, insertLine or edits — a call carrying more than one of them is refused rather than resolved.'
|
|
327
376
|
|
|
@@ -381,7 +430,10 @@ export const EditTool = defineTool({
|
|
|
381
430
|
* decide this by matching two names.
|
|
382
431
|
*/
|
|
383
432
|
presentResult(_input: EditInput, result: ToolResult) {
|
|
384
|
-
return {
|
|
433
|
+
return {
|
|
434
|
+
kind: 'generic' as const,
|
|
435
|
+
label: result.output?.split('\n')[0] ?? '',
|
|
436
|
+
}
|
|
385
437
|
},
|
|
386
438
|
|
|
387
439
|
async execute(input: EditInput, context) {
|
|
@@ -439,12 +491,9 @@ export const EditTool = defineTool({
|
|
|
439
491
|
if (context.sandbox) {
|
|
440
492
|
const buffer = await context.sandbox.readFile(parsed.data.path)
|
|
441
493
|
const content = buffer.toString('utf-8')
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
return { success: false as const, output: '', error: staleFileError(parsed.data.path) }
|
|
446
|
-
}
|
|
447
|
-
const result = applyEdit(content, normalized.operations)
|
|
494
|
+
const admitted = admitAgainstRead(context, parsed.data.path, content, normalized.operations)
|
|
495
|
+
if (!admitted.success) return admitted
|
|
496
|
+
const result = admitted.result
|
|
448
497
|
if (!result.success) {
|
|
449
498
|
return { success: false as const, output: '', error: result.error }
|
|
450
499
|
}
|
|
@@ -454,7 +503,11 @@ export const EditTool = defineTool({
|
|
|
454
503
|
return {
|
|
455
504
|
success: true as const,
|
|
456
505
|
output: `Edited ${parsed.data.path}: ${result.replacements} replacement(s) [sandboxed]`,
|
|
457
|
-
data: {
|
|
506
|
+
data: {
|
|
507
|
+
path: parsed.data.path,
|
|
508
|
+
replacements: result.replacements,
|
|
509
|
+
sandboxed: true,
|
|
510
|
+
},
|
|
458
511
|
facts: {
|
|
459
512
|
kind: 'edit' as const,
|
|
460
513
|
path: parsed.data.path,
|
|
@@ -466,12 +519,9 @@ export const EditTool = defineTool({
|
|
|
466
519
|
const hostPath = filePath as string
|
|
467
520
|
const content = await readFile(hostPath, 'utf-8')
|
|
468
521
|
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
return { success: false as const, output: '', error: staleFileError(hostPath) }
|
|
473
|
-
}
|
|
474
|
-
const result = applyEdit(content, normalized.operations)
|
|
522
|
+
const admitted = admitAgainstRead(context, hostPath, content, normalized.operations)
|
|
523
|
+
if (!admitted.success) return admitted
|
|
524
|
+
const result = admitted.result
|
|
475
525
|
if (!result.success) return { success: false as const, output: '', error: result.error }
|
|
476
526
|
|
|
477
527
|
// Temp file, fsync, rename — a reader sees the old body or the new
|