@namzu/sdk 57.1.0 → 58.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 (254) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -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/context-window.d.ts +1 -1
  14. package/dist/compaction/context-window.d.ts.map +1 -1
  15. package/dist/compaction/context-window.js +8 -1
  16. package/dist/compaction/context-window.js.map +1 -1
  17. package/dist/compaction/plan.d.ts.map +1 -1
  18. package/dist/compaction/plan.js +2 -2
  19. package/dist/compaction/plan.js.map +1 -1
  20. package/dist/config/runtime.d.ts +10 -0
  21. package/dist/config/runtime.d.ts.map +1 -1
  22. package/dist/config/runtime.js +2 -0
  23. package/dist/config/runtime.js.map +1 -1
  24. package/dist/connector/mcp/client.d.ts +8 -0
  25. package/dist/connector/mcp/client.d.ts.map +1 -1
  26. package/dist/connector/mcp/client.js +75 -2
  27. package/dist/connector/mcp/client.js.map +1 -1
  28. package/dist/constants/mcp/index.d.ts +2 -0
  29. package/dist/constants/mcp/index.d.ts.map +1 -1
  30. package/dist/constants/mcp/index.js +2 -0
  31. package/dist/constants/mcp/index.js.map +1 -1
  32. package/dist/constants/plugin/index.d.ts +2 -0
  33. package/dist/constants/plugin/index.d.ts.map +1 -1
  34. package/dist/constants/plugin/index.js +2 -0
  35. package/dist/constants/plugin/index.js.map +1 -1
  36. package/dist/manager/agent/lifecycle.d.ts +1 -1
  37. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  38. package/dist/manager/agent/lifecycle.js +25 -4
  39. package/dist/manager/agent/lifecycle.js.map +1 -1
  40. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  41. package/dist/manager/session/turn-recorder.js +2 -1
  42. package/dist/manager/session/turn-recorder.js.map +1 -1
  43. package/dist/persona/assembler.d.ts.map +1 -1
  44. package/dist/persona/assembler.js +2 -2
  45. package/dist/persona/assembler.js.map +1 -1
  46. package/dist/plugin/index.d.ts +1 -1
  47. package/dist/plugin/index.d.ts.map +1 -1
  48. package/dist/plugin/lifecycle.d.ts +8 -1
  49. package/dist/plugin/lifecycle.d.ts.map +1 -1
  50. package/dist/plugin/lifecycle.js +9 -2
  51. package/dist/plugin/lifecycle.js.map +1 -1
  52. package/dist/plugin/shell-hook-http.d.ts +39 -0
  53. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  54. package/dist/plugin/shell-hook-http.js +163 -0
  55. package/dist/plugin/shell-hook-http.js.map +1 -0
  56. package/dist/plugin/shell-hook.d.ts +93 -11
  57. package/dist/plugin/shell-hook.d.ts.map +1 -1
  58. package/dist/plugin/shell-hook.js +223 -39
  59. package/dist/plugin/shell-hook.js.map +1 -1
  60. package/dist/public-runtime.d.ts +2 -2
  61. package/dist/public-runtime.d.ts.map +1 -1
  62. package/dist/public-runtime.js +2 -2
  63. package/dist/public-runtime.js.map +1 -1
  64. package/dist/public-tools.d.ts +1 -0
  65. package/dist/public-tools.d.ts.map +1 -1
  66. package/dist/public-tools.js +2 -0
  67. package/dist/public-tools.js.map +1 -1
  68. package/dist/public-types.d.ts +1 -1
  69. package/dist/public-types.d.ts.map +1 -1
  70. package/dist/runtime/query/executor.d.ts +1 -0
  71. package/dist/runtime/query/executor.d.ts.map +1 -1
  72. package/dist/runtime/query/executor.js +34 -9
  73. package/dist/runtime/query/executor.js.map +1 -1
  74. package/dist/runtime/query/index.d.ts +2 -0
  75. package/dist/runtime/query/index.d.ts.map +1 -1
  76. package/dist/runtime/query/index.js +25 -1
  77. package/dist/runtime/query/index.js.map +1 -1
  78. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/index.js +47 -10
  80. package/dist/runtime/query/iteration/index.js.map +1 -1
  81. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  82. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  83. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  84. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  85. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  86. package/dist/runtime/query/iteration/phases/compaction.js +7 -5
  87. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  88. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  89. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  90. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  91. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  93. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  94. package/dist/runtime/query/path-escalation.d.ts +2 -0
  95. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  96. package/dist/runtime/query/path-escalation.js +7 -4
  97. package/dist/runtime/query/path-escalation.js.map +1 -1
  98. package/dist/runtime/query/plugin-hooks.js +2 -2
  99. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  100. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  101. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  102. package/dist/runtime/query/prepare-turn.js +25 -0
  103. package/dist/runtime/query/prepare-turn.js.map +1 -1
  104. package/dist/runtime/query/review-policy.d.ts +3 -2
  105. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  106. package/dist/runtime/query/review-policy.js +32 -13
  107. package/dist/runtime/query/review-policy.js.map +1 -1
  108. package/dist/session/retention/archive.js +1 -1
  109. package/dist/session/retention/archive.js.map +1 -1
  110. package/dist/session/workspace/driver.d.ts +6 -0
  111. package/dist/session/workspace/driver.d.ts.map +1 -1
  112. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  113. package/dist/session/workspace/git-worktree.js +28 -0
  114. package/dist/session/workspace/git-worktree.js.map +1 -1
  115. package/dist/skills/index.d.ts +1 -1
  116. package/dist/skills/index.d.ts.map +1 -1
  117. package/dist/skills/index.js +1 -1
  118. package/dist/skills/index.js.map +1 -1
  119. package/dist/skills/loader.d.ts +7 -0
  120. package/dist/skills/loader.d.ts.map +1 -1
  121. package/dist/skills/loader.js +53 -0
  122. package/dist/skills/loader.js.map +1 -1
  123. package/dist/skills/registry.d.ts +1 -1
  124. package/dist/skills/registry.d.ts.map +1 -1
  125. package/dist/skills/registry.js +2 -1
  126. package/dist/skills/registry.js.map +1 -1
  127. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  128. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  129. package/dist/tools/builtins/bash-cwd.js +127 -0
  130. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  131. package/dist/tools/builtins/bash.d.ts.map +1 -1
  132. package/dist/tools/builtins/bash.js +41 -6
  133. package/dist/tools/builtins/bash.js.map +1 -1
  134. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  135. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  136. package/dist/tools/builtins/bounded-file-read.js +67 -0
  137. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  138. package/dist/tools/builtins/grep.d.ts +6 -1
  139. package/dist/tools/builtins/grep.d.ts.map +1 -1
  140. package/dist/tools/builtins/grep.js +187 -24
  141. package/dist/tools/builtins/grep.js.map +1 -1
  142. package/dist/tools/builtins/read-file.d.ts +1 -0
  143. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  144. package/dist/tools/builtins/read-file.js +113 -28
  145. package/dist/tools/builtins/read-file.js.map +1 -1
  146. package/dist/tools/builtins/read-media.d.ts +68 -0
  147. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  148. package/dist/tools/builtins/read-media.js +397 -0
  149. package/dist/tools/builtins/read-media.js.map +1 -0
  150. package/dist/tools/builtins/search-tools.js +1 -1
  151. package/dist/tools/builtins/search-tools.js.map +1 -1
  152. package/dist/tools/builtins/skill.d.ts +4 -2
  153. package/dist/tools/builtins/skill.d.ts.map +1 -1
  154. package/dist/tools/builtins/skill.js +61 -2
  155. package/dist/tools/builtins/skill.js.map +1 -1
  156. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  157. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  158. package/dist/tools/builtins/sleep-guard.js +33 -0
  159. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  160. package/dist/tools/builtins/view-image.d.ts +23 -1
  161. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  162. package/dist/tools/builtins/view-image.js +66 -58
  163. package/dist/tools/builtins/view-image.js.map +1 -1
  164. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  165. package/dist/tools/file-read-tracker.js +31 -0
  166. package/dist/tools/file-read-tracker.js.map +1 -1
  167. package/dist/toolsets/manager.js +3 -3
  168. package/dist/toolsets/manager.js.map +1 -1
  169. package/dist/types/agent/query.d.ts +2 -0
  170. package/dist/types/agent/query.d.ts.map +1 -1
  171. package/dist/types/agent/task.d.ts +6 -2
  172. package/dist/types/agent/task.d.ts.map +1 -1
  173. package/dist/types/agent/task.js.map +1 -1
  174. package/dist/types/connector/mcp.d.ts +46 -0
  175. package/dist/types/connector/mcp.d.ts.map +1 -1
  176. package/dist/types/hitl/index.d.ts +1 -1
  177. package/dist/types/plugin/index.d.ts +72 -5
  178. package/dist/types/plugin/index.d.ts.map +1 -1
  179. package/dist/types/plugin/index.js +12 -1
  180. package/dist/types/plugin/index.js.map +1 -1
  181. package/dist/types/session/sub-session.d.ts +6 -2
  182. package/dist/types/session/sub-session.d.ts.map +1 -1
  183. package/dist/types/skills/index.d.ts +36 -0
  184. package/dist/types/skills/index.d.ts.map +1 -1
  185. package/dist/types/skills/index.js +9 -0
  186. package/dist/types/skills/index.js.map +1 -1
  187. package/dist/types/tool/index.d.ts +22 -0
  188. package/dist/types/tool/index.d.ts.map +1 -1
  189. package/dist/types/tool/index.js.map +1 -1
  190. package/dist/types/workspace/ref.d.ts +2 -0
  191. package/dist/types/workspace/ref.d.ts.map +1 -1
  192. package/dist/utils/frontmatter.d.ts +12 -0
  193. package/dist/utils/frontmatter.d.ts.map +1 -1
  194. package/dist/utils/frontmatter.js +6 -0
  195. package/dist/utils/frontmatter.js.map +1 -1
  196. package/package.json +1 -1
  197. package/src/agents/QueryAgent.ts +2 -0
  198. package/src/agents/file-definitions.ts +126 -6
  199. package/src/authorization/protected-paths.ts +30 -2
  200. package/src/compaction/context-window.ts +10 -0
  201. package/src/compaction/plan.ts +12 -2
  202. package/src/config/runtime.ts +2 -0
  203. package/src/connector/mcp/client.ts +87 -1
  204. package/src/constants/mcp/index.ts +3 -0
  205. package/src/constants/plugin/index.ts +2 -0
  206. package/src/manager/agent/lifecycle.ts +24 -5
  207. package/src/manager/session/turn-recorder.ts +2 -1
  208. package/src/persona/assembler.ts +2 -2
  209. package/src/plugin/index.ts +5 -0
  210. package/src/plugin/lifecycle.ts +16 -2
  211. package/src/plugin/shell-hook-http.ts +181 -0
  212. package/src/plugin/shell-hook.ts +386 -89
  213. package/src/public-runtime.ts +2 -0
  214. package/src/public-tools.ts +2 -0
  215. package/src/public-types.ts +5 -0
  216. package/src/runtime/query/executor.ts +40 -10
  217. package/src/runtime/query/index.ts +33 -0
  218. package/src/runtime/query/iteration/index.ts +51 -11
  219. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  220. package/src/runtime/query/iteration/phases/compaction.ts +27 -5
  221. package/src/runtime/query/iteration/phases/context.ts +2 -0
  222. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  223. package/src/runtime/query/path-escalation.ts +11 -5
  224. package/src/runtime/query/plugin-hooks.ts +2 -2
  225. package/src/runtime/query/prepare-turn.ts +33 -0
  226. package/src/runtime/query/review-policy.ts +45 -16
  227. package/src/session/retention/archive.ts +1 -1
  228. package/src/session/workspace/driver.ts +6 -0
  229. package/src/session/workspace/git-worktree.ts +31 -0
  230. package/src/skills/index.ts +6 -1
  231. package/src/skills/loader.ts +54 -0
  232. package/src/skills/registry.ts +7 -6
  233. package/src/tools/builtins/bash-cwd.ts +156 -0
  234. package/src/tools/builtins/bash.ts +79 -34
  235. package/src/tools/builtins/bounded-file-read.ts +80 -0
  236. package/src/tools/builtins/grep.ts +198 -28
  237. package/src/tools/builtins/read-file.ts +151 -30
  238. package/src/tools/builtins/read-media.ts +483 -0
  239. package/src/tools/builtins/search-tools.ts +1 -1
  240. package/src/tools/builtins/skill.ts +91 -2
  241. package/src/tools/builtins/sleep-guard.ts +31 -0
  242. package/src/tools/builtins/view-image.ts +74 -62
  243. package/src/tools/file-read-tracker.ts +30 -0
  244. package/src/toolsets/manager.ts +3 -3
  245. package/src/types/agent/query.ts +2 -0
  246. package/src/types/agent/task.ts +6 -2
  247. package/src/types/connector/mcp.ts +48 -0
  248. package/src/types/hitl/index.ts +1 -1
  249. package/src/types/plugin/index.ts +102 -16
  250. package/src/types/session/sub-session.ts +6 -2
  251. package/src/types/skills/index.ts +48 -0
  252. package/src/types/tool/index.ts +22 -0
  253. package/src/types/workspace/ref.ts +2 -0
  254. package/src/utils/frontmatter.ts +21 -0
@@ -133,7 +133,17 @@ export function resolveTriggerThreshold(
133
133
  configured: number | undefined,
134
134
  windowTokens: number,
135
135
  maxOutputTokens?: number,
136
+ autoCompactWindowTokens?: number,
136
137
  ): number {
138
+ if (
139
+ autoCompactWindowTokens !== undefined &&
140
+ (!Number.isSafeInteger(autoCompactWindowTokens) || autoCompactWindowTokens <= 0)
141
+ )
142
+ throw new RangeError('autoCompactWindowTokens must be a positive safe integer.')
143
+ if (autoCompactWindowTokens !== undefined && windowTokens > 0) {
144
+ const window = Math.min(windowTokens, autoCompactWindowTokens)
145
+ return (resolveTriggerThreshold(configured, window, maxOutputTokens) * window) / windowTokens
146
+ }
137
147
  if (configured !== undefined) return configured
138
148
  if (!(windowTokens > 0)) return MIN_DERIVED_TRIGGER_FRACTION
139
149
  const reserve = Math.min(
@@ -168,7 +168,12 @@ export function planSalienceWorkingSet(input: {
168
168
  reclaimedTokens: working.reclaimedTokens,
169
169
  reliefWasEnough:
170
170
  (estimatedTokens - working.reclaimedTokens) / budget <
171
- resolveTriggerThreshold(config.triggerThreshold, budget, input.maxOutputTokens),
171
+ resolveTriggerThreshold(
172
+ config.triggerThreshold,
173
+ budget,
174
+ input.maxOutputTokens,
175
+ config.autoCompactWindowTokens,
176
+ ),
172
177
  working,
173
178
  }
174
179
  }
@@ -198,7 +203,12 @@ export function planCompaction(input: CompactionPlanInput): CompactionPlan {
198
203
  const reliefWasEnough =
199
204
  !input.force &&
200
205
  (estimatedTokens - reclaimedTokens) / budget <
201
- resolveTriggerThreshold(config.triggerThreshold, budget, input.maxOutputTokens)
206
+ resolveTriggerThreshold(
207
+ config.triggerThreshold,
208
+ budget,
209
+ input.maxOutputTokens,
210
+ config.autoCompactWindowTokens,
211
+ )
202
212
  return {
203
213
  kind: 'cleared',
204
214
  messages: edit.messages,
@@ -51,6 +51,8 @@ export const CompactionConfigSchema = z.object({
51
51
  * to a flat `0.7`; pass that to keep the earlier behaviour.
52
52
  */
53
53
  triggerThreshold: z.number().min(0).max(1).optional(),
54
+ /** Optional automatic-summary window; capped to the actual model window. */
55
+ autoCompactWindowTokens: z.number().int().positive().safe().optional(),
54
56
  /**
55
57
  * Fraction the pass must get the context BELOW to be worth keeping.
56
58
  *
@@ -6,6 +6,8 @@ import type {
6
6
  MCPConnectionStatus,
7
7
  MCPContentBlock,
8
8
  MCPDiscoverResult,
9
+ MCPElicitationRequest,
10
+ MCPElicitationResult,
9
11
  MCPEraCache,
10
12
  MCPEventListener,
11
13
  MCPInitializeResult,
@@ -58,6 +60,7 @@ import { type McpParamHeaderBinding, validateMcpHeaderAnnotations } from './x-mc
58
60
  import {
59
61
  DEFAULT_MCP_ERA_PROBE_TIMEOUT_MS,
60
62
  DEFAULT_MCP_REQUEST_TIMEOUT_MS,
63
+ JSON_RPC_INVALID_PARAMS,
61
64
  JSON_RPC_METHOD_NOT_FOUND,
62
65
  MCP_DISCOVER_METHOD,
63
66
  MCP_LEGACY_VERSIONS,
@@ -243,6 +246,8 @@ export class MCPClient {
243
246
  private subscriptionFailures = 0
244
247
  /** Best-effort protocol cancellations still belong to this connection generation. */
245
248
  private cancellationControllers = new Set<AbortController>()
249
+ /** Elicitations waiting on the person, by the server's request id. */
250
+ private elicitationControllers = new Map<string | number, AbortController>()
246
251
  private log: Logger
247
252
  private readonly config: MCPClientConfig
248
253
  private readonly requestTimeoutMs: number
@@ -383,7 +388,10 @@ export class MCPClient {
383
388
  const offered = MCP_LEGACY_VERSIONS[0]
384
389
  const result = (await this.request('initialize', {
385
390
  protocolVersion: offered,
386
- capabilities: this.config.capabilities ?? {},
391
+ capabilities: {
392
+ ...(this.config.capabilities ?? {}),
393
+ ...(this.config.onElicitation ? { elicitation: {} } : {}),
394
+ },
387
395
  clientInfo: this.config.clientInfo ?? NAMZU_CLIENT_INFO,
388
396
  })) as MCPInitializeResult
389
397
 
@@ -1555,6 +1563,13 @@ export class MCPClient {
1555
1563
  }
1556
1564
  }
1557
1565
 
1566
+ if (message.method === 'notifications/cancelled' && message.id === undefined) {
1567
+ const requestId = message.params?.requestId
1568
+ if (typeof requestId === 'string' || typeof requestId === 'number') {
1569
+ this.elicitationControllers.get(requestId)?.abort(new Error('Withdrawn by the server'))
1570
+ }
1571
+ }
1572
+
1558
1573
  if (message.method && message.id === undefined) {
1559
1574
  for (const handler of this.notificationHandlers) {
1560
1575
  handler(message.method, message.params)
@@ -1568,6 +1583,15 @@ export class MCPClient {
1568
1583
  // dropped on the floor, so a spec-current server sat waiting for a
1569
1584
  // reply that would never come, which looks exactly like a hang.
1570
1585
  // Answer honestly: we do not implement these yet.
1586
+ if (
1587
+ message.method === 'elicitation/create' &&
1588
+ message.id !== undefined &&
1589
+ this.config.onElicitation &&
1590
+ this.era?.kind === 'legacy'
1591
+ ) {
1592
+ void this.answerElicitation(message.id, message.params)
1593
+ return
1594
+ }
1571
1595
  if (message.method && message.id !== undefined) {
1572
1596
  this.log.warn('Declining unsupported server-initiated MCP request', {
1573
1597
  'namzu.connector.server': this.config.serverName,
@@ -1590,6 +1614,68 @@ export class MCPClient {
1590
1614
  }
1591
1615
  }
1592
1616
 
1617
+ /**
1618
+ * Answer one `elicitation/create`: form mode goes to the host's handler,
1619
+ * anything else is refused as invalid params so the server is never left
1620
+ * waiting. Only the action is logged, never the values the person typed.
1621
+ */
1622
+ private async answerElicitation(
1623
+ id: string | number,
1624
+ params: Record<string, unknown> | undefined,
1625
+ ): Promise<void> {
1626
+ const handler = this.config.onElicitation
1627
+ const reply = (body: { result: unknown } | { error: { code: number; message: string } }) =>
1628
+ this.transport.send({ jsonrpc: '2.0', id, ...body }).catch((err) => {
1629
+ this.log.debug('Failed to send elicitation reply', {
1630
+ 'exception.message': toErrorMessage(err),
1631
+ })
1632
+ })
1633
+ const schema = params?.requestedSchema
1634
+ const isForm = params?.mode === undefined || params.mode === 'form'
1635
+ if (
1636
+ !handler ||
1637
+ !isForm ||
1638
+ typeof params?.message !== 'string' ||
1639
+ typeof schema !== 'object' ||
1640
+ schema === null
1641
+ ) {
1642
+ await reply({
1643
+ error: {
1644
+ code: JSON_RPC_INVALID_PARAMS,
1645
+ message: isForm ? 'Invalid elicitation request' : 'Only form elicitation is supported',
1646
+ },
1647
+ })
1648
+ return
1649
+ }
1650
+ const controller = new AbortController()
1651
+ this.elicitationControllers.set(id, controller)
1652
+ this.cancellationControllers.add(controller)
1653
+ let result: MCPElicitationResult
1654
+ try {
1655
+ result = await handler(
1656
+ {
1657
+ serverName: this.config.serverName,
1658
+ message: params.message,
1659
+ requestedSchema: schema as MCPElicitationRequest['requestedSchema'],
1660
+ },
1661
+ { signal: controller.signal },
1662
+ )
1663
+ } catch (err) {
1664
+ this.log.warn('Elicitation handler failed', { 'exception.message': toErrorMessage(err) })
1665
+ result = { action: 'cancel' }
1666
+ } finally {
1667
+ this.elicitationControllers.delete(id)
1668
+ this.cancellationControllers.delete(controller)
1669
+ }
1670
+ // The server withdrew the request: it is no longer listening.
1671
+ if (controller.signal.aborted) return
1672
+ this.log.info('Elicitation answered', {
1673
+ 'namzu.connector.server': this.config.serverName,
1674
+ 'namzu.connector.elicitation.action': result.action,
1675
+ })
1676
+ await reply({ result: result.action === 'accept' ? result : { action: result.action } })
1677
+ }
1678
+
1593
1679
  private handleProgressNotification(params: Record<string, unknown> | undefined): void {
1594
1680
  const token = params?.progressToken
1595
1681
  if (typeof token !== 'string') return
@@ -70,6 +70,9 @@ export const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [
70
70
  */
71
71
  export const DEFAULT_MCP_REQUEST_TIMEOUT_MS = 30_000
72
72
 
73
+ /** JSON-RPC 2.0 reserved code for parameters the receiver cannot accept. */
74
+ export const JSON_RPC_INVALID_PARAMS = -32602
75
+
73
76
  /** JSON-RPC 2.0 reserved code for an unimplemented method. */
74
77
  export const JSON_RPC_METHOD_NOT_FOUND = -32601
75
78
 
@@ -32,6 +32,8 @@ export const MAX_MCP_SERVERS_PER_PLUGIN = 5
32
32
  export const MAX_SKILLS_PER_PLUGIN = 20
33
33
  export const MAX_CONNECTORS_PER_PLUGIN = 10
34
34
  export const MAX_PERSONAS_PER_PLUGIN = 5
35
+ export const MAX_COMMAND_DIRS_PER_PLUGIN = 10
36
+ export const MAX_AGENT_DIRS_PER_PLUGIN = 10
35
37
 
36
38
  /** Hook execution */
37
39
  export const HOOK_TIMEOUT_MS = 600_000
@@ -170,7 +170,7 @@ interface ChildSpawnRecord {
170
170
  /** Removes the child's log from the process-local lookup. */
171
171
  releaseLog?: () => void
172
172
  workspaceRef?: WorkspaceRef
173
- workspaceRetention?: 'retain'
173
+ workspaceRetention?: 'retain' | 'retain-changed'
174
174
  /**
175
175
  * What this child was actually granted, after the ancestor union.
176
176
  *
@@ -1739,8 +1739,9 @@ export class AgentManager {
1739
1739
  subSession = {
1740
1740
  ...subSession,
1741
1741
  workspaceId: workspaceRef.id,
1742
- ...(request?.mode === 'isolated' && request.retention === 'retain'
1743
- ? { workspaceRetention: 'retain' as const }
1742
+ ...(request?.mode === 'isolated' &&
1743
+ (request.retention === 'retain' || request.retention === 'retain-changed')
1744
+ ? { workspaceRetention: request.retention }
1744
1745
  : {}),
1745
1746
  }
1746
1747
  await store.updateSubSession(subSession, context.tenantId)
@@ -1781,8 +1782,9 @@ export class AgentManager {
1781
1782
  childDepth,
1782
1783
  childOwnerVersion: childSession.ownerVersion,
1783
1784
  workspaceRef,
1784
- ...(options.workspace?.mode === 'isolated' && options.workspace.retention === 'retain'
1785
- ? { workspaceRetention: 'retain' as const }
1785
+ ...(options.workspace?.mode === 'isolated' &&
1786
+ (options.workspace.retention === 'retain' || options.workspace.retention === 'retain-changed')
1787
+ ? { workspaceRetention: options.workspace.retention }
1786
1788
  : {}),
1787
1789
  }
1788
1790
  }
@@ -2394,6 +2396,23 @@ export class AgentManager {
2394
2396
  if (!spawnRecord.workspaceRef) return
2395
2397
  const backend = spawnRecord.workspaceRef.meta.backend
2396
2398
  if (!this.deps.workspaceRegistry.has(backend)) return
2399
+ if (spawnRecord.workspaceRetention === 'retain-changed') {
2400
+ // Keep the checkout unless it provably holds nothing. A look that fails,
2401
+ // or a backend that cannot say whether it moved, keeps it too.
2402
+ const seen = await this.deps.workspaceRegistry
2403
+ .get(backend)
2404
+ .inspect(spawnRecord.workspaceRef)
2405
+ .catch((inspectErr) => {
2406
+ this.log.warn('Workspace inspect failed; keeping it', {
2407
+ 'namzu.manager.backend': backend,
2408
+ 'namzu.sub_session.id': spawnRecord.subSessionId,
2409
+ 'exception.message': toErrorMessage(inspectErr),
2410
+ })
2411
+ return undefined
2412
+ })
2413
+ if (!seen) return
2414
+ if (seen.exists && (seen.isDirty || seen.hasNewCommits !== false)) return
2415
+ }
2397
2416
  await this.deps.workspaceRegistry
2398
2417
  .get(backend)
2399
2418
  .dispose(spawnRecord.workspaceRef)
@@ -228,7 +228,8 @@ export class TurnRecorder {
228
228
  #chain: Promise<void> = Promise.resolve()
229
229
  #failure: unknown = undefined
230
230
  #view: ViewEntry[] = []
231
- readonly #ids = new Map<Message, MessageId>()
231
+ // Identity bookkeeping must not keep messages removed by compaction alive.
232
+ readonly #ids = new WeakMap<Message, MessageId>()
232
233
  /** Every message's record id, kept after a compaction drops it from the fold. */
233
234
  readonly #recorded = new WeakMap<Message, MessageId>()
234
235
  readonly #transient = new WeakSet<Message>()
@@ -1,5 +1,5 @@
1
1
  import type { AgentPersona, OutputDiscipline } from '../types/persona/index.js'
2
- import { type Skill, isInvocableBy } from '../types/skills/index.js'
2
+ import { type Skill, isInvocableBy, skillModelDescription } from '../types/skills/index.js'
3
3
 
4
4
  export function assembleSystemPrompt(persona: AgentPersona, skills?: Skill[]): string {
5
5
  const sections: string[] = []
@@ -58,7 +58,7 @@ export function renderSkillsSection(skills?: Skill[]): string | null {
58
58
  const lines = [
59
59
  '<skill>',
60
60
  `<name>${s.metadata.name}</name>`,
61
- `<description>${s.metadata.description.trim()}</description>`,
61
+ `<description>${skillModelDescription(s.metadata)}</description>`,
62
62
  `<location>${s.dirPath}/SKILL.md</location>`,
63
63
  ]
64
64
  if (s.metadata.compatibility) {
@@ -17,8 +17,13 @@ export {
17
17
  shellHookVerdict,
18
18
  } from './shell-hook.js'
19
19
  export type {
20
+ HttpHookEntry,
21
+ PromptHookEntry,
20
22
  ShellHookEntry,
21
23
  ShellHookEvent,
24
+ ShellHookHandler,
25
+ ShellHookJudge,
26
+ ShellHookOnFailure,
22
27
  ShellHookInput,
23
28
  ShellHookOptions,
24
29
  ShellHookOutcome,
@@ -180,6 +180,8 @@ function immutableManifest(manifest: PluginDefinition['manifest']): PluginDefini
180
180
  ...(mcpServers ? { mcpServers: Object.freeze(mcpServers) } : {}),
181
181
  ...(manifest.connectors ? { connectors: Object.freeze([...manifest.connectors]) } : {}),
182
182
  ...(manifest.personas ? { personas: Object.freeze([...manifest.personas]) } : {}),
183
+ ...(manifest.commands ? { commands: Object.freeze([...manifest.commands]) } : {}),
184
+ ...(manifest.agents ? { agents: Object.freeze([...manifest.agents]) } : {}),
183
185
  })
184
186
  }
185
187
 
@@ -438,11 +440,23 @@ export class PluginLifecycleManager {
438
440
  if (index >= 0) this.listeners.splice(index, 1)
439
441
  }
440
442
 
441
- async install(pluginDir: string, scope: PluginScope): Promise<PluginDefinition> {
443
+ /**
444
+ * `options.authorityRoot` names the folder the plugin must stay inside when
445
+ * it is not the scope's own root: a plugin a person points at directly for
446
+ * one session is its own authority. It applies to this admission only.
447
+ */
448
+ async install(
449
+ pluginDir: string,
450
+ scope: PluginScope,
451
+ options: { readonly authorityRoot?: string } = {},
452
+ ): Promise<PluginDefinition> {
442
453
  // Admission and enable read the same constructor-owned capability. The
443
454
  // loader defaults this to false for direct callers; a manager that really
444
455
  // owns the registry is the authority that can opt the manifest in.
445
- const rootDir = await resolveWithinReal(this.scopeRoots[scope], pluginDir)
456
+ const rootDir = await resolveWithinReal(
457
+ options.authorityRoot ?? this.scopeRoots[scope],
458
+ pluginDir,
459
+ )
446
460
  const manifest = immutableManifest(
447
461
  await loadPluginManifest(rootDir, {
448
462
  skillsSupported: Boolean(this.skillRegistry),
@@ -0,0 +1,181 @@
1
+ /**
2
+ * An HTTP hook: the event as a JSON POST, the response as the hook's answer.
3
+ *
4
+ * A 2xx response is exit `0` with the body as stdout, so the JSON answer is
5
+ * read exactly as a command's is. Any other status, a network error and a
6
+ * deadline are the hook's own failure. The address is checked where the
7
+ * connection is made, not before it: a name that resolves to a private or
8
+ * link-local address (a cloud metadata service, a router) is refused, so a
9
+ * hook cannot be pointed at the machine's neighbours and a name cannot be
10
+ * re-resolved between the check and the connect. Loopback stays allowed for
11
+ * local development. Redirects are not followed.
12
+ */
13
+
14
+ import { type LookupAddress, lookup } from 'node:dns'
15
+ import { request as httpRequest } from 'node:http'
16
+ import { request as httpsRequest } from 'node:https'
17
+ import { isIP } from 'node:net'
18
+
19
+ /** Captured response bound. */
20
+ const MAX_BODY_BYTES = 64 * 1024
21
+
22
+ export interface HttpHookRequest {
23
+ readonly url: string
24
+ readonly body: string
25
+ readonly headers?: Readonly<Record<string, string>> | undefined
26
+ readonly allowedEnvVars?: readonly string[] | undefined
27
+ readonly timeoutMs: number
28
+ readonly env?: Readonly<Record<string, string | undefined>> | undefined
29
+ readonly signal?: AbortSignal | undefined
30
+ }
31
+
32
+ export interface HttpHookResponse {
33
+ readonly status: number | null
34
+ readonly body: string
35
+ readonly timedOut: boolean
36
+ readonly error?: string
37
+ }
38
+
39
+ /**
40
+ * Replace `$NAME` and `${NAME}` in a header value with the environment's
41
+ * value, but only for names the hook listed. Any other reference becomes the
42
+ * empty string, so a header can never carry a variable its author did not
43
+ * name. Line breaks and NUL are removed from the result, so a value cannot add
44
+ * a header of its own.
45
+ */
46
+ export function interpolateHookHeader(
47
+ value: string,
48
+ allowed: ReadonlySet<string>,
49
+ env: Readonly<Record<string, string | undefined>>,
50
+ ): string {
51
+ return value
52
+ .replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_all, braced, bare) => {
53
+ const name = (braced ?? bare) as string
54
+ return allowed.has(name) ? (env[name] ?? '') : ''
55
+ })
56
+ .replace(/[\r\n\0]/g, '')
57
+ }
58
+
59
+ /** Whether an address may be connected to: public, or loopback. */
60
+ export function isReachableHookAddress(address: string): boolean {
61
+ const family = isIP(address)
62
+ if (family === 4) {
63
+ const [a = 0, b = 0] = address.split('.').map(Number)
64
+ if (a === 127) return true
65
+ if (a === 0 || a === 10) return false
66
+ if (a === 169 && b === 254) return false
67
+ if (a === 172 && b >= 16 && b <= 31) return false
68
+ if (a === 192 && b === 168) return false
69
+ if (a === 100 && b >= 64 && b <= 127) return false
70
+ if (a >= 224) return false
71
+ return true
72
+ }
73
+ if (family === 6) {
74
+ const lower = address.toLowerCase()
75
+ if (lower === '::1') return true
76
+ if (lower === '::') return false
77
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower)
78
+ if (mapped?.[1]) return isReachableHookAddress(mapped[1])
79
+ if (/^f[cd]/.test(lower) || /^fe[89ab]/.test(lower)) return false
80
+ return true
81
+ }
82
+ return false
83
+ }
84
+
85
+ /** `dns.lookup` that refuses an address a hook may not reach. */
86
+ function guardedLookup(
87
+ hostname: string,
88
+ options: { all?: boolean; family?: number | 'IPv4' | 'IPv6' },
89
+ callback: (error: Error | null, address?: string | LookupAddress[], family?: number) => void,
90
+ ): void {
91
+ lookup(hostname, { ...options, all: true }, (error: Error | null, addresses: LookupAddress[]) => {
92
+ if (error) {
93
+ callback(error)
94
+ return
95
+ }
96
+ const list = addresses
97
+ const refused = list.find((entry) => !isReachableHookAddress(entry.address))
98
+ if (refused || list.length === 0) {
99
+ callback(
100
+ new Error(
101
+ `${hostname} resolves to ${refused?.address ?? 'nothing'}, a private or link-local address`,
102
+ ),
103
+ )
104
+ return
105
+ }
106
+ if (options.all) {
107
+ callback(null, list)
108
+ return
109
+ }
110
+ const first = list[0] as LookupAddress
111
+ callback(null, first.address, first.family)
112
+ })
113
+ }
114
+
115
+ export function postHookJson(input: HttpHookRequest): Promise<HttpHookResponse> {
116
+ return new Promise((resolve) => {
117
+ let url: URL
118
+ try {
119
+ url = new URL(input.url)
120
+ } catch {
121
+ resolve({ status: null, body: '', timedOut: false, error: 'the url is not valid' })
122
+ return
123
+ }
124
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
125
+ resolve({ status: null, body: '', timedOut: false, error: 'the url must be http or https' })
126
+ return
127
+ }
128
+ // A literal address never reaches the resolver, so it is checked here.
129
+ const literal = url.hostname.replace(/^\[|\]$/g, '')
130
+ if (isIP(literal) !== 0 && !isReachableHookAddress(literal)) {
131
+ resolve({
132
+ status: null,
133
+ body: '',
134
+ timedOut: false,
135
+ error: `${literal} is a private or link-local address`,
136
+ })
137
+ return
138
+ }
139
+ const allowed = new Set(input.allowedEnvVars ?? [])
140
+ const env = input.env ?? process.env
141
+ const headers: Record<string, string> = {
142
+ 'Content-Type': 'application/json',
143
+ 'Content-Length': String(Buffer.byteLength(input.body)),
144
+ }
145
+ for (const [name, value] of Object.entries(input.headers ?? {})) {
146
+ headers[name] = interpolateHookHeader(value, allowed, env)
147
+ }
148
+ let settled = false
149
+ let timedOut = false
150
+ const finish = (response: HttpHookResponse) => {
151
+ if (settled) return
152
+ settled = true
153
+ clearTimeout(timer)
154
+ input.signal?.removeEventListener('abort', onAbort)
155
+ resolve(response)
156
+ }
157
+ const onAbort = () => {
158
+ req.destroy()
159
+ finish({ status: null, body: '', timedOut: false, error: 'cancelled' })
160
+ }
161
+ const send = url.protocol === 'https:' ? httpsRequest : httpRequest
162
+ const req = send(url, { method: 'POST', headers, lookup: guardedLookup as never }, (res) => {
163
+ let body = ''
164
+ res.setEncoding('utf8')
165
+ res.on('data', (chunk: string) => {
166
+ if (body.length < MAX_BODY_BYTES) body = (body + chunk).slice(0, MAX_BODY_BYTES)
167
+ })
168
+ res.on('end', () => finish({ status: res.statusCode ?? null, body, timedOut: false }))
169
+ res.on('error', (error) => finish({ status: null, body, timedOut, error: error.message }))
170
+ })
171
+ const timer = setTimeout(() => {
172
+ timedOut = true
173
+ req.destroy()
174
+ finish({ status: null, body: '', timedOut: true })
175
+ }, input.timeoutMs)
176
+ req.on('error', (error) => finish({ status: null, body: '', timedOut, error: error.message }))
177
+ if (input.signal?.aborted) onAbort()
178
+ else input.signal?.addEventListener('abort', onAbort, { once: true })
179
+ req.end(input.body)
180
+ })
181
+ }