@namzu/sdk 57.0.0 → 58.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.
Files changed (278) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +3 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/file-definitions.d.ts +35 -4
  6. package/dist/agents/file-definitions.d.ts.map +1 -1
  7. package/dist/agents/file-definitions.js +98 -8
  8. package/dist/agents/file-definitions.js.map +1 -1
  9. package/dist/authorization/protected-paths.d.ts +9 -0
  10. package/dist/authorization/protected-paths.d.ts.map +1 -1
  11. package/dist/authorization/protected-paths.js +29 -3
  12. package/dist/authorization/protected-paths.js.map +1 -1
  13. package/dist/compaction/index.d.ts +1 -0
  14. package/dist/compaction/index.d.ts.map +1 -1
  15. package/dist/compaction/index.js.map +1 -1
  16. package/dist/compaction/manual.d.ts +7 -0
  17. package/dist/compaction/manual.d.ts.map +1 -1
  18. package/dist/compaction/manual.js +31 -12
  19. package/dist/compaction/manual.js.map +1 -1
  20. package/dist/compaction/model-summary.d.ts +101 -0
  21. package/dist/compaction/model-summary.d.ts.map +1 -0
  22. package/dist/compaction/model-summary.js +333 -0
  23. package/dist/compaction/model-summary.js.map +1 -0
  24. package/dist/compaction/verifier.d.ts +6 -1
  25. package/dist/compaction/verifier.d.ts.map +1 -1
  26. package/dist/compaction/verifier.js +3 -3
  27. package/dist/compaction/verifier.js.map +1 -1
  28. package/dist/config/runtime.d.ts +62 -0
  29. package/dist/config/runtime.d.ts.map +1 -1
  30. package/dist/config/runtime.js +16 -0
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/connector/mcp/client.d.ts +8 -0
  33. package/dist/connector/mcp/client.d.ts.map +1 -1
  34. package/dist/connector/mcp/client.js +75 -2
  35. package/dist/connector/mcp/client.js.map +1 -1
  36. package/dist/constants/mcp/index.d.ts +2 -0
  37. package/dist/constants/mcp/index.d.ts.map +1 -1
  38. package/dist/constants/mcp/index.js +2 -0
  39. package/dist/constants/mcp/index.js.map +1 -1
  40. package/dist/constants/plugin/index.d.ts +2 -0
  41. package/dist/constants/plugin/index.d.ts.map +1 -1
  42. package/dist/constants/plugin/index.js +2 -0
  43. package/dist/constants/plugin/index.js.map +1 -1
  44. package/dist/manager/agent/lifecycle.d.ts +1 -1
  45. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  46. package/dist/manager/agent/lifecycle.js +25 -4
  47. package/dist/manager/agent/lifecycle.js.map +1 -1
  48. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  49. package/dist/manager/session/turn-recorder.js +2 -1
  50. package/dist/manager/session/turn-recorder.js.map +1 -1
  51. package/dist/persona/assembler.d.ts.map +1 -1
  52. package/dist/persona/assembler.js +2 -2
  53. package/dist/persona/assembler.js.map +1 -1
  54. package/dist/plugin/index.d.ts +1 -1
  55. package/dist/plugin/index.d.ts.map +1 -1
  56. package/dist/plugin/lifecycle.d.ts +8 -1
  57. package/dist/plugin/lifecycle.d.ts.map +1 -1
  58. package/dist/plugin/lifecycle.js +9 -2
  59. package/dist/plugin/lifecycle.js.map +1 -1
  60. package/dist/plugin/shell-hook-http.d.ts +39 -0
  61. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  62. package/dist/plugin/shell-hook-http.js +163 -0
  63. package/dist/plugin/shell-hook-http.js.map +1 -0
  64. package/dist/plugin/shell-hook.d.ts +93 -11
  65. package/dist/plugin/shell-hook.d.ts.map +1 -1
  66. package/dist/plugin/shell-hook.js +223 -39
  67. package/dist/plugin/shell-hook.js.map +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  69. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  70. package/dist/prompt/coding-agent-doctrine.js +2 -1
  71. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -2
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +2 -2
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +1 -0
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +2 -0
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +1 -1
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/executor.d.ts +4 -0
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +47 -9
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +29 -1
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +6 -2
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  94. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  96. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/phases/compaction.js +52 -5
  99. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  101. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  105. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  106. package/dist/runtime/query/path-escalation.d.ts +2 -0
  107. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  108. package/dist/runtime/query/path-escalation.js +7 -4
  109. package/dist/runtime/query/path-escalation.js.map +1 -1
  110. package/dist/runtime/query/plugin-hooks.js +2 -2
  111. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  112. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  113. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  114. package/dist/runtime/query/prepare-turn.js +25 -0
  115. package/dist/runtime/query/prepare-turn.js.map +1 -1
  116. package/dist/runtime/query/review-policy.d.ts +3 -2
  117. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  118. package/dist/runtime/query/review-policy.js +32 -13
  119. package/dist/runtime/query/review-policy.js.map +1 -1
  120. package/dist/scheduler/local.js +1 -1
  121. package/dist/scheduler/local.js.map +1 -1
  122. package/dist/session/retention/archive.js +1 -1
  123. package/dist/session/retention/archive.js.map +1 -1
  124. package/dist/session/workspace/driver.d.ts +6 -0
  125. package/dist/session/workspace/driver.d.ts.map +1 -1
  126. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  127. package/dist/session/workspace/git-worktree.js +28 -0
  128. package/dist/session/workspace/git-worktree.js.map +1 -1
  129. package/dist/skills/index.d.ts +1 -1
  130. package/dist/skills/index.d.ts.map +1 -1
  131. package/dist/skills/index.js +1 -1
  132. package/dist/skills/index.js.map +1 -1
  133. package/dist/skills/loader.d.ts +7 -0
  134. package/dist/skills/loader.d.ts.map +1 -1
  135. package/dist/skills/loader.js +53 -0
  136. package/dist/skills/loader.js.map +1 -1
  137. package/dist/skills/registry.d.ts +1 -1
  138. package/dist/skills/registry.d.ts.map +1 -1
  139. package/dist/skills/registry.js +2 -1
  140. package/dist/skills/registry.js.map +1 -1
  141. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  142. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  143. package/dist/tools/builtins/bash-cwd.js +127 -0
  144. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  145. package/dist/tools/builtins/bash.d.ts.map +1 -1
  146. package/dist/tools/builtins/bash.js +41 -6
  147. package/dist/tools/builtins/bash.js.map +1 -1
  148. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  149. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  150. package/dist/tools/builtins/bounded-file-read.js +67 -0
  151. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  152. package/dist/tools/builtins/grep.d.ts +6 -1
  153. package/dist/tools/builtins/grep.d.ts.map +1 -1
  154. package/dist/tools/builtins/grep.js +187 -24
  155. package/dist/tools/builtins/grep.js.map +1 -1
  156. package/dist/tools/builtins/read-file.d.ts +1 -0
  157. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  158. package/dist/tools/builtins/read-file.js +113 -28
  159. package/dist/tools/builtins/read-file.js.map +1 -1
  160. package/dist/tools/builtins/read-media.d.ts +68 -0
  161. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  162. package/dist/tools/builtins/read-media.js +397 -0
  163. package/dist/tools/builtins/read-media.js.map +1 -0
  164. package/dist/tools/builtins/search-tools.js +1 -1
  165. package/dist/tools/builtins/search-tools.js.map +1 -1
  166. package/dist/tools/builtins/skill.d.ts +4 -2
  167. package/dist/tools/builtins/skill.d.ts.map +1 -1
  168. package/dist/tools/builtins/skill.js +61 -2
  169. package/dist/tools/builtins/skill.js.map +1 -1
  170. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  171. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  172. package/dist/tools/builtins/sleep-guard.js +33 -0
  173. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  174. package/dist/tools/builtins/view-image.d.ts +23 -1
  175. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  176. package/dist/tools/builtins/view-image.js +66 -58
  177. package/dist/tools/builtins/view-image.js.map +1 -1
  178. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  179. package/dist/tools/file-read-tracker.js +31 -0
  180. package/dist/tools/file-read-tracker.js.map +1 -1
  181. package/dist/toolsets/manager.js +3 -3
  182. package/dist/toolsets/manager.js.map +1 -1
  183. package/dist/types/agent/base.d.ts +8 -0
  184. package/dist/types/agent/base.d.ts.map +1 -1
  185. package/dist/types/agent/query.d.ts +2 -0
  186. package/dist/types/agent/query.d.ts.map +1 -1
  187. package/dist/types/agent/scheduler.d.ts +8 -0
  188. package/dist/types/agent/scheduler.d.ts.map +1 -1
  189. package/dist/types/agent/task.d.ts +6 -2
  190. package/dist/types/agent/task.d.ts.map +1 -1
  191. package/dist/types/agent/task.js.map +1 -1
  192. package/dist/types/connector/mcp.d.ts +46 -0
  193. package/dist/types/connector/mcp.d.ts.map +1 -1
  194. package/dist/types/hitl/index.d.ts +1 -1
  195. package/dist/types/plugin/index.d.ts +74 -7
  196. package/dist/types/plugin/index.d.ts.map +1 -1
  197. package/dist/types/plugin/index.js +12 -1
  198. package/dist/types/plugin/index.js.map +1 -1
  199. package/dist/types/session/sub-session.d.ts +6 -2
  200. package/dist/types/session/sub-session.d.ts.map +1 -1
  201. package/dist/types/skills/index.d.ts +36 -0
  202. package/dist/types/skills/index.d.ts.map +1 -1
  203. package/dist/types/skills/index.js +9 -0
  204. package/dist/types/skills/index.js.map +1 -1
  205. package/dist/types/tool/index.d.ts +37 -0
  206. package/dist/types/tool/index.d.ts.map +1 -1
  207. package/dist/types/tool/index.js.map +1 -1
  208. package/dist/types/workspace/ref.d.ts +2 -0
  209. package/dist/types/workspace/ref.d.ts.map +1 -1
  210. package/dist/utils/frontmatter.d.ts +12 -0
  211. package/dist/utils/frontmatter.d.ts.map +1 -1
  212. package/dist/utils/frontmatter.js +6 -0
  213. package/dist/utils/frontmatter.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/agents/QueryAgent.ts +3 -0
  216. package/src/agents/file-definitions.ts +126 -6
  217. package/src/authorization/protected-paths.ts +30 -2
  218. package/src/compaction/index.ts +1 -0
  219. package/src/compaction/manual.ts +68 -30
  220. package/src/compaction/model-summary.ts +412 -0
  221. package/src/compaction/verifier.ts +3 -3
  222. package/src/config/runtime.ts +16 -0
  223. package/src/connector/mcp/client.ts +87 -1
  224. package/src/constants/mcp/index.ts +3 -0
  225. package/src/constants/plugin/index.ts +2 -0
  226. package/src/manager/agent/lifecycle.ts +24 -5
  227. package/src/manager/session/turn-recorder.ts +2 -1
  228. package/src/persona/assembler.ts +2 -2
  229. package/src/plugin/index.ts +5 -0
  230. package/src/plugin/lifecycle.ts +16 -2
  231. package/src/plugin/shell-hook-http.ts +181 -0
  232. package/src/plugin/shell-hook.ts +386 -89
  233. package/src/prompt/coding-agent-doctrine.ts +2 -1
  234. package/src/public-runtime.ts +6 -0
  235. package/src/public-tools.ts +2 -0
  236. package/src/public-types.ts +5 -0
  237. package/src/runtime/query/executor.ts +54 -10
  238. package/src/runtime/query/index.ts +44 -0
  239. package/src/runtime/query/iteration/index.ts +11 -3
  240. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  241. package/src/runtime/query/iteration/phases/compaction.ts +67 -5
  242. package/src/runtime/query/iteration/phases/context.ts +6 -0
  243. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  244. package/src/runtime/query/path-escalation.ts +11 -5
  245. package/src/runtime/query/plugin-hooks.ts +2 -2
  246. package/src/runtime/query/prepare-turn.ts +33 -0
  247. package/src/runtime/query/review-policy.ts +45 -16
  248. package/src/scheduler/local.ts +1 -1
  249. package/src/session/retention/archive.ts +1 -1
  250. package/src/session/workspace/driver.ts +6 -0
  251. package/src/session/workspace/git-worktree.ts +31 -0
  252. package/src/skills/index.ts +6 -1
  253. package/src/skills/loader.ts +54 -0
  254. package/src/skills/registry.ts +7 -6
  255. package/src/tools/builtins/bash-cwd.ts +156 -0
  256. package/src/tools/builtins/bash.ts +79 -34
  257. package/src/tools/builtins/bounded-file-read.ts +80 -0
  258. package/src/tools/builtins/grep.ts +198 -28
  259. package/src/tools/builtins/read-file.ts +151 -30
  260. package/src/tools/builtins/read-media.ts +483 -0
  261. package/src/tools/builtins/search-tools.ts +1 -1
  262. package/src/tools/builtins/skill.ts +91 -2
  263. package/src/tools/builtins/sleep-guard.ts +31 -0
  264. package/src/tools/builtins/view-image.ts +74 -62
  265. package/src/tools/file-read-tracker.ts +30 -0
  266. package/src/toolsets/manager.ts +3 -3
  267. package/src/types/agent/base.ts +9 -0
  268. package/src/types/agent/query.ts +2 -0
  269. package/src/types/agent/scheduler.ts +9 -0
  270. package/src/types/agent/task.ts +6 -2
  271. package/src/types/connector/mcp.ts +48 -0
  272. package/src/types/hitl/index.ts +1 -1
  273. package/src/types/plugin/index.ts +102 -16
  274. package/src/types/session/sub-session.ts +6 -2
  275. package/src/types/skills/index.ts +48 -0
  276. package/src/types/tool/index.ts +38 -0
  277. package/src/types/workspace/ref.ts +2 -0
  278. package/src/utils/frontmatter.ts +21 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The directory a `bash` call starts in, carried from one call to the next.
3
+ *
4
+ * A model that runs `cd packages/app` expects the next call to be there; shell
5
+ * state (variables, functions, aliases) does not carry over, but the directory
6
+ * does. After a foreground command the shell writes its physical directory to a
7
+ * private file, and the next call of the same session starts there.
8
+ *
9
+ * Two limits keep this from widening what a command can reach. The new
10
+ * directory has to lie inside the turn's roots (the working directory, the
11
+ * directories added for the session, any path this call was approved for); a
12
+ * command that leaves them is put back at the session's working directory and
13
+ * the result says so. And a directory that no longer exists is dropped without
14
+ * ceremony. Nothing here applies to a sandboxed command or to Windows shells,
15
+ * whose paths a host process cannot use as they are printed.
16
+ */
17
+
18
+ import { randomUUID } from 'node:crypto'
19
+ import { statSync } from 'node:fs'
20
+ import { rm } from 'node:fs/promises'
21
+ import { tmpdir } from 'node:os'
22
+ import { join } from 'node:path'
23
+
24
+ import { type CommandShell, hostCommandShell } from '../command-shell.js'
25
+ import { pathOutsideRoots, toolRoots } from '../paths.js'
26
+ import { readBoundedFile } from './bounded-file-read.js'
27
+
28
+ interface Carried {
29
+ /** The working directory the session started with; the fallback. */
30
+ readonly base: string
31
+ cwd: string
32
+ }
33
+
34
+ /** Sessions remembered; the oldest is forgotten first. */
35
+ const MAX_SESSIONS = 256
36
+ const carried = new Map<string, Carried>()
37
+
38
+ /** The notice appended to a result when the command left the allowed directories. */
39
+ export function cwdResetNotice(base: string): string {
40
+ return `Shell cwd was reset to ${base}`
41
+ }
42
+
43
+ /** Single-quote `text` for a POSIX shell. */
44
+ function shellQuote(text: string): string {
45
+ return `'${text.replace(/'/g, `'\\''`)}'`
46
+ }
47
+
48
+ /**
49
+ * Runs `command` unchanged and records the directory it ended in. The exit
50
+ * status is the command's own. A command that ends the shell itself (`exit`)
51
+ * records nothing, so the directory stays as it was.
52
+ */
53
+ export function withCwdCapture(command: string, file: string): string {
54
+ return `eval ${shellQuote(command)}\n__namzu_status=$?\npwd -P >| ${shellQuote(file)} 2>/dev/null\nexit $__namzu_status`
55
+ }
56
+
57
+ export function newCwdFile(): string {
58
+ return join(tmpdir(), `namzu-cwd-${randomUUID()}`)
59
+ }
60
+
61
+ /** Where this session's next foreground call starts. Synchronous: the spawn must not wait on it. */
62
+ export function startingDirectory(sessionId: string, base: string): string {
63
+ const entry = carried.get(sessionId)
64
+ if (entry === undefined) return base
65
+ if (entry.base !== base) {
66
+ carried.delete(sessionId)
67
+ return base
68
+ }
69
+ try {
70
+ if (statSync(entry.cwd).isDirectory()) return entry.cwd
71
+ } catch {
72
+ // Removed or unreadable: start again from the base.
73
+ }
74
+ entry.cwd = base
75
+ return base
76
+ }
77
+
78
+ /**
79
+ * The cwd used by bash execution and host authorization. Hosts must resolve it
80
+ * for each call, not cache the session's initial directory. Sandboxed commands
81
+ * (including sandbox escapes) and Windows never use the carried POSIX cwd.
82
+ */
83
+ export function effectiveBashWorkingDirectory(
84
+ sessionId: string | undefined,
85
+ workingDirectory: string,
86
+ options: {
87
+ sandboxed?: boolean
88
+ shell?: Pick<CommandShell, 'path' | 'windows'>
89
+ } = {},
90
+ ): string {
91
+ const shell = options.shell ?? hostCommandShell()
92
+ if (options.sandboxed || sessionId === undefined || shell.windows || shell.path === undefined)
93
+ return workingDirectory
94
+ return startingDirectory(sessionId, workingDirectory)
95
+ }
96
+
97
+ /**
98
+ * Reads what the finished command recorded and keeps it for the next call.
99
+ * Returns the notice to append when the directory was outside the roots.
100
+ */
101
+ export async function recordDirectory(
102
+ sessionId: string,
103
+ file: string,
104
+ context: Parameters<typeof toolRoots>[0],
105
+ ): Promise<string | undefined> {
106
+ let reported: string
107
+ try {
108
+ reported = (await readBoundedFile(file, 64 * 1024)).toString('utf8').trim()
109
+ } catch {
110
+ return undefined
111
+ } finally {
112
+ await rm(file, { force: true })
113
+ }
114
+ if (reported === '') return undefined
115
+ const base = context.workingDirectory
116
+ const outside = await pathOutsideRoots(toolRoots(context), reported)
117
+ const next = outside === undefined ? reported : base
118
+ carried.delete(sessionId)
119
+ if (next !== base) {
120
+ carried.set(sessionId, { base, cwd: next })
121
+ if (carried.size > MAX_SESSIONS) {
122
+ const oldest = carried.keys().next().value
123
+ if (oldest !== undefined) carried.delete(oldest)
124
+ }
125
+ }
126
+ return outside === undefined ? undefined : cwdResetNotice(base)
127
+ }
128
+
129
+ /** For tests: forget every session. */
130
+ export function forgetCarriedDirectories(): void {
131
+ carried.clear()
132
+ }
133
+
134
+ // Enumerable symbol crosses the executor's context copies, never the model's
135
+ // JSON input. No public ToolContext field or caller-authored cwd is trusted.
136
+ const REVIEWED_CWD = Symbol('namzu.bash.reviewed-cwd')
137
+
138
+ export function pinBashWorkingDirectory(context: object, cwd: string): void {
139
+ Object.defineProperty(context, REVIEWED_CWD, { value: cwd, enumerable: true, configurable: true })
140
+ }
141
+
142
+ export function bashWorkingDirectoryForExecution(context: {
143
+ sessionId?: string
144
+ workingDirectory: string
145
+ sandbox?: unknown
146
+ }): string {
147
+ const current = effectiveBashWorkingDirectory(context.sessionId, context.workingDirectory, {
148
+ sandboxed: context.sandbox !== undefined,
149
+ })
150
+ const reviewed = (context as { [REVIEWED_CWD]?: string })[REVIEWED_CWD]
151
+ if (reviewed !== undefined && current !== reviewed)
152
+ throw new Error(
153
+ 'Shell working directory changed after this call was reviewed. Nothing ran. Retry the call so its paths can be checked in the current directory.',
154
+ )
155
+ return reviewed ?? current
156
+ }
@@ -1,3 +1,4 @@
1
+ import { rm } from 'node:fs/promises'
1
2
  import { z } from 'zod'
2
3
  import { isReadOnlyCommandLine } from '../../authorization/readonly-commands.js'
3
4
  import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
@@ -24,8 +25,15 @@ import { defineTool } from '../defineTool.js'
24
25
  import { scrubInheritedEnv, withheldEnvNoticeText } from '../env-scrub.js'
25
26
  import { createHostOutputDecoder } from '../host-output.js'
26
27
  import { looksBinary, windowText } from '../text-window.js'
28
+ import {
29
+ bashWorkingDirectoryForExecution,
30
+ newCwdFile,
31
+ recordDirectory,
32
+ withCwdCapture,
33
+ } from './bash-cwd.js'
27
34
  import { judgeExit } from './exit-semantics.js'
28
35
  import { jsonStringEscapes } from './json-string-hint.js'
36
+ import { leadingLongSleep, longSleepRefusal } from './sleep-guard.js'
29
37
 
30
38
  // Namzu owns its own bash timeout knob — `NAMZU_BASH_TIMEOUT_MS` — and
31
39
  // reads no consumer's env name. A consumer that wants one knob aliases
@@ -526,7 +534,7 @@ export const SANDBOX_ESCAPE_NOT_APPROVED =
526
534
  'dangerously_disable_sandbox was not approved for this call, so the command did not run. Leaving the sandbox needs the user to confirm it each time, and this turn either cannot ask or does not allow it. Run the command inside the sandbox, or tell the user what it needs and let them run it.'
527
535
 
528
536
  const BASH_DESCRIPTION =
529
- 'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
537
+ 'Executes a command line using the execution shell disclosed in the system context and returns stdout/stderr output. The working directory persists between calls, but shell state does not. Command timeout is configurable. The `command` parameter is required, so every call carries one. Long file content goes through `write`, not a heredoc, so it cannot hit the output token limit mid-stream.'
530
538
 
531
539
  const BashToolBase = defineTool({
532
540
  name: 'bash',
@@ -579,6 +587,7 @@ const BashToolBase = defineTool({
579
587
  },
580
588
 
581
589
  async execute(input, context) {
590
+ const startDirectory = bashWorkingDirectoryForExecution(context)
582
591
  if (isDangerousCommand(input.command)) {
583
592
  return {
584
593
  success: false,
@@ -588,6 +597,15 @@ const BashToolBase = defineTool({
588
597
  }
589
598
  }
590
599
 
600
+ // Where a job registry exists to wait on, a long leading sleep only
601
+ // holds the turn; it is refused with the better ways to wait.
602
+ if (!input.run_in_background && context.backgroundJobs && !backgroundTasksDisabled()) {
603
+ const longSleep = leadingLongSleep(input.command)
604
+ if (longSleep !== null) {
605
+ return { success: false, output: '', error: longSleepRefusal(longSleep) }
606
+ }
607
+ }
608
+
591
609
  // Asked to leave the sandbox. Honoured only on the executor's word that
592
610
  // a reviewer confirmed THIS call (`sandboxEscapeApproved`); the input
593
611
  // alone is the model's request, never the permission. Without a
@@ -648,7 +666,7 @@ const BashToolBase = defineTool({
648
666
  try {
649
667
  const job = context.backgroundJobs.start({
650
668
  command: input.command,
651
- workingDirectory: context.workingDirectory,
669
+ workingDirectory: startDirectory,
652
670
  })
653
671
  // `start` returns before the process exists, and a spawn that
654
672
  // fails (no such shell, bad directory) reports a tick later.
@@ -799,38 +817,57 @@ const BashToolBase = defineTool({
799
817
  let withdrawOffer: (() => void) | undefined
800
818
  const offer = context.backgroundJobs?.registerForeground?.bind(context.backgroundJobs)
801
819
 
820
+ // The directory carries over between calls of a session on a POSIX host
821
+ // shell; the shell records where it ended, read back in `finally`.
822
+ const hostShell = hostCommandShell()
823
+ const carryDirectory =
824
+ context.sandbox === undefined &&
825
+ typeof context.sessionId === 'string' &&
826
+ hostShell.path !== undefined &&
827
+ hostShell.windows !== true
828
+ const cwdFile = carryDirectory ? newCwdFile() : undefined
829
+ let settled = false
830
+ const settle = async (): Promise<string | undefined> => {
831
+ if (cwdFile === undefined) return undefined
832
+ settled = true
833
+ return recordDirectory(context.sessionId, cwdFile, context)
834
+ }
835
+
802
836
  try {
803
837
  const { stdout, stderr, stdoutBytes, stderrBytes, backgroundedJobId, backgroundedBy } =
804
- await execHostShell(input.command, {
805
- cwd: context.workingDirectory,
806
- timeout: input.timeout,
807
- env: { ...inherited.env, ...context.env },
808
- maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
809
- signal: context.abortSignal,
810
- onOutput,
811
- ...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
812
- ...(canHandOff
813
- ? {
814
- adopt: adoptRunning,
815
- adoptOnInterrupt: true,
816
- ...(offer
817
- ? {
818
- onRunning: (control: {
819
- background(reason: 'user' | 'message'): boolean
820
- }) => {
821
- offerTimer = setTimeout(() => {
822
- withdrawOffer = offer({
823
- command: input.command,
824
- background: (reason) => control.background(reason),
825
- })
826
- }, FOREGROUND_HANDOFF_AFTER_MS)
827
- offerTimer.unref?.()
828
- },
829
- }
830
- : {}),
831
- }
832
- : {}),
833
- })
838
+ await execHostShell(
839
+ cwdFile === undefined ? input.command : withCwdCapture(input.command, cwdFile),
840
+ {
841
+ cwd: startDirectory,
842
+ timeout: input.timeout,
843
+ env: { ...inherited.env, ...context.env },
844
+ maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
845
+ signal: context.abortSignal,
846
+ onOutput,
847
+ ...(canMoveOnTimeout ? { onTimeout: adoptRunning } : {}),
848
+ ...(canHandOff
849
+ ? {
850
+ adopt: adoptRunning,
851
+ adoptOnInterrupt: true,
852
+ ...(offer
853
+ ? {
854
+ onRunning: (control: {
855
+ background(reason: 'user' | 'message'): boolean
856
+ }) => {
857
+ offerTimer = setTimeout(() => {
858
+ withdrawOffer = offer({
859
+ command: input.command,
860
+ background: (reason) => control.background(reason),
861
+ })
862
+ }, FOREGROUND_HANDOFF_AFTER_MS)
863
+ offerTimer.unref?.()
864
+ },
865
+ }
866
+ : {}),
867
+ }
868
+ : {}),
869
+ },
870
+ )
834
871
  if (backgroundedJobId !== undefined) {
835
872
  const timedOut = backgroundedBy === 'timeout'
836
873
  return {
@@ -853,9 +890,10 @@ const BashToolBase = defineTool({
853
890
  facts: { kind: 'command', command: input.command, background: true, continued: true },
854
891
  }
855
892
  }
893
+ const reset = await settle()
856
894
  return {
857
895
  success: true,
858
- output: formatShellOutput(stdout, stderr) || '(no output)',
896
+ output: withResetNotice(formatShellOutput(stdout, stderr) || '(no output)', reset),
859
897
  data: {
860
898
  exitCode: 0,
861
899
  ...(leaveSandbox ? { sandboxed: false, sandboxEscape: true } : {}),
@@ -903,6 +941,7 @@ const BashToolBase = defineTool({
903
941
  const judged =
904
942
  !timedOut && exitCode !== undefined ? judgeExit(input.command, exitCode) : undefined
905
943
  const answered = judged !== undefined && !judged.isError
944
+ const reset = await settle()
906
945
  const output = [
907
946
  formatShellOutput(failure.stdout, failure.stderr),
908
947
  clipped.length > 0
@@ -914,7 +953,7 @@ const BashToolBase = defineTool({
914
953
 
915
954
  return {
916
955
  success: answered,
917
- output: output || '(no output)',
956
+ output: withResetNotice(output || '(no output)', reset),
918
957
  data: {
919
958
  ...(exitCode !== undefined ? { exitCode } : {}),
920
959
  ...(judged?.note ? { exitNote: judged.note } : {}),
@@ -949,6 +988,7 @@ const BashToolBase = defineTool({
949
988
  }),
950
989
  }
951
990
  } finally {
991
+ if (cwdFile !== undefined && !settled) await rm(cwdFile, { force: true })
952
992
  if (offerTimer !== undefined) clearTimeout(offerTimer)
953
993
  withdrawOffer?.()
954
994
  }
@@ -1002,6 +1042,11 @@ function formatShellOutput(stdout: string | undefined, stderr: string | undefine
1002
1042
  .join('\n\n')
1003
1043
  }
1004
1044
 
1045
+ /** The result text with the directory-reset line, when there is one, on its own last line. */
1046
+ function withResetNotice(output: string, notice: string | undefined): string {
1047
+ return notice === undefined ? output : `${output}\n${notice}`
1048
+ }
1049
+
1005
1050
  /**
1006
1051
  * The notice text for the credential-shaped variables left out of a command's
1007
1052
  * environment (see {@link withheldEnvNoticeText}). It is sent as a notice after
@@ -0,0 +1,80 @@
1
+ import { constants } from 'node:fs'
2
+ import { open } from 'node:fs/promises'
3
+
4
+ const CHUNK_BYTES = 64 * 1024
5
+
6
+ /** A hard input ceiling, independent of output windows and allow_large. */
7
+ export const TEXT_MAX_INPUT_BYTES = 32 * 1024 * 1024
8
+
9
+ /**
10
+ * Read at most the limit plus one sentinel byte, even if the file grows after
11
+ * stat. No allocation or request is derived from an untrusted file size.
12
+ * The first bounded chunk can choose a stricter limit from the file signature.
13
+ */
14
+ export async function readBoundedChunks(
15
+ read: (offset: number, length: number) => Promise<Buffer>,
16
+ maximum: number,
17
+ signal?: AbortSignal,
18
+ selectLimit?: (prefix: Buffer) => number,
19
+ ): Promise<Buffer> {
20
+ const chunks: Buffer[] = []
21
+ let offset = 0
22
+ let limit = maximum
23
+ for (;;) {
24
+ signal?.throwIfAborted()
25
+ const length = Math.min(CHUNK_BYTES, limit + 1 - offset)
26
+ const chunk = await read(offset, length)
27
+ signal?.throwIfAborted()
28
+ if (chunk.length > length)
29
+ throw new Error('The file backend returned more bytes than the requested bounded range.')
30
+ if (offset === 0 && selectLimit) limit = Math.min(maximum, selectLimit(chunk))
31
+ offset += chunk.length
32
+ if (offset > limit) throw inputTooLarge(limit)
33
+ chunks.push(chunk)
34
+ if (chunk.length < length) return Buffer.concat(chunks, offset)
35
+ }
36
+ }
37
+
38
+ export async function readBoundedFile(
39
+ path: string,
40
+ maximum: number,
41
+ signal?: AbortSignal,
42
+ selectLimit?: (prefix: Buffer) => number,
43
+ ): Promise<Buffer> {
44
+ // O_NONBLOCK prevents a substituted FIFO from blocking open before fstat.
45
+ signal?.throwIfAborted()
46
+ const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK)
47
+ try {
48
+ const info = await file.stat()
49
+ if (!info.isFile()) throw new Error('Read requires a regular file.')
50
+ if (info.size > maximum) throw inputTooLarge(maximum)
51
+ return await readBoundedChunks(
52
+ async (offset, length) => {
53
+ const buffer = Buffer.allocUnsafe(length)
54
+ let used = 0
55
+ while (used < length) {
56
+ signal?.throwIfAborted()
57
+ const { bytesRead } = await file.read(buffer, used, length - used, offset + used)
58
+ if (bytesRead === 0) break
59
+ used += bytesRead
60
+ }
61
+ return buffer.subarray(0, used)
62
+ },
63
+ maximum,
64
+ signal,
65
+ (prefix) => {
66
+ const limit = selectLimit?.(prefix) ?? maximum
67
+ if (info.size > limit) throw inputTooLarge(limit)
68
+ return limit
69
+ },
70
+ )
71
+ } finally {
72
+ await file.close()
73
+ }
74
+ }
75
+
76
+ function inputTooLarge(limit: number): Error {
77
+ return new Error(
78
+ `File exceeds the bounded read limit (${limit} bytes). Extract a smaller file with a shell command.`,
79
+ )
80
+ }