@namzu/sdk 3.0.0 → 3.2.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 (307) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/dist/advisory/__tests__/consultation-context.test.d.ts +2 -0
  3. package/dist/advisory/__tests__/consultation-context.test.d.ts.map +1 -0
  4. package/dist/advisory/__tests__/consultation-context.test.js +124 -0
  5. package/dist/advisory/__tests__/consultation-context.test.js.map +1 -0
  6. package/dist/advisory/context.d.ts +25 -0
  7. package/dist/advisory/context.d.ts.map +1 -1
  8. package/dist/advisory/context.js +18 -0
  9. package/dist/advisory/context.js.map +1 -1
  10. package/dist/advisory/executor.d.ts.map +1 -1
  11. package/dist/advisory/executor.js +25 -3
  12. package/dist/advisory/executor.js.map +1 -1
  13. package/dist/compaction/__tests__/context-reducer.test.d.ts +2 -0
  14. package/dist/compaction/__tests__/context-reducer.test.d.ts.map +1 -0
  15. package/dist/compaction/__tests__/context-reducer.test.js +197 -0
  16. package/dist/compaction/__tests__/context-reducer.test.js.map +1 -0
  17. package/dist/compaction/factory.d.ts +7 -0
  18. package/dist/compaction/factory.d.ts.map +1 -1
  19. package/dist/compaction/factory.js +7 -0
  20. package/dist/compaction/factory.js.map +1 -1
  21. package/dist/compaction/index.d.ts +2 -0
  22. package/dist/compaction/index.d.ts.map +1 -1
  23. package/dist/compaction/index.js +1 -0
  24. package/dist/compaction/index.js.map +1 -1
  25. package/dist/compaction/interface.d.ts +13 -0
  26. package/dist/compaction/interface.d.ts.map +1 -1
  27. package/dist/compaction/managers/null.d.ts +3 -0
  28. package/dist/compaction/managers/null.d.ts.map +1 -1
  29. package/dist/compaction/managers/null.js +3 -0
  30. package/dist/compaction/managers/null.js.map +1 -1
  31. package/dist/compaction/managers/slidingWindow.d.ts +6 -0
  32. package/dist/compaction/managers/slidingWindow.d.ts.map +1 -1
  33. package/dist/compaction/managers/slidingWindow.js +6 -0
  34. package/dist/compaction/managers/slidingWindow.js.map +1 -1
  35. package/dist/compaction/managers/structured.d.ts +10 -0
  36. package/dist/compaction/managers/structured.d.ts.map +1 -1
  37. package/dist/compaction/managers/structured.js +10 -0
  38. package/dist/compaction/managers/structured.js.map +1 -1
  39. package/dist/compaction/reducer.d.ts +86 -0
  40. package/dist/compaction/reducer.d.ts.map +1 -0
  41. package/dist/compaction/reducer.js +77 -0
  42. package/dist/compaction/reducer.js.map +1 -0
  43. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts +2 -0
  44. package/dist/connector/builtins/__tests__/oauth2-auth.test.d.ts.map +1 -0
  45. package/dist/connector/builtins/__tests__/oauth2-auth.test.js +54 -0
  46. package/dist/connector/builtins/__tests__/oauth2-auth.test.js.map +1 -0
  47. package/dist/connector/builtins/http.d.ts.map +1 -1
  48. package/dist/connector/builtins/http.js +24 -2
  49. package/dist/connector/builtins/http.js.map +1 -1
  50. package/dist/connector/builtins/http.test.js +18 -2
  51. package/dist/connector/builtins/http.test.js.map +1 -1
  52. package/dist/connector/index.d.ts +2 -2
  53. package/dist/connector/index.d.ts.map +1 -1
  54. package/dist/connector/index.js +1 -1
  55. package/dist/connector/index.js.map +1 -1
  56. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts +2 -0
  57. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.d.ts.map +1 -0
  58. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js +214 -0
  59. package/dist/connector/mcp/__tests__/prompts-and-lifecycle.test.js.map +1 -0
  60. package/dist/connector/mcp/client.d.ts +52 -1
  61. package/dist/connector/mcp/client.d.ts.map +1 -1
  62. package/dist/connector/mcp/client.js +86 -0
  63. package/dist/connector/mcp/client.js.map +1 -1
  64. package/dist/connector/mcp/discovery.d.ts +12 -1
  65. package/dist/connector/mcp/discovery.d.ts.map +1 -1
  66. package/dist/connector/mcp/discovery.js +19 -4
  67. package/dist/connector/mcp/discovery.js.map +1 -1
  68. package/dist/connector/mcp/index.d.ts +2 -2
  69. package/dist/connector/mcp/index.d.ts.map +1 -1
  70. package/dist/connector/mcp/index.js +1 -1
  71. package/dist/connector/mcp/index.js.map +1 -1
  72. package/dist/connector/mcp/server.d.ts +42 -1
  73. package/dist/connector/mcp/server.d.ts.map +1 -1
  74. package/dist/connector/mcp/server.js +77 -4
  75. package/dist/connector/mcp/server.js.map +1 -1
  76. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts +2 -0
  77. package/dist/manager/agent/__tests__/depth-limit-authority.test.d.ts.map +1 -0
  78. package/dist/manager/agent/__tests__/depth-limit-authority.test.js +58 -0
  79. package/dist/manager/agent/__tests__/depth-limit-authority.test.js.map +1 -0
  80. package/dist/plugin/__tests__/discovery-scopes.test.d.ts +2 -0
  81. package/dist/plugin/__tests__/discovery-scopes.test.d.ts.map +1 -0
  82. package/dist/plugin/__tests__/discovery-scopes.test.js +97 -0
  83. package/dist/plugin/__tests__/discovery-scopes.test.js.map +1 -0
  84. package/dist/plugin/__tests__/enable-contributions.test.js +5 -1
  85. package/dist/plugin/__tests__/enable-contributions.test.js.map +1 -1
  86. package/dist/plugin/__tests__/mcp-admission.test.d.ts +2 -0
  87. package/dist/plugin/__tests__/mcp-admission.test.d.ts.map +1 -0
  88. package/dist/plugin/__tests__/mcp-admission.test.js +192 -0
  89. package/dist/plugin/__tests__/mcp-admission.test.js.map +1 -0
  90. package/dist/plugin/lifecycle.d.ts +41 -0
  91. package/dist/plugin/lifecycle.d.ts.map +1 -1
  92. package/dist/plugin/lifecycle.js +29 -1
  93. package/dist/plugin/lifecycle.js.map +1 -1
  94. package/dist/plugin/loader.d.ts +39 -3
  95. package/dist/plugin/loader.d.ts.map +1 -1
  96. package/dist/plugin/loader.js +37 -4
  97. package/dist/plugin/loader.js.map +1 -1
  98. package/dist/public-runtime.d.ts +5 -3
  99. package/dist/public-runtime.d.ts.map +1 -1
  100. package/dist/public-runtime.js +11 -3
  101. package/dist/public-runtime.js.map +1 -1
  102. package/dist/public-types.d.ts +2 -2
  103. package/dist/public-types.d.ts.map +1 -1
  104. package/dist/rag/__tests__/namespace-isolation.test.d.ts +2 -0
  105. package/dist/rag/__tests__/namespace-isolation.test.d.ts.map +1 -0
  106. package/dist/rag/__tests__/namespace-isolation.test.js +80 -0
  107. package/dist/rag/__tests__/namespace-isolation.test.js.map +1 -0
  108. package/dist/rag/ingestion.d.ts.map +1 -1
  109. package/dist/rag/ingestion.js +1 -0
  110. package/dist/rag/ingestion.js.map +1 -1
  111. package/dist/rag/retriever.d.ts.map +1 -1
  112. package/dist/rag/retriever.js +2 -0
  113. package/dist/rag/retriever.js.map +1 -1
  114. package/dist/rag/vector-store.d.ts.map +1 -1
  115. package/dist/rag/vector-store.js +6 -0
  116. package/dist/rag/vector-store.js.map +1 -1
  117. package/dist/registry/tool/execute.d.ts.map +1 -1
  118. package/dist/registry/tool/execute.js +113 -109
  119. package/dist/registry/tool/execute.js.map +1 -1
  120. package/dist/runtime/query/__tests__/capability-negotiation.test.js +59 -0
  121. package/dist/runtime/query/__tests__/capability-negotiation.test.js.map +1 -1
  122. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts +10 -0
  123. package/dist/runtime/query/__tests__/per-step-skills.test.d.ts.map +1 -0
  124. package/dist/runtime/query/__tests__/per-step-skills.test.js +122 -0
  125. package/dist/runtime/query/__tests__/per-step-skills.test.js.map +1 -0
  126. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts +2 -0
  127. package/dist/runtime/query/__tests__/per-step-tool-choice.test.d.ts.map +1 -0
  128. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js +153 -0
  129. package/dist/runtime/query/__tests__/per-step-tool-choice.test.js.map +1 -0
  130. package/dist/runtime/query/__tests__/resume-run.test.d.ts +2 -0
  131. package/dist/runtime/query/__tests__/resume-run.test.d.ts.map +1 -0
  132. package/dist/runtime/query/__tests__/resume-run.test.js +211 -0
  133. package/dist/runtime/query/__tests__/resume-run.test.js.map +1 -0
  134. package/dist/runtime/query/executor.d.ts +1 -0
  135. package/dist/runtime/query/executor.d.ts.map +1 -1
  136. package/dist/runtime/query/executor.js +10 -1
  137. package/dist/runtime/query/executor.js.map +1 -1
  138. package/dist/runtime/query/index.d.ts +10 -0
  139. package/dist/runtime/query/index.d.ts.map +1 -1
  140. package/dist/runtime/query/index.js +26 -0
  141. package/dist/runtime/query/index.js.map +1 -1
  142. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  143. package/dist/runtime/query/iteration/index.js +118 -34
  144. package/dist/runtime/query/iteration/index.js.map +1 -1
  145. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts +2 -0
  146. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.d.ts.map +1 -0
  147. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js +96 -0
  148. package/dist/runtime/query/iteration/phases/compaction-model-routing.test.js.map +1 -0
  149. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  150. package/dist/runtime/query/iteration/phases/compaction.js +95 -5
  151. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  152. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts +2 -0
  153. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.d.ts.map +1 -0
  154. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js +180 -0
  155. package/dist/runtime/query/iteration/phases/context-reducer-dispatch.test.js.map +1 -0
  156. package/dist/runtime/query/iteration/phases/context.d.ts +9 -0
  157. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  158. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  159. package/dist/runtime/query/resume-run.d.ts +70 -0
  160. package/dist/runtime/query/resume-run.d.ts.map +1 -0
  161. package/dist/runtime/query/resume-run.js +46 -0
  162. package/dist/runtime/query/resume-run.js.map +1 -0
  163. package/dist/telemetry/__tests__/model-call-span.test.d.ts +2 -0
  164. package/dist/telemetry/__tests__/model-call-span.test.d.ts.map +1 -0
  165. package/dist/telemetry/__tests__/model-call-span.test.js +147 -0
  166. package/dist/telemetry/__tests__/model-call-span.test.js.map +1 -0
  167. package/dist/telemetry/__tests__/span-closure.test.d.ts +2 -0
  168. package/dist/telemetry/__tests__/span-closure.test.d.ts.map +1 -0
  169. package/dist/telemetry/__tests__/span-closure.test.js +124 -0
  170. package/dist/telemetry/__tests__/span-closure.test.js.map +1 -0
  171. package/dist/tools/advisory/index.js +1 -1
  172. package/dist/tools/advisory/index.js.map +1 -1
  173. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts +2 -0
  174. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.d.ts.map +1 -0
  175. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js +78 -0
  176. package/dist/tools/builtins/__tests__/edit-commits-atomically.test.js.map +1 -0
  177. package/dist/tools/builtins/__tests__/edit.test.js +242 -1
  178. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -1
  179. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts +2 -0
  180. package/dist/tools/builtins/__tests__/stale-file-refusal.test.d.ts.map +1 -0
  181. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js +187 -0
  182. package/dist/tools/builtins/__tests__/stale-file-refusal.test.js.map +1 -0
  183. package/dist/tools/builtins/atomic-write-file.d.ts +1 -1
  184. package/dist/tools/builtins/atomic-write-file.d.ts.map +1 -1
  185. package/dist/tools/builtins/atomic-write-file.js +31 -2
  186. package/dist/tools/builtins/atomic-write-file.js.map +1 -1
  187. package/dist/tools/builtins/content-fingerprint.d.ts +23 -0
  188. package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -0
  189. package/dist/tools/builtins/content-fingerprint.js +30 -0
  190. package/dist/tools/builtins/content-fingerprint.js.map +1 -0
  191. package/dist/tools/builtins/edit.d.ts.map +1 -1
  192. package/dist/tools/builtins/edit.js +172 -31
  193. package/dist/tools/builtins/edit.js.map +1 -1
  194. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  195. package/dist/tools/builtins/read-file.js +5 -2
  196. package/dist/tools/builtins/read-file.js.map +1 -1
  197. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  198. package/dist/tools/builtins/write-file.js +75 -27
  199. package/dist/tools/builtins/write-file.js.map +1 -1
  200. package/dist/tools/coordinator/__tests__/ask-user-question.test.js +55 -0
  201. package/dist/tools/coordinator/__tests__/ask-user-question.test.js.map +1 -1
  202. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts +2 -0
  203. package/dist/tools/coordinator/__tests__/plan-dependencies.test.d.ts.map +1 -0
  204. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js +126 -0
  205. package/dist/tools/coordinator/__tests__/plan-dependencies.test.js.map +1 -0
  206. package/dist/tools/coordinator/index.d.ts.map +1 -1
  207. package/dist/tools/coordinator/index.js +75 -5
  208. package/dist/tools/coordinator/index.js.map +1 -1
  209. package/dist/tools/coordinator/plan-dependencies.d.ts +43 -0
  210. package/dist/tools/coordinator/plan-dependencies.d.ts.map +1 -0
  211. package/dist/tools/coordinator/plan-dependencies.js +148 -0
  212. package/dist/tools/coordinator/plan-dependencies.js.map +1 -0
  213. package/dist/types/agent/supervisor.d.ts +15 -0
  214. package/dist/types/agent/supervisor.d.ts.map +1 -1
  215. package/dist/types/connector/core.d.ts +34 -0
  216. package/dist/types/connector/core.d.ts.map +1 -1
  217. package/dist/types/connector/definition.d.ts +10 -0
  218. package/dist/types/connector/definition.d.ts.map +1 -1
  219. package/dist/types/connector/mcp.d.ts +13 -0
  220. package/dist/types/connector/mcp.d.ts.map +1 -1
  221. package/dist/types/rag/retrieval.d.ts +16 -0
  222. package/dist/types/rag/retrieval.d.ts.map +1 -1
  223. package/dist/types/rag/storage.d.ts +9 -0
  224. package/dist/types/rag/storage.d.ts.map +1 -1
  225. package/dist/types/rag/vector.d.ts +11 -0
  226. package/dist/types/rag/vector.d.ts.map +1 -1
  227. package/dist/types/router/task-router.d.ts +19 -0
  228. package/dist/types/router/task-router.d.ts.map +1 -1
  229. package/dist/types/run/prepare-step.d.ts +56 -3
  230. package/dist/types/run/prepare-step.d.ts.map +1 -1
  231. package/dist/types/tool/index.d.ts +17 -1
  232. package/dist/types/tool/index.d.ts.map +1 -1
  233. package/dist/types/tool/index.js.map +1 -1
  234. package/dist/types/toolset/index.d.ts +22 -0
  235. package/dist/types/toolset/index.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/advisory/__tests__/consultation-context.test.ts +191 -0
  238. package/src/advisory/context.ts +32 -0
  239. package/src/advisory/executor.ts +30 -3
  240. package/src/compaction/__tests__/context-reducer.test.ts +239 -0
  241. package/src/compaction/factory.ts +7 -0
  242. package/src/compaction/index.ts +8 -0
  243. package/src/compaction/interface.ts +13 -0
  244. package/src/compaction/managers/null.ts +3 -0
  245. package/src/compaction/managers/slidingWindow.ts +6 -0
  246. package/src/compaction/managers/structured.ts +10 -0
  247. package/src/compaction/reducer.ts +154 -0
  248. package/src/connector/builtins/__tests__/oauth2-auth.test.ts +73 -0
  249. package/src/connector/builtins/http.test.ts +28 -2
  250. package/src/connector/builtins/http.ts +26 -2
  251. package/src/connector/index.ts +6 -2
  252. package/src/connector/mcp/__tests__/prompts-and-lifecycle.test.ts +286 -0
  253. package/src/connector/mcp/client.ts +95 -0
  254. package/src/connector/mcp/discovery.ts +19 -4
  255. package/src/connector/mcp/index.ts +6 -2
  256. package/src/connector/mcp/server.ts +101 -3
  257. package/src/manager/agent/__tests__/depth-limit-authority.test.ts +74 -0
  258. package/src/plugin/__tests__/discovery-scopes.test.ts +133 -0
  259. package/src/plugin/__tests__/enable-contributions.test.ts +5 -1
  260. package/src/plugin/__tests__/mcp-admission.test.ts +242 -0
  261. package/src/plugin/lifecycle.ts +57 -1
  262. package/src/plugin/loader.ts +57 -3
  263. package/src/public-runtime.ts +16 -1
  264. package/src/public-types.ts +5 -0
  265. package/src/rag/__tests__/namespace-isolation.test.ts +109 -0
  266. package/src/rag/ingestion.ts +1 -0
  267. package/src/rag/retriever.ts +2 -0
  268. package/src/rag/vector-store.ts +5 -0
  269. package/src/registry/tool/execute.ts +123 -119
  270. package/src/runtime/query/__tests__/capability-negotiation.test.ts +69 -0
  271. package/src/runtime/query/__tests__/per-step-skills.test.ts +154 -0
  272. package/src/runtime/query/__tests__/per-step-tool-choice.test.ts +180 -0
  273. package/src/runtime/query/__tests__/resume-run.test.ts +262 -0
  274. package/src/runtime/query/executor.ts +9 -1
  275. package/src/runtime/query/index.ts +39 -0
  276. package/src/runtime/query/iteration/index.ts +130 -34
  277. package/src/runtime/query/iteration/phases/compaction-model-routing.test.ts +125 -0
  278. package/src/runtime/query/iteration/phases/compaction.ts +106 -5
  279. package/src/runtime/query/iteration/phases/context-reducer-dispatch.test.ts +238 -0
  280. package/src/runtime/query/iteration/phases/context.ts +11 -0
  281. package/src/runtime/query/resume-run.ts +93 -0
  282. package/src/telemetry/__tests__/model-call-span.test.ts +189 -0
  283. package/src/telemetry/__tests__/span-closure.test.ts +153 -0
  284. package/src/tools/advisory/index.ts +1 -1
  285. package/src/tools/builtins/__tests__/edit-commits-atomically.test.ts +103 -0
  286. package/src/tools/builtins/__tests__/edit.test.ts +324 -1
  287. package/src/tools/builtins/__tests__/stale-file-refusal.test.ts +258 -0
  288. package/src/tools/builtins/atomic-write-file.ts +30 -2
  289. package/src/tools/builtins/content-fingerprint.ts +33 -0
  290. package/src/tools/builtins/edit.ts +188 -36
  291. package/src/tools/builtins/read-file.ts +5 -2
  292. package/src/tools/builtins/write-file.ts +79 -28
  293. package/src/tools/coordinator/__tests__/ask-user-question.test.ts +68 -0
  294. package/src/tools/coordinator/__tests__/plan-dependencies.test.ts +186 -0
  295. package/src/tools/coordinator/index.ts +116 -42
  296. package/src/tools/coordinator/plan-dependencies.ts +175 -0
  297. package/src/types/agent/supervisor.ts +15 -0
  298. package/src/types/connector/core.ts +34 -0
  299. package/src/types/connector/definition.ts +10 -0
  300. package/src/types/connector/mcp.ts +14 -0
  301. package/src/types/rag/retrieval.ts +16 -0
  302. package/src/types/rag/storage.ts +9 -0
  303. package/src/types/rag/vector.ts +11 -0
  304. package/src/types/router/task-router.ts +19 -0
  305. package/src/types/run/prepare-step.ts +58 -3
  306. package/src/types/tool/index.ts +17 -1
  307. package/src/types/toolset/index.ts +22 -0
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { AgentTaskContext } from '../../../types/agent/index.js'
4
+
5
+ /**
6
+ * Which `maxDepth` is in force.
7
+ *
8
+ * Two exist. `AgentManagerConfig.maxDepth` is enforced in
9
+ * `AgentManager.sendMessage`; `SupervisorAgentConfig.maxDepth` is read by
10
+ * nothing. For a recursion bound that is the worst way to be wrong — the
11
+ * number a reviewer sees in the supervisor config is not the number
12
+ * stopping runaway delegation.
13
+ *
14
+ * The first version of these tests passed the config where the registry
15
+ * goes, so every case silently ran against the default of 3 — and the case
16
+ * that used 3 "passed" while proving nothing. Configuring a value the
17
+ * default is not is what makes this a test.
18
+ */
19
+
20
+ async function manager(maxDepth: number) {
21
+ const { AgentManager } = await import('../lifecycle.js')
22
+ const registry = { resolve: vi.fn(), get: vi.fn(), getAll: vi.fn(() => []) }
23
+ const deps = { createAgent: vi.fn(), taskStore: undefined }
24
+ return new AgentManager(registry as never, { maxDepth }, deps as never)
25
+ }
26
+
27
+ function contextAtDepth(depth: number): AgentTaskContext {
28
+ return {
29
+ parentRunId: 'run_x',
30
+ parentAgentId: 'agent_x',
31
+ parentAbortController: new AbortController(),
32
+ depth,
33
+ tenantId: 'tnt_x',
34
+ } as unknown as AgentTaskContext
35
+ }
36
+
37
+ function send(m: Awaited<ReturnType<typeof manager>>, depth: number) {
38
+ return m.sendMessage(
39
+ { agentId: 'a', prompt: 'go', tenantId: 'tnt_x' } as never,
40
+ contextAtDepth(depth),
41
+ )
42
+ }
43
+
44
+ describe('the manager owns the recursion bound', () => {
45
+ it('refuses a task at the configured depth', async () => {
46
+ // 5, deliberately not the default of 3: a limit that happens to equal
47
+ // the default cannot tell "configured" from "ignored".
48
+ const mgr = await manager(5)
49
+
50
+ await expect(send(mgr, 5)).rejects.toThrow(/Max task depth 5/)
51
+ })
52
+
53
+ it('refuses past it too, not only exactly at it', async () => {
54
+ const mgr = await manager(2)
55
+
56
+ await expect(send(mgr, 7)).rejects.toThrow(/Max task depth 2/)
57
+ })
58
+
59
+ it('reports the depth reached alongside the limit', async () => {
60
+ const mgr = await manager(2)
61
+
62
+ // A limit without the current value sends someone to instrument the
63
+ // run to learn how deep it actually got.
64
+ await expect(send(mgr, 4)).rejects.toThrow(/current: 4/)
65
+ })
66
+
67
+ it('allows a task below the bound to proceed past this check', async () => {
68
+ const mgr = await manager(5)
69
+
70
+ // It gets past the depth gate and fails later on the stubbed
71
+ // registry — which is the point: the gate did not stop it.
72
+ await expect(send(mgr, 1)).rejects.not.toThrow(/Max task depth/)
73
+ })
74
+ })
@@ -0,0 +1,133 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import { PluginRuntimeConfigSchema } from '../../config/runtime.js'
7
+ import {
8
+ PLUGIN_MANIFEST_FILENAME,
9
+ PROJECT_PLUGIN_DIR,
10
+ USER_PLUGIN_DIR,
11
+ } from '../../constants/plugin/index.js'
12
+
13
+ /**
14
+ * `allowedScopes` read exactly like a trust boundary and enforced nothing.
15
+ *
16
+ * A plugin is arbitrary code with hooks into tool execution, and the two
17
+ * scopes are not equally trusted: a project plugin is reviewable in the repo
18
+ * the agent works on, a user plugin comes from a home directory the repo's
19
+ * reviewers never see. `PluginRuntimeConfig` carried `enabled`,
20
+ * `autoDiscovery` and `allowedScopes`; nothing anywhere consulted any of the
21
+ * three, and discovery scanned both locations unconditionally.
22
+ */
23
+
24
+ let home: string
25
+ let workdir: string
26
+
27
+ vi.mock('node:os', async () => {
28
+ const actual = await vi.importActual<typeof import('node:os')>('node:os')
29
+ return { ...actual, homedir: () => home }
30
+ })
31
+
32
+ async function plantPlugin(dir: string, name: string): Promise<void> {
33
+ const pluginDir = join(dir, name)
34
+ await mkdir(pluginDir, { recursive: true })
35
+ await writeFile(
36
+ join(pluginDir, PLUGIN_MANIFEST_FILENAME),
37
+ JSON.stringify({ name, version: '1.0.0' }),
38
+ 'utf-8',
39
+ )
40
+ }
41
+
42
+ beforeEach(async () => {
43
+ home = await mkdtemp(join(tmpdir(), 'namzu-home-'))
44
+ workdir = await mkdtemp(join(tmpdir(), 'namzu-work-'))
45
+ await plantPlugin(join(workdir, PROJECT_PLUGIN_DIR), 'project-plugin')
46
+ await plantPlugin(join(home, USER_PLUGIN_DIR), 'user-plugin')
47
+ })
48
+
49
+ afterEach(async () => {
50
+ await Promise.all([
51
+ rm(home, { recursive: true, force: true }),
52
+ rm(workdir, { recursive: true, force: true }),
53
+ ])
54
+ })
55
+
56
+ async function discover(
57
+ options?: Parameters<typeof import('../loader.js').discoverAllPluginDirs>[1],
58
+ ) {
59
+ const { discoverAllPluginDirs } = await import('../loader.js')
60
+ return discoverAllPluginDirs(workdir, options)
61
+ }
62
+
63
+ describe('a scope the host did not allow is not scanned', () => {
64
+ it('finds both when nothing is said', async () => {
65
+ const found = await discover()
66
+
67
+ expect(found.project).toHaveLength(1)
68
+ expect(found.user).toHaveLength(1)
69
+ })
70
+
71
+ it('keeps user plugins out when only project is allowed', async () => {
72
+ const found = await discover({ allowedScopes: ['project'] })
73
+
74
+ expect(found.project).toHaveLength(1)
75
+ // The setting that reads like a boundary now is one.
76
+ expect(found.user).toEqual([])
77
+ })
78
+
79
+ it('keeps project plugins out when only user is allowed', async () => {
80
+ const found = await discover({ allowedScopes: ['user'] })
81
+
82
+ expect(found.project).toEqual([])
83
+ expect(found.user).toHaveLength(1)
84
+ })
85
+
86
+ it('finds nothing when neither scope is allowed', async () => {
87
+ const found = await discover({ allowedScopes: [] })
88
+
89
+ expect(found).toEqual({ project: [], user: [] })
90
+ })
91
+ })
92
+
93
+ describe('the runtime switches turn discovery off', () => {
94
+ it('discovers nothing when the plugin runtime is disabled', async () => {
95
+ const found = await discover({ enabled: false })
96
+
97
+ expect(found).toEqual({ project: [], user: [] })
98
+ })
99
+
100
+ it('discovers nothing when auto-discovery is off', async () => {
101
+ const found = await discover({ autoDiscovery: false })
102
+
103
+ expect(found).toEqual({ project: [], user: [] })
104
+ })
105
+
106
+ it('a default-parsed config discovers nothing, because the runtime ships off', async () => {
107
+ // `enabled` defaults to false in the schema. A host who parses the
108
+ // config and passes it gets the documented default rather than a
109
+ // silently-on plugin runtime.
110
+ const found = await discover(PluginRuntimeConfigSchema.parse({}))
111
+
112
+ expect(found).toEqual({ project: [], user: [] })
113
+ })
114
+
115
+ it('an enabled default-parsed config finds both scopes', async () => {
116
+ const found = await discover(PluginRuntimeConfigSchema.parse({ enabled: true }))
117
+
118
+ expect(found.project).toHaveLength(1)
119
+ expect(found.user).toHaveLength(1)
120
+ })
121
+
122
+ it('a parsed config carries its scope restriction through', async () => {
123
+ const config = PluginRuntimeConfigSchema.parse({
124
+ enabled: true,
125
+ allowedScopes: ['project'],
126
+ })
127
+
128
+ const found = await discover(config)
129
+
130
+ expect(found.project).toHaveLength(1)
131
+ expect(found.user).toEqual([])
132
+ })
133
+ })
@@ -11,11 +11,15 @@ const mockDisconnect = vi.fn(async (): Promise<void> => undefined)
11
11
  const mockListTools = vi.fn(async (): Promise<unknown[]> => [])
12
12
 
13
13
  vi.mock('../../connector/mcp/client.js', () => ({
14
- MCPClient: vi.fn().mockImplementation(() => ({
14
+ MCPClient: vi.fn().mockImplementation((config: { serverName: string }) => ({
15
15
  id: 'mcp-client-mock',
16
16
  connect: mockConnect,
17
17
  disconnect: mockDisconnect,
18
18
  listTools: mockListTools,
19
+ // The real client has always had this; the mock did not, which went
20
+ // unnoticed while nothing on this path asked the client which server
21
+ // it was talking to. Admission does — a policy is per server name.
22
+ getState: () => ({ serverName: config.serverName }),
19
23
  })),
20
24
  }))
21
25
 
@@ -0,0 +1,242 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import type { MCPToolDefinition } from '../../types/connector/index.js'
7
+
8
+ /**
9
+ * A plugin's MCP server decided what entered the agent's tool registry.
10
+ *
11
+ * `MCPToolDiscovery` has held the admission boundary — a per-server
12
+ * allow/deny policy, and detection for a server whose tool set changes
13
+ * between discoveries — since it was written, and nothing outside its own
14
+ * tests ever constructed one. `PluginLifecycleManager.attachMCPServer`, the
15
+ * only code in the tree that connects a real MCP server, called
16
+ * `client.listTools()` and registered whatever came back.
17
+ *
18
+ * So least privilege was inverted at the one place it mattered: the remote
19
+ * side chose. Tools land as `deferred` and a run's `allowedTools` filters the
20
+ * model-visible catalogue, so this was not "arbitrary tools reach the model
21
+ * immediately" — but the check written for exactly this was not on the path.
22
+ */
23
+
24
+ let advertised: MCPToolDefinition[] = []
25
+ let clientCount = 0
26
+
27
+ vi.mock('../../connector/mcp/client.js', () => ({
28
+ MCPClient: class {
29
+ readonly id: string
30
+ private serverName: string
31
+ constructor(config: { serverName: string }) {
32
+ this.id = `client_${clientCount++}`
33
+ this.serverName = config.serverName
34
+ }
35
+ async connect(): Promise<void> {}
36
+ async disconnect(): Promise<void> {}
37
+ isConnected(): boolean {
38
+ return true
39
+ }
40
+ getState() {
41
+ return { serverName: this.serverName }
42
+ }
43
+ async listTools(): Promise<MCPToolDefinition[]> {
44
+ return advertised
45
+ }
46
+ },
47
+ }))
48
+
49
+ function tool(name: string): MCPToolDefinition {
50
+ return { name, description: name, inputSchema: { type: 'object', properties: {} } }
51
+ }
52
+
53
+ const log = {
54
+ debug: vi.fn(),
55
+ info: vi.fn(),
56
+ warn: vi.fn(),
57
+ error: vi.fn(),
58
+ child: () => log,
59
+ } as never
60
+
61
+ let root: string
62
+
63
+ interface Harness {
64
+ readonly registered: string[]
65
+ readonly manager: import('../lifecycle.js').PluginLifecycleManager
66
+ enable(name: string): Promise<import('../../types/ids/index.js').PluginId>
67
+ }
68
+
69
+ async function harness(config: Record<string, unknown> = {}): Promise<Harness> {
70
+ const { PluginLifecycleManager } = await import('../lifecycle.js')
71
+ const { PluginRegistry } = await import('../../registry/plugin/index.js')
72
+
73
+ const registered: string[] = []
74
+ const toolRegistry = {
75
+ register: (definition: { name: string }) => registered.push(definition.name),
76
+ unregister: () => undefined,
77
+ get: () => undefined,
78
+ } as never
79
+
80
+ const manager = new PluginLifecycleManager({
81
+ pluginRegistry: new PluginRegistry(),
82
+ toolRegistry,
83
+ log,
84
+ ...config,
85
+ } as never)
86
+
87
+ return {
88
+ registered,
89
+ manager,
90
+ enable: async (name: string) => {
91
+ const dir = join(root, name)
92
+ await mkdir(dir, { recursive: true })
93
+ await writeFile(
94
+ join(dir, 'plugin.json'),
95
+ JSON.stringify({
96
+ name,
97
+ version: '1.0.0',
98
+ description: 'MCP server plugin fixture',
99
+ mcpServers: [{ name: 'files', command: 'node', args: ['server.js'] }],
100
+ }),
101
+ 'utf-8',
102
+ )
103
+ const plugin = await manager.install(dir, 'project')
104
+ await manager.enable(plugin.id)
105
+ return plugin.id
106
+ },
107
+ }
108
+ }
109
+
110
+ beforeEach(async () => {
111
+ root = await mkdtemp(join(tmpdir(), 'namzu-mcp-admit-'))
112
+ clientCount = 0
113
+ advertised = [tool('read_file'), tool('write_file'), tool('delete_everything')]
114
+ })
115
+
116
+ afterEach(async () => {
117
+ await rm(root, { recursive: true, force: true })
118
+ vi.clearAllMocks()
119
+ })
120
+
121
+ describe('what a plugin server advertises is not what the registry gets', () => {
122
+ it('admits only the tools an allowlist names', async () => {
123
+ const h = await harness({ mcpToolPolicies: { files: { allow: ['read_file', 'write_file'] } } })
124
+
125
+ await h.enable('srv')
126
+
127
+ expect(h.registered.some((n) => n.endsWith('read_file'))).toBe(true)
128
+ expect(h.registered.some((n) => n.endsWith('write_file'))).toBe(true)
129
+ // The one the server offered and the host never agreed to.
130
+ expect(h.registered.some((n) => n.endsWith('delete_everything'))).toBe(false)
131
+ })
132
+
133
+ it('refuses a denied tool through the wildcard policy', async () => {
134
+ const h = await harness({ mcpToolPolicies: { '*': { deny: ['delete_everything'] } } })
135
+
136
+ await h.enable('srv')
137
+
138
+ expect(h.registered).toHaveLength(2)
139
+ expect(h.registered.some((n) => n.endsWith('delete_everything'))).toBe(false)
140
+ })
141
+
142
+ it('still admits everything when no policy is configured', async () => {
143
+ // Adding a boundary must not turn every existing host's working plugin
144
+ // into a broken one.
145
+ const h = await harness()
146
+
147
+ await h.enable('srv')
148
+
149
+ expect(h.registered).toHaveLength(3)
150
+ })
151
+
152
+ it('namespaces what it admits, exactly as before', async () => {
153
+ const h = await harness({ mcpToolPolicies: { files: { allow: ['read_file'] } } })
154
+
155
+ await h.enable('srv')
156
+
157
+ expect(h.registered[0]).toContain('mcp__files__read_file')
158
+ })
159
+ })
160
+
161
+ describe('a server that changes its tools between connections is reported', () => {
162
+ it('says nothing the first time, having nothing to compare against', async () => {
163
+ const onMCPToolDrift = vi.fn()
164
+ const h = await harness({ onMCPToolDrift })
165
+
166
+ await h.enable('srv')
167
+
168
+ expect(onMCPToolDrift).not.toHaveBeenCalled()
169
+ })
170
+
171
+ it('reports a tool the server grew after it was first approved', async () => {
172
+ const onMCPToolDrift = vi.fn()
173
+ const h = await harness({ onMCPToolDrift })
174
+ const first = await h.enable('srv-a')
175
+
176
+ // The rug pull: advertise something benign while a host is deciding,
177
+ // swap it afterwards. A NEW client connects to the SAME server, which
178
+ // is why the remembered set is keyed by server name — keyed by client
179
+ // id, as it was, every connection was its own first and this could
180
+ // never fire.
181
+ await h.manager.disable(first)
182
+ advertised = [...advertised, tool('exfiltrate')]
183
+ await h.enable('srv-b')
184
+
185
+ expect(onMCPToolDrift).toHaveBeenCalledTimes(1)
186
+ expect(onMCPToolDrift.mock.calls[0]?.[0]).toMatchObject({
187
+ serverName: 'files',
188
+ drift: { added: ['exfiltrate'] },
189
+ })
190
+ })
191
+
192
+ it('reports a tool whose shape changed under the same name', async () => {
193
+ const onMCPToolDrift = vi.fn()
194
+ const h = await harness({ onMCPToolDrift })
195
+ const first = await h.enable('srv-a')
196
+
197
+ await h.manager.disable(first)
198
+ // Same name, different input shape — the version a name-only check
199
+ // misses entirely.
200
+ advertised = [
201
+ {
202
+ name: 'read_file',
203
+ description: 'read a file',
204
+ inputSchema: { type: 'object', properties: { path: { type: 'string' } } },
205
+ },
206
+ tool('write_file'),
207
+ tool('delete_everything'),
208
+ ]
209
+ await h.enable('srv-b')
210
+
211
+ expect(onMCPToolDrift).toHaveBeenCalledTimes(1)
212
+ expect(onMCPToolDrift.mock.calls[0]?.[0]).toMatchObject({ drift: { changed: ['read_file'] } })
213
+ })
214
+
215
+ it('says nothing when the server offers exactly what it offered before', async () => {
216
+ const onMCPToolDrift = vi.fn()
217
+ const h = await harness({ onMCPToolDrift })
218
+ const first = await h.enable('srv-a')
219
+
220
+ await h.manager.disable(first)
221
+ await h.enable('srv-b')
222
+
223
+ expect(onMCPToolDrift).not.toHaveBeenCalled()
224
+ })
225
+
226
+ it('compares what was ADMITTED, not what was advertised', async () => {
227
+ const onMCPToolDrift = vi.fn()
228
+ const h = await harness({
229
+ mcpToolPolicies: { files: { allow: ['read_file'] } },
230
+ onMCPToolDrift,
231
+ })
232
+ const first = await h.enable('srv-a')
233
+
234
+ await h.manager.disable(first)
235
+ // A tool the policy refuses either way. Reporting drift for it would
236
+ // train a host to ignore the warning that matters.
237
+ advertised = [...advertised, tool('another_denied_one')]
238
+ await h.enable('srv-b')
239
+
240
+ expect(onMCPToolDrift).not.toHaveBeenCalled()
241
+ })
242
+ })
@@ -2,6 +2,9 @@ import { join } from 'node:path'
2
2
  import { pathToFileURL } from 'node:url'
3
3
  import { mcpToolToToolDefinition } from '../connector/mcp/adapter.js'
4
4
  import { MCPClient } from '../connector/mcp/client.js'
5
+ import { MCPToolDiscovery } from '../connector/mcp/discovery.js'
6
+ import type { MCPToolDiscoveryOptions } from '../connector/mcp/discovery.js'
7
+ import type { MCPToolPolicy } from '../connector/mcp/policy.js'
5
8
  import {
6
9
  DEFAULT_HOOK_PRIORITY,
7
10
  HOOK_TIMEOUT_MS,
@@ -37,6 +40,32 @@ export interface PluginLifecycleManagerConfig {
37
40
  toolRegistry: ToolRegistryContract
38
41
  log: Logger
39
42
  hookTimeoutMs?: number
43
+
44
+ /**
45
+ * What each MCP server a plugin brings is allowed to contribute, keyed by
46
+ * server name. `'*'` covers every server not named explicitly.
47
+ *
48
+ * Absent admits everything, which is what this path did unconditionally
49
+ * until now. `MCPToolDiscovery` has held this boundary — and the drift
50
+ * detection below — since it was written, and nothing outside its own
51
+ * tests ever constructed one: `attachMCPServer` called `listTools()`
52
+ * directly and registered whatever came back. So the least-privilege
53
+ * check existed, was tested, was exported, and was not on the path any
54
+ * real MCP server takes.
55
+ */
56
+ mcpToolPolicies?: Readonly<Record<string, MCPToolPolicy>>
57
+
58
+ /**
59
+ * Called when a server's admitted tool set differs from the last time it
60
+ * was discovered.
61
+ *
62
+ * Reported rather than blocked, for the reason `MCPToolDiscovery`
63
+ * already gives: a development server legitimately changes between runs,
64
+ * while a production one changing mid-session is the rug pull — advertise
65
+ * something benign at approval time, swap it afterwards. Only the host
66
+ * knows which it is looking at.
67
+ */
68
+ onMCPToolDrift?: MCPToolDiscoveryOptions['onDrift']
40
69
  }
41
70
 
42
71
  export class PluginLifecycleManager {
@@ -55,12 +84,32 @@ export class PluginLifecycleManager {
55
84
  private pluginContributions: Map<PluginId, PluginContributionRecord> = new Map()
56
85
  private hookTimeoutMs: number
57
86
  private log: Logger
87
+ /**
88
+ * The admission boundary for everything a plugin's MCP servers advertise.
89
+ *
90
+ * One instance for the whole manager rather than one per plugin, and that
91
+ * is the part that matters: the instance is what remembers each server's
92
+ * previously admitted tool set, so a server that changes between one
93
+ * plugin being disabled and the next being enabled is still noticed. A
94
+ * per-plugin instance would forget on every teardown, which is precisely
95
+ * the window a rug pull uses.
96
+ *
97
+ * Its client list is deliberately left empty — `discoverFrom` takes the
98
+ * client directly, so registering it here would be bookkeeping nothing
99
+ * reads.
100
+ */
101
+ private mcpDiscovery: MCPToolDiscovery
58
102
 
59
103
  constructor(config: PluginLifecycleManagerConfig) {
60
104
  this.pluginRegistry = config.pluginRegistry
61
105
  this.toolRegistry = config.toolRegistry
62
106
  this.hookTimeoutMs = config.hookTimeoutMs ?? HOOK_TIMEOUT_MS
63
107
  this.log = config.log.child({ component: 'PluginLifecycleManager' })
108
+ this.mcpDiscovery = new MCPToolDiscovery([], {
109
+ ...(config.mcpToolPolicies ? { policies: config.mcpToolPolicies } : {}),
110
+ ...(config.onMCPToolDrift ? { onDrift: config.onMCPToolDrift } : {}),
111
+ logger: this.log,
112
+ })
64
113
  }
65
114
 
66
115
  /**
@@ -257,7 +306,14 @@ export class PluginLifecycleManager {
257
306
  await client.connect()
258
307
  contributions.mcpClients.push(client)
259
308
 
260
- const mcpTools = await client.listTools()
309
+ // Through the boundary, not around it. This used to call
310
+ // `client.listTools()` and register everything the server answered
311
+ // with, so the remote side decided what entered the agent's registry
312
+ // — least privilege inverted. `discoverFrom` applies the per-server
313
+ // allow/deny policy and reports a tool set that changed since the
314
+ // last discovery.
315
+ const discovered = await this.mcpDiscovery.discoverFrom(client)
316
+ const mcpTools = discovered.map((d) => d.tool)
261
317
  for (const mcpTool of mcpTools) {
262
318
  const baseDef = mcpToolToToolDefinition(mcpTool, client, config.name)
263
319
  // Double-underscore between serverName and toolName so that e.g.
@@ -88,23 +88,77 @@ export function assertEnableable(manifest: PluginManifest): void {
88
88
  )
89
89
  }
90
90
 
91
+ /** Where a plugin came from. Project plugins ship with the repo; user plugins live in the home directory. */
92
+ export type PluginScope = 'project' | 'user'
93
+
91
94
  /**
92
- * Discovers plugin directories from both project-level and user-level locations.
93
- * Returns categorized arrays of absolute paths.
95
+ * The discovery half of {@link PluginRuntimeConfig}.
96
+ *
97
+ * Structural rather than an import so this module keeps depending on nothing
98
+ * — a parsed `PluginRuntimeConfig` satisfies it as-is.
99
+ */
100
+ export interface PluginDiscoveryOptions {
101
+ /** Whether the plugin runtime is on at all. `false` discovers nothing. */
102
+ readonly enabled?: boolean
103
+ /** Whether to scan for plugins. `false` discovers nothing. */
104
+ readonly autoDiscovery?: boolean
105
+ /** Which locations may be scanned. Absent means both. */
106
+ readonly allowedScopes?: readonly PluginScope[]
107
+ }
108
+
109
+ /**
110
+ * Find plugin directories, in the locations the caller permits.
111
+ *
112
+ * `allowedScopes` is a trust boundary, not a filter applied afterwards. A
113
+ * plugin is arbitrary code with hooks into tool execution, and the two scopes
114
+ * are not equally trusted: project plugins are reviewable in the repo the
115
+ * agent is working on, while user plugins come from a home directory the
116
+ * repo's reviewers never see. `['project']` is how a host says the second
117
+ * kind is not allowed to run here.
118
+ *
119
+ * It was previously declared and unread — `PluginRuntimeConfig` carried
120
+ * `enabled`, `autoDiscovery` and `allowedScopes`, nothing anywhere consulted
121
+ * any of them, and this function scanned both locations unconditionally. A
122
+ * host who set `allowedScopes: ['project']` got user plugins anyway, from a
123
+ * setting that reads exactly like a boundary.
124
+ *
125
+ * A disallowed scope is NOT SCANNED rather than scanned and dropped.
126
+ * Filtering after the fact still reads the directory, which is both pointless
127
+ * work and a small disclosure — the returned count would tell a caller how
128
+ * many plugins live somewhere they said they would not look.
129
+ *
130
+ * Passing nothing keeps the old behaviour: both scopes, no gate. Existing
131
+ * callers are unaffected, and a caller who opts in gets what the config says.
94
132
  */
95
133
  export async function discoverAllPluginDirs(
96
134
  workingDirectory?: string,
135
+ options?: PluginDiscoveryOptions,
97
136
  ): Promise<{ project: string[]; user: string[] }> {
137
+ if (options?.enabled === false || options?.autoDiscovery === false) {
138
+ logger.debug('Plugin discovery skipped', {
139
+ enabled: options.enabled,
140
+ autoDiscovery: options.autoDiscovery,
141
+ })
142
+ return { project: [], user: [] }
143
+ }
144
+
145
+ const scopes = options?.allowedScopes
146
+ const mayScan = (scope: PluginScope): boolean => !scopes || scopes.includes(scope)
147
+
98
148
  const projectDir = workingDirectory
99
149
  ? join(workingDirectory, PROJECT_PLUGIN_DIR)
100
150
  : join(process.cwd(), PROJECT_PLUGIN_DIR)
101
151
  const userDir = join(homedir(), USER_PLUGIN_DIR)
102
152
 
103
- const [project, user] = await Promise.all([discoverPlugins(projectDir), discoverPlugins(userDir)])
153
+ const [project, user] = await Promise.all([
154
+ mayScan('project') ? discoverPlugins(projectDir) : Promise.resolve([]),
155
+ mayScan('user') ? discoverPlugins(userDir) : Promise.resolve([]),
156
+ ])
104
157
 
105
158
  logger.debug('Plugin discovery complete', {
106
159
  projectCount: project.length,
107
160
  userCount: user.length,
161
+ ...(scopes ? { allowedScopes: scopes } : {}),
108
162
  })
109
163
 
110
164
  return { project, user }
@@ -89,6 +89,12 @@ export type { ToolGrantKeys } from './runtime/query/tool-grants.js'
89
89
  export { toWireRunStatus } from './contracts/run-status.js'
90
90
  // Durable run state: the snapshot a different process picks a run up from.
91
91
  export { captureRunState, loadRunState } from './runtime/query/run-state.js'
92
+ // …and the driver that joins the snapshot back to a running loop. Without
93
+ // it every host wrote the same wiring, and in practice none did.
94
+ export { resumeRun } from './runtime/query/resume-run.js'
95
+ export type { ResumeOutcome, ResumeRunParams } from './runtime/query/resume-run.js'
96
+ // The scope type was internal, so a host calling `loadRunState` could not
97
+ // name the argument it had to construct.
92
98
  export type { RunStateScope } from './runtime/query/run-state.js'
93
99
  export { prepareReplayState } from './runtime/query/replay/prepare.js'
94
100
  export { listCheckpoints } from './runtime/query/replay/list.js'
@@ -215,7 +221,14 @@ export type { LocalSandboxProviderOptions } from './sandbox/index.js'
215
221
  // The classified provider-failure surface: a driver states what went wrong
216
222
  // first-hand, and the run boundary reads it to choose between a pause and a
217
223
  // failure.
218
- export {
224
+ // `classifyProviderHttpStatus` and `bodySaysContextOverflow` are here
225
+ // because a driver outside this repo needs the same classification the
226
+ // first-party ones use — a status code alone does not separate a context
227
+ // overflow from an ordinary bad request, and re-deriving that per driver
228
+ // is how the classifications drift apart.
229
+ export {
230
+ bodySaysContextOverflow,
231
+ classifyProviderHttpStatus,
219
232
  isCallerAbortError,
220
233
  isProviderRequestError,
221
234
  providerHttpError,
@@ -254,6 +267,7 @@ export {
254
267
  HybridExecutionContext,
255
268
  LocalExecutionContext,
256
269
  MCPClient,
270
+ MCPMethodNotFound,
257
271
  MCPConnectorBridge,
258
272
  MCPServer,
259
273
  MCPToolDiscovery,
@@ -489,6 +503,7 @@ export {
489
503
  lookupContextWindow,
490
504
  resolveContextWindow,
491
505
  createConversationManager,
506
+ createSlidingWindowReducer,
492
507
  extractFromAssistantMessage,
493
508
  extractFromToolCall,
494
509
  extractFromToolResult,