@stigmer/runner 3.9.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 (270) 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 +25 -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 +59 -58
  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 +65 -26
  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/attachment-vision.d.ts +43 -2
  100. package/dist/shared/attachment-vision.js +72 -6
  101. package/dist/shared/attachment-vision.js.map +1 -1
  102. package/dist/shared/caller-identity.d.ts +23 -2
  103. package/dist/shared/caller-identity.js +36 -5
  104. package/dist/shared/caller-identity.js.map +1 -1
  105. package/dist/shared/channel-attachment.js +1 -0
  106. package/dist/shared/channel-attachment.js.map +1 -1
  107. package/dist/shared/checkpointer/http-saver.d.ts +42 -1
  108. package/dist/shared/checkpointer/http-saver.js +96 -8
  109. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  110. package/dist/shared/conversation-attachment.js +1 -0
  111. package/dist/shared/conversation-attachment.js.map +1 -1
  112. package/dist/shared/datastore-attachment.d.ts +50 -7
  113. package/dist/shared/datastore-attachment.js +93 -11
  114. package/dist/shared/datastore-attachment.js.map +1 -1
  115. package/dist/shared/http-retry.d.ts +43 -0
  116. package/dist/shared/http-retry.js +50 -0
  117. package/dist/shared/http-retry.js.map +1 -0
  118. package/dist/shared/llm-backend.d.ts +275 -0
  119. package/dist/shared/llm-backend.js +425 -0
  120. package/dist/shared/llm-backend.js.map +1 -0
  121. package/dist/shared/llm-proxy.d.ts +8 -0
  122. package/dist/shared/llm-proxy.js +15 -0
  123. package/dist/shared/llm-proxy.js.map +1 -1
  124. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  125. package/dist/shared/mcp-enabled-tools.js +86 -0
  126. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  127. package/dist/shared/mcp-manager.d.ts +17 -2
  128. package/dist/shared/mcp-manager.js +37 -25
  129. package/dist/shared/mcp-manager.js.map +1 -1
  130. package/dist/shared/mcp-resolver.d.ts +39 -2
  131. package/dist/shared/mcp-resolver.js +38 -2
  132. package/dist/shared/mcp-resolver.js.map +1 -1
  133. package/dist/shared/model-client.d.ts +12 -5
  134. package/dist/shared/model-client.js +138 -18
  135. package/dist/shared/model-client.js.map +1 -1
  136. package/dist/shared/model-error.js +198 -5
  137. package/dist/shared/model-error.js.map +1 -1
  138. package/dist/shared/model-registry.d.ts +20 -2
  139. package/dist/shared/model-registry.js +37 -2
  140. package/dist/shared/model-registry.js.map +1 -1
  141. package/dist/shared/plan-mode-permissions.d.ts +26 -0
  142. package/dist/shared/plan-mode-permissions.js +28 -0
  143. package/dist/shared/plan-mode-permissions.js.map +1 -0
  144. package/dist/worker.d.ts +2 -1
  145. package/dist/worker.js +2 -4
  146. package/dist/worker.js.map +1 -1
  147. package/dist/workflow-engine/types.d.ts +18 -0
  148. package/dist/workflow-engine/types.js.map +1 -1
  149. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  150. package/dist/workflows/call-agent-orchestrator.js +1 -0
  151. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  152. package/dist/workflows/connect-mcp-server.js +7 -0
  153. package/dist/workflows/connect-mcp-server.js.map +1 -1
  154. package/dist/workflows/engine-core.js +23 -2
  155. package/dist/workflows/engine-core.js.map +1 -1
  156. package/dist/workflows/execute-from-execution.d.ts +1 -1
  157. package/dist/workflows/execute-from-execution.js +11 -1
  158. package/dist/workflows/execute-from-execution.js.map +1 -1
  159. package/package.json +8 -2
  160. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  161. package/src/__tests__/encryption-codec.test.ts +234 -0
  162. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  163. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  164. package/src/__tests__/preflight.test.ts +50 -2
  165. package/src/activities/__tests__/call-llm.test.ts +75 -0
  166. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  167. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  168. package/src/activities/__tests__/discover-mcp-server.test.ts +203 -0
  169. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  170. package/src/activities/call-llm.ts +9 -16
  171. package/src/activities/classify-tool-approvals.ts +34 -4
  172. package/src/activities/discover-mcp-server.ts +190 -32
  173. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  174. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  175. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +92 -0
  176. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +40 -14
  177. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  178. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  179. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  180. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  181. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  182. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  183. package/src/activities/execute-cursor/approval-state.ts +30 -1
  184. package/src/activities/execute-cursor/attachment-resolver.ts +38 -3
  185. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  186. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  187. package/src/activities/execute-cursor/error-classifier.ts +91 -4
  188. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  189. package/src/activities/execute-cursor/hook-script.ts +74 -10
  190. package/src/activities/execute-cursor/index.ts +63 -72
  191. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  192. package/src/activities/execute-cursor/prompt-builder.ts +34 -9
  193. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  194. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +132 -8
  195. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  196. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  197. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  198. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  199. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  200. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  201. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +173 -0
  202. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  203. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  204. package/src/activities/execute-deep-agent/environment.ts +3 -1
  205. package/src/activities/execute-deep-agent/index.ts +20 -0
  206. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  207. package/src/activities/execute-deep-agent/setup.ts +86 -31
  208. package/src/activities/execute-deep-agent/subagent-transformer.ts +23 -1
  209. package/src/activities/execute-deep-agent/subagent-wiring.ts +14 -4
  210. package/src/activities/hydrate-workflow-execution.ts +3 -1
  211. package/src/activities/workflow-event-activities.ts +96 -69
  212. package/src/claimcheck/payload-codec.ts +33 -1
  213. package/src/client/__tests__/stigmer-client.test.ts +8 -8
  214. package/src/client/stigmer-client.ts +32 -18
  215. package/src/encryption/config.ts +91 -0
  216. package/src/encryption/index.ts +3 -0
  217. package/src/encryption/payload-codec.ts +152 -0
  218. package/src/payload-codecs.ts +56 -0
  219. package/src/preflight.ts +45 -0
  220. package/src/runner-manager.ts +6 -24
  221. package/src/runner.ts +6 -25
  222. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  223. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  224. package/src/shared/__tests__/attachment-vision.test.ts +97 -0
  225. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  226. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  227. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  228. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  229. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  230. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  231. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  232. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  233. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  234. package/src/shared/__tests__/http-retry.test.ts +67 -0
  235. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  236. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  237. package/src/shared/__tests__/mcp-manager.test.ts +170 -2
  238. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  239. package/src/shared/__tests__/model-client.test.ts +154 -0
  240. package/src/shared/__tests__/model-error.test.ts +289 -1
  241. package/src/shared/__tests__/model-registry.test.ts +71 -0
  242. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  243. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  244. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  245. package/src/shared/approval-policy.ts +14 -7
  246. package/src/shared/attachment-naming.ts +78 -0
  247. package/src/shared/attachment-vision.ts +92 -9
  248. package/src/shared/caller-identity.ts +40 -5
  249. package/src/shared/channel-attachment.ts +1 -0
  250. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  251. package/src/shared/checkpointer/http-saver.ts +117 -9
  252. package/src/shared/conversation-attachment.ts +1 -0
  253. package/src/shared/datastore-attachment.ts +106 -11
  254. package/src/shared/http-retry.ts +50 -0
  255. package/src/shared/llm-backend.ts +544 -0
  256. package/src/shared/llm-proxy.ts +15 -0
  257. package/src/shared/mcp-enabled-tools.ts +105 -0
  258. package/src/shared/mcp-manager.ts +43 -26
  259. package/src/shared/mcp-resolver.ts +73 -2
  260. package/src/shared/model-client.ts +161 -19
  261. package/src/shared/model-error.ts +222 -4
  262. package/src/shared/model-registry.ts +50 -2
  263. package/src/shared/plan-mode-permissions.ts +30 -0
  264. package/src/worker.ts +4 -5
  265. package/src/workflow-engine/types.ts +18 -0
  266. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  267. package/src/workflows/call-agent-orchestrator.ts +10 -0
  268. package/src/workflows/connect-mcp-server.ts +7 -0
  269. package/src/workflows/engine-core.ts +23 -2
  270. package/src/workflows/execute-from-execution.ts +12 -2
@@ -22,6 +22,13 @@
22
22
  * directives are built from the RESOLVED paths, so prompt and filesystem can
23
23
  * never disagree.
24
24
  *
25
+ * Duplicate filenames are renamed, never overwritten (issue #364): because
26
+ * placement keys purely on the filename, two attachments with the same name
27
+ * contend for one path — the later one takes the platform's `stem-2.ext`
28
+ * rename (shared/attachment-naming.ts, same semantics as the deep-agent
29
+ * injector and the React composer) and the rename is disclosed in the
30
+ * prompt's `<input_files>` section via {@link ResolvedAttachment.renamedFrom}.
31
+ *
25
32
  * Error model: fail-hard, matching the native harness's attachment injector.
26
33
  * Attachments are explicit user inputs — an execution that silently runs
27
34
  * without one produces silently incorrect results (the "plan file wasn't
@@ -33,6 +40,7 @@ import { mkdir, copyFile, readFile, stat, writeFile } from "node:fs/promises";
33
40
  import { join, basename } from "node:path";
34
41
  import type { Attachment } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
35
42
  import type { ArtifactStorage } from "../../shared/artifact-storage.js";
43
+ import { allocateUniqueName } from "../../shared/attachment-naming.js";
36
44
  import {
37
45
  isVisionCandidate,
38
46
  type VisionBudget,
@@ -46,9 +54,16 @@ import { ensureStigmerSymlink, STIGMER_LOCAL_STATE_DIR } from "../../shared/work
46
54
  const INPUTS_SUBDIR = "inputs";
47
55
 
48
56
  export interface ResolvedAttachment {
57
+ /** The final on-disk basename — after any duplicate rename. */
49
58
  filename: string;
50
59
  /** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
51
60
  relativePath: string;
61
+ /**
62
+ * The attachment's original filename, present only when a duplicate name
63
+ * was renamed (shared/attachment-naming.ts) — rendered as disclosure in
64
+ * the prompt's `<input_files>` section.
65
+ */
66
+ renamedFrom?: string;
52
67
  /** Present when the attachment was accepted into the turn's vision payload. */
53
68
  vision?: VisionImage;
54
69
  /**
@@ -111,9 +126,13 @@ export async function resolveAttachments(
111
126
  // it, but only when the agent has skills).
112
127
  await ensureStigmerSymlink(options.primaryWorkspaceDir, platformDir);
113
128
 
129
+ // Placement keys purely on the filename, so this set is the whole
130
+ // collision domain — sequential resolution means each attachment sees
131
+ // every name claimed before it (see module doc on duplicate handling).
132
+ const takenNames = new Set<string>();
114
133
  const results: ResolvedAttachment[] = [];
115
134
  for (const attachment of attachments) {
116
- results.push(await resolveAttachment(attachment, inputsDir, options));
135
+ results.push(await resolveAttachment(attachment, inputsDir, takenNames, options));
117
136
  }
118
137
 
119
138
  console.log(
@@ -127,11 +146,15 @@ export async function resolveAttachments(
127
146
  async function resolveAttachment(
128
147
  attachment: Attachment,
129
148
  inputsDir: string,
149
+ takenNames: Set<string>,
130
150
  options: AttachmentResolverOptions,
131
151
  ): Promise<ResolvedAttachment> {
132
152
  // Local-mode fast path: the file is already on this machine's disk.
133
153
  if (options.mode === "local" && attachment.localPath) {
134
- const filename = safeInputName(attachment.filename || attachment.localPath);
154
+ const { name: filename, renamedFrom } = allocateUniqueName(
155
+ safeInputName(attachment.filename || attachment.localPath),
156
+ takenNames,
157
+ );
135
158
  let vision: VisionOutcome | undefined;
136
159
  try {
137
160
  vision = await materializeLocalFile(attachment, filename, inputsDir, options.visionBudget);
@@ -145,6 +168,7 @@ async function resolveAttachment(
145
168
  return {
146
169
  filename,
147
170
  relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
171
+ ...(renamedFrom !== undefined ? { renamedFrom } : {}),
148
172
  ...visionOutcomeFields(vision),
149
173
  };
150
174
  }
@@ -164,7 +188,10 @@ async function resolveAttachment(
164
188
  );
165
189
  }
166
190
 
167
- const filename = safeInputName(attachment.filename || attachment.storageKey);
191
+ const { name: filename, renamedFrom } = allocateUniqueName(
192
+ safeInputName(attachment.filename || attachment.storageKey),
193
+ takenNames,
194
+ );
168
195
  let content: Buffer;
169
196
  try {
170
197
  content = await options.storage.download(attachment.storageKey);
@@ -185,6 +212,7 @@ async function resolveAttachment(
185
212
  return {
186
213
  filename,
187
214
  relativePath: join(STIGMER_LOCAL_STATE_DIR, INPUTS_SUBDIR, filename),
215
+ ...(renamedFrom !== undefined ? { renamedFrom } : {}),
188
216
  ...visionOutcomeFields(vision),
189
217
  };
190
218
  }
@@ -207,6 +235,13 @@ async function materializeLocalFile(
207
235
  await copyFile(attachment.localPath, dest);
208
236
  return undefined;
209
237
  }
238
+ // Blind-model check BEFORE the size check: a blind model's oversized image
239
+ // must report the honest model_no_vision reason, never too_large's "resend
240
+ // smaller" advice — and an in-cap image needn't be read at all.
241
+ if (visionBudget.modelCannotSee()) {
242
+ await copyFile(attachment.localPath, dest);
243
+ return visionBudget.offerBlind();
244
+ }
210
245
  const info = await stat(attachment.localPath);
211
246
  if (visionBudget.exceedsImageCap(info.size)) {
212
247
  await copyFile(attachment.localPath, dest);
@@ -20,6 +20,13 @@ import type { SessionSpec } from "@stigmer/protos/ai/stigmer/agentic/session/v1/
20
20
  import type { WorkspaceEntry } from "@stigmer/protos/ai/stigmer/agentic/session/v1/workspace_pb";
21
21
  import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
22
22
  import type { CloudRepo } from "./session-lifecycle.js";
23
+ import { mergeMcpServerUsages } from "../../shared/mcp-resolver.js";
24
+
25
+ // Both harnesses must merge agent + session usages identically (session wins
26
+ // per slug — the usage whose enabled_tools the enforcement honors), so the
27
+ // merge lives in shared/mcp-resolver.ts. Re-exported here for its historical
28
+ // home alongside mergeSkillRefs.
29
+ export { mergeMcpServerUsages } from "../../shared/mcp-resolver.js";
23
30
 
24
31
  /**
25
32
  * Path segments that identify runner-internal directories. Any workspace dir
@@ -130,33 +137,6 @@ export function resolveCloudRepos(workspaceEntries: WorkspaceEntry[]): CloudRepo
130
137
  // MCP and skill merging
131
138
  // ---------------------------------------------------------------------------
132
139
 
133
- /**
134
- * Merge MCP server usages from agent (base) and session (overlay).
135
- *
136
- * Replicates session_context_merge.py::merge_mcp_server_usages():
137
- * - Agent-level usages are the base set
138
- * - Session-level usages extend or override by mcp_server_ref.slug
139
- * - If both reference the same slug, session-level takes precedence
140
- */
141
- export function mergeMcpServerUsages(
142
- agentUsages: McpServerUsage[],
143
- sessionUsages: McpServerUsage[],
144
- ): McpServerUsage[] {
145
- const bySlug = new Map<string, McpServerUsage>();
146
-
147
- for (const usage of agentUsages) {
148
- const slug = usage.mcpServerRef?.slug;
149
- if (slug) bySlug.set(slug, usage);
150
- }
151
-
152
- for (const usage of sessionUsages) {
153
- const slug = usage.mcpServerRef?.slug;
154
- if (slug) bySlug.set(slug, usage);
155
- }
156
-
157
- return [...bySlug.values()];
158
- }
159
-
160
140
  /**
161
141
  * Merge skill refs from agent and session.
162
142
  *
@@ -25,7 +25,9 @@ export async function resolveExecutionEnv(
25
25
  ): Promise<EnvResult> {
26
26
  // A desktop runner exchanges its bootstrap credential for a token scoped to
27
27
  // this execution's session, so cloud's decrypt gate binds the read (#156).
28
- // No-op for cloud sandbox and OSS runners.
28
+ // No-op for cloud sandbox and OSS runners. A failed exchange throws and
29
+ // fails the activity: the bootstrap credential no longer decrypts
30
+ // (stigmer-cloud#218), so proceeding would resolve redacted placeholders.
29
31
  const scopedToken = await client.acquireScopedRunnerToken({
30
32
  agentExecutionId: executionId,
31
33
  });
@@ -7,8 +7,9 @@
7
7
  * 3. Surface isRetryable for future workflow-level retry decisions
8
8
  *
9
9
  * Error detail can come from several sources (in priority order):
10
- * - A thrown CursorSdkError's structured fields (highest fidelity)
11
- * - run.wait().result (SDK-provided string, often bare/generic)
10
+ * - Structured fields, either from a thrown CursorSdkError or lifted from a
11
+ * structured run.wait() error value (highest fidelity)
12
+ * - run.wait() error text (SDK-provided string, often bare/generic)
12
13
  * - SDKStatusMessage with status "ERROR" from the stream
13
14
  * - ConnectError captured from process unhandledRejection
14
15
  * - Text extracted from the failing run.conversation() turn
@@ -39,7 +40,8 @@ export interface ClassifiedError {
39
40
 
40
41
  /**
41
42
  * Structured fields lifted from a thrown CursorSdkError (errors.d.ts:
42
- * { code, status, isRetryable, cause, endpoint, requestId, operation }).
43
+ * { code, status, isRetryable, cause, endpoint, requestId, operation }) or
44
+ * from a structured run.wait() error value (see extractRunErrorSources).
43
45
  * Only the fields used for classification are retained.
44
46
  */
45
47
  export interface SdkErrorFields {
@@ -48,6 +50,87 @@ export interface SdkErrorFields {
48
50
  message?: string;
49
51
  }
50
52
 
53
+ /**
54
+ * What String() produces for any plain object. Carries zero signal, so it is
55
+ * refused everywhere: extraction never emits it, and classifyFromSources
56
+ * treats it as absent should any other producer leak it through.
57
+ */
58
+ const OBJECT_JUNK_STRING = "[object Object]";
59
+
60
+ /**
61
+ * Error detail lifted from a run.wait() result, split by fidelity: structured
62
+ * values land in sdkError, plain text in sdkResultFields. At most one of the
63
+ * two is set; both undefined means the result carried nothing usable and the
64
+ * classifier's lower-priority sources should decide.
65
+ */
66
+ export interface RunErrorSources {
67
+ sdkError: SdkErrorFields | undefined;
68
+ sdkResultFields: string | undefined;
69
+ }
70
+
71
+ const NO_RUN_ERROR_SOURCES: RunErrorSources = {
72
+ sdkError: undefined,
73
+ sdkResultFields: undefined,
74
+ };
75
+
76
+ /**
77
+ * Lift error detail from a run.wait() result whose status is "error".
78
+ *
79
+ * The SDK types RunResult.result as `string`, but structured values (Error
80
+ * instances, { code, message } objects) have been observed at runtime, both
81
+ * in `result` and in the undeclared error/message/reason fields (oss#299).
82
+ * A bare String() on those yields "[object Object]", which both destroys the
83
+ * message users see and — worse — shadows the lower-priority classifier
84
+ * sources (stream, rejection, conversation introspection) that often hold
85
+ * the real reason.
86
+ *
87
+ * Walks the candidate fields in order and answers from the FIRST one that
88
+ * yields usable content: strings keep flowing to the string channel
89
+ * (sdkResultFields), structured values are lifted into the same structured
90
+ * channel a thrown CursorSdkError uses (sdkError), and hopeless values are
91
+ * skipped so a later candidate — or the classifier's fallback sources — can
92
+ * win. (The previous `??` chain stopped at the first non-nullish value, so a
93
+ * hopeless object or empty string hid usable text one field later.)
94
+ *
95
+ * Deliberately NO JSON.stringify fallback for unrecognized object shapes:
96
+ * the error arm already logs the raw result in full server-side, and a JSON
97
+ * blob shown to the user would shadow the introspection sources that exist
98
+ * precisely to recover the real reason.
99
+ */
100
+ export function extractRunErrorSources(result: unknown): RunErrorSources {
101
+ if (result === null || typeof result !== "object") return NO_RUN_ERROR_SOURCES;
102
+ const r = result as Record<string, unknown>;
103
+ for (const candidate of [r.result, r.error, r.message, r.reason]) {
104
+ const extracted = extractFromCandidate(candidate);
105
+ if (extracted) return extracted;
106
+ }
107
+ return NO_RUN_ERROR_SOURCES;
108
+ }
109
+
110
+ function extractFromCandidate(v: unknown): RunErrorSources | undefined {
111
+ if (v == null) return undefined;
112
+ if (typeof v === "string") {
113
+ if (v.length === 0 || v === OBJECT_JUNK_STRING) return undefined;
114
+ return { sdkError: undefined, sdkResultFields: v };
115
+ }
116
+ if (typeof v === "object") {
117
+ // Covers Error instances too: their message (and, on SDK/Node error
118
+ // shapes, code/status) are readable as plain properties.
119
+ const o = v as Record<string, unknown>;
120
+ const fields: SdkErrorFields = {};
121
+ if (typeof o.code === "string" && o.code.length > 0) fields.code = o.code;
122
+ if (typeof o.status === "number") fields.status = o.status;
123
+ if (typeof o.message === "string" && o.message.length > 0) fields.message = o.message;
124
+ if (fields.code !== undefined || fields.status !== undefined || fields.message !== undefined) {
125
+ return { sdkError: fields, sdkResultFields: undefined };
126
+ }
127
+ return undefined;
128
+ }
129
+ // Remaining primitives (number, boolean, ...) stringify losslessly.
130
+ const text = String(v);
131
+ return text.length > 0 ? { sdkError: undefined, sdkResultFields: text } : undefined;
132
+ }
133
+
51
134
  const AUTH_PATTERNS = [
52
135
  "unauthenticated", "unauthorized", "401", "forbidden",
53
136
  "permission_denied", "invalid api key", "not logged in",
@@ -168,7 +251,11 @@ function classifyFromSources(opts: SynthesizeErrorOpts): ClassifiedError {
168
251
  }
169
252
 
170
253
  if (opts.sdkResultFields) {
171
- const isBareGeneric = opts.sdkResultFields === "Cursor run failed";
254
+ // "Cursor run failed" is the SDK's bare generic; "[object Object]" is
255
+ // String()-coerced junk from any producer that bypassed the shape-aware
256
+ // extraction. Neither carries signal — fall through to better sources.
257
+ const isBareGeneric = opts.sdkResultFields === "Cursor run failed"
258
+ || opts.sdkResultFields === OBJECT_JUNK_STRING;
172
259
  if (!isBareGeneric) {
173
260
  const { category, retryable } = classifyText(opts.sdkResultFields);
174
261
  return {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Tier-2 structured-output extraction for the Cursor harness.
3
+ *
4
+ * When tier-1 text extraction (shared/extract-json.ts) cannot find JSON in
5
+ * the agent's free-text response, this tier asks an economy-tier LLM to
6
+ * extract it via withStructuredOutput (function-calling), which guarantees
7
+ * schema-conformant output through the API's tool-use mechanism.
8
+ *
9
+ * Lives in its own module (rather than inside execute-cursor/index.ts) so
10
+ * the LangChain construction path stays out of the Cursor activity's module
11
+ * graph until a run actually needs tier 2 — index.ts imports this module
12
+ * lazily at the call site, mirroring its tier-1 import, which is what
13
+ * bundle-slim's deferred evaluation preserves.
14
+ */
15
+
16
+ import type { Config } from "../../config.js";
17
+ import { getEconomyModel } from "../../shared/model-registry.js";
18
+ import { buildChatModel } from "../../shared/model-client.js";
19
+ import { checkDirectCredentials } from "../../shared/llm-backend.js";
20
+ import { tryInferProvider } from "../../shared/llm-proxy.js";
21
+ import { jsonSchemaToZod } from "../../shared/json-schema-to-zod.js";
22
+
23
+ /**
24
+ * Extract structured data from an agent's free-text response using an
25
+ * economy-tier LLM with withStructuredOutput (function-calling).
26
+ *
27
+ * Construction (registry-id resolution, provider inference, proxy wiring) is
28
+ * delegated to the shared buildChatModel so the economy model's registry id
29
+ * is always resolved to a provider API id before the call.
30
+ *
31
+ * Throws when no LLM is reachable (no proxy and no credential path for the
32
+ * extraction model's provider) — the caller treats any throw here as "tier 2
33
+ * unavailable", logs it, and returns the agent's text without structured
34
+ * output, so the failure mode is a diagnosable log line, never a lost run.
35
+ */
36
+ export async function extractStructuredOutput(
37
+ agentResponse: string,
38
+ schema: Record<string, unknown>,
39
+ config: Config,
40
+ primaryModel: string,
41
+ ): Promise<unknown | null> {
42
+ const extractionModel = await getEconomyModel(primaryModel);
43
+ const proxyEndpoint = config.proxyEndpoint ?? undefined;
44
+
45
+ if (!proxyEndpoint) {
46
+ const provider = tryInferProvider(extractionModel);
47
+ const missing = provider === null ? null : checkDirectCredentials(provider);
48
+ if (missing !== null) {
49
+ throw new Error(
50
+ `Structured-output extraction needs the ${provider} model ` +
51
+ `'${extractionModel}' but has no credential path. ${missing}`,
52
+ );
53
+ }
54
+ }
55
+
56
+ const { model: llm } = await buildChatModel({
57
+ modelName: extractionModel,
58
+ proxyEndpoint,
59
+ stigmerToken: config.stigmerToken ?? undefined,
60
+ maxTokens: 4096,
61
+ });
62
+
63
+ const zodSchema = jsonSchemaToZod(schema);
64
+ const structured = llm.withStructuredOutput(zodSchema);
65
+
66
+ const result = await structured.invoke([
67
+ { role: "system", content: "Extract the structured data from the agent's response. Return only the data that matches the schema." },
68
+ { role: "user", content: agentResponse },
69
+ ]);
70
+
71
+ return result ?? null;
72
+ }
@@ -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