@namzu/sdk 32.0.0 → 33.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 (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -19,13 +19,19 @@ import type { WorkspaceBackendKind, WorkspaceRef } from '../../types/workspace/r
19
19
  */
20
20
  export interface CreateWorkspaceParams {
21
21
  baseRef?: string
22
+ /**
23
+ * Traceable name below the configured workspace root. Nested names are
24
+ * accepted; absolute paths, traversal, and paths rewritten by a filesystem
25
+ * link are refused by the reference Git driver.
26
+ */
22
27
  label?: string
23
28
  }
24
29
 
25
30
  /**
26
- * Params for {@link WorkspaceBackendDriver.branch}. `label` is a free-form
27
- * tag embedded into the new workspace name so operators can trace provenance
28
- * without reaching into the driver's internal naming scheme.
31
+ * Params for {@link WorkspaceBackendDriver.branch}. `label` is a traceable tag
32
+ * embedded into the new workspace name so operators can identify provenance
33
+ * without reaching into the driver's internal naming scheme. Backend path and
34
+ * identifier constraints still apply.
29
35
  */
30
36
  export interface BranchWorkspaceParams {
31
37
  label?: string
@@ -47,7 +53,9 @@ export interface WorkspaceInspection {
47
53
  * Backend driver contract. All methods are async and may throw
48
54
  * {@link WorkspaceBackendError} on I/O failures. `dispose` is idempotent —
49
55
  * calling it against an already-disposed ref is not an error (mitigates
50
- * roadmap Risk #3: broadcast rollback must not fail on partial state).
56
+ * roadmap Risk #3: broadcast rollback must not fail on partial state). A ref
57
+ * remains affiliated with the driver configuration that created it; drivers
58
+ * refuse foreign backend, repository, or ownership-root metadata.
51
59
  */
52
60
  export interface WorkspaceBackendDriver {
53
61
  readonly kind: WorkspaceBackendKind
@@ -4,14 +4,16 @@
4
4
  *
5
5
  * See session-hierarchy.md §7.2 (Git-worktree reference backend).
6
6
  *
7
- * Safety: every subprocess invocation uses `execFile` with an argv array
8
- * no shell interpolation so caller-supplied `label`/`baseRef` strings
9
- * cannot escape their argument slot. Failures surface as
7
+ * Safety: every subprocess invocation uses `execFile` with an argv array and
8
+ * separates caller-controlled positional arguments from Git options with
9
+ * `--`. Filesystem paths are canonicalized and confined to the configured
10
+ * worktree root before Git receives them. Failures surface as
10
11
  * {@link WorkspaceBackendError} carrying the underlying cause.
11
12
  */
12
13
 
13
14
  import { execFile } from 'node:child_process'
14
- import { join } from 'node:path'
15
+ import { realpath } from 'node:fs/promises'
16
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
15
17
  import { promisify } from 'node:util'
16
18
  import { NAMZU } from '../../constants/telemetry/index.js'
17
19
  import type { GitWorktreeBackendMeta, WorkspaceRef } from '../../types/workspace/ref.js'
@@ -71,8 +73,8 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
71
73
  private readonly exec: ExecFile
72
74
 
73
75
  constructor(config: GitWorktreeDriverConfig) {
74
- this.repoRoot = config.repoRoot
75
- this.worktreesDir = config.worktreesDir ?? join(config.repoRoot, '.namzu', 'worktrees')
76
+ this.repoRoot = resolve(config.repoRoot)
77
+ this.worktreesDir = resolve(config.worktreesDir ?? join(config.repoRoot, '.namzu', 'worktrees'))
76
78
  this.log = config.logger.child({ [SCOPE_ATTRIBUTE]: 'session/workspace/git-worktree' })
77
79
  this.exec = config.execFile ?? defaultExecFile
78
80
  }
@@ -80,10 +82,38 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
80
82
  async create(params: CreateWorkspaceParams): Promise<WorkspaceRef> {
81
83
  const id = generateWorkspaceId()
82
84
  const label = params.label ?? id
83
- const branch = `namzu/${label}`
84
- const worktreePath = join(this.worktreesDir, label)
85
+ let repoRoot: string
86
+ let worktreesDir: string
87
+ let worktreePath: string
88
+ try {
89
+ repoRoot = await canonicalizeThroughExisting(this.repoRoot)
90
+ worktreesDir = await canonicalizeThroughExisting(this.worktreesDir)
91
+ const lexicalPath = resolve(worktreesDir, label)
92
+ if (!isStrictDescendant(worktreesDir, lexicalPath)) {
93
+ throw new Error(`Workspace label escapes the managed worktree directory: ${label}`)
94
+ }
95
+ worktreePath = await canonicalizeThroughExisting(lexicalPath)
96
+ if (!isStrictDescendant(worktreesDir, worktreePath)) {
97
+ throw new Error(
98
+ `Workspace label resolves through a link outside the managed worktree directory: ${label}`,
99
+ )
100
+ }
101
+ if (relative(worktreesDir, lexicalPath) !== relative(worktreesDir, worktreePath)) {
102
+ throw new Error(
103
+ `Workspace label resolves through a link to a different managed path: ${label}`,
104
+ )
105
+ }
106
+ } catch (cause) {
107
+ throw new WorkspaceBackendError({ op: 'create:ownership', kind: this.kind, cause })
108
+ }
109
+
110
+ const relativePath = relative(worktreesDir, worktreePath).split(sep).join('/')
111
+ const branch = `namzu/${relativePath}`
85
112
 
86
- const argv = ['-C', this.repoRoot, 'worktree', 'add', '-b', branch, worktreePath]
113
+ // `--` belongs before caller-controlled positional arguments. Without it,
114
+ // a base ref such as `--no-checkout` is accepted as a Git option and the
115
+ // driver publishes a checkout containing only its `.git` link.
116
+ const argv = ['-C', repoRoot, 'worktree', 'add', '-b', branch, '--', worktreePath]
87
117
  if (params.baseRef !== undefined) {
88
118
  argv.push(params.baseRef)
89
119
  }
@@ -103,7 +133,7 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
103
133
  // The bar is deliberately high: registered under this exact path
104
134
  // AND carrying the branch this call asked for. A registered path
105
135
  // alone can be a half-finished checkout, or somebody else's.
106
- if (!(await this.createdDespite(worktreePath, branch))) {
136
+ if (!(await this.createdDespite(repoRoot, worktreePath, branch))) {
107
137
  throw new WorkspaceBackendError({ op: 'create', kind: this.kind, cause })
108
138
  }
109
139
  this.log.warn('git-worktree add reported failure but the worktree is present', {
@@ -115,7 +145,7 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
115
145
 
116
146
  const meta: GitWorktreeBackendMeta = {
117
147
  backend: 'git-worktree',
118
- repoRoot: this.repoRoot,
148
+ repoRoot,
119
149
  branch,
120
150
  worktreePath,
121
151
  }
@@ -135,34 +165,50 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
135
165
  async branch(source: WorkspaceRef, params: BranchWorkspaceParams): Promise<WorkspaceRef> {
136
166
  // Branch from the source worktree's current branch. The source's
137
167
  // `meta.branch` is the ref we base off.
138
- return this.create({ baseRef: source.meta.branch, label: params.label })
168
+ const owned = await this.requireOwnedRef(source, 'branch')
169
+ return this.create({ baseRef: owned.branch, label: params.label })
139
170
  }
140
171
 
141
172
  async dispose(ref: WorkspaceRef): Promise<void> {
173
+ const owned = await this.requireOwnedRef(ref, 'dispose')
174
+ const registered = await this.registeredEntry(owned, 'dispose:list')
175
+ if (!registered) {
176
+ this.log.debug('git-worktree already gone; dispose idempotent', {
177
+ [NAMZU.SESSION_ID]: ref.id,
178
+ 'namzu.session.path': owned.worktreePath,
179
+ })
180
+ return
181
+ }
182
+
142
183
  // Tolerate missing directories per roadmap Risk #3 mitigation: the
143
184
  // broadcast-rollback compensating step calls dispose on partially
144
- // provisioned refs and must not propagate "already gone" errors.
185
+ // provisioned refs and must not propagate "already gone" errors. The
186
+ // repository list, rather than an error-message regex, is the evidence:
187
+ // an unavailable repository can contain the same "No such file" words.
145
188
  try {
146
189
  await this.exec('git', [
147
190
  '-C',
148
- this.repoRoot,
191
+ owned.repoRoot,
149
192
  'worktree',
150
193
  'remove',
151
- ref.meta.worktreePath,
194
+ owned.worktreePath,
152
195
  '--force',
153
196
  ])
154
197
  this.log.info('git-worktree disposed', {
155
198
  [NAMZU.SESSION_ID]: ref.id,
156
- 'namzu.session.path': ref.meta.worktreePath,
199
+ 'namzu.session.path': owned.worktreePath,
157
200
  })
158
201
  } catch (cause) {
159
- const message = cause instanceof Error ? cause.message : String(cause)
160
- // git exits non-zero with "is not a working tree" when the path is
161
- // absent; treat as idempotent success and log at debug.
162
- if (/not a working tree|does not exist|No such file/i.test(message)) {
202
+ // Two concurrent disposals can both observe the entry before one wins.
203
+ // Re-read after failure and accept only repository evidence that the
204
+ // exact owned branch/path pair is now absent.
205
+ const nowRegistered = await this.registeredEntry(owned, 'dispose:recheck').catch(
206
+ () => undefined,
207
+ )
208
+ if (nowRegistered === null) {
163
209
  this.log.debug('git-worktree already gone; dispose idempotent', {
164
210
  [NAMZU.SESSION_ID]: ref.id,
165
- 'namzu.session.path': ref.meta.worktreePath,
211
+ 'namzu.session.path': owned.worktreePath,
166
212
  })
167
213
  return
168
214
  }
@@ -184,15 +230,13 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
184
230
  * has already gone wrong once, and guessing optimistically there is how
185
231
  * a recovery turns a bad situation into a wrong one.
186
232
  */
187
- private async createdDespite(worktreePath: string, branch: string): Promise<boolean> {
233
+ private async createdDespite(
234
+ repoRoot: string,
235
+ worktreePath: string,
236
+ branch: string,
237
+ ): Promise<boolean> {
188
238
  try {
189
- const { stdout } = await this.exec('git', [
190
- '-C',
191
- this.repoRoot,
192
- 'worktree',
193
- 'list',
194
- '--porcelain',
195
- ])
239
+ const { stdout } = await this.exec('git', ['-C', repoRoot, 'worktree', 'list', '--porcelain'])
196
240
  const entry = parseWorktreeList(stdout, worktreePath)
197
241
  // `--porcelain` writes the branch as a full ref (`refs/heads/x`),
198
242
  // and `branch` here is the short name this call passed to `-b`.
@@ -206,11 +250,12 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
206
250
  }
207
251
 
208
252
  async inspect(ref: WorkspaceRef): Promise<WorkspaceInspection> {
253
+ const owned = await this.requireOwnedRef(ref, 'inspect')
209
254
  let listStdout: string
210
255
  try {
211
256
  const result = await this.exec('git', [
212
257
  '-C',
213
- this.repoRoot,
258
+ owned.repoRoot,
214
259
  'worktree',
215
260
  'list',
216
261
  '--porcelain',
@@ -220,14 +265,14 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
220
265
  throw new WorkspaceBackendError({ op: 'inspect:list', kind: this.kind, cause })
221
266
  }
222
267
 
223
- const entry = parseWorktreeList(listStdout, ref.meta.worktreePath)
224
- if (!entry) {
225
- return { exists: false, currentRef: ref.meta.branch, isDirty: false }
268
+ const entry = parseWorktreeList(listStdout, owned.worktreePath)
269
+ if (!entry || entry.branch !== `refs/heads/${owned.branch}`) {
270
+ return { exists: false, currentRef: owned.branch, isDirty: false }
226
271
  }
227
272
 
228
273
  let statusStdout: string
229
274
  try {
230
- const result = await this.exec('git', ['-C', ref.meta.worktreePath, 'status', '--porcelain'])
275
+ const result = await this.exec('git', ['-C', owned.worktreePath, 'status', '--porcelain'])
231
276
  statusStdout = result.stdout
232
277
  } catch (cause) {
233
278
  throw new WorkspaceBackendError({ op: 'inspect:status', kind: this.kind, cause })
@@ -239,6 +284,100 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
239
284
  isDirty: statusStdout.trim().length > 0,
240
285
  }
241
286
  }
287
+
288
+ /**
289
+ * Turn persisted metadata back into an owned driver capability.
290
+ *
291
+ * A WorkspaceRef is recovery data, not authority to make this driver act on
292
+ * any worktree registered in the same repository. Existing links are
293
+ * resolved before the boundary is decided, and every Git call receives the
294
+ * canonical path that was checked. This closes static symlink escapes. It
295
+ * does not claim protection from a host process concurrently swapping path
296
+ * components or mount points; the Git CLI exposes no descriptor-relative
297
+ * removal primitive with which to make that guarantee.
298
+ */
299
+ private async requireOwnedRef(ref: WorkspaceRef, op: string): Promise<GitWorktreeBackendMeta> {
300
+ try {
301
+ if (ref.meta.backend !== this.kind) {
302
+ throw new Error(`Workspace backend ${ref.meta.backend} does not belong to ${this.kind}`)
303
+ }
304
+
305
+ const repoRoot = await canonicalizeThroughExisting(this.repoRoot)
306
+ const refRepoRoot = await canonicalizeThroughExisting(ref.meta.repoRoot)
307
+ if (refRepoRoot !== repoRoot) {
308
+ throw new Error('Workspace reference belongs to a different repository')
309
+ }
310
+
311
+ const worktreesDir = await canonicalizeThroughExisting(this.worktreesDir)
312
+ const worktreePath = await canonicalizeThroughExisting(ref.meta.worktreePath)
313
+ if (!isStrictDescendant(worktreesDir, worktreePath)) {
314
+ throw new Error('Workspace reference escapes the managed worktree directory')
315
+ }
316
+
317
+ const label = relative(worktreesDir, worktreePath).split(sep).join('/')
318
+ const branch = `namzu/${label}`
319
+ if (ref.meta.branch !== branch) {
320
+ throw new Error('Workspace reference branch does not match its managed path')
321
+ }
322
+
323
+ return {
324
+ backend: 'git-worktree',
325
+ repoRoot,
326
+ branch,
327
+ worktreePath,
328
+ }
329
+ } catch (cause) {
330
+ throw new WorkspaceBackendError({ op: `${op}:ownership`, kind: this.kind, cause })
331
+ }
332
+ }
333
+
334
+ private async registeredEntry(
335
+ owned: GitWorktreeBackendMeta,
336
+ op: string,
337
+ ): Promise<{ path: string; head?: string; branch?: string } | null> {
338
+ let stdout: string
339
+ try {
340
+ const result = await this.exec('git', [
341
+ '-C',
342
+ owned.repoRoot,
343
+ 'worktree',
344
+ 'list',
345
+ '--porcelain',
346
+ ])
347
+ stdout = result.stdout
348
+ } catch (cause) {
349
+ throw new WorkspaceBackendError({ op, kind: this.kind, cause })
350
+ }
351
+
352
+ const entry = parseWorktreeList(stdout, owned.worktreePath)
353
+ return entry?.branch === `refs/heads/${owned.branch}` ? entry : null
354
+ }
355
+ }
356
+
357
+ /** Resolve every existing path component, preserving only a missing suffix. */
358
+ async function canonicalizeThroughExisting(input: string): Promise<string> {
359
+ let existing = resolve(input)
360
+ const remainder: string[] = []
361
+
362
+ for (;;) {
363
+ try {
364
+ const canonical = await realpath(existing)
365
+ return resolve(canonical, ...remainder)
366
+ } catch (cause) {
367
+ const code = (cause as NodeJS.ErrnoException).code
368
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') throw cause
369
+ }
370
+
371
+ const parent = dirname(existing)
372
+ if (parent === existing) return resolve(existing, ...remainder)
373
+ remainder.unshift(basename(existing))
374
+ existing = parent
375
+ }
376
+ }
377
+
378
+ function isStrictDescendant(root: string, candidate: string): boolean {
379
+ const rel = relative(root, candidate)
380
+ return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
242
381
  }
243
382
 
244
383
  /**
@@ -207,6 +207,53 @@ export class SkillRegistry {
207
207
  return [...this.skills.values()]
208
208
  }
209
209
 
210
+ /**
211
+ * The model-facing catalog, keyed by what this registry actually accepts.
212
+ *
213
+ * Reading the Map key is load-bearing for host namespaces. A plugin may file
214
+ * `reconcile` as `ledger__reconcile`; reconstructing the name from the skill
215
+ * body would advertise a call that cannot resolve (and could collide with a
216
+ * different plugin's unqualified name).
217
+ */
218
+ async catalog(): Promise<
219
+ readonly {
220
+ registeredName: string
221
+ description: string
222
+ location: string
223
+ allowedTools?: string
224
+ invocation?: 'model' | 'operator' | 'both'
225
+ }[]
226
+ > {
227
+ const entries: Array<{
228
+ registeredName: string
229
+ description: string
230
+ location: string
231
+ allowedTools?: string
232
+ invocation?: 'model' | 'operator' | 'both'
233
+ }> = []
234
+ // `load` owns file freshness and deletion. Listing straight from the Map
235
+ // would make an edited SKILL.md stale here while a named read in the same
236
+ // run saw the new metadata — two catalogs depending on which operation the
237
+ // model happened to try first.
238
+ for (const registeredName of [...this.skills.keys()]) {
239
+ const loaded = await this.load(registeredName, 'metadata')
240
+ if (!loaded) continue
241
+ const skill = loaded.skill
242
+ entries.push({
243
+ registeredName,
244
+ description: skill.metadata.description,
245
+ location: join(skill.dirPath, SKILL_FILENAME),
246
+ ...(skill.metadata.allowedTools === undefined
247
+ ? {}
248
+ : { allowedTools: skill.metadata.allowedTools }),
249
+ ...(skill.metadata.invocation === undefined
250
+ ? {}
251
+ : { invocation: skill.metadata.invocation }),
252
+ })
253
+ }
254
+ return entries
255
+ }
256
+
210
257
  /**
211
258
  * Every registered name.
212
259
  *
@@ -3,6 +3,7 @@ import type {
3
3
  ComputerUseAction,
4
4
  ComputerUseCapabilities,
5
5
  ComputerUseHost,
6
+ ComputerUseOutcomeUnknown,
6
7
  ComputerUseResult,
7
8
  } from '../../types/computer-use/index.js'
8
9
  import type { ToolDefinition, ToolResult } from '../../types/tool/index.js'
@@ -105,6 +106,38 @@ function buildDescription(host: ComputerUseHost): string {
105
106
  return lines.join(' ')
106
107
  }
107
108
 
109
+ function pointLabel(point: { readonly x: number; readonly y: number }): string {
110
+ return `(${point.x}, ${point.y})`
111
+ }
112
+
113
+ function quotedText(value: string): string {
114
+ const oneLine = value.replace(/\s+/g, ' ')
115
+ const visible = oneLine.length > 64 ? `${oneLine.slice(0, 63)}…` : oneLine
116
+ return JSON.stringify(visible)
117
+ }
118
+
119
+ /** Human activity text; the raw action union remains the model-facing input. */
120
+ function actionLabel(input: ActionInput): string {
121
+ switch (input.type) {
122
+ case 'screenshot':
123
+ return 'Capture screenshot'
124
+ case 'cursor_position':
125
+ return 'Read cursor position'
126
+ case 'mouse_move':
127
+ return `Move pointer to ${pointLabel(input.to)}`
128
+ case 'mouse_click':
129
+ return `Click ${input.button} at ${pointLabel(input.at)}`
130
+ case 'mouse_drag':
131
+ return `Drag ${input.button} from ${pointLabel(input.from)} to ${pointLabel(input.to)}`
132
+ case 'scroll':
133
+ return `Scroll ${input.direction} ${input.amount} at ${pointLabel(input.at)}`
134
+ case 'type_text':
135
+ return `Type ${quotedText(input.text)}`
136
+ case 'key':
137
+ return `Press ${input.keys}`
138
+ }
139
+ }
140
+
108
141
  function resultToToolResult(result: ComputerUseResult): ToolResult {
109
142
  switch (result.type) {
110
143
  case 'screenshot': {
@@ -132,6 +165,41 @@ function resultToToolResult(result: ComputerUseResult): ToolResult {
132
165
  }
133
166
  }
134
167
 
168
+ function isOutcomeUnknown(
169
+ value: unknown,
170
+ action: ComputerUseAction['type'],
171
+ ): value is ComputerUseOutcomeUnknown {
172
+ if (typeof value !== 'object' || value === null) return false
173
+ const candidate = value as Partial<ComputerUseOutcomeUnknown>
174
+ return (
175
+ candidate.code === 'computer_use_outcome_unknown' &&
176
+ candidate.action === action &&
177
+ candidate.outcome === 'unknown' &&
178
+ candidate.retrySafety === 'unsafe' &&
179
+ typeof candidate.timedOut === 'boolean' &&
180
+ typeof candidate.exitCode === 'number' &&
181
+ Number.isInteger(candidate.exitCode) &&
182
+ typeof candidate.message === 'string' &&
183
+ candidate.message.length > 0
184
+ )
185
+ }
186
+
187
+ function unknownOutcomeToToolResult(error: ComputerUseOutcomeUnknown): ToolResult {
188
+ return {
189
+ success: false,
190
+ output: '',
191
+ error: error.message,
192
+ data: {
193
+ code: error.code,
194
+ action: error.action,
195
+ outcome: error.outcome,
196
+ retrySafety: error.retrySafety,
197
+ timedOut: error.timedOut,
198
+ exitCode: error.exitCode,
199
+ },
200
+ }
201
+ }
202
+
135
203
  /**
136
204
  * Factory: given a ComputerUseHost (provided by the consumer — e.g.
137
205
  * @namzu/computer-use's SubprocessComputerUseHost), returns a ToolDefinition
@@ -162,6 +230,15 @@ export function createComputerUseTool(host: ComputerUseHost): ToolDefinition<Act
162
230
  readOnly: false,
163
231
  destructive: (input: ActionInput) => DESTRUCTIVE_ACTION_TYPES.has(input.type),
164
232
  concurrencySafe: false,
233
+ presentCall: (input) => ({
234
+ kind: 'generic',
235
+ label: actionLabel(input),
236
+ presentation: 'activity',
237
+ }),
238
+ presentResult: (_input, result) =>
239
+ result.success && result.output.trim().toLowerCase() === 'ok'
240
+ ? { kind: 'generic', label: result.output, visibility: 'hidden' }
241
+ : undefined,
165
242
 
166
243
  async execute(input, _context): Promise<ToolResult> {
167
244
  const required = requiredCapability(input.type)
@@ -172,8 +249,15 @@ export function createComputerUseTool(host: ComputerUseHost): ToolDefinition<Act
172
249
  error: `computer_use: action "${input.type}" requires capability "${required}" which is not available on this host (displayServer=${host.capabilities.displayServer}).`,
173
250
  }
174
251
  }
175
- const result = await host.execute(input as ComputerUseAction)
176
- return resultToToolResult(result)
252
+ try {
253
+ const result = await host.execute(input as ComputerUseAction)
254
+ return resultToToolResult(result)
255
+ } catch (error) {
256
+ if (isOutcomeUnknown(error, input.type)) {
257
+ return unknownOutcomeToToolResult(error)
258
+ }
259
+ throw error
260
+ }
177
261
  },
178
262
  })
179
263
  }
@@ -13,12 +13,12 @@ import { defineTool } from '../defineTool.js'
13
13
  * loop cannot reach further than the twenty calls could have.
14
14
  *
15
15
  * **The program's reach is the RUN's reach, and nothing wider.** Every
16
- * capability it can call is a tool already in this run's registry, already
17
- * narrowed by the turn's `allowedTools`, already subject to the same
18
- * dispatch that a model-issued call goes through the permission gate, the
19
- * approval policy, the audit record. There is no second path: the runtime
20
- * hands the host a name and an input, and the host does exactly what it
21
- * would have done for a `tool_use` block.
16
+ * capability it can call is a tool already in this run's registry and
17
+ * narrowed by the turn's `allowedTools`. The host dispatch records every
18
+ * child in the run and applies the run's operator authorization gate. An
19
+ * explicit allow proceeds; a denial or an undecided child fails closed and
20
+ * leaves a durable refusal, because an already-executing parent cannot open
21
+ * a second durable human-review turn on the program's behalf.
22
22
  *
23
23
  * Opt-in, and not in the default builtin set. A run that does not need
24
24
  * model-authored control flow should not have a way to execute
@@ -100,11 +100,9 @@ export function buildRunCodeTool(options: RunCodeToolOptions = {}) {
100
100
  maxOutputBytes: options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT,
101
101
  ...(context.abortSignal ? { signal: context.abortSignal } : {}),
102
102
  onHostCall: async (request, operation) => {
103
- // Through the SAME registry, with the SAME context. Not a
104
- // parallel dispatch path: a second path is a second place
105
- // for the permission gate to be forgotten, and the one that
106
- // forgot it would be the one a model reached through a
107
- // program.
103
+ // Through the run-owned nested dispatch: registry narrowing,
104
+ // operator authorization, audit/event lineage and invocation
105
+ // cancellation remain host-owned rather than worker-owned.
108
106
  try {
109
107
  const toolResult = await dispatch(request.name, request.input, {
110
108
  signal: operation.signal,