@stigmer/runner 3.40.1 → 3.41.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 (248) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  34. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  35. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  37. package/dist/activities/execute-deep-agent/translator.js +5 -4
  38. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  40. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  41. package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
  42. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  43. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  44. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  45. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  46. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  47. package/dist/activities/execute-deep-agent/turn.js +15 -6
  48. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  49. package/dist/activities/workflow-event-activities.js +2 -0
  50. package/dist/activities/workflow-event-activities.js.map +1 -1
  51. package/dist/config.d.ts +9 -0
  52. package/dist/config.js +2 -0
  53. package/dist/config.js.map +1 -1
  54. package/dist/exit-backstop.d.ts +64 -0
  55. package/dist/exit-backstop.js +79 -0
  56. package/dist/exit-backstop.js.map +1 -0
  57. package/dist/harness/terminal-table.d.ts +12 -5
  58. package/dist/harness/terminal-table.js +13 -13
  59. package/dist/harness/terminal-table.js.map +1 -1
  60. package/dist/harness/transcript/builder.d.ts +21 -0
  61. package/dist/harness/transcript/builder.js +53 -31
  62. package/dist/harness/transcript/builder.js.map +1 -1
  63. package/dist/harness/transcript/events.d.ts +22 -26
  64. package/dist/harness/transcript/events.js +6 -5
  65. package/dist/harness/transcript/events.js.map +1 -1
  66. package/dist/harness/turn-context.d.ts +3 -1
  67. package/dist/harness/turn-context.js +5 -3
  68. package/dist/harness/turn-context.js.map +1 -1
  69. package/dist/main.js +10 -4
  70. package/dist/main.js.map +1 -1
  71. package/dist/middleware/cost-advisory.d.ts +6 -3
  72. package/dist/middleware/cost-advisory.js +6 -3
  73. package/dist/middleware/cost-advisory.js.map +1 -1
  74. package/dist/middleware/execution-budget.d.ts +22 -7
  75. package/dist/middleware/execution-budget.js +57 -35
  76. package/dist/middleware/execution-budget.js.map +1 -1
  77. package/dist/middleware/index.d.ts +10 -7
  78. package/dist/middleware/index.js +10 -7
  79. package/dist/middleware/index.js.map +1 -1
  80. package/dist/middleware/types.d.ts +6 -1
  81. package/dist/runner-manager.js +3 -2
  82. package/dist/runner-manager.js.map +1 -1
  83. package/dist/runner.d.ts +25 -0
  84. package/dist/runner.js +41 -14
  85. package/dist/runner.js.map +1 -1
  86. package/dist/shared/approval-policy.d.ts +23 -13
  87. package/dist/shared/approval-policy.js +47 -14
  88. package/dist/shared/approval-policy.js.map +1 -1
  89. package/dist/shared/caller-identity.d.ts +4 -5
  90. package/dist/shared/caller-identity.js +4 -5
  91. package/dist/shared/caller-identity.js.map +1 -1
  92. package/dist/shared/channel-attachment.js +3 -2
  93. package/dist/shared/channel-attachment.js.map +1 -1
  94. package/dist/shared/connect-backfill.d.ts +2 -1
  95. package/dist/shared/connect-backfill.js +2 -2
  96. package/dist/shared/connect-backfill.js.map +1 -1
  97. package/dist/shared/mcp-resolver.d.ts +10 -1
  98. package/dist/shared/mcp-resolver.js +11 -2
  99. package/dist/shared/mcp-resolver.js.map +1 -1
  100. package/dist/shared/memory-attachment.js +3 -2
  101. package/dist/shared/memory-attachment.js.map +1 -1
  102. package/dist/shared/model-error.d.ts +25 -3
  103. package/dist/shared/model-error.js +78 -10
  104. package/dist/shared/model-error.js.map +1 -1
  105. package/dist/shared/platform-server-address.d.ts +71 -0
  106. package/dist/shared/platform-server-address.js +103 -0
  107. package/dist/shared/platform-server-address.js.map +1 -0
  108. package/dist/shared/synthesized-attachment.d.ts +0 -5
  109. package/dist/shared/synthesized-attachment.js +0 -12
  110. package/dist/shared/synthesized-attachment.js.map +1 -1
  111. package/dist/shared/tool-rounds.d.ts +51 -23
  112. package/dist/shared/tool-rounds.js +67 -24
  113. package/dist/shared/tool-rounds.js.map +1 -1
  114. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  115. package/dist/shared/workspace/platform-dir.js +18 -1
  116. package/dist/shared/workspace/platform-dir.js.map +1 -1
  117. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  118. package/dist/shared/workspace/stigmer-link.js +91 -29
  119. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  120. package/dist/tools/url-guard.d.ts +5 -1
  121. package/dist/tools/url-guard.js +9 -2
  122. package/dist/tools/url-guard.js.map +1 -1
  123. package/dist/worker.d.ts +11 -2
  124. package/dist/worker.js +1 -1
  125. package/dist/worker.js.map +1 -1
  126. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  127. package/dist/workflow-engine/tasks/human-input.js +79 -37
  128. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  129. package/dist/workflow-engine/types.d.ts +14 -4
  130. package/dist/workflow-engine/types.js.map +1 -1
  131. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  132. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  133. package/dist/workflows/engine-core.d.ts +25 -0
  134. package/dist/workflows/engine-core.js.map +1 -1
  135. package/dist/workflows/execute-from-execution.d.ts +3 -0
  136. package/dist/workflows/execute-from-execution.js.map +1 -1
  137. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  138. package/dist/workflows/human-input-orchestrator.js +12 -44
  139. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  140. package/package.json +5 -5
  141. package/src/__test-utils__/config-fixture.ts +1 -0
  142. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  143. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  144. package/src/__test-utils__/test-gate.ts +8 -0
  145. package/src/__tests__/exit-backstop.test.ts +91 -0
  146. package/src/__tests__/golden-e2e.test.ts +80 -62
  147. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  148. package/src/__tests__/runner.test.ts +60 -2
  149. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  150. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  151. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  152. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  153. package/src/activities/call-agent.ts +1 -1
  154. package/src/activities/discover-mcp-server.ts +27 -37
  155. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  156. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  157. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  158. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  159. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  160. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  161. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  162. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  163. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  164. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  165. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  166. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  167. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  168. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  169. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  170. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  171. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  172. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  173. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  174. package/src/activities/execute-cursor/translator.ts +15 -32
  175. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  176. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  177. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  178. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  179. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  180. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  181. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  182. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  183. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  184. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  185. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  186. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  187. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  188. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  189. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  190. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  191. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  192. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  193. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  194. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  195. package/src/activities/execute-deep-agent/translator.ts +5 -4
  196. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  197. package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
  198. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  199. package/src/activities/execute-deep-agent/turn.ts +16 -7
  200. package/src/activities/workflow-event-activities.ts +2 -0
  201. package/src/config.ts +11 -0
  202. package/src/exit-backstop.ts +97 -0
  203. package/src/harness/README.md +5 -5
  204. package/src/harness/terminal-table.ts +13 -13
  205. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  206. package/src/harness/transcript/builder.ts +53 -30
  207. package/src/harness/transcript/events.ts +22 -27
  208. package/src/harness/turn-context.ts +5 -3
  209. package/src/main.ts +9 -5
  210. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  211. package/src/middleware/cost-advisory.ts +6 -3
  212. package/src/middleware/execution-budget.ts +60 -34
  213. package/src/middleware/index.ts +10 -7
  214. package/src/middleware/types.ts +6 -1
  215. package/src/runner-manager.ts +3 -2
  216. package/src/runner.ts +57 -15
  217. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  218. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  219. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  220. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  221. package/src/shared/__tests__/model-error.test.ts +71 -3
  222. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  223. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  224. package/src/shared/approval-policy.ts +57 -14
  225. package/src/shared/caller-identity.ts +4 -5
  226. package/src/shared/channel-attachment.ts +2 -2
  227. package/src/shared/connect-backfill.ts +5 -1
  228. package/src/shared/mcp-resolver.ts +11 -1
  229. package/src/shared/memory-attachment.ts +2 -2
  230. package/src/shared/model-error.ts +87 -9
  231. package/src/shared/platform-server-address.ts +123 -0
  232. package/src/shared/synthesized-attachment.ts +0 -12
  233. package/src/shared/tool-rounds.ts +70 -24
  234. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  235. package/src/shared/workspace/platform-dir.ts +19 -1
  236. package/src/shared/workspace/stigmer-link.ts +99 -29
  237. package/src/tools/__tests__/url-guard.test.ts +19 -1
  238. package/src/tools/url-guard.ts +9 -1
  239. package/src/worker.ts +12 -2
  240. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  241. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  242. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  243. package/src/workflow-engine/tasks/human-input.ts +92 -42
  244. package/src/workflow-engine/types.ts +16 -4
  245. package/src/workflows/call-agent-orchestrator.ts +7 -0
  246. package/src/workflows/engine-core.ts +18 -0
  247. package/src/workflows/execute-from-execution.ts +2 -1
  248. package/src/workflows/human-input-orchestrator.ts +15 -56
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Hermetic: a GATED built-in that runs under the execution's global bypass
3
+ * (`spec.auto_approve_all`), through the whole `ExecuteCursor` activity.
4
+ *
5
+ * Invariant pinned (#1117): `requiresApproval` says whether THIS call was held
6
+ * for a person's decision, on every harness. A shell the bypass lets run is
7
+ * never held, so its row carries no approval fields — the same answer the
8
+ * native harness gives, where a call that starts was authorized by
9
+ * construction. The row still carries the provenance that explains why it
10
+ * ran (`AUTO_APPROVE_ALL`), which is the field that answers "which policy
11
+ * layer governed this call". Until #1117 the Cursor translator stamped the
12
+ * policy's category verdict on the row at tool start, lease- and bypass-blind,
13
+ * so this row read `requiresApproval: true` with an approval message nobody
14
+ * was ever shown.
15
+ */
16
+
17
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
18
+ import { ApprovalPolicySource, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
19
+
20
+ vi.mock("@cursor/sdk", async () =>
21
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
22
+ );
23
+ vi.mock("@cursor/sdk/sqlite", async () =>
24
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule(),
25
+ );
26
+ vi.mock("../../../../client/stigmer-client.js", async () =>
27
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
28
+ );
29
+
30
+ import {
31
+ ScriptedClock,
32
+ createHermeticEnvironment,
33
+ type HermeticEnvironment,
34
+ } from "../../../../__test-utils__/hermetic-activity.js";
35
+ import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
36
+ import {
37
+ FIXTURE,
38
+ SDK_CATALOG,
39
+ beginCursorScenario,
40
+ cursorExecutionRecord,
41
+ runCursorTurn,
42
+ } from "../../__test-utils__/hermetic-cursor.js";
43
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
44
+
45
+ const AGENT_ID = "agent-hermetic-bypass-0001";
46
+ const RUN_ID = "run-hermetic-bypass-0001";
47
+ const CALL_ID = "call-hermetic-shell-0001";
48
+ const USER_MESSAGE = "Run the tests.";
49
+ const SHELL_ARGS = { command: "npm test" };
50
+ const SHELL_RESULT = "ok 12 tests\n";
51
+ const ASSISTANT_TEXT = "All twelve tests pass.";
52
+
53
+ describe("ExecuteCursor hermetic — a gated built-in under the global bypass", () => {
54
+ let env: HermeticEnvironment;
55
+ let registry: ReturnType<typeof stubRegistryFetch>;
56
+ const clock = new ScriptedClock();
57
+
58
+ beforeAll(() => {
59
+ env = createHermeticEnvironment();
60
+ registry = stubRegistryFetch();
61
+ clock.install();
62
+ });
63
+
64
+ afterAll(() => {
65
+ clock.uninstall();
66
+ registry.restore();
67
+ env.dispose();
68
+ });
69
+
70
+ it("persists the shell it ran as not held: no approval fields, AUTO_APPROVE_ALL provenance", async () => {
71
+ // ── Arrange ──────────────────────────────────────────────────────────────
72
+ const ev = sdkEvents(AGENT_ID, RUN_ID);
73
+ const agent = new ScriptedCursorAgent({
74
+ agentId: AGENT_ID,
75
+ runIds: [RUN_ID],
76
+ observeStep: () => clock.tick(),
77
+ turns: [
78
+ [
79
+ step.event(ev.init()),
80
+ step.event(ev.assistant("Running them now.")),
81
+ step.event(ev.toolCall(CALL_ID, "shell", "running", SHELL_ARGS)),
82
+ step.event(ev.toolCall(CALL_ID, "shell", "completed", SHELL_ARGS, SHELL_RESULT)),
83
+ step.event(ev.assistant(ASSISTANT_TEXT)),
84
+ step.turnEnded({ inputTokens: 1_500, outputTokens: 40, cacheReadTokens: 0, cacheWriteTokens: 0 }),
85
+ step.finished({ result: ASSISTANT_TEXT, model: { id: FIXTURE.model, params: [] } }),
86
+ ],
87
+ ],
88
+ });
89
+ const record = cursorExecutionRecord({ message: USER_MESSAGE, autoApproveAll: true });
90
+ const scenario = beginCursorScenario({
91
+ env,
92
+ clock,
93
+ record,
94
+ sdk: { agents: [agent], catalog: SDK_CATALOG },
95
+ });
96
+
97
+ // ── Act ──────────────────────────────────────────────────────────────────
98
+ const invocation = await runCursorTurn(scenario);
99
+
100
+ // ── Assert: the turn ran to completion, nothing parked ───────────────────
101
+ expect(invocation.outcome.kind).toBe("returned");
102
+ expect((invocation.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
103
+ expect(record.waitingToolCalls(), "a bypassed call is never parked").toHaveLength(0);
104
+ expect(agent.runs[0].cancelCalls, "nothing was denied, so nothing was cancelled").toHaveLength(0);
105
+
106
+ // ── Assert: the row says it ran, and why, and that nobody was asked ───────
107
+ const rows = record.toolCalls();
108
+ expect(rows).toHaveLength(1);
109
+ const row = rows[0];
110
+ expect(row.name).toBe("shell");
111
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
112
+ expect(row.approvalPolicySource, "the layer that let it run").toBe(ApprovalPolicySource.AUTO_APPROVE_ALL);
113
+ expect(row.requiresApproval, "never held for a decision").toBe(false);
114
+ expect(row.approvalMessage, "no card was ever shown").toBe("");
115
+ expect(row.approvalRequestedAt, "no approval was ever requested").toBe("");
116
+
117
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
118
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
119
+ });
120
+ });
@@ -160,15 +160,15 @@ describe.skipIf(!hasBash)("ExecuteCursor hermetic — file-review capture on a g
160
160
  expect(candidate.value.changeSetId).toBe(CHANGE_SET_ID);
161
161
  expect(candidate.value.changes.map((c) => c.pathAfter)).toEqual([FILE]);
162
162
 
163
- // The flowed edit row: the built-in policy still classifies `edit` as a
164
- // gated category (`requiresApproval: true` is the policy's verdict, stamped
165
- // at fold time), but capture mode let it FLOW — so the row is COMPLETED,
166
- // never WAITING_APPROVAL, and the review happens on the file card instead.
163
+ // The flowed edit row: `edit` is a gated category, but capture mode let it
164
+ // FLOW, so nobody was asked — the row is COMPLETED, never WAITING_APPROVAL,
165
+ // carries no approval fields (#1117: `requiresApproval` means the call was
166
+ // held), and the review happens on the file card instead.
167
167
  const rows = record.toolCalls();
168
168
  expect(rows).toHaveLength(1);
169
169
  expect(rows[0].name).toBe("edit");
170
170
  expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
171
- expect(rows[0].requiresApproval, "policy verdict, not the gate outcome").toBe(true);
171
+ expect(rows[0].requiresApproval, "held for a decision? no: capture mode let it flow").toBe(false);
172
172
  expect(record.waitingToolCalls(), "no gate row — the review is on the file card").toHaveLength(0);
173
173
 
174
174
  // ── Assert: hermeticity ──────────────────────────────────────────────────
@@ -16,8 +16,6 @@
16
16
  "status": "TOOL_CALL_COMPLETED",
17
17
  "startedAt": "2026-01-01T00:00:05.000Z",
18
18
  "completedAt": "2026-01-01T00:00:06.000Z",
19
- "requiresApproval": true,
20
- "approvalMessage": "Write file: notes.md",
21
19
  "argsPreview": "{\"path\":\"notes.md\",\"content\":\"# Notes\\n\\nA fixture file.\\n\\nReviewed by the hermetic run.\\n\"}",
22
20
  "toolKind": "TOOL_KIND_FILE_EDIT",
23
21
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -12,7 +12,7 @@
12
12
  }
13
13
  ],
14
14
  "phase": "EXECUTION_FAILED",
15
- "error": "Execution failed: [Error] hermetic fault injected at getAgent",
15
+ "error": "[Error] hermetic fault injected at getAgent",
16
16
  "startedAt": "2026-01-01T00:00:00.000Z",
17
17
  "completedAt": "2026-01-01T00:00:00.000Z"
18
18
  }
@@ -15,9 +15,6 @@
15
15
  "startedAt": "2026-01-01T00:00:25.000Z",
16
16
  "completedAt": "2026-01-01T00:00:27.000Z",
17
17
  "error": "sh: npm: command not found",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
- "approvalRequestedAt": "2026-01-01T00:00:27.000Z",
21
18
  "argsPreview": "{\"command\":\"npm test\"}",
22
19
  "toolKind": "TOOL_KIND_SHELL",
23
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,8 +15,6 @@
15
15
  "status": "TOOL_CALL_COMPLETED",
16
16
  "startedAt": "2026-01-01T00:00:16.000Z",
17
17
  "completedAt": "2026-01-01T00:00:19.000Z",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
18
  "argsPreview": "{\"command\":\"npm test\"}",
21
19
  "toolKind": "TOOL_KIND_SHELL",
22
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,8 +15,6 @@
15
15
  "status": "TOOL_CALL_COMPLETED",
16
16
  "startedAt": "2026-01-01T00:00:03.000Z",
17
17
  "completedAt": "2026-01-01T00:00:08.000Z",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: npm test",
20
18
  "argsPreview": "{\"command\":\"npm test\"}",
21
19
  "toolKind": "TOOL_KIND_SHELL",
22
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -13,8 +13,6 @@
13
13
  },
14
14
  "status": "TOOL_CALL_RUNNING",
15
15
  "startedAt": "2026-01-01T00:00:03.000Z",
16
- "requiresApproval": true,
17
- "approvalMessage": "Run command: npm test",
18
16
  "argsPreview": "{\"command\":\"npm test\"}",
19
17
  "toolKind": "TOOL_KIND_SHELL",
20
18
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -15,9 +15,6 @@
15
15
  "startedAt": "2026-01-01T00:00:03.000Z",
16
16
  "completedAt": "2026-01-01T00:00:03.000Z",
17
17
  "error": "blocked by a hook",
18
- "requiresApproval": true,
19
- "approvalMessage": "Run command: rm -rf build",
20
- "approvalRequestedAt": "2026-01-01T00:00:03.000Z",
21
18
  "argsPreview": "{\"command\":\"rm -rf build\"}",
22
19
  "toolKind": "TOOL_KIND_SHELL",
23
20
  "approvalPolicySource": "APPROVAL_POLICY_SOURCE_BUILTIN_CATEGORY",
@@ -17,8 +17,9 @@
17
17
  * the stream's `running` instant; the live output reaches `result`
18
18
  * (`streamingSource: OUTPUT` while it streams, cleared at finalize); the
19
19
  * stream's `completed` stamps `completedAt` and its result wins; the shell
20
- * row is a gated built-in (`requiresApproval`, "Run command: …") that the
21
- * hook never saw, so no `approvalRequestedAt`. Predicted: NO move (the
20
+ * row is a gated built-in the hook never saw, so it was never held and
21
+ * carries no approval fields (#1117 moved `requiresApproval` and "Run
22
+ * command: …" off rows that ran). Predicted: NO move (the
22
23
  * translator drains the queued deltas AFTER the stream event of the
23
24
  * same window, as the enricher applied them, so the stream's instant wins
24
25
  * exactly as before).
@@ -33,9 +34,10 @@
33
34
  * Arm 3 — the completion delta reports an ERROR, and a model event sits
34
35
  * between it and the stream's own `error`. Pinned: the translator honours the
35
36
  * delta's `result.status: "error"` — the row is FAILED from the
36
- * instant the failure was observed (`completedAt` and, for a gated shell,
37
- * `approvalRequestedAt` at the delta's own tick), and the stream's `error`
38
- * event that follows supplies the text. Until #1097 (as this net first
37
+ * instant the failure was observed (`completedAt` at the delta's own tick),
38
+ * and the stream's `error` event that follows supplies the text. Until #1117
39
+ * a gated shell's ordinary failure also stamped `approvalRequestedAt`, though
40
+ * nobody was ever asked; a failure is not a request. Until #1097 (as this net first
39
41
  * pinned it) the enricher promoted every completion delta to
40
42
  * COMPLETED, the monotonic merge then refused the stream's FAILED, and the
41
43
  * row ended COMPLETED carrying an `error` — a row that lied. Moved in #1097
@@ -178,7 +180,7 @@ describe("ExecuteCursor hermetic — the delta channel on a shell row", () => {
178
180
  expect(row.result, "the live output reached the row").toBe(FULL_OUTPUT);
179
181
  expect(row.isStreaming, "finalize closed the OUTPUT stream").toBe(false);
180
182
  expect(row.startedAt < row.completedAt).toBe(true);
181
- expect(row.requiresApproval, "a shell is a gated built-in the hook never saw").toBe(true);
183
+ expect(row.requiresApproval, "a shell the hook never saw was never held").toBe(false);
182
184
  expect(row.approvalRequestedAt).toBe("");
183
185
  expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
184
186
 
@@ -268,7 +270,7 @@ describe("ExecuteCursor hermetic — the delta channel on a shell row", () => {
268
270
  expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
269
271
  expect(row.error, "the stream's error event supplied the text").toBe(SHELL_ERROR);
270
272
  expect(row.result).toBe("");
271
- expect(row.approvalRequestedAt, "a gated shell's gate stamp shares the observed instant").toBe(row.completedAt);
273
+ expect(row.approvalRequestedAt, "a failure is not an approval request").toBe("");
272
274
 
273
275
  const json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
274
276
  await expect(json).toMatchFileSnapshot("./goldens/shell-output-delta.error-delta.status.json");
@@ -37,6 +37,11 @@
37
37
  * golden is unchanged: its turn fails before the workspace is provisioned,
38
38
  * so no capture is pinned.
39
39
  *
40
+ * Golden hunk since stigmer#1122: `resolution-error`'s `status.error` is
41
+ * `[Error] <message>`, the details row's text, where it was
42
+ * `Execution failed: [Error] <message>`; one grammar for a FAILED error,
43
+ * whichever side caught the fault.
44
+ *
40
45
  * Regenerate ONLY after a deliberate behavior change:
41
46
  * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
42
47
  */
@@ -180,7 +185,7 @@ describe("ExecuteCursor hermetic — thrown-error arms of the outer catch", () =
180
185
  expect(slim.phase).toBe("EXECUTION_FAILED");
181
186
  expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_FAILED]);
182
187
  const final = record.lastFullStatus!;
183
- expect(final.error).toBe(`Execution failed: [Error] ${CONTROL_PLANE_FAULT}`);
188
+ expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
184
189
  expect(final.completedAt).not.toBe("");
185
190
  expect(systemMessages(final)).toEqual([
186
191
  GENERIC_SYSTEM_MESSAGE,
@@ -296,14 +296,13 @@ describe("Cursor HITL resume — append-only transcript", () => {
296
296
  expect(status.subAgentExecutions.some((s) => s.id === "sub_1")).toBe(true);
297
297
  });
298
298
 
299
- it("FIX: the approved tools are ALLOWED by the hook on resume — not re-denied (the loop's source)", () => {
300
- // The transcript-superset fix keeps the run off the watchdog, but the OTHER
301
- // half of "no loop" is that the approved tools' grants actually let the hook
302
- // ALLOW the re-issued calls. Drive the real bash hook with the grants the
303
- // resume mints from the persisted run-1 decisions and prove the two approved
304
- // MCP tools pass while the genuinely-new `click` is still gated.
305
- if (!hasBash) return; // hook tests require bash; mirrored skip of the d() guard.
306
-
299
+ // The transcript-superset fix keeps the run off the watchdog, but the OTHER
300
+ // half of "no loop" is that the approved tools' grants actually let the hook
301
+ // ALLOW the re-issued calls. Drive the real bash hook with the grants the
302
+ // resume mints from the persisted run-1 decisions and prove the two approved
303
+ // MCP tools pass while the genuinely-new `click` is still gated. The hook
304
+ // needs bash, mirroring the d() guard.
305
+ it.skipIf(!hasBash)("FIX: the approved tools are ALLOWED by the hook on resume — not re-denied (the loop's source)", () => {
307
306
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(persistedRunOneMessages());
308
307
  const grants = buildApprovalGrants(pendingApprovals, decisions);
309
308
  const state = buildApprovalState(new Map(), false, new Set(), grants);
@@ -426,9 +425,7 @@ describe("Cursor HITL resume — two approvals then clean completion (no loop)",
426
425
  expect(guardRejectionReason(committed, seeded)).toBeUndefined();
427
426
  });
428
427
 
429
- it("the built-in grants actually let the hook allow both re-issues (full closure)", () => {
430
- if (!hasBash) return;
431
-
428
+ it.skipIf(!hasBash)("the built-in grants actually let the hook allow both re-issues (full closure)", () => {
432
429
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(committedBuiltInApprovals());
433
430
  const grants = buildApprovalGrants(pendingApprovals, decisions);
434
431
  const state = buildApprovalState(new Map(), false, new Set(), grants);
@@ -170,9 +170,7 @@ describe("Cursor sequential gates A->B across resume", () => {
170
170
  expect(guardRejectionReason(committed, seeded)).toBeUndefined();
171
171
  });
172
172
 
173
- it("the hook ALLOWS gate A's re-issue and DENIES gate B (out-of-process closure)", () => {
174
- if (!hasBash) return;
175
-
173
+ it.skipIf(!hasBash)("the hook ALLOWS gate A's re-issue and DENIES gate B (out-of-process closure)", () => {
176
174
  // Only gate A is approved going into the resume; its grant is what the hook
177
175
  // reads. Gate B has no grant yet (it gates for the first time this turn).
178
176
  const { pendingApprovals, decisions } = reconstructAdjudicatedApprovals(approvedGateA());
@@ -17,10 +17,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
17
17
  import { create } from "@bufbuild/protobuf";
18
18
  import type { InteractionUpdate, SDKMessage } from "@cursor/sdk";
19
19
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
20
- import { ApprovalAction, MessageType, SubAgentStatus, ToolCallStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
20
+ import { ApprovalAction, ApprovalPolicySource, MessageType, SubAgentStatus, ToolCallStatus, ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
21
21
  import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
22
22
  import type { TranscriptEvent } from "../../../harness/transcript/events.js";
23
23
  import type { MergedToolPolicy } from "../../../shared/approval-policy.js";
24
+ import type { ToolApprovalCategory } from "../../../shared/tool-kind.js";
24
25
  import { CursorFold, foldCursorEvents } from "../__test-utils__/fold.js";
25
26
  import { sdkEvents } from "../__test-utils__/scripted-agent.js";
26
27
  import { grantToken } from "../approval-state.js";
@@ -118,19 +119,26 @@ describe("CursorTranslator — a tool_call event is the row's start and, when te
118
119
  expect(started).toEqual({ kind: "tool_started", callId: "c1", name: "read", input: { path: "a.md" }, mcpServerSlug: "" });
119
120
  });
120
121
 
121
- it("a gated built-in carries the policy's word: the category's message over the args", () => {
122
+ it("a gated built-in's start carries its provenance and no approval: only the boundary says a call was held (#1117)", () => {
122
123
  const [started] = translator().translate(ev.toolCall("c1", "shell", "running", { command: "npm test" }));
123
- expect(started).toMatchObject({ kind: "tool_started", gate: { message: "Run command: npm test" }, provenance: "builtin_category" });
124
+ expect(started).toEqual({ kind: "tool_started", callId: "c1", name: "shell", input: { command: "npm test" }, mcpServerSlug: "", provenance: "builtin_category" });
124
125
  const fold = foldCursorEvents([ev.toolCall("c1", "shell", "running", { command: "npm test" })]);
125
- expect(fold.row("c1").requiresApproval).toBe(true);
126
- expect(fold.row("c1").approvalMessage).toBe("Run command: npm test");
127
- expect(fold.row("c1").status, "gated but RUNNING: the boundary parks it, never the stream").toBe(ToolCallStatus.TOOL_CALL_RUNNING);
126
+ expect(fold.row("c1").requiresApproval, "not held yet: the hook has not denied it").toBe(false);
127
+ expect(fold.row("c1").approvalMessage).toBe("");
128
+ expect(fold.row("c1").status, "RUNNING: the boundary parks a denied call, never the stream").toBe(ToolCallStatus.TOOL_CALL_RUNNING);
128
129
  });
129
130
 
130
- it("a leased category is still the policy's word (lease-blind, as the accumulator was; the harness difference is #1117)", () => {
131
- const t = new CursorTranslator({ policies: new Map(), leases: { global: false, categories: new Set(["shell"]) }, seeded: [] });
132
- const [started] = t.translate(ev.toolCall("c1", "shell", "running", { command: "ls" }));
133
- expect(started).toMatchObject({ gate: { message: "Run command: ls" }, provenance: "approval_lease" });
131
+ it("a call under an approve-all lease runs as not held, the lease named as its provenance (#1117)", () => {
132
+ const leases = { global: false, categories: new Set<ToolApprovalCategory>(["shell"]) };
133
+ const fold = new CursorFold({ leases }).events(
134
+ ev.toolCall("c1", "shell", "running", { command: "ls" }),
135
+ ev.toolCall("c1", "shell", "completed", { command: "ls" }, "a.md"),
136
+ );
137
+ expect(fold.row("c1").status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
138
+ expect(fold.row("c1").approvalPolicySource).toBe(ApprovalPolicySource.APPROVAL_LEASE);
139
+ expect(fold.row("c1").requiresApproval, "the lease let it run; nobody was asked").toBe(false);
140
+ expect(fold.row("c1").approvalMessage).toBe("");
141
+ expect(fold.row("c1").approvalRequestedAt).toBe("");
134
142
  });
135
143
 
136
144
  it("completed → tool_finished with the result rendered to the row's string; a string as-is, an envelope stringified", () => {
@@ -163,6 +171,28 @@ describe("CursorTranslator — a tool_call event is the row's start and, when te
163
171
  expect(fold.messages[0].toolCalls.map((tc) => tc.id)).toEqual(["c1"]);
164
172
  });
165
173
 
174
+ describe("one stream event is one instant, whatever the clock does between its folds (stigmer#1390)", () => {
175
+ beforeEach(() => {
176
+ vi.useFakeTimers({ toFake: ["Date"] });
177
+ vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
178
+ });
179
+ afterEach(() => {
180
+ vi.useRealTimers();
181
+ });
182
+
183
+ it("a completion or an error with no running before it starts and ends at the same instant", () => {
184
+ // The observer moves the clock 1 ms after every fold, so a start and a
185
+ // finish read separately would be a millisecond apart, as in the flake.
186
+ const fold = new CursorFold({ observer: () => vi.advanceTimersByTime(1) }).events(
187
+ ev.toolCall("c1", "read", "completed", { path: "x" }, "done"),
188
+ ev.toolCall("c2", "read", "error", { path: "y" }, "ENOENT"),
189
+ );
190
+ expect([fold.row("c1").startedAt, fold.row("c1").completedAt]).toEqual(["2026-01-01T00:00:00.000Z", "2026-01-01T00:00:00.000Z"]);
191
+ expect(fold.row("c2").startedAt).not.toBe("2026-01-01T00:00:00.000Z");
192
+ expect(fold.row("c2").completedAt).toBe(fold.row("c2").startedAt);
193
+ });
194
+ });
195
+
166
196
  it("a completion after later text lands on the row where it started, never on the new message (cross-message completion)", () => {
167
197
  const fold = foldCursorEvents([
168
198
  ev.assistant("Running two things."),
@@ -230,19 +260,19 @@ describe("CursorTranslator — the MCP envelope is unwrapped to the inner tool",
230
260
  expect(started).toEqual({ kind: "tool_started", callId: "m1", name: "search_services", input: { query: "db" }, mcpServerSlug: "planton", provenance: "classifier_default" });
231
261
  });
232
262
 
233
- it("a policy that requires approval is the gate, its message resolved over the inner args", () => {
263
+ it("a policy that requires approval is the call's provenance; the row is not held until the boundary parks it", () => {
234
264
  const t = new CursorTranslator({ policies, leases: { global: false, categories: new Set() }, seeded: [] });
235
265
  const [started] = t.translate(ev.toolCall("m1", "mcp", "running", MCP_ARGS));
236
- expect(started).toMatchObject({ gate: { message: "Search db on search_services" }, provenance: "pinned_override" });
266
+ expect(started).toEqual({ kind: "tool_started", callId: "m1", name: "search_services", input: { query: "db" }, mcpServerSlug: "planton", provenance: "pinned_override" });
237
267
  const fold = new CursorFold({ policies }).events(ev.toolCall("m1", "mcp", "running", MCP_ARGS));
238
268
  expect(fold.row("m1").toolKind).toBe(ToolKind.MCP);
269
+ expect(fold.row("m1").requiresApproval).toBe(false);
239
270
  expect(fold.row("m1").argsPreview, "the row's own args, never the envelope").toBe(JSON.stringify({ query: "db" }));
240
271
  });
241
272
 
242
273
  it("an mcp event with no toolName is an ordinary tool named mcp, and is never gated by a built-in category", () => {
243
274
  const [started] = translator().translate(ev.toolCall("m1", "mcp", "running", { providerIdentifier: "x" }));
244
- expect(started).toMatchObject({ name: "mcp", mcpServerSlug: "", input: { providerIdentifier: "x" } });
245
- expect((started as { gate?: unknown }).gate).toBeUndefined();
275
+ expect(started).toEqual({ kind: "tool_started", callId: "m1", name: "mcp", input: { providerIdentifier: "x" }, mcpServerSlug: "" });
246
276
  });
247
277
  });
248
278
 
@@ -314,6 +344,29 @@ describe("CursorTranslator — a task tool call is a sub-agent, its transcript d
314
344
  expect(fold.status.subAgentExecutions[0].messages).toHaveLength(2);
315
345
  });
316
346
 
347
+ describe("one stream event is one instant, whatever the clock does between its folds (stigmer#1390)", () => {
348
+ beforeEach(() => {
349
+ vi.useFakeTimers({ toFake: ["Date"] });
350
+ vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
351
+ });
352
+ afterEach(() => {
353
+ vi.useRealTimers();
354
+ });
355
+
356
+ it("a task completion with no running before it: the task row, the sub-agent row and the child's rows and messages share it", () => {
357
+ const fold = new CursorFold({ observer: () => vi.advanceTimersByTime(1) }).event(ev.toolCall("task-1", "task", "completed", TASK_ARGS, STEPS));
358
+ const task = fold.row("task-1");
359
+ const [sub] = fold.status.subAgentExecutions;
360
+ const read = sub.messages.flatMap((m) => m.toolCalls)[0];
361
+ expect([
362
+ task.startedAt, task.completedAt,
363
+ sub.startedAt, sub.completedAt,
364
+ read.startedAt, read.completedAt,
365
+ ...sub.messages.map((m) => m.timestamp),
366
+ ]).toEqual(Array(8).fill("2026-01-01T00:00:00.000Z"));
367
+ });
368
+ });
369
+
317
370
  it("error → the row's error then sub_agent_failed with the same text", () => {
318
371
  const t = translator();
319
372
  t.translate(ev.toolCall("task-1", "task", "running", TASK_ARGS));
@@ -549,7 +602,7 @@ describe("CursorTranslator — the delta channel is queued, held for unannounced
549
602
  fold.event(ev.thinking("It failed."));
550
603
  expect(fold.row("c2").status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
551
604
  expect(fold.row("c2").completedAt).toBe("2026-01-01T00:00:07.000Z");
552
- expect(fold.row("c2").approvalRequestedAt, "a gated shell").toBe("2026-01-01T00:00:07.000Z");
605
+ expect(fold.row("c2").approvalRequestedAt, "a failure is not an approval request (#1117)").toBe("");
553
606
  fold.event(ev.toolCall("c2", "shell", "error", { command: "make" }, "sh: make: command not found"));
554
607
  expect(fold.row("c2").error).toBe("sh: make: command not found");
555
608
  expect(fold.row("c2").status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
@@ -27,9 +27,9 @@ export {
27
27
  mergeApprovalPolicies,
28
28
  lookupMcpToolPolicy,
29
29
  resolveApprovalMessage,
30
+ resolveBuiltInApprovalMessage,
30
31
  POLICY_ENGINE_VERSION,
31
32
  } from "../../shared/approval-policy.js";
32
- import { CATEGORY_APPROVAL_MESSAGE } from "../../shared/approval-policy.js";
33
33
  export type { MergedToolPolicy, PolicySource } from "../../shared/approval-policy.js";
34
34
 
35
35
  /**
@@ -43,6 +43,16 @@ export type { MergedToolPolicy, PolicySource } from "../../shared/approval-polic
43
43
  * taxonomy because it is consulted only to build the hook's gated set and its
44
44
  * name->category mapping. Cross-layer correlation never compares these raw
45
45
  * names — it uses {@link approvalCategory} (see below).
46
+ *
47
+ * Only mutating and destructive tools are gated; everything else (read-only
48
+ * built-ins, and auto-approved MCP tools) is allowed. This "gate the dangerous
49
+ * set, allow the rest" model is the platform's one approval rule, read through
50
+ * the shared `toolApprovalCategory` (`shared/tool-kind.ts`) that the native gate
51
+ * (`middleware/approval-gate.ts` `resolveToolApproval`) also reads: one
52
+ * taxonomy, two enforcement points. It is deliberately fail-OPEN for unknown
53
+ * tools: the merged MCP policy map carries only the tools that REQUIRE approval,
54
+ * so a fail-closed default would deny every auto-approved MCP tool, which the
55
+ * hook cannot distinguish from an unknown built-in by name.
46
56
  */
47
57
  const BUILT_IN_GATED: ReadonlySet<string> = new Set([
48
58
  "Write",
@@ -83,27 +93,6 @@ export const approvalCategory = toolApprovalCategory;
83
93
  export { SALIENT_ARG_FIELDS } from "../../shared/args-preview.js";
84
94
  import { SALIENT_ARG_FIELDS } from "../../shared/args-preview.js";
85
95
 
86
- /**
87
- * Check whether a built-in (non-MCP) Cursor tool requires user approval.
88
- *
89
- * Resolved via {@link approvalCategory} so it answers correctly for BOTH
90
- * taxonomies — the hook's `Write`/`Shell`/`Delete` and the stream's
91
- * `edit`/`shell`/`delete` all return true. Only mutating/destructive tools are
92
- * gated; everything else (read-only built-ins, and — at the hook layer —
93
- * auto-approved MCP tools) is allowed. This "gate the dangerous set, allow the
94
- * rest" model is the platform's one approval rule, read through the shared
95
- * `approvalCategory` / `toolApprovalCategory` (`shared/tool-kind.ts`) that the
96
- * native gate (`middleware/approval-gate.ts` `resolveToolApproval`) also
97
- * reads — one taxonomy, two enforcement points. It is deliberately fail-OPEN
98
- * for unknown tools: the merged MCP policy map carries
99
- * only the tools that REQUIRE approval, so a fail-closed default would wrongly
100
- * deny every auto-approved MCP tool, which the hook cannot distinguish from an
101
- * unknown built-in by name.
102
- */
103
- export function builtInRequiresApproval(toolName: string): boolean {
104
- return approvalCategory(toolName) !== undefined;
105
- }
106
-
107
96
  /**
108
97
  * Returns the built-in tool names that require approval (the gated set the
109
98
  * preToolUse hook denies unless auto-approved or granted on reinvocation).
@@ -134,20 +123,6 @@ export function getBuiltInGatedCategories(): Array<[string, ApprovalCategory]> {
134
123
  return pairs;
135
124
  }
136
125
 
137
- /**
138
- * Approval-message template for a gated built-in tool (either taxonomy), or
139
- * undefined when the tool is not gated. Resolved via {@link approvalCategory}
140
- * so stream-side names (`edit`/`shell`/`delete`) and hook-side names
141
- * (`Write`/`Shell`/`Delete`) both map to the same template — the platform's
142
- * ONE table, `shared/approval-policy.ts` `CATEGORY_APPROVAL_MESSAGE`, shared
143
- * with the native gate since #1097. Callers resolve the
144
- * placeholders against the tool args via resolveApprovalMessage.
145
- */
146
- export function getBuiltInApprovalMessage(toolName: string): string | undefined {
147
- const category = approvalCategory(toolName);
148
- return category ? CATEGORY_APPROVAL_MESSAGE[category] : undefined;
149
- }
150
-
151
126
  /**
152
127
  * Extract the canonical "salient" argument value that identifies the resource a
153
128
  * built-in tool acts on (the file path, the shell command, …). Returns "" when
@@ -34,7 +34,7 @@ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agent
34
34
  import { ApprovalPolicySource, MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
35
35
  import type { TranscriptBuilder } from "../../harness/transcript/builder.js";
36
36
  import type { MergedToolPolicy } from "./approval-policy.js";
37
- import { lookupMcpToolPolicy, resolveApprovalMessage, getBuiltInApprovalMessage } from "./approval-policy.js";
37
+ import { lookupMcpToolPolicy, resolveApprovalMessage, resolveBuiltInApprovalMessage } from "./approval-policy.js";
38
38
  import {
39
39
  POLICY_ENGINE_VERSION,
40
40
  resolveApprovalProvenance,
@@ -1125,8 +1125,8 @@ function resolveDeniedApprovalMessage(
1125
1125
  if (policy) return resolveApprovalMessage(policy.approvalMessage, name, args);
1126
1126
  }
1127
1127
  if (!mcpServerSlug) {
1128
- const template = getBuiltInApprovalMessage(name);
1129
- if (template) return resolveApprovalMessage(template, name, args);
1128
+ const message = resolveBuiltInApprovalMessage(name, args);
1129
+ if (message !== undefined) return message;
1130
1130
  }
1131
1131
  return `Tool requires approval: ${name}`;
1132
1132
  }