@stigmer/runner 3.10.0 → 3.11.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 (260) hide show
  1. package/README.md +12 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/call-llm.js +9 -10
  4. package/dist/activities/call-llm.js.map +1 -1
  5. package/dist/activities/classify-tool-approvals.d.ts +2 -1
  6. package/dist/activities/classify-tool-approvals.js +28 -2
  7. package/dist/activities/classify-tool-approvals.js.map +1 -1
  8. package/dist/activities/discover-mcp-server.d.ts +32 -0
  9. package/dist/activities/discover-mcp-server.js +162 -27
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  12. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  13. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  15. package/dist/activities/execute-cursor/approval-state.js +7 -1
  16. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  17. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  18. package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
  19. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  20. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  21. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  22. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  24. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  25. package/dist/activities/execute-cursor/error-classifier.d.ts +40 -3
  26. package/dist/activities/execute-cursor/error-classifier.js +81 -3
  27. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  28. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  29. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  30. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  31. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  32. package/dist/activities/execute-cursor/hook-script.js +72 -10
  33. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  34. package/dist/activities/execute-cursor/index.d.ts +5 -1
  35. package/dist/activities/execute-cursor/index.js +51 -57
  36. package/dist/activities/execute-cursor/index.js.map +1 -1
  37. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  38. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  39. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  40. package/dist/activities/execute-cursor/prompt-builder.d.ts +18 -4
  41. package/dist/activities/execute-cursor/prompt-builder.js +12 -7
  42. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  43. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  44. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  45. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  46. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  47. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  48. package/dist/activities/execute-deep-agent/environment.js +3 -1
  49. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/index.js +15 -0
  51. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  52. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  53. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  54. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  56. package/dist/activities/execute-deep-agent/setup.js +55 -23
  57. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  58. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  59. package/dist/activities/execute-deep-agent/subagent-transformer.js +8 -1
  60. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +11 -4
  62. package/dist/activities/execute-deep-agent/subagent-wiring.js +13 -4
  63. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  64. package/dist/activities/hydrate-workflow-execution.js +3 -1
  65. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  66. package/dist/activities/workflow-event-activities.d.ts +28 -10
  67. package/dist/activities/workflow-event-activities.js +87 -58
  68. package/dist/activities/workflow-event-activities.js.map +1 -1
  69. package/dist/claimcheck/payload-codec.js +21 -1
  70. package/dist/claimcheck/payload-codec.js.map +1 -1
  71. package/dist/client/stigmer-client.d.ts +9 -4
  72. package/dist/client/stigmer-client.js +28 -15
  73. package/dist/client/stigmer-client.js.map +1 -1
  74. package/dist/encryption/config.d.ts +32 -0
  75. package/dist/encryption/config.js +68 -0
  76. package/dist/encryption/config.js.map +1 -0
  77. package/dist/encryption/index.d.ts +3 -0
  78. package/dist/encryption/index.js +3 -0
  79. package/dist/encryption/index.js.map +1 -0
  80. package/dist/encryption/payload-codec.d.ts +41 -0
  81. package/dist/encryption/payload-codec.js +130 -0
  82. package/dist/encryption/payload-codec.js.map +1 -0
  83. package/dist/payload-codecs.d.ts +16 -0
  84. package/dist/payload-codecs.js +38 -0
  85. package/dist/payload-codecs.js.map +1 -0
  86. package/dist/preflight.d.ts +31 -0
  87. package/dist/preflight.js +43 -0
  88. package/dist/preflight.js.map +1 -1
  89. package/dist/runner-manager.js +5 -15
  90. package/dist/runner-manager.js.map +1 -1
  91. package/dist/runner.js +5 -16
  92. package/dist/runner.js.map +1 -1
  93. package/dist/shared/approval-policy.d.ts +9 -3
  94. package/dist/shared/approval-policy.js +15 -6
  95. package/dist/shared/approval-policy.js.map +1 -1
  96. package/dist/shared/attachment-naming.d.ts +53 -0
  97. package/dist/shared/attachment-naming.js +59 -0
  98. package/dist/shared/attachment-naming.js.map +1 -0
  99. package/dist/shared/caller-identity.d.ts +23 -2
  100. package/dist/shared/caller-identity.js +36 -5
  101. package/dist/shared/caller-identity.js.map +1 -1
  102. package/dist/shared/channel-attachment.js +1 -0
  103. package/dist/shared/channel-attachment.js.map +1 -1
  104. package/dist/shared/checkpointer/http-saver.d.ts +42 -1
  105. package/dist/shared/checkpointer/http-saver.js +96 -8
  106. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  107. package/dist/shared/conversation-attachment.js +1 -0
  108. package/dist/shared/conversation-attachment.js.map +1 -1
  109. package/dist/shared/datastore-attachment.d.ts +50 -7
  110. package/dist/shared/datastore-attachment.js +93 -11
  111. package/dist/shared/datastore-attachment.js.map +1 -1
  112. package/dist/shared/http-retry.d.ts +43 -0
  113. package/dist/shared/http-retry.js +50 -0
  114. package/dist/shared/http-retry.js.map +1 -0
  115. package/dist/shared/llm-backend.d.ts +275 -0
  116. package/dist/shared/llm-backend.js +425 -0
  117. package/dist/shared/llm-backend.js.map +1 -0
  118. package/dist/shared/llm-proxy.d.ts +8 -0
  119. package/dist/shared/llm-proxy.js +15 -0
  120. package/dist/shared/llm-proxy.js.map +1 -1
  121. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  122. package/dist/shared/mcp-enabled-tools.js +86 -0
  123. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  124. package/dist/shared/mcp-manager.d.ts +3 -1
  125. package/dist/shared/mcp-manager.js +17 -4
  126. package/dist/shared/mcp-manager.js.map +1 -1
  127. package/dist/shared/mcp-resolver.d.ts +39 -2
  128. package/dist/shared/mcp-resolver.js +38 -2
  129. package/dist/shared/mcp-resolver.js.map +1 -1
  130. package/dist/shared/model-client.d.ts +12 -5
  131. package/dist/shared/model-client.js +138 -18
  132. package/dist/shared/model-client.js.map +1 -1
  133. package/dist/shared/model-error.js +198 -5
  134. package/dist/shared/model-error.js.map +1 -1
  135. package/dist/shared/plan-mode-permissions.d.ts +26 -0
  136. package/dist/shared/plan-mode-permissions.js +28 -0
  137. package/dist/shared/plan-mode-permissions.js.map +1 -0
  138. package/dist/worker.d.ts +2 -1
  139. package/dist/worker.js +2 -4
  140. package/dist/worker.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +18 -0
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  144. package/dist/workflows/call-agent-orchestrator.js +1 -0
  145. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  146. package/dist/workflows/connect-mcp-server.js +7 -0
  147. package/dist/workflows/connect-mcp-server.js.map +1 -1
  148. package/dist/workflows/engine-core.js +23 -2
  149. package/dist/workflows/engine-core.js.map +1 -1
  150. package/dist/workflows/execute-from-execution.d.ts +1 -1
  151. package/dist/workflows/execute-from-execution.js +11 -1
  152. package/dist/workflows/execute-from-execution.js.map +1 -1
  153. package/package.json +8 -2
  154. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  155. package/src/__tests__/encryption-codec.test.ts +234 -0
  156. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  157. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  158. package/src/__tests__/preflight.test.ts +50 -2
  159. package/src/activities/__tests__/call-llm.test.ts +75 -0
  160. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  161. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  162. package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
  163. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  164. package/src/activities/call-llm.ts +9 -16
  165. package/src/activities/classify-tool-approvals.ts +34 -4
  166. package/src/activities/discover-mcp-server.ts +190 -32
  167. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  168. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  169. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
  170. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
  171. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  172. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  173. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  174. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  175. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  176. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  177. package/src/activities/execute-cursor/approval-state.ts +30 -1
  178. package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
  179. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  180. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  181. package/src/activities/execute-cursor/error-classifier.ts +91 -4
  182. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  183. package/src/activities/execute-cursor/hook-script.ts +74 -10
  184. package/src/activities/execute-cursor/index.ts +55 -71
  185. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  186. package/src/activities/execute-cursor/prompt-builder.ts +34 -9
  187. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  188. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
  189. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  190. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  191. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  192. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  193. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  194. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  195. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
  196. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  197. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  198. package/src/activities/execute-deep-agent/environment.ts +3 -1
  199. package/src/activities/execute-deep-agent/index.ts +20 -0
  200. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  201. package/src/activities/execute-deep-agent/setup.ts +76 -28
  202. package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
  203. package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
  204. package/src/activities/hydrate-workflow-execution.ts +3 -1
  205. package/src/activities/workflow-event-activities.ts +96 -69
  206. package/src/claimcheck/payload-codec.ts +33 -1
  207. package/src/client/__tests__/stigmer-client.test.ts +8 -8
  208. package/src/client/stigmer-client.ts +32 -18
  209. package/src/encryption/config.ts +91 -0
  210. package/src/encryption/index.ts +3 -0
  211. package/src/encryption/payload-codec.ts +152 -0
  212. package/src/payload-codecs.ts +56 -0
  213. package/src/preflight.ts +45 -0
  214. package/src/runner-manager.ts +6 -24
  215. package/src/runner.ts +6 -25
  216. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  217. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  218. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  219. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  220. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  221. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  222. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  223. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  224. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  225. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  226. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  227. package/src/shared/__tests__/http-retry.test.ts +67 -0
  228. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  229. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  230. package/src/shared/__tests__/mcp-manager.test.ts +84 -2
  231. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  232. package/src/shared/__tests__/model-client.test.ts +154 -0
  233. package/src/shared/__tests__/model-error.test.ts +289 -1
  234. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  235. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  236. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  237. package/src/shared/approval-policy.ts +14 -7
  238. package/src/shared/attachment-naming.ts +78 -0
  239. package/src/shared/caller-identity.ts +40 -5
  240. package/src/shared/channel-attachment.ts +1 -0
  241. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  242. package/src/shared/checkpointer/http-saver.ts +117 -9
  243. package/src/shared/conversation-attachment.ts +1 -0
  244. package/src/shared/datastore-attachment.ts +106 -11
  245. package/src/shared/http-retry.ts +50 -0
  246. package/src/shared/llm-backend.ts +544 -0
  247. package/src/shared/llm-proxy.ts +15 -0
  248. package/src/shared/mcp-enabled-tools.ts +105 -0
  249. package/src/shared/mcp-manager.ts +21 -4
  250. package/src/shared/mcp-resolver.ts +73 -2
  251. package/src/shared/model-client.ts +161 -19
  252. package/src/shared/model-error.ts +222 -4
  253. package/src/shared/plan-mode-permissions.ts +30 -0
  254. package/src/worker.ts +4 -5
  255. package/src/workflow-engine/types.ts +18 -0
  256. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  257. package/src/workflows/call-agent-orchestrator.ts +10 -0
  258. package/src/workflows/connect-mcp-server.ts +7 -0
  259. package/src/workflows/engine-core.ts +23 -2
  260. package/src/workflows/execute-from-execution.ts +12 -2
@@ -32,6 +32,8 @@
32
32
  * - "secret" — DD-26 secret hard-block: the agent continues, no pause.
33
33
  * - "capture-error" — CAS staging failed, write kept on the deny-gate.
34
34
  * - "fail-closed" — approval state file missing, everything gated denies.
35
+ * - "disabled" — enabled_tools manifest exclusion (issue #350): the
36
+ * agent continues, no pause, permanent for the run.
35
37
  * Only approval-kind records carry the captured tool_input: a secret write's
36
38
  * content must never be persisted (DD-26), a capture-error's content is
37
39
  * UNCLASSIFIED (the staging error means secret classification may never have
@@ -84,8 +86,16 @@
84
86
  * set, allow the rest" — matching the native harness and avoiding denial of
85
87
  * auto-approved MCP tools (which are absent from mcpToolPolicies):
86
88
  * 0. Scope guard: not the runner's own agent → allow (never touch the ledger)
87
- * 1. Missing state file → deny (fail-closed); autoApproveAll (the pre-armed
88
- * spec.auto_approve_all global bypass)allow
89
+ * 1. Missing state file → deny (fail-closed)
90
+ * 1a. beforeMCPExecution eventtool excluded by the server's
91
+ * mcpServerEnabledTools allow-list → record kind "disabled", deny.
92
+ * Deliberately BEFORE autoApproveAll and the grant checks: enabled_tools
93
+ * is a capability manifest, not an approval gate (issue #350) — no bypass
94
+ * may resurrect a disabled tool, and no human may be offered "approve" on
95
+ * one. Server-scoped via the payload's mcp_server_name; an absent slug in
96
+ * the map means unrestricted.
97
+ * 1b. autoApproveAll (the pre-armed spec.auto_approve_all global bypass) →
98
+ * allow
89
99
  * 2. beforeMCPExecution event → MCP tool present in mcpToolPolicies
90
100
  * (require-approval):
91
101
  * a. name token in approvedGrantTokens → allow (reinvocation grant)
@@ -155,6 +165,23 @@ const SECRET_BLOCKED_AGENT_MESSAGE =
155
165
  "hooks. Do not retry this write or attempt a workaround; the write will not be " +
156
166
  "applied. Continue with the rest of the task.";
157
167
 
168
+ // Shown to the model when an MCP tool call is denied because the agent's
169
+ // enabled_tools manifest excludes the tool (issue #350). Like
170
+ // SECRET_BLOCKED_AGENT_MESSAGE this must NOT promise a resume — the exclusion
171
+ // is permanent for the run and mode-independent (it is a capability manifest,
172
+ // not an approval, so nothing can be granted). Same embedding constraint
173
+ // (single-quoted bash echo of a JSON object): no double quotes, apostrophes,
174
+ // or backslashes.
175
+ const DISABLED_TOOL_AGENT_MESSAGE =
176
+ "This tool is not enabled for this agent: the MCP server exposes it, but the " +
177
+ "agent manifest (enabled_tools) excludes it. This is the platform capability " +
178
+ "manifest working as intended — it is not an error and not a Cursor " +
179
+ "misconfiguration, so never tell the user to change Cursor settings or enable " +
180
+ "hooks. Do not retry this tool or attempt a workaround; it will stay " +
181
+ "unavailable for this entire run. Use a different tool or adapt your plan, " +
182
+ "and if the task cannot proceed without it, tell the user plainly what was " +
183
+ "unavailable.";
184
+
158
185
  /**
159
186
  * Build the bash `case` arms that map an incoming hook `tool_name` to its
160
187
  * canonical approval category. Generated from approval-policy.ts so the hook and
@@ -209,14 +236,17 @@ function buildContentDigestScript(): string {
209
236
  * Build the inline Node.js identity extractor embedded in the hook script.
210
237
  *
211
238
  * Parses the hook's stdin JSON properly (the bash fallback's grep truncates
212
- * string values at the first escaped quote) and emits SEVEN lines: tool_name,
239
+ * string values at the first escaped quote) and emits NINE lines: tool_name,
213
240
  * canonical category, coarse identity token, MCP name-token, hook_event_name
214
241
  * (the event discriminator: `preToolUse` for built-ins, `beforeMCPExecution`
215
242
  * for MCP), base64(JSON(tool_input)) — the authoritative pre-execution args the
216
- * runner overlays onto the gated tool call for the approval preview — and the
243
+ * runner overlays onto the gated tool call for the approval preview — the
217
244
  * CONTENT token (base64(category \n salient \n contentDigest), empty when the
218
- * tool has no edit content). The token encodings must stay byte-identical to
219
- * grantToken()/contentToken() in approval-state.ts.
245
+ * tool has no edit content), base64(salient), and mcp_server_name (the MCP
246
+ * server slug the beforeMCPExecution payload carries; empty for built-ins) —
247
+ * the server scope for the enabled_tools manifest arm. The token encodings
248
+ * must stay byte-identical to grantToken()/contentToken() in
249
+ * approval-state.ts.
220
250
  *
221
251
  * Authored as a single-quoted bash string, so the JS must not contain single
222
252
  * quotes. The category map, salient field list, and edit/content field lists are
@@ -247,6 +277,7 @@ function buildNodeIdentityScript(): string {
247
277
  // is "" for a non-edit tool, in which case the content token (line 7) is "".
248
278
  buildContentDigestScript(),
249
279
  `const ev=typeof t.hook_event_name==="string"?t.hook_event_name:"";`,
280
+ `const srv=typeof t.mcp_server_name==="string"?t.mcp_server_name:"";`,
250
281
  // Line 6 is base64(JSON(tool_input)): the AUTHORITATIVE pre-execution args
251
282
  // the runner overlays onto the gated tool call so the approval card can show
252
283
  // the proposed change before the user approves. Base64 keeps the bash side
@@ -255,8 +286,10 @@ function buildNodeIdentityScript(): string {
255
286
  // grant the runner authorizes for a file edit. Line 8 is base64(salient) —
256
287
  // the raw resource value (file path / command) capture mode needs to run
257
288
  // `git check-ignore` on a file path; base64 keeps newlines/quotes out of the
258
- // line-oriented bash parse.
259
- `process.stdout.write(name+"\\n"+cat+"\\n"+b(cat+"\\n"+s)+"\\n"+b(name+"\\n")+"\\n"+ev+"\\n"+b(JSON.stringify(a))+"\\n"+(dig?b(cat+"\\n"+s+"\\n"+dig):"")+"\\n"+b(s));`,
289
+ // line-oriented bash parse. Line 9 is mcp_server_name — the server scope
290
+ // the enabled_tools manifest arm matches against mcpServerEnabledTools
291
+ // (a bare slug, never quoted/escaped, so it rides as a plain line).
292
+ `process.stdout.write(name+"\\n"+cat+"\\n"+b(cat+"\\n"+s)+"\\n"+b(name+"\\n")+"\\n"+ev+"\\n"+b(JSON.stringify(a))+"\\n"+(dig?b(cat+"\\n"+s+"\\n"+dig):"")+"\\n"+b(s)+"\\n"+srv);`,
260
293
  ].join("");
261
294
  }
262
295
 
@@ -279,7 +312,8 @@ function buildNodeIdentityScript(): string {
279
312
  *
280
313
  * From the pointer the script reads the current turn's approval-state file (the
281
314
  * single source of truth for the dynamic inputs: autoApproveAll, leasedCategories,
282
- * mcpToolPolicies, approvedGrantTokens), denial ledger, and runner PID. The
315
+ * mcpToolPolicies, mcpServerEnabledTools, approvedGrantTokens), denial ledger,
316
+ * and runner PID. The
283
317
  * static policy (which built-ins are gated, their categories, the salient arg
284
318
  * fields) is baked at generation time from approval-policy.ts.
285
319
  *
@@ -440,6 +474,9 @@ if [ -n "$IDENTITY" ]; then
440
474
  # Raw salient (base64) — the file path / command. Capture mode decodes it to
441
475
  # run git check-ignore on a file path.
442
476
  SALIENT=$(printf '%s\\n' "$IDENTITY" | sed -n 8p | base64 -d 2>/dev/null || true)
477
+ # MCP server slug (beforeMCPExecution payloads only; empty for built-ins) —
478
+ # the server scope for the enabled_tools manifest arm.
479
+ MCP_SERVER=$(printf '%s\\n' "$IDENTITY" | sed -n 9p)
443
480
  else
444
481
  # Fallback when the Node binary cannot run: grep/cut extraction. Best-effort
445
482
  # only — '"field":"[^"]*"' truncates at the first JSON-escaped quote, so the
@@ -449,6 +486,10 @@ else
449
486
  # would otherwise abort the script and emit no decision.
450
487
  TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
451
488
  HOOK_EVENT=$(echo "$INPUT" | grep -o '"hook_event_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
489
+ # Server slugs are plain identifiers (no JSON-escaped quotes), so the grep
490
+ # fallback extracts mcp_server_name reliably — the manifest arm keeps its
491
+ # full precision even without the Node binary.
492
+ MCP_SERVER=$(echo "$INPUT" | grep -o '"mcp_server_name":"[^"]*"' | head -1 | cut -d'"' -f4 || true)
452
493
  SALIENT=""
453
494
  for field in ${salientFields}; do
454
495
  v=$(echo "$INPUT" | grep -o "\\"$field\\":\\"[^\\"]*\\"" | head -1 | cut -d'"' -f4 || true)
@@ -588,7 +629,30 @@ if [ "$CAPTURE_IGNORED" = "true" ] && [ "$CATEGORY" = "write" ] && [ -n "$SALIEN
588
629
  fi
589
630
  fi
590
631
 
591
- # --- 1. Auto-approve all ---
632
+ # --- 1a. MCP capability manifest: enabled_tools (issue #350) ---
633
+ # Runs BEFORE the auto-approve-all shortcut and every grant check because
634
+ # enabled_tools is a capability manifest, not an approval gate: no bypass may
635
+ # resurrect a disabled tool and no human may be offered approval on one.
636
+ # mcpServerEnabledTools holds ONLY restricted servers (an absent slug means
637
+ # unrestricted, so this arm is inert for the common case). Matching is
638
+ # server-scoped via the payload's mcp_server_name — equal tool names on
639
+ # different servers cannot cross-grant — and the quoted-name membership check
640
+ # is exact, never a substring match. Kind "disabled": attributable,
641
+ # non-pausing (the model adapts, same consumer semantics as "secret"), and
642
+ # permanent for the run.
643
+ if [ "$HOOK_EVENT" = "beforeMCPExecution" ] && [ -n "$MCP_SERVER" ] && [ -n "$TOOL_NAME" ]; then
644
+ ENABLED_MAP=$(echo "$STATE" | grep -o '"mcpServerEnabledTools":{[^}]*}' | head -1 || true)
645
+ if [ -n "$ENABLED_MAP" ]; then
646
+ SERVER_ENABLED=$(echo "$ENABLED_MAP" | grep -o "\\"$MCP_SERVER\\":\\[[^]]*\\]" | head -1 || true)
647
+ if [ -n "$SERVER_ENABLED" ] && ! echo "$SERVER_ENABLED" | grep -qF "\\"$TOOL_NAME\\""; then
648
+ record_denial "$MCP_TOKEN" "disabled"
649
+ echo '{"permission":"deny","agent_message":"${DISABLED_TOOL_AGENT_MESSAGE}","user_message":"Tool not enabled for this agent: '"$TOOL_NAME"'"}'
650
+ exit 0
651
+ fi
652
+ fi
653
+ fi
654
+
655
+ # --- 1b. Auto-approve all ---
592
656
  if echo "$STATE" | grep -q '"autoApproveAll":true'; then
593
657
  echo '{"permission":"allow"}'
594
658
  exit 0
@@ -84,6 +84,7 @@ import {
84
84
  import { injectSynthesizedAttachment } from "../../shared/synthesized-attachment.js";
85
85
  import { mergeApprovalPolicies } from "./approval-policy.js";
86
86
  import { deriveActiveLeases, isUnattendedApprovalMode } from "../../shared/approval-policy.js";
87
+ import { enabledToolsBySlug } from "../../shared/mcp-enabled-tools.js";
87
88
  import { backfillMcpServersIfNeeded } from "./connect-backfill.js";
88
89
  import { resolveExecutionEnv } from "./env-resolver.js";
89
90
  import { resolveBlueprint } from "./blueprint-resolver.js";
@@ -139,7 +140,7 @@ import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/
139
140
  import { activityStarted, activityFinished } from "../../idle-watchdog.js";
140
141
  import { normalizeActivityInput, type ExecuteActivityInput } from "../../shared/activity-input.js";
141
142
  import { getCapturedRejection, clearCapturedRejection } from "./rejection-capture.js";
142
- import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent } from "./error-classifier.js";
143
+ import { synthesizeError, formatClassifiedError, shouldRetryWithFreshAgent, extractRunErrorSources } from "./error-classifier.js";
143
144
  import type { ClassifiedError } from "./error-classifier.js";
144
145
  import { createAgent, createCloudAgent } from "./session-lifecycle.js";
145
146
  import { setMaxListeners } from "node:events";
@@ -644,9 +645,21 @@ async function executeCursorInner(
644
645
  // the messaging reach; undefined lets a cloud sandbox runner's
645
646
  // ambient session-scoped token or OSS's no-auth apply). The
646
647
  // attachment header falls back to the ambient credential where no
647
- // exchange happens.
648
- const exchangedRunnerToken =
649
- await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
648
+ // exchange happens. Unlike the env read (which hard-fails on a broken
649
+ // exchange — secrets are load-bearing there), this exchange is
650
+ // opportunistic: every consumer below degrades to an empty answer by
651
+ // contract, and the server refuses the ambient fallback safely, so a
652
+ // failed exchange must not kill the run.
653
+ let exchangedRunnerToken: string | undefined;
654
+ try {
655
+ exchangedRunnerToken =
656
+ await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
657
+ } catch (err) {
658
+ console.warn(
659
+ "[execute-cursor] Scoped-token exchange failed for attachment/discovery " +
660
+ `reads; degrading to the ambient credential: ${err instanceof Error ? err.message : err}`,
661
+ );
662
+ }
650
663
  const attachmentCredential = exchangedRunnerToken
651
664
  ?? config.stigmerTokenRef?.current
652
665
  ?? config.stigmerToken;
@@ -734,11 +747,11 @@ async function executeCursorInner(
734
747
  // them as auto-approved); the global bypass empties the map entirely.
735
748
  const leases = deriveActiveLeases(execution);
736
749
  const globalBypass = leases.global;
737
- const agentOverrides = blueprint.mergedMcpServerUsages
738
- .flatMap((u) => u.toolApprovalOverrides ?? []);
750
+ // Layer-3 overrides ride each resolved server from its merged usage —
751
+ // see ResolvedMcpServer.toolApprovalOverrides (issue #349) — so there
752
+ // is no separate override input to pass here.
739
753
  const mergedPolicies = mergeApprovalPolicies(
740
754
  mcpResolution.resolvedServers,
741
- agentOverrides,
742
755
  leases,
743
756
  );
744
757
  heartbeat();
@@ -785,7 +798,10 @@ async function executeCursorInner(
785
798
  storage: artifactStorage,
786
799
  visionBudget,
787
800
  });
788
- const attachmentPaths = attachmentResults.map((a) => a.relativePath);
801
+ const attachmentEntries = attachmentResults.map((a) => ({
802
+ path: a.relativePath,
803
+ ...(a.renamedFrom !== undefined ? { renamedFrom: a.renamedFrom } : {}),
804
+ }));
789
805
  // Vision facts, derived once from the single resolution result: the
790
806
  // images the model will see inline (in attachment order) and the ones
791
807
  // that degraded to path-only, disclosed in the prompt.
@@ -906,6 +922,11 @@ async function executeCursorInner(
906
922
  captureIgnored,
907
923
  gitWorkspace,
908
924
  isUnattendedApprovalMode(execution),
925
+ // The enabled_tools capability manifest (issue #350): restricted
926
+ // servers' allow-lists, enforced by the hook's "disabled" arm ahead of
927
+ // every approval bypass. The Cursor SDK config cannot hide a server's
928
+ // tools, so this deny-at-call is the harness's enforcement.
929
+ enabledToolsBySlug(mcpResolution.resolvedServers),
909
930
  );
910
931
  const hitlGate = await installHitlGate({
911
932
  workspaceRoot: primaryWorkspaceDir,
@@ -1130,7 +1151,7 @@ async function executeCursorInner(
1130
1151
  subAgents: blueprint.subAgents,
1131
1152
  workspaceDirs: blueprint.workspaceDirs,
1132
1153
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1133
- attachmentPaths,
1154
+ attachments: attachmentEntries,
1134
1155
  vision: visionPromptInfo,
1135
1156
  pendingApprovals: adjudicatedApprovals,
1136
1157
  appliedToolCallIds,
@@ -1721,12 +1742,10 @@ async function executeCursorInner(
1721
1742
  status.phase = ExecutionPhase.EXECUTION_COMPLETED;
1722
1743
  break;
1723
1744
  case "error": {
1724
- const resultAny = result as unknown as Record<string, unknown>;
1725
- const sdkError = result.result
1726
- ?? resultAny.error
1727
- ?? resultAny.message
1728
- ?? resultAny.reason;
1729
- const sdkErrorStr = sdkError ? String(sdkError) : undefined;
1745
+ // Shape-aware extraction, NOT String(): the result's error fields are
1746
+ // structured at runtime often enough that a bare coercion showed users
1747
+ // "[object Object]" and shadowed every fallback source below (oss#299).
1748
+ const runErrorSources = extractRunErrorSources(result);
1730
1749
 
1731
1750
  // The SDK frequently resolves run.wait() to a bare { status: "error" }
1732
1751
  // while the real reason (e.g. the original grpc-status 12 routing
@@ -1738,7 +1757,8 @@ async function executeCursorInner(
1738
1757
  if (capturedRejection) clearCapturedRejection(executionId);
1739
1758
 
1740
1759
  const classified = synthesizeError({
1741
- sdkResultFields: sdkErrorStr,
1760
+ sdkError: runErrorSources.sdkError,
1761
+ sdkResultFields: runErrorSources.sdkResultFields,
1742
1762
  streamErrorMessage: turnState.streamErrorMessage,
1743
1763
  capturedRejection,
1744
1764
  conversationErrorText,
@@ -1789,7 +1809,7 @@ async function executeCursorInner(
1789
1809
  subAgents: blueprint.subAgents,
1790
1810
  workspaceDirs: blueprint.workspaceDirs,
1791
1811
  workspaceFileRefs: spec.workspaceFileRefs ?? [],
1792
- attachmentPaths,
1812
+ attachments: attachmentEntries,
1793
1813
  vision: visionPromptInfo,
1794
1814
  pendingApprovals: adjudicatedApprovals,
1795
1815
  interactionMode,
@@ -1856,8 +1876,14 @@ async function executeCursorInner(
1856
1876
 
1857
1877
  const retryConversationErrorText = await introspectConversation(retryRun, executionId);
1858
1878
 
1879
+ // Same shape-aware extraction as the primary error arm — the retry
1880
+ // previously String()-coerced result.result alone, so a structured
1881
+ // retry failure both read "[object Object]" and ignored the
1882
+ // error/message/reason fields the primary arm consults.
1883
+ const retryErrorSources = extractRunErrorSources(retryResult);
1859
1884
  const retryClassified = synthesizeError({
1860
- sdkResultFields: retryResult.result ? String(retryResult.result) : undefined,
1885
+ sdkError: retryErrorSources.sdkError,
1886
+ sdkResultFields: retryErrorSources.sdkResultFields,
1861
1887
  streamErrorMessage: turnState.streamErrorMessage,
1862
1888
  capturedRejection: retryRejection,
1863
1889
  conversationErrorText: retryConversationErrorText,
@@ -1974,6 +2000,7 @@ async function executeCursorInner(
1974
2000
  `finalTextLength=${finalText.length}`,
1975
2001
  );
1976
2002
  try {
2003
+ const { extractStructuredOutput } = await import("./extract-structured-output.js");
1977
2004
  structuredOutput = await extractStructuredOutput(
1978
2005
  finalText, structuredOutputSchema, config, requestedModel,
1979
2006
  );
@@ -2298,53 +2325,6 @@ function seedCursorTranscriptFromExecution(
2298
2325
  return persisted.subAgentExecutions.map((sub) => clone(SubAgentExecutionSchema, sub));
2299
2326
  }
2300
2327
 
2301
- // ---------------------------------------------------------------------------
2302
- // Structured Output Extraction (Cursor Harness Tier 2)
2303
- // ---------------------------------------------------------------------------
2304
-
2305
- /**
2306
- * Extract structured data from an agent's free-text response using an
2307
- * economy-tier LLM with withStructuredOutput (function-calling).
2308
- * Guarantees schema-conformant JSON output via the API's tool-use mechanism.
2309
- *
2310
- * Construction (registry-id resolution, provider inference, proxy wiring) is
2311
- * delegated to the shared buildChatModel so the economy model's registry id is
2312
- * always resolved to a provider API id before the call.
2313
- */
2314
- async function extractStructuredOutput(
2315
- agentResponse: string,
2316
- schema: Record<string, unknown>,
2317
- config: Config,
2318
- primaryModel: string,
2319
- ): Promise<unknown | null> {
2320
- const { getEconomyModel } = await import("../../shared/model-registry.js");
2321
- const { buildChatModel } = await import("../../shared/model-client.js");
2322
-
2323
- const extractionModel = await getEconomyModel(primaryModel);
2324
- const proxyEndpoint = config.proxyEndpoint ?? config.stigmerBackendEndpoint;
2325
-
2326
- const { model: llm } = await buildChatModel({
2327
- modelName: extractionModel,
2328
- proxyEndpoint,
2329
- stigmerToken: config.stigmerToken ?? undefined,
2330
- maxTokens: 4096,
2331
- });
2332
-
2333
- const zodSchema = jsonSchemaToZod(schema);
2334
- const structured = llm.withStructuredOutput(zodSchema);
2335
-
2336
- const result = await structured.invoke([
2337
- { role: "system", content: "Extract the structured data from the agent's response. Return only the data that matches the schema." },
2338
- { role: "user", content: agentResponse },
2339
- ]);
2340
-
2341
- return result ?? null;
2342
- }
2343
-
2344
- // Re-export for use within this module; shared implementation eliminates
2345
- // the three duplicate converters that previously drifted independently.
2346
- import { jsonSchemaToZod } from "../../shared/json-schema-to-zod.js";
2347
-
2348
2328
  // ---------------------------------------------------------------------------
2349
2329
  // Prompt selection
2350
2330
  // ---------------------------------------------------------------------------
@@ -2365,7 +2345,11 @@ export interface BuildPromptInput {
2365
2345
  subAgents: import("@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb").SubAgent[];
2366
2346
  workspaceDirs: string[];
2367
2347
  workspaceFileRefs: string[];
2368
- attachmentPaths: string[];
2348
+ /**
2349
+ * This turn's resolved attachments for the `<input_files>` section —
2350
+ * final paths plus duplicate-rename disclosure (attachment-resolver.ts).
2351
+ */
2352
+ attachments: import("./prompt-builder.js").AttachmentPromptEntry[];
2369
2353
  /**
2370
2354
  * Vision facts for the input-files section (T04): which attachments the
2371
2355
  * model sees inline and which degraded to path-only. PER-TURN like the
@@ -2458,7 +2442,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2458
2442
  subAgents,
2459
2443
  workspaceDirs,
2460
2444
  workspaceFileRefs,
2461
- attachmentPaths,
2445
+ attachments,
2462
2446
  interactionMode,
2463
2447
  buildFromPlan,
2464
2448
  conversationCatchup,
@@ -2492,9 +2476,9 @@ export function buildPrompt(input: BuildPromptInput): string {
2492
2476
  if (resolution.reason === "resumed_successfully") {
2493
2477
  const prefixes = [
2494
2478
  formatInteractionModePrefix(interactionMode),
2495
- formatImplementPlanSection(buildFromPlan, attachmentPaths),
2496
- attachmentPaths.length > 0
2497
- ? formatInputFiles(attachmentPaths, input.vision)
2479
+ formatImplementPlanSection(buildFromPlan, attachments),
2480
+ attachments.length > 0
2481
+ ? formatInputFiles(attachments, input.vision)
2498
2482
  : undefined,
2499
2483
  conversationCatchup !== undefined
2500
2484
  ? formatConversationCatchupSection(conversationCatchup)
@@ -2517,7 +2501,7 @@ export function buildPrompt(input: BuildPromptInput): string {
2517
2501
  subAgents,
2518
2502
  workspaceDirs,
2519
2503
  workspaceFileRefs,
2520
- attachmentPaths,
2504
+ attachments,
2521
2505
  vision: input.vision,
2522
2506
  interactionMode,
2523
2507
  buildFromPlan,
@@ -14,7 +14,7 @@
14
14
  * same pipeline as the Python agent-runner's config_transformer.py.
15
15
  */
16
16
 
17
- import type { McpServerUsage } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
17
+ import type { McpServerUsage, ToolApprovalOverride } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
18
18
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
19
19
  import type { ToolApprovalPolicy } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
20
20
  import type { StigmerClient } from "../../client/stigmer-client.js";
@@ -29,6 +29,7 @@ import {
29
29
  filterEnvToDeclaredKeys,
30
30
  PlaceholderResolutionError,
31
31
  } from "./placeholder-resolver.js";
32
+ import { effectiveEnabledTools } from "../../shared/mcp-enabled-tools.js";
32
33
 
33
34
  /**
34
35
  * Cursor SDK MCP server config shape (matches @cursor/sdk McpServerConfig).
@@ -70,6 +71,29 @@ export interface ResolvedMcpServer {
70
71
  pinnedToolApprovals: ToolApprovalPolicy[];
71
72
  /** True when the server has never been connected (no tool discovery yet). */
72
73
  discoveredCapabilitiesEmpty: boolean;
74
+ /**
75
+ * The EFFECTIVE tool allow-list for this server (issue #350): the usage's
76
+ * enabled_tools, falling back to the server's default_enabled_tools when
77
+ * the usage's list is empty (see shared/mcp-enabled-tools.ts). Absent when
78
+ * unrestricted. The Cursor SDK config cannot hide tools, so this harness
79
+ * enforces it in the HITL hook: the restricted map is written into the
80
+ * approval state (mcpServerEnabledTools) and non-enabled calls are denied
81
+ * with the non-pausing "disabled" kind (see hook-script.ts).
82
+ */
83
+ enabledTools?: string[];
84
+ /**
85
+ * Layer-3 per-agent approval overrides from the usage that resolved this
86
+ * server (issue #349): riding the resolved server is what SCOPES an
87
+ * override to its own server — a flat cross-server list is how an
88
+ * override once leaked onto (or silently un-gated) a same-named tool on
89
+ * another server. Deliberately REQUIRED, not optional: empty means "no
90
+ * overrides", and every construction site — including the shared
91
+ * resolver this one mirrors and the synthesized attachments, which have
92
+ * no usage and therefore no layer 3 — must say so explicitly, so a
93
+ * forgotten mirror cannot compile. Consumed by mergeApprovalPolicies
94
+ * (shared/approval-policy.ts).
95
+ */
96
+ toolApprovalOverrides: ToolApprovalOverride[];
73
97
  }
74
98
 
75
99
  /**
@@ -115,7 +139,13 @@ export async function resolveMcpServers(
115
139
  envVars,
116
140
  ref.slug,
117
141
  );
118
- const server = mcpServerToResolved(mcpServer, ref.slug, serverEnv);
142
+ const server = mcpServerToResolved(
143
+ mcpServer,
144
+ ref.slug,
145
+ serverEnv,
146
+ usage.enabledTools,
147
+ usage.toolApprovalOverrides ?? [],
148
+ );
119
149
  if (server) {
120
150
  assertTransportAllowed(server.slug, server.connectionType, transportPosture);
121
151
  resolved.push(server);
@@ -152,6 +182,8 @@ function mcpServerToResolved(
152
182
  server: McpServer,
153
183
  slug: string,
154
184
  envVars: Record<string, string>,
185
+ usageEnabledTools?: readonly string[],
186
+ usageToolApprovalOverrides: ToolApprovalOverride[] = [],
155
187
  ): ResolvedMcpServer | null {
156
188
  const spec = server.spec;
157
189
  if (!spec) return null;
@@ -167,6 +199,8 @@ function mcpServerToResolved(
167
199
  toolApprovals,
168
200
  pinnedToolApprovals,
169
201
  discoveredCapabilitiesEmpty,
202
+ enabledTools: effectiveEnabledTools(usageEnabledTools, spec.defaultEnabledTools),
203
+ toolApprovalOverrides: usageToolApprovalOverrides,
170
204
  };
171
205
 
172
206
  switch (spec.serverType.case) {
@@ -71,6 +71,18 @@ export interface VisionPromptInfo {
71
71
  notViewable: NotViewableEntry[];
72
72
  }
73
73
 
74
+ /**
75
+ * One resolved attachment as the `<input_files>` section renders it —
76
+ * the final workspace-relative path plus the duplicate-rename disclosure
77
+ * (attachment-resolver.ts / shared/attachment-naming.ts).
78
+ */
79
+ export interface AttachmentPromptEntry {
80
+ /** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
81
+ path: string;
82
+ /** Original filename, present only when a duplicate name was renamed. */
83
+ renamedFrom?: string;
84
+ }
85
+
74
86
  export interface EnhancedPromptOptions {
75
87
  instructions: string;
76
88
  userMessage: string;
@@ -78,7 +90,10 @@ export interface EnhancedPromptOptions {
78
90
  /**
79
91
  * Datastores attached via `datastore_usages` — rendered as the
80
92
  * `<available_datastores>` section (DD-005 SD-5, skills precedent)
81
- * pointing the model at the synthesized record tools.
93
+ * pointing the model at the synthesized record tools. Always the healthy
94
+ * rendering here: the Cursor SDK connects MCP itself, so this harness can
95
+ * never observe the live roster — the section's standing
96
+ * failure-disclosure instruction is its whole coverage (issue #325).
82
97
  */
83
98
  datastoreUsages?: DatastoreUsage[];
84
99
  /**
@@ -90,7 +105,7 @@ export interface EnhancedPromptOptions {
90
105
  subAgents: SubAgent[];
91
106
  workspaceDirs: string[];
92
107
  workspaceFileRefs: string[];
93
- attachmentPaths: string[];
108
+ attachments: AttachmentPromptEntry[];
94
109
  /** Inline/degraded image facts for the input-files section (T04 vision). */
95
110
  vision?: VisionPromptInfo;
96
111
  interactionMode?: InteractionMode;
@@ -153,7 +168,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
153
168
 
154
169
  const implementPlan = formatImplementPlanSection(
155
170
  options.buildFromPlan,
156
- options.attachmentPaths,
171
+ options.attachments,
157
172
  );
158
173
  if (implementPlan) {
159
174
  sections.push(implementPlan);
@@ -195,8 +210,8 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
195
210
  sections.push(formatWorkspaceContext(safeDirs));
196
211
  }
197
212
 
198
- if (options.attachmentPaths.length > 0) {
199
- sections.push(formatInputFiles(options.attachmentPaths, options.vision));
213
+ if (options.attachments.length > 0) {
214
+ sections.push(formatInputFiles(options.attachments, options.vision));
200
215
  }
201
216
 
202
217
  if (options.workspaceFileRefs.length > 0) {
@@ -479,8 +494,18 @@ export function formatWorkspaceContext(dirs: string[]): string {
479
494
  ].join("\n");
480
495
  }
481
496
 
482
- export function formatInputFiles(paths: string[], vision?: VisionPromptInfo): string {
483
- const entries = paths.map((p) => `- \`${p}\``);
497
+ export function formatInputFiles(
498
+ attachments: readonly AttachmentPromptEntry[],
499
+ vision?: VisionPromptInfo,
500
+ ): string {
501
+ // A duplicate-renamed file (attachment-naming.ts) discloses its original
502
+ // name so the agent can connect "the two report.pdfs" in the user's
503
+ // message to distinct files on disk.
504
+ const entries = attachments.map((a) =>
505
+ a.renamedFrom !== undefined
506
+ ? `- \`${a.path}\` (renamed from duplicate '${a.renamedFrom}')`
507
+ : `- \`${a.path}\``,
508
+ );
484
509
  // The vision lines (shared wording, attachment-vision.ts) tell the model
485
510
  // which of these files it can already SEE inline versus which degraded to
486
511
  // path-only — without them an agent silently ignores a photo the user
@@ -541,14 +566,14 @@ export function formatInteractionModePrefix(
541
566
  */
542
567
  export function formatImplementPlanSection(
543
568
  buildFromPlan: boolean | undefined,
544
- attachmentPaths: string[],
569
+ attachments: readonly AttachmentPromptEntry[],
545
570
  ): string | undefined {
546
571
  if (!buildFromPlan) {
547
572
  return undefined;
548
573
  }
549
574
 
550
575
  const directive = buildImplementPlanDirective(
551
- findApprovedPlanPath(attachmentPaths),
576
+ findApprovedPlanPath(attachments.map((a) => a.path)),
552
577
  );
553
578
  return ["<implement_plan>", directive, "</implement_plan>"].join("\n");
554
579
  }
@@ -387,8 +387,11 @@ export async function consumeCursorTurnStream(
387
387
  console.log(
388
388
  `ExecuteCursor stream status: execution=${executionId}, status=${JSON.stringify(event)}`,
389
389
  );
390
- const statusEvent = event as { status?: string; message?: string };
391
- if (statusEvent.status === "ERROR" && statusEvent.message) {
390
+ const statusEvent = event as { status?: string; message?: unknown };
391
+ // The cast is a claim, not a guarantee: message is untyped at runtime
392
+ // (the oss#299 class of bug). A structured value here would crash
393
+ // classification downstream, so only capture actual text.
394
+ if (statusEvent.status === "ERROR" && typeof statusEvent.message === "string" && statusEvent.message.length > 0) {
392
395
  state.streamErrorMessage = statusEvent.message;
393
396
  }
394
397
  }