@namzu/sdk 3.0.0 → 3.1.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/public-runtime.d.ts +1 -1
  3. package/dist/public-runtime.d.ts.map +1 -1
  4. package/dist/public-runtime.js +6 -1
  5. package/dist/public-runtime.js.map +1 -1
  6. package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
  7. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  8. package/dist/runtime/query/executor.d.ts +1 -0
  9. package/dist/runtime/query/executor.d.ts.map +1 -1
  10. package/dist/runtime/query/executor.js +10 -1
  11. package/dist/runtime/query/executor.js.map +1 -1
  12. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  13. package/dist/runtime/query/iteration/index.js +22 -0
  14. package/dist/runtime/query/iteration/index.js.map +1 -1
  15. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
  16. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
  17. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
  18. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
  19. package/dist/tools/builtins/__tests__/edit.test.js +242 -1
  20. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  21. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
  22. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
  23. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
  24. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
  25. package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
  26. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
  27. package/dist/tools/builtins/atomic-write-file.js +31 -2
  28. package/dist/tools/builtins/atomic-write-file.js.map +1 -1
  29. package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
  30. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
  31. package/dist/tools/builtins/content-fingerprint.js +30 -0
  32. package/dist/tools/builtins/content-fingerprint.js.map +1 -0
  33. package/dist/tools/builtins/edit.d.ts.map +1 -1
  34. package/dist/tools/builtins/edit.js +172 -31
  35. package/dist/tools/builtins/edit.js.map +1 -1
  36. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  37. package/dist/tools/builtins/read-file.js +5 -2
  38. package/dist/tools/builtins/read-file.js.map +1 -1
  39. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  40. package/dist/tools/builtins/write-file.js +75 -27
  41. package/dist/tools/builtins/write-file.js.map +1 -1
  42. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
  43. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
  44. package/dist/tools/coordinator/index.d.ts.map +1 -1
  45. package/dist/tools/coordinator/index.js +62 -4
  46. package/dist/tools/coordinator/index.js.map +1 -1
  47. package/dist/types/tool/index.d.ts +17 -1
  48. package/dist/types/tool/index.d.ts.map +1 -1
  49. package/dist/types/tool/index.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/public-runtime.ts +8 -1
  52. package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
  53. package/src/runtime/query/executor.ts +9 -1
  54. package/src/runtime/query/iteration/index.ts +27 -0
  55. package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
  56. package/src/tools/builtins/__tests__/edit.test.ts +324 -1
  57. package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
  58. package/src/tools/builtins/atomic-write-file.ts +30 -2
  59. package/src/tools/builtins/content-fingerprint.ts +33 -0
  60. package/src/tools/builtins/edit.ts +188 -36
  61. package/src/tools/builtins/read-file.ts +5 -2
  62. package/src/tools/builtins/write-file.ts +79 -28
  63. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
  64. package/src/tools/coordinator/index.ts +102 -41
  65. package/src/types/tool/index.ts +17 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"AAyXA,cAAc,aAAa,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/tool/index.ts"],"names":[],"mappings":"AAyYA,cAAc,aAAa,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -215,7 +215,14 @@ export type { LocalSandboxProviderOptions } from './sandbox/index.js'
215
215
  // The classified provider-failure surface: a driver states what went wrong
216
216
  // first-hand, and the run boundary reads it to choose between a pause and a
217
217
  // failure.
218
- export {
218
+ // `classifyProviderHttpStatus` and `bodySaysContextOverflow` are here
219
+ // because a driver outside this repo needs the same classification the
220
+ // first-party ones use — a status code alone does not separate a context
221
+ // overflow from an ordinary bad request, and re-deriving that per driver
222
+ // is how the classifications drift apart.
223
+ export {
224
+ bodySaysContextOverflow,
225
+ classifyProviderHttpStatus,
219
226
  isCallerAbortError,
220
227
  isProviderRequestError,
221
228
  providerHttpError,
@@ -44,6 +44,26 @@ const NO_VISION_CAPABILITIES: ProviderCapabilities = {
44
44
  supportsVision: false,
45
45
  }
46
46
 
47
+ /**
48
+ * A tool that opts in to constrained generation, which is the only thing
49
+ * that makes `enforceToolInputSchema` non-empty on the wire.
50
+ */
51
+ function registerEnforcedTool(tools: ToolRegistry, name: string): void {
52
+ tools.register({
53
+ name,
54
+ description: `${name} tool`,
55
+ inputSchema: z.object({ new_string: z.string().optional(), newStr: z.string().optional() }),
56
+ modelInputSchema: {
57
+ type: 'object',
58
+ properties: { new_string: { type: 'string' } },
59
+ required: ['new_string'],
60
+ additionalProperties: false,
61
+ },
62
+ enforceModelInput: true,
63
+ execute: async () => ({ success: true, output: 'ok' }),
64
+ })
65
+ }
66
+
47
67
  function registerEchoTool(tools: ToolRegistry): void {
48
68
  tools.register({
49
69
  name: 'echo',
@@ -229,4 +249,53 @@ describe('query() capability negotiation', () => {
229
249
  }),
230
250
  ).rejects.toThrow(/supportsVision: false/)
231
251
  })
252
+
253
+ it('names the enforced tools on the request, so a driver can constrain them', async () => {
254
+ const provider = capturingProvider()
255
+ const tools = new ToolRegistry()
256
+ registerEnforcedTool(tools, 'edit')
257
+ registerEnforcedTool(tools, 'write')
258
+ registerEchoTool(tools)
259
+
260
+ const run = await drainQuery({
261
+ ...baseParams(provider, tools, await mkWorkdir()),
262
+ messages: [createUserMessage('hello')],
263
+ })
264
+
265
+ expect(run.status).toBe('completed')
266
+ // The producer was deleted, so this was undefined on every request and
267
+ // the three drivers that read it were reading a field nothing ever
268
+ // set — `enforceModelInput` on a tool meant nothing end to end.
269
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit', 'write'])
270
+ })
271
+
272
+ it('omits the field entirely when no tool opts in', async () => {
273
+ const provider = capturingProvider()
274
+ const tools = new ToolRegistry()
275
+ registerEchoTool(tools)
276
+
277
+ await drainQuery({
278
+ ...baseParams(provider, tools, await mkWorkdir()),
279
+ messages: [createUserMessage('hello')],
280
+ })
281
+
282
+ // An empty array would read as "enforce nothing" rather than "nothing
283
+ // asked", and a driver cannot tell those two apart.
284
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toBeUndefined()
285
+ })
286
+
287
+ it('follows the allowed set rather than everything registered', async () => {
288
+ const provider = capturingProvider()
289
+ const tools = new ToolRegistry()
290
+ registerEnforcedTool(tools, 'edit')
291
+ registerEnforcedTool(tools, 'excluded')
292
+
293
+ await drainQuery({
294
+ ...baseParams(provider, tools, await mkWorkdir()),
295
+ allowedTools: ['edit'],
296
+ messages: [createUserMessage('hello')],
297
+ })
298
+
299
+ expect(provider.requests.at(-1)?.enforceToolInputSchema).toEqual(['edit'])
300
+ })
232
301
  })
@@ -7,6 +7,7 @@ import { ProbeVetoError } from '../../probe/errors.js'
7
7
  import { type ProbeRegistry, probe as defaultProbeRegistry } from '../../probe/registry.js'
8
8
  import { renderToolSchema } from '../../registry/tool/schema.js'
9
9
  import type { ActivityStore } from '../../store/activity/memory.js'
10
+ import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
10
11
  import type { RunId, ToolUseId } from '../../types/ids/index.js'
11
12
  import type { InvocationState } from '../../types/invocation/index.js'
12
13
  import {
@@ -205,11 +206,18 @@ export class ToolExecutor {
205
206
  private probes: ProbeRegistry
206
207
  private parentSpan?: Span
207
208
  private readonly readPaths: Set<string> = new Set()
209
+ private readonly readFingerprints: Map<string, string> = new Map()
208
210
  private readonly fileReadTracker: FileReadTracker = {
209
- recordRead: (key: string) => {
211
+ recordRead: (key: string, content?: string) => {
210
212
  this.readPaths.add(key)
213
+ // Only when the reader had the body. A tool that records a read
214
+ // without one leaves the previous fingerprint alone rather than
215
+ // clearing it, so a later write is still checked against the last
216
+ // body anyone actually saw.
217
+ if (content !== undefined) this.readFingerprints.set(key, fingerprintContent(content))
211
218
  },
212
219
  hasRead: (key: string) => this.readPaths.has(key),
220
+ fingerprint: (key: string) => this.readFingerprints.get(key),
213
221
  }
214
222
 
215
223
  constructor(
@@ -30,6 +30,7 @@ import type {
30
30
  StepResult,
31
31
  StopReason,
32
32
  } from '../../../types/run/index.js'
33
+ import type { LLMToolSchema, ToolRegistryContract } from '../../../types/tool/index.js'
33
34
  import { toErrorMessage } from '../../../utils/error.js'
34
35
  import { generateMessageId } from '../../../utils/id.js'
35
36
  import type { ToolCallOutcome } from '../executor.js'
@@ -200,6 +201,7 @@ export class IterationOrchestrator {
200
201
  const step = await this.prepareStep(iterationNum)
201
202
 
202
203
  const llmTools = this.ctx.tools.toLLMTools(step.allowedTools ?? this.ctx.allowedTools)
204
+ const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
203
205
  const stepModel = step.model ?? model
204
206
 
205
207
  const baseMessages = forceFinalize
@@ -266,6 +268,7 @@ export class IterationOrchestrator {
266
268
  model: stepModel,
267
269
  messages,
268
270
  tools: llmTools.length > 0 ? llmTools : undefined,
271
+ ...(enforceToolInputSchema ? { enforceToolInputSchema } : {}),
269
272
  toolChoice: forceFinalize && llmTools.length > 0 ? 'none' : undefined,
270
273
  temperature: step.temperature ?? runConfig.temperature,
271
274
  maxTokens: step.maxResponseTokens ?? runConfig.maxResponseTokens,
@@ -1042,11 +1045,13 @@ export class IterationOrchestrator {
1042
1045
  // tools param identical to prior iterations (cache prefix intact,
1043
1046
  // no 400 on tool blocks in history) and forbid use via tool_choice.
1044
1047
  const finalTools = this.ctx.tools.toLLMTools(this.ctx.allowedTools)
1048
+ const finalEnforced = enforcedModelInputToolNames(this.ctx.tools, finalTools)
1045
1049
  const response = await collect(
1046
1050
  this.ctx.provider.chatStream({
1047
1051
  model,
1048
1052
  messages: finalMessages,
1049
1053
  tools: finalTools.length > 0 ? finalTools : undefined,
1054
+ ...(finalEnforced ? { enforceToolInputSchema: finalEnforced } : {}),
1050
1055
  toolChoice: finalTools.length > 0 ? 'none' : undefined,
1051
1056
  temperature: this.ctx.runConfig.temperature,
1052
1057
  maxTokens: this.ctx.runConfig.maxResponseTokens,
@@ -1102,3 +1107,25 @@ function subtractUsage(after: TokenUsage, before: TokenUsage): TokenUsage {
1102
1107
  function round6(n: number): number {
1103
1108
  return Math.round(n * 1e6) / 1e6
1104
1109
  }
1110
+
1111
+ /**
1112
+ * Which of the tools going out on this request have a closed model schema.
1113
+ *
1114
+ * A driver reads `enforceToolInputSchema` to decide which tool schemas to
1115
+ * constrain generation against. Nothing populated it, so every driver that
1116
+ * consumed it — three of them — was reading a permanently undefined field
1117
+ * and `enforceModelInput: true` on a tool meant nothing end to end.
1118
+ *
1119
+ * Computed per request rather than once, because the allowed set changes:
1120
+ * a deferred tool activated mid-run has to start being enforced from the
1121
+ * next call, not the next process.
1122
+ */
1123
+ function enforcedModelInputToolNames(
1124
+ registry: ToolRegistryContract,
1125
+ tools: readonly LLMToolSchema[],
1126
+ ): readonly string[] | undefined {
1127
+ const names = tools
1128
+ .map((tool) => tool.function.name)
1129
+ .filter((name) => registry.get(name)?.enforceModelInput === true)
1130
+ return names.length > 0 ? names : undefined
1131
+ }
@@ -0,0 +1,103 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+
6
+ import type { ToolContext } from '../../../types/tool/index.js'
7
+
8
+ /**
9
+ * That the atomic writer works is tested next door. This tests the thing
10
+ * that actually regressed: whether the TOOL goes through it.
11
+ *
12
+ * `edit` and `write` were reverted to a bare `writeFile`, and the existing
13
+ * atomicity test kept passing because it called `atomicWriteFile` directly —
14
+ * proving the module, never the caller. A write that fails partway then
15
+ * truncates the user's file, which is the one outcome `edit` exists to
16
+ * avoid.
17
+ */
18
+
19
+ const atomicWriteFile = vi.fn(async () => {})
20
+ vi.mock('../atomic-write-file.js', () => ({ atomicWriteFile }))
21
+
22
+ function makeContext(workingDirectory: string): ToolContext {
23
+ return {
24
+ runId: 'run_atomic' as ToolContext['runId'],
25
+ workingDirectory,
26
+ abortSignal: new AbortController().signal,
27
+ env: {},
28
+ log: () => {},
29
+ }
30
+ }
31
+
32
+ describe('the file-mutating tools commit through the atomic writer', () => {
33
+ it('edit routes its local write through it', async () => {
34
+ const { EditTool } = await import('../edit.js')
35
+ atomicWriteFile.mockClear()
36
+
37
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
38
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
39
+
40
+ const result = await EditTool.execute(
41
+ { path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
42
+ makeContext(dir),
43
+ )
44
+
45
+ expect(result.success).toBe(true)
46
+ expect(atomicWriteFile).toHaveBeenCalledTimes(1)
47
+ expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'doc.md'), 'alpha\ngamma\n')
48
+ // The mock swallowed the write, so the file is untouched — which is
49
+ // itself the proof that nothing else wrote it.
50
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nbeta\n')
51
+ })
52
+
53
+ it('write routes its local write through it', async () => {
54
+ const { WriteFileTool } = await import('../write-file.js')
55
+ atomicWriteFile.mockClear()
56
+
57
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-tool-'))
58
+
59
+ const result = await WriteFileTool.execute(
60
+ { path: 'out.md', content: 'a complete body' },
61
+ makeContext(dir),
62
+ )
63
+
64
+ expect(result.success).toBe(true)
65
+ expect(atomicWriteFile).toHaveBeenCalledTimes(1)
66
+ expect(atomicWriteFile).toHaveBeenCalledWith(resolve(dir, 'out.md'), 'a complete body')
67
+ })
68
+
69
+ it('edit does not use it for a sandboxed write, which the sandbox owns', async () => {
70
+ const { EditTool } = await import('../edit.js')
71
+ atomicWriteFile.mockClear()
72
+
73
+ let body = 'alpha\nbeta\n'
74
+ const sandbox = {
75
+ id: 'sbx',
76
+ status: 'ready',
77
+ rootDir: '/workspace',
78
+ environment: 'basic',
79
+ async readFile() {
80
+ return Buffer.from(body)
81
+ },
82
+ async writeFile(_path: string, content: string | Buffer) {
83
+ body = content.toString()
84
+ },
85
+ async exec() {
86
+ throw new Error('not used')
87
+ },
88
+ async listFiles() {
89
+ return []
90
+ },
91
+ async destroy() {},
92
+ }
93
+
94
+ const result = await EditTool.execute(
95
+ { path: 'doc.md', old_string: 'beta', new_string: 'gamma', replace_all: false },
96
+ { ...makeContext('/workspace'), sandbox } as ToolContext,
97
+ )
98
+
99
+ expect(result.success).toBe(true)
100
+ expect(body).toBe('alpha\ngamma\n')
101
+ expect(atomicWriteFile).not.toHaveBeenCalled()
102
+ })
103
+ })
@@ -1,8 +1,11 @@
1
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
1
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
4
  import { describe, expect, it } from 'vitest'
5
+ import { ToolRegistry } from '../../../registry/tool/execute.js'
6
+ import type { Sandbox } from '../../../types/sandbox/index.js'
5
7
  import type { ToolContext } from '../../../types/tool/index.js'
8
+ import { atomicWriteFile } from '../atomic-write-file.js'
6
9
  import { EditTool } from '../edit.js'
7
10
 
8
11
  function makeContext(workingDirectory: string): ToolContext {
@@ -16,6 +19,326 @@ function makeContext(workingDirectory: string): ToolContext {
16
19
  }
17
20
 
18
21
  describe('EditTool', () => {
22
+ it('publishes one closed canonical replacement contract', () => {
23
+ const schema = EditTool.modelInputSchema
24
+ expect(schema).toEqual({
25
+ type: 'object',
26
+ properties: {
27
+ path: {
28
+ type: 'string',
29
+ description: 'Path to the file to edit. Must not be empty.',
30
+ },
31
+ old_string: {
32
+ type: 'string',
33
+ description:
34
+ 'Exact unique text from the file, without read-tool line-number prefixes. Must not be empty.',
35
+ },
36
+ new_string: {
37
+ type: 'string',
38
+ description:
39
+ 'Exact replacement text. May be empty to delete old_string. Keep under 12000 characters.',
40
+ },
41
+ replace_all: {
42
+ type: 'boolean',
43
+ description: 'Replace every occurrence instead of requiring one unique match.',
44
+ },
45
+ },
46
+ required: ['path', 'old_string', 'new_string'],
47
+ additionalProperties: false,
48
+ })
49
+
50
+ expect(
51
+ EditTool.inputSchema.safeParse({
52
+ path: 'doc.md',
53
+ old_string: 'old',
54
+ new_string: 'new',
55
+ replace_all: true,
56
+ }).success,
57
+ ).toBe(true)
58
+
59
+ // The MODEL schema above is the closed canonical one. The HOST schema
60
+ // is wider on purpose — aliases and line insertion are declared — and
61
+ // the point of `.strict()` is that wider still is not open.
62
+ for (const supported of [
63
+ { path: 'doc.md', oldStr: 'old', newStr: 'new' },
64
+ { path: 'doc.md', insertLine: 'end', new_string: 'append' },
65
+ { path: 'doc.md', old_string: 'old', new_string: 'new', newStr: 'legacy' },
66
+ ]) {
67
+ expect(EditTool.inputSchema.safeParse(supported).success, JSON.stringify(supported)).toBe(
68
+ true,
69
+ )
70
+ }
71
+
72
+ for (const unknown of [
73
+ { path: 'doc.md', old_str: 'old', new_string: 'new' },
74
+ { path: 'doc.md', old_string: 'old', new_string: 'new', replaceAll: true },
75
+ ]) {
76
+ expect(EditTool.inputSchema.safeParse(unknown).success, JSON.stringify(unknown)).toBe(false)
77
+ }
78
+ expect(
79
+ EditTool.inputSchema.safeParse({
80
+ path: ' ',
81
+ old_string: 'old',
82
+ new_string: 'new',
83
+ }).success,
84
+ ).toBe(false)
85
+ })
86
+
87
+ it('replaces one exact unique string', async () => {
88
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
89
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
90
+
91
+ const result = await EditTool.execute(
92
+ {
93
+ path: 'doc.md',
94
+ old_string: 'beta',
95
+ new_string: 'gamma',
96
+ replace_all: false,
97
+ },
98
+ makeContext(dir),
99
+ )
100
+
101
+ expect(result.success).toBe(true)
102
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\ngamma\n')
103
+ })
104
+
105
+ it('validates a path without rewriting edge whitespace', async () => {
106
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
107
+ writeFileSync(join(dir, ' doc.md '), 'alpha\n')
108
+
109
+ const result = await EditTool.execute(
110
+ {
111
+ path: ' doc.md ',
112
+ old_string: 'alpha',
113
+ new_string: 'beta',
114
+ replace_all: false,
115
+ },
116
+ makeContext(dir),
117
+ )
118
+
119
+ expect(result.success).toBe(true)
120
+ expect(readFileSync(join(dir, ' doc.md '), 'utf-8')).toBe('beta\n')
121
+ expect(existsSync(join(dir, 'doc.md'))).toBe(false)
122
+ })
123
+
124
+ it('allows deletion with an empty new_string', async () => {
125
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
126
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nremove me\nomega\n')
127
+
128
+ const result = await EditTool.execute(
129
+ {
130
+ path: 'doc.md',
131
+ old_string: 'remove me\n',
132
+ new_string: '',
133
+ replace_all: false,
134
+ },
135
+ makeContext(dir),
136
+ )
137
+
138
+ expect(result.success).toBe(true)
139
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nomega\n')
140
+ })
141
+
142
+ it('replaces every exact occurrence only when replace_all is true', async () => {
143
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
144
+ writeFileSync(join(dir, 'doc.md'), 'alpha alpha alpha')
145
+
146
+ const result = await EditTool.execute(
147
+ {
148
+ path: 'doc.md',
149
+ old_string: 'alpha',
150
+ new_string: 'beta',
151
+ replace_all: true,
152
+ },
153
+ makeContext(dir),
154
+ )
155
+
156
+ expect(result.success).toBe(true)
157
+ expect(result.data).toMatchObject({ replacements: 3 })
158
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta beta beta')
159
+ })
160
+
161
+ it('refuses ambiguous exact matches instead of guessing', async () => {
162
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
163
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nalpha\n')
164
+
165
+ const result = await EditTool.execute(
166
+ {
167
+ path: 'doc.md',
168
+ old_string: 'alpha',
169
+ new_string: 'beta',
170
+ replace_all: false,
171
+ },
172
+ makeContext(dir),
173
+ )
174
+
175
+ expect(result.success).toBe(false)
176
+ expect(result.error).toContain('is not unique')
177
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nalpha\n')
178
+ })
179
+
180
+ it('normalizes consistent CRLF/LF boundaries without fuzzy matching', async () => {
181
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
182
+ writeFileSync(join(dir, 'doc.md'), 'alpha\r\nbeta\r\nomega\r\n')
183
+
184
+ const result = await EditTool.execute(
185
+ {
186
+ path: 'doc.md',
187
+ old_string: 'alpha\nbeta',
188
+ new_string: 'gamma\ndelta',
189
+ replace_all: false,
190
+ },
191
+ makeContext(dir),
192
+ )
193
+
194
+ expect(result.success).toBe(true)
195
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('gamma\r\ndelta\r\nomega\r\n')
196
+ })
197
+
198
+ it('rejects a field it does not accept, even when execute is called directly', async () => {
199
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
200
+ writeFileSync(join(dir, 'doc.md'), 'alpha\n')
201
+
202
+ // Without `.strict()` zod STRIPS an unrecognised key, so each of these
203
+ // would run as if the caller had never written it — a misspelling
204
+ // becomes a silent no-op rather than an error anyone can act on.
205
+ for (const input of [
206
+ { path: 'doc.md', old_str: 'alpha', new_string: 'beta' },
207
+ { path: 'doc.md', old_string: 'alpha', new_string: 'beta', overwrite: true },
208
+ { path: 'doc.md', old_string: 'alpha', new_string: 'beta', replaceAll: true },
209
+ ]) {
210
+ const result = await EditTool.execute(input as never, makeContext(dir))
211
+ expect(result.success, JSON.stringify(input)).toBe(false)
212
+ expect(result.error).toContain('Invalid edit input')
213
+ }
214
+
215
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\n')
216
+ })
217
+
218
+ it('accepts the host aliases the schema does declare', async () => {
219
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
220
+ writeFileSync(join(dir, 'doc.md'), 'alpha\n')
221
+
222
+ // The contract is closed, not narrow. `oldStr`/`newStr` and
223
+ // `insertLine` are in the schema for hosts that speak those names, so
224
+ // strictness must reject the unknown without rejecting these.
225
+ const result = await EditTool.execute(
226
+ { path: 'doc.md', oldStr: 'alpha', newStr: 'beta' } as never,
227
+ makeContext(dir),
228
+ )
229
+
230
+ expect(result.success).toBe(true)
231
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('beta\n')
232
+ })
233
+
234
+ it('returns one canonical recovery shape after registry validation fails', async () => {
235
+ const registry = new ToolRegistry()
236
+ registry.register(EditTool)
237
+
238
+ const result = await registry.execute(
239
+ 'edit',
240
+ { insertLine: '"end"', newStr: 'content' },
241
+ makeContext('/tmp'),
242
+ )
243
+
244
+ expect(result.success).toBe(false)
245
+ expect(result.error).toContain('Validation failed for "edit":')
246
+ expect(result.error).toContain('Required: path: string')
247
+ expect(result.error).toContain(
248
+ '{"path":"file.md","old_string":"exact unique text","new_string":"replacement text"}',
249
+ )
250
+ expect(result.error).not.toContain('Accepted shapes')
251
+ })
252
+
253
+ it('serializes independent EditTool read-modify-write cycles for one sandbox path', async () => {
254
+ let body = Buffer.from('alpha')
255
+ let reads = 0
256
+ let writes = 0
257
+ let releaseFirstWrite = () => {}
258
+ let markFirstWriteStarted = () => {}
259
+ const firstWriteStarted = new Promise<void>((resolve) => {
260
+ markFirstWriteStarted = resolve
261
+ })
262
+ const firstWriteGate = new Promise<void>((resolve) => {
263
+ releaseFirstWrite = resolve
264
+ })
265
+ const sandbox = {
266
+ id: 'sbx_edit_lock',
267
+ status: 'ready',
268
+ rootDir: '/workspace',
269
+ environment: 'basic',
270
+ async readFile() {
271
+ reads += 1
272
+ return Buffer.from(body)
273
+ },
274
+ async writeFile(_path: string, content: string | Buffer) {
275
+ writes += 1
276
+ if (writes === 1) {
277
+ markFirstWriteStarted()
278
+ await firstWriteGate
279
+ }
280
+ body = Buffer.isBuffer(content) ? Buffer.from(content) : Buffer.from(content)
281
+ },
282
+ async exec() {
283
+ throw new Error('not used')
284
+ },
285
+ async listFiles() {
286
+ return []
287
+ },
288
+ async destroy() {},
289
+ } as Sandbox
290
+ const firstContext = { ...makeContext('/workspace'), sandbox }
291
+ const secondContext = { ...makeContext('/workspace'), sandbox }
292
+
293
+ const first = EditTool.execute(
294
+ {
295
+ path: 'doc.md',
296
+ old_string: 'alpha',
297
+ new_string: 'beta',
298
+ replace_all: false,
299
+ },
300
+ firstContext,
301
+ )
302
+ await firstWriteStarted
303
+ const second = EditTool.execute(
304
+ {
305
+ path: 'doc.md',
306
+ old_string: 'beta',
307
+ new_string: 'gamma',
308
+ replace_all: false,
309
+ },
310
+ secondContext,
311
+ )
312
+ await Promise.resolve()
313
+ expect(reads).toBe(1)
314
+
315
+ releaseFirstWrite()
316
+ const results = await Promise.all([first, second])
317
+ expect(results.every((result) => result.success)).toBe(true)
318
+ expect(reads).toBe(2)
319
+ expect(writes).toBe(2)
320
+ expect(body.toString('utf-8')).toBe('gamma')
321
+ })
322
+
323
+ it('leaves the original intact when an atomic local write fails before commit', async () => {
324
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-atomic-write-'))
325
+ const filePath = join(dir, 'doc.md')
326
+ writeFileSync(filePath, 'original')
327
+
328
+ await expect(
329
+ atomicWriteFile(filePath, 'replacement', {
330
+ beforeCommit: async () => {
331
+ throw new Error('injected pre-commit failure')
332
+ },
333
+ }),
334
+ ).rejects.toThrow('injected pre-commit failure')
335
+
336
+ expect(readFileSync(filePath, 'utf-8')).toBe('original')
337
+ expect(readdirSync(dir).filter((entry) => entry.includes('.namzu-'))).toEqual([])
338
+ })
339
+
340
+ // ── the alias + insertion surface this build adds on top ──────────
341
+
19
342
  it('accepts oldStr/newStr aliases for string replacement', async () => {
20
343
  const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
21
344
  writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')