@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.
- package/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +7 -7
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- 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:
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
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, "
|
|
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 ──────────────────────────────────────────────────
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json
CHANGED
|
@@ -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",
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"phase": "EXECUTION_FAILED",
|
|
15
|
-
"error": "
|
|
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",
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json
CHANGED
|
@@ -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",
|
package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json
CHANGED
|
@@ -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
|
|
21
|
-
*
|
|
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`
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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
|
|
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(`
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
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).
|
|
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(
|
|
126
|
-
expect(fold.row("c1").approvalMessage).toBe("
|
|
127
|
-
expect(fold.row("c1").status, "
|
|
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
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
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
|
|
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).
|
|
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).
|
|
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
|
|
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,
|
|
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
|
|
1129
|
-
if (
|
|
1128
|
+
const message = resolveBuiltInApprovalMessage(name, args);
|
|
1129
|
+
if (message !== undefined) return message;
|
|
1130
1130
|
}
|
|
1131
1131
|
return `Tool requires approval: ${name}`;
|
|
1132
1132
|
}
|