@stigmer/runner 3.0.2-dev.20260609093630 → 3.0.3
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +55 -16
- package/dist/activities/execute-cursor/approval-policy.js +93 -31
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +54 -26
- package/dist/activities/execute-cursor/approval-state.js +41 -26
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/hook-script.d.ts +31 -12
- package/dist/activities/execute-cursor/hook-script.js +93 -52
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +23 -0
- package/dist/activities/execute-cursor/message-translator.js +100 -54
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/package.json +2 -2
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +93 -37
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +33 -18
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +149 -0
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +93 -0
- package/src/activities/execute-cursor/approval-policy.ts +113 -31
- package/src/activities/execute-cursor/approval-state.ts +74 -32
- package/src/activities/execute-cursor/hook-script.ts +94 -52
- package/src/activities/execute-cursor/message-translator.ts +114 -57
package/dist/.build-fingerprint
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"hash":"
|
|
1
|
+
{"hash":"f39ecbdabee6cf8e","builtAt":"2026-06-10T07:04:09.007Z","fileCount":192}
|
|
@@ -26,36 +26,75 @@ export interface MergedToolPolicy {
|
|
|
26
26
|
requiresApproval: boolean;
|
|
27
27
|
approvalMessage: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Canonical approval category for a gated tool, derived from EITHER taxonomy's
|
|
31
|
+
* name via the shared {@link classifyTool}.
|
|
32
|
+
*
|
|
33
|
+
* The hook (`Write`/`Shell`/`Delete`) and the stream (`edit`/`shell`/`delete`)
|
|
34
|
+
* name the same operation differently, so neither raw name is a stable
|
|
35
|
+
* cross-layer identity. The category collapses both onto one value so the denial
|
|
36
|
+
* ledger (recorded by the hook) correlates to the streamed tool call (read by
|
|
37
|
+
* the runner) and so an approval grant matches the agent's re-attempt on
|
|
38
|
+
* reinvocation regardless of which taxonomy named it. `FILE_WRITE` and
|
|
39
|
+
* `FILE_EDIT` both map to `write` because the Cursor hook reports every file
|
|
40
|
+
* mutation — create or edit — as `Write`.
|
|
41
|
+
*
|
|
42
|
+
* Returns undefined for non-gated tools (read-only built-ins, MCP tools, and
|
|
43
|
+
* anything `classifyTool` does not place in a mutating kind).
|
|
44
|
+
*/
|
|
45
|
+
export type ApprovalCategory = "write" | "delete" | "shell";
|
|
46
|
+
export declare function approvalCategory(toolName: string): ApprovalCategory | undefined;
|
|
29
47
|
/**
|
|
30
48
|
* Top-level tool-argument fields, in priority order, that identify the specific
|
|
31
|
-
* resource a built-in tool acts on.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
49
|
+
* resource a built-in tool acts on. The list deliberately spans BOTH taxonomies'
|
|
50
|
+
* arg shapes: the hook input names a file `file_path` and the stream names it
|
|
51
|
+
* `path`; both name a shell command `command`. Extracting the same resource
|
|
52
|
+
* VALUE on both sides (the absolute path / the command string) is what lets the
|
|
53
|
+
* hook-recorded denial token equal the stream-computed token. Authored here once
|
|
54
|
+
* and injected into the generated preToolUse hook script so the runner and the
|
|
55
|
+
* hook never disagree on which field to match.
|
|
35
56
|
*/
|
|
36
|
-
export declare const SALIENT_ARG_FIELDS: readonly ["
|
|
57
|
+
export declare const SALIENT_ARG_FIELDS: readonly ["file_path", "path", "target_notebook", "command"];
|
|
37
58
|
/**
|
|
38
59
|
* Check whether a built-in (non-MCP) Cursor tool requires user approval.
|
|
39
60
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
61
|
+
* Resolved via {@link approvalCategory} so it answers correctly for BOTH
|
|
62
|
+
* taxonomies — the hook's `Write`/`Shell`/`Delete` and the stream's
|
|
63
|
+
* `edit`/`shell`/`delete` all return true. Only mutating/destructive tools are
|
|
64
|
+
* gated; everything else (read-only built-ins, and — at the hook layer —
|
|
65
|
+
* auto-approved MCP tools) is allowed. This "gate the dangerous set, allow the
|
|
66
|
+
* rest" model mirrors the native harness's resolveToolApproval. It is
|
|
67
|
+
* deliberately fail-OPEN for unknown tools: the merged MCP policy map carries
|
|
68
|
+
* only the tools that REQUIRE approval, so a fail-closed default would wrongly
|
|
69
|
+
* deny every auto-approved MCP tool, which the hook cannot distinguish from an
|
|
70
|
+
* unknown built-in by name.
|
|
48
71
|
*/
|
|
49
72
|
export declare function builtInRequiresApproval(toolName: string): boolean;
|
|
50
73
|
/**
|
|
51
74
|
* Returns the built-in tool names that require approval (the gated set the
|
|
52
75
|
* preToolUse hook denies unless auto-approved or granted on reinvocation).
|
|
76
|
+
*
|
|
77
|
+
* These are HOOK-taxonomy names (PascalCase), because the hook matches its own
|
|
78
|
+
* `tool_name`. See {@link approvalCategory} for the cross-layer identity.
|
|
53
79
|
*/
|
|
54
80
|
export declare function getBuiltInGatedList(): string[];
|
|
55
81
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* the
|
|
82
|
+
* Returns the gated built-in tools as `(hookToolName, category)` pairs.
|
|
83
|
+
*
|
|
84
|
+
* Injected into the generated preToolUse hook so the bash script can map its
|
|
85
|
+
* incoming `tool_name` to the canonical category used for the denial/grant
|
|
86
|
+
* token — the same category the runner computes from the stream side via
|
|
87
|
+
* {@link approvalCategory}. Authoring it here keeps the mapping single-sourced;
|
|
88
|
+
* a gated built-in with no category would be a programming error, so it is
|
|
89
|
+
* filtered out (and would simply not be gated rather than crash the hook).
|
|
90
|
+
*/
|
|
91
|
+
export declare function getBuiltInGatedCategories(): Array<[string, ApprovalCategory]>;
|
|
92
|
+
/**
|
|
93
|
+
* Approval-message template for a gated built-in tool (either taxonomy), or
|
|
94
|
+
* undefined when the tool is not gated. Resolved via {@link approvalCategory}
|
|
95
|
+
* so stream-side names (`edit`/`shell`/`delete`) and hook-side names
|
|
96
|
+
* (`Write`/`Shell`/`Delete`) both map to the same template. Callers resolve the
|
|
97
|
+
* placeholders against the tool args via resolveApprovalMessage.
|
|
59
98
|
*/
|
|
60
99
|
export declare function getBuiltInApprovalMessage(toolName: string): string | undefined;
|
|
61
100
|
/**
|
|
@@ -15,58 +15,120 @@
|
|
|
15
15
|
* allows or denies the call. For built-in Cursor tools (Shell, Read, etc.),
|
|
16
16
|
* a separate local policy applies.
|
|
17
17
|
*/
|
|
18
|
+
import { ToolKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
19
|
+
import { classifyTool } from "../../shared/tool-kind.js";
|
|
18
20
|
/**
|
|
19
|
-
* Built-in
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* Built-in Cursor tools the preToolUse hook gates, named as the hook receives
|
|
22
|
+
* them.
|
|
23
|
+
*
|
|
24
|
+
* Critical: the Cursor preToolUse hook and the SDK event stream use DIFFERENT
|
|
25
|
+
* tool taxonomies for the same operation. The hook's `tool_name` is PascalCase
|
|
26
|
+
* (`Write` for any file create/edit, `Shell`, `Delete`); the stream's
|
|
27
|
+
* `event.name` is lowercase (`edit`, `shell`, `delete`). This set is the HOOK
|
|
28
|
+
* taxonomy because it is consulted only to build the hook's gated set and its
|
|
29
|
+
* name->category mapping. Cross-layer correlation never compares these raw
|
|
30
|
+
* names — it uses {@link approvalCategory} (see below).
|
|
25
31
|
*/
|
|
26
|
-
const BUILT_IN_GATED = new
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
const BUILT_IN_GATED = new Set([
|
|
33
|
+
"Write",
|
|
34
|
+
"StrReplace",
|
|
35
|
+
"EditNotebook",
|
|
36
|
+
"Shell",
|
|
37
|
+
"Delete",
|
|
32
38
|
]);
|
|
39
|
+
export function approvalCategory(toolName) {
|
|
40
|
+
switch (classifyTool(toolName)) {
|
|
41
|
+
case ToolKind.FILE_WRITE:
|
|
42
|
+
case ToolKind.FILE_EDIT:
|
|
43
|
+
return "write";
|
|
44
|
+
case ToolKind.FILE_DELETE:
|
|
45
|
+
return "delete";
|
|
46
|
+
case ToolKind.SHELL:
|
|
47
|
+
return "shell";
|
|
48
|
+
default:
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Human-readable approval-message template per canonical category. Keyed by
|
|
54
|
+
* category (not raw tool name) so a denial surfaced from either taxonomy renders
|
|
55
|
+
* the same message. Placeholders resolve against the tool args via
|
|
56
|
+
* {@link resolveApprovalMessage}; `{{args.path}}` and `{{args.command}}` are the
|
|
57
|
+
* stream-side field names (the runner builds the approval surface from the
|
|
58
|
+
* streamed tool call, whose args use `path`/`command`).
|
|
59
|
+
*/
|
|
60
|
+
const CATEGORY_APPROVAL_MESSAGE = {
|
|
61
|
+
write: "Write file: {{args.path}}",
|
|
62
|
+
delete: "Delete: {{args.path}}",
|
|
63
|
+
shell: "Run command: {{args.command}}",
|
|
64
|
+
};
|
|
33
65
|
/**
|
|
34
66
|
* Top-level tool-argument fields, in priority order, that identify the specific
|
|
35
|
-
* resource a built-in tool acts on.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
67
|
+
* resource a built-in tool acts on. The list deliberately spans BOTH taxonomies'
|
|
68
|
+
* arg shapes: the hook input names a file `file_path` and the stream names it
|
|
69
|
+
* `path`; both name a shell command `command`. Extracting the same resource
|
|
70
|
+
* VALUE on both sides (the absolute path / the command string) is what lets the
|
|
71
|
+
* hook-recorded denial token equal the stream-computed token. Authored here once
|
|
72
|
+
* and injected into the generated preToolUse hook script so the runner and the
|
|
73
|
+
* hook never disagree on which field to match.
|
|
39
74
|
*/
|
|
40
|
-
export const SALIENT_ARG_FIELDS = ["
|
|
75
|
+
export const SALIENT_ARG_FIELDS = ["file_path", "path", "target_notebook", "command"];
|
|
41
76
|
/**
|
|
42
77
|
* Check whether a built-in (non-MCP) Cursor tool requires user approval.
|
|
43
78
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
79
|
+
* Resolved via {@link approvalCategory} so it answers correctly for BOTH
|
|
80
|
+
* taxonomies — the hook's `Write`/`Shell`/`Delete` and the stream's
|
|
81
|
+
* `edit`/`shell`/`delete` all return true. Only mutating/destructive tools are
|
|
82
|
+
* gated; everything else (read-only built-ins, and — at the hook layer —
|
|
83
|
+
* auto-approved MCP tools) is allowed. This "gate the dangerous set, allow the
|
|
84
|
+
* rest" model mirrors the native harness's resolveToolApproval. It is
|
|
85
|
+
* deliberately fail-OPEN for unknown tools: the merged MCP policy map carries
|
|
86
|
+
* only the tools that REQUIRE approval, so a fail-closed default would wrongly
|
|
87
|
+
* deny every auto-approved MCP tool, which the hook cannot distinguish from an
|
|
88
|
+
* unknown built-in by name.
|
|
52
89
|
*/
|
|
53
90
|
export function builtInRequiresApproval(toolName) {
|
|
54
|
-
return
|
|
91
|
+
return approvalCategory(toolName) !== undefined;
|
|
55
92
|
}
|
|
56
93
|
/**
|
|
57
94
|
* Returns the built-in tool names that require approval (the gated set the
|
|
58
95
|
* preToolUse hook denies unless auto-approved or granted on reinvocation).
|
|
96
|
+
*
|
|
97
|
+
* These are HOOK-taxonomy names (PascalCase), because the hook matches its own
|
|
98
|
+
* `tool_name`. See {@link approvalCategory} for the cross-layer identity.
|
|
59
99
|
*/
|
|
60
100
|
export function getBuiltInGatedList() {
|
|
61
|
-
return [...BUILT_IN_GATED
|
|
101
|
+
return [...BUILT_IN_GATED];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Returns the gated built-in tools as `(hookToolName, category)` pairs.
|
|
105
|
+
*
|
|
106
|
+
* Injected into the generated preToolUse hook so the bash script can map its
|
|
107
|
+
* incoming `tool_name` to the canonical category used for the denial/grant
|
|
108
|
+
* token — the same category the runner computes from the stream side via
|
|
109
|
+
* {@link approvalCategory}. Authoring it here keeps the mapping single-sourced;
|
|
110
|
+
* a gated built-in with no category would be a programming error, so it is
|
|
111
|
+
* filtered out (and would simply not be gated rather than crash the hook).
|
|
112
|
+
*/
|
|
113
|
+
export function getBuiltInGatedCategories() {
|
|
114
|
+
const pairs = [];
|
|
115
|
+
for (const name of BUILT_IN_GATED) {
|
|
116
|
+
const category = approvalCategory(name);
|
|
117
|
+
if (category)
|
|
118
|
+
pairs.push([name, category]);
|
|
119
|
+
}
|
|
120
|
+
return pairs;
|
|
62
121
|
}
|
|
63
122
|
/**
|
|
64
|
-
* Approval-message template for a gated built-in tool, or
|
|
65
|
-
* tool is not
|
|
66
|
-
*
|
|
123
|
+
* Approval-message template for a gated built-in tool (either taxonomy), or
|
|
124
|
+
* undefined when the tool is not gated. Resolved via {@link approvalCategory}
|
|
125
|
+
* so stream-side names (`edit`/`shell`/`delete`) and hook-side names
|
|
126
|
+
* (`Write`/`Shell`/`Delete`) both map to the same template. Callers resolve the
|
|
127
|
+
* placeholders against the tool args via resolveApprovalMessage.
|
|
67
128
|
*/
|
|
68
129
|
export function getBuiltInApprovalMessage(toolName) {
|
|
69
|
-
|
|
130
|
+
const category = approvalCategory(toolName);
|
|
131
|
+
return category ? CATEGORY_APPROVAL_MESSAGE[category] : undefined;
|
|
70
132
|
}
|
|
71
133
|
/**
|
|
72
134
|
* Extract the canonical "salient" argument value that identifies the resource a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/approval-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"approval-policy.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/approval-policy.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,27 +8,29 @@
|
|
|
8
8
|
* State file format (JSON):
|
|
9
9
|
* {
|
|
10
10
|
* "autoApproveAll": false,
|
|
11
|
-
* "builtInGatedList": ["Write", "StrReplace", "Shell", ...],
|
|
12
11
|
* "mcpToolPolicies": {
|
|
13
12
|
* "apply_cloud_resource": { "requiresApproval": true, "message": "..." }
|
|
14
13
|
* },
|
|
15
|
-
* "approvedGrants": [{ "toolName": "
|
|
16
|
-
* "approvedGrantTokens": ["
|
|
14
|
+
* "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt" }],
|
|
15
|
+
* "approvedGrantTokens": ["d3JpdGUKYS50eHQ="]
|
|
17
16
|
* }
|
|
18
17
|
*
|
|
19
|
-
* The hook gates
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* The hook gates the dangerous built-in set and the MCP tools that require
|
|
19
|
+
* approval (mcpToolPolicies, which by construction holds only require-approval
|
|
20
|
+
* entries); every other tool is allowed. The gated built-in set and its
|
|
21
|
+
* name->category mapping are baked into the generated hook script (from
|
|
22
|
+
* approval-policy.ts), not carried in the state file — only the dynamic inputs
|
|
23
|
+
* (autoApproveAll, mcpToolPolicies, approvedGrantTokens) live here. This mirrors
|
|
24
|
+
* the native harness and avoids denying auto-approved MCP tools, which are
|
|
25
|
+
* absent from the policy map and indistinguishable from unknown tools by name.
|
|
24
26
|
*
|
|
25
27
|
* Why grants instead of tool-call ids: a resumed Cursor agent re-issues the
|
|
26
28
|
* approved tool with a BRAND NEW call id, so matching on the original call id
|
|
27
|
-
* can never let the re-attempt through. Instead we grant by tool
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* only if its (
|
|
31
|
-
* tools and any newly proposed dangerous tool are re-gated.
|
|
29
|
+
* can never let the re-attempt through. Instead we grant by canonical tool
|
|
30
|
+
* identity — the approval category plus a "salient" resource value (the file
|
|
31
|
+
* path, the shell command; see {@link toolIdentity}). On reinvocation the hook
|
|
32
|
+
* allows a tool call only if its (category, salient) matches an approved grant;
|
|
33
|
+
* rejected/skipped tools and any newly proposed dangerous tool are re-gated.
|
|
32
34
|
*
|
|
33
35
|
* Tokens: the hook is a self-contained bash script, so it cannot parse an array
|
|
34
36
|
* of grant objects. `approvedGrantTokens` is the flat, base64-encoded form of
|
|
@@ -55,47 +57,73 @@ export interface McpToolPolicyEntry {
|
|
|
55
57
|
requiresApproval: boolean;
|
|
56
58
|
message?: string;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The canonical, taxonomy-agnostic identity of a tool call.
|
|
62
|
+
*
|
|
63
|
+
* The Cursor preToolUse hook and the SDK stream name the same operation
|
|
64
|
+
* differently (hook `Write`/`Shell`/`Delete`; stream `edit`/`shell`/`delete`),
|
|
65
|
+
* so the raw tool name cannot be a cross-layer identity. Instead:
|
|
66
|
+
* - `key` is the {@link approvalCategory} (`write`/`delete`/`shell`) for gated
|
|
67
|
+
* built-ins, and the tool name for MCP tools (whose name is consistent across
|
|
68
|
+
* layers). It is the part that survives the name divergence.
|
|
69
|
+
* - `salient` is the resource the tool acts on (the absolute file path or the
|
|
70
|
+
* shell command) — identical on both sides because it is the argument VALUE,
|
|
71
|
+
* not the field name. Empty for MCP tools, matched by `key` alone.
|
|
72
|
+
*
|
|
73
|
+
* The denial ledger (hook) and the stream reconciliation (runner) both reduce a
|
|
74
|
+
* tool call to this identity, so they correlate exactly; an approval grant uses
|
|
75
|
+
* the same identity so the agent's re-attempt is allowed on reinvocation even
|
|
76
|
+
* though it carries a fresh tool-call id and a different-taxonomy name.
|
|
77
|
+
*/
|
|
78
|
+
export interface ToolIdentity {
|
|
79
|
+
key: string;
|
|
80
|
+
salient: string;
|
|
81
|
+
}
|
|
82
|
+
export declare function toolIdentity(toolName: string, mcpServerSlug: string, args: Record<string, unknown> | undefined): ToolIdentity;
|
|
58
83
|
/**
|
|
59
84
|
* The identity of an approved tool call, stable across agent resume.
|
|
60
85
|
*
|
|
61
|
-
* -
|
|
62
|
-
*
|
|
63
|
-
* -
|
|
64
|
-
* the grant then matches by name alone, since the user approved that tool.
|
|
86
|
+
* - `key`/`salient` are the canonical {@link ToolIdentity} the hook matches on.
|
|
87
|
+
* - `toolName`/`mcpServerSlug` are retained for readability, debugging, and the
|
|
88
|
+
* structured-vs-token cross-check (the two are always generated together).
|
|
65
89
|
*/
|
|
66
90
|
export interface ApprovalGrant {
|
|
67
91
|
toolName: string;
|
|
68
92
|
mcpServerSlug: string;
|
|
69
|
-
|
|
93
|
+
key: string;
|
|
94
|
+
salient: string;
|
|
70
95
|
}
|
|
71
96
|
export interface ApprovalStateFile {
|
|
72
97
|
autoApproveAll: boolean;
|
|
73
|
-
builtInGatedList: string[];
|
|
74
98
|
mcpToolPolicies: Record<string, McpToolPolicyEntry>;
|
|
75
99
|
approvedGrants: ApprovalGrant[];
|
|
76
100
|
approvedGrantTokens: string[];
|
|
77
101
|
}
|
|
78
102
|
/**
|
|
79
103
|
* Compute the flat token the bash hook matches on. The hook recomputes the same
|
|
80
|
-
* token from the incoming tool call (`base64(
|
|
81
|
-
* encoding here must stay byte-identical to the
|
|
104
|
+
* token from the incoming tool call (`base64(key \n salient)` — see
|
|
105
|
+
* {@link toolIdentity}), so the encoding here must stay byte-identical to the
|
|
106
|
+
* hook script in hook-script.ts.
|
|
82
107
|
*/
|
|
83
|
-
export declare function grantToken(
|
|
108
|
+
export declare function grantToken(key: string, salient: string): string;
|
|
84
109
|
/**
|
|
85
110
|
* Build approval grants from the pending approvals the user adjudicated and
|
|
86
|
-
* their decisions. Only APPROVE decisions produce grants.
|
|
87
|
-
*
|
|
111
|
+
* their decisions. Only APPROVE / APPROVE_ALL decisions produce grants. Each
|
|
112
|
+
* grant carries the canonical {@link ToolIdentity} (category + salient resource)
|
|
113
|
+
* so the hook allows the exact approved resource on the resumed turn.
|
|
88
114
|
*/
|
|
89
115
|
export declare function buildApprovalGrants(pendingApprovals: PendingApproval[], decisions: Map<string, ApprovalAction>): ApprovalGrant[];
|
|
90
116
|
/**
|
|
91
117
|
* Build the approval state file content from merged policies and any approval
|
|
92
118
|
* grants from a previous HITL cycle.
|
|
93
119
|
*
|
|
94
|
-
* The state file
|
|
95
|
-
* - builtInGatedList: dangerous built-in tools the hook denies (unless granted)
|
|
120
|
+
* The state file carries the hook script's DYNAMIC inputs:
|
|
96
121
|
* - mcpToolPolicies: per-tool policy for MCP tools requiring approval
|
|
97
122
|
* - approvedGrants / approvedGrantTokens: tools approved in the current HITL
|
|
98
123
|
* cycle, allowed through on reinvocation
|
|
124
|
+
*
|
|
125
|
+
* The static gated built-in set and its category mapping are baked into the
|
|
126
|
+
* generated hook script (from approval-policy.ts), not carried here.
|
|
99
127
|
*/
|
|
100
128
|
export declare function buildApprovalState(mergedPolicies: Map<string, MergedToolPolicy>, autoApproveAll: boolean, grants?: ApprovalGrant[]): ApprovalStateFile;
|
|
101
129
|
/**
|
|
@@ -8,27 +8,29 @@
|
|
|
8
8
|
* State file format (JSON):
|
|
9
9
|
* {
|
|
10
10
|
* "autoApproveAll": false,
|
|
11
|
-
* "builtInGatedList": ["Write", "StrReplace", "Shell", ...],
|
|
12
11
|
* "mcpToolPolicies": {
|
|
13
12
|
* "apply_cloud_resource": { "requiresApproval": true, "message": "..." }
|
|
14
13
|
* },
|
|
15
|
-
* "approvedGrants": [{ "toolName": "
|
|
16
|
-
* "approvedGrantTokens": ["
|
|
14
|
+
* "approvedGrants": [{ "toolName": "edit", "mcpServerSlug": "", "key": "write", "salient": "a.txt" }],
|
|
15
|
+
* "approvedGrantTokens": ["d3JpdGUKYS50eHQ="]
|
|
17
16
|
* }
|
|
18
17
|
*
|
|
19
|
-
* The hook gates
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
18
|
+
* The hook gates the dangerous built-in set and the MCP tools that require
|
|
19
|
+
* approval (mcpToolPolicies, which by construction holds only require-approval
|
|
20
|
+
* entries); every other tool is allowed. The gated built-in set and its
|
|
21
|
+
* name->category mapping are baked into the generated hook script (from
|
|
22
|
+
* approval-policy.ts), not carried in the state file — only the dynamic inputs
|
|
23
|
+
* (autoApproveAll, mcpToolPolicies, approvedGrantTokens) live here. This mirrors
|
|
24
|
+
* the native harness and avoids denying auto-approved MCP tools, which are
|
|
25
|
+
* absent from the policy map and indistinguishable from unknown tools by name.
|
|
24
26
|
*
|
|
25
27
|
* Why grants instead of tool-call ids: a resumed Cursor agent re-issues the
|
|
26
28
|
* approved tool with a BRAND NEW call id, so matching on the original call id
|
|
27
|
-
* can never let the re-attempt through. Instead we grant by tool
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* only if its (
|
|
31
|
-
* tools and any newly proposed dangerous tool are re-gated.
|
|
29
|
+
* can never let the re-attempt through. Instead we grant by canonical tool
|
|
30
|
+
* identity — the approval category plus a "salient" resource value (the file
|
|
31
|
+
* path, the shell command; see {@link toolIdentity}). On reinvocation the hook
|
|
32
|
+
* allows a tool call only if its (category, salient) matches an approved grant;
|
|
33
|
+
* rejected/skipped tools and any newly proposed dangerous tool are re-gated.
|
|
32
34
|
*
|
|
33
35
|
* Tokens: the hook is a self-contained bash script, so it cannot parse an array
|
|
34
36
|
* of grant objects. `approvedGrantTokens` is the flat, base64-encoded form of
|
|
@@ -52,19 +54,30 @@ import { join } from "node:path";
|
|
|
52
54
|
import { create } from "@bufbuild/protobuf";
|
|
53
55
|
import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
54
56
|
import { PendingApprovalSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
55
|
-
import {
|
|
57
|
+
import { extractArgKey, approvalCategory } from "./approval-policy.js";
|
|
58
|
+
export function toolIdentity(toolName, mcpServerSlug, args) {
|
|
59
|
+
if (mcpServerSlug) {
|
|
60
|
+
return { key: toolName, salient: "" };
|
|
61
|
+
}
|
|
62
|
+
const category = approvalCategory(toolName);
|
|
63
|
+
// A gated built-in keys on its category; an unknown/non-gated tool falls back
|
|
64
|
+
// to its own name (harmless — it is not gated, so it never enters the ledger).
|
|
65
|
+
return { key: category ?? toolName, salient: extractArgKey(args) };
|
|
66
|
+
}
|
|
56
67
|
/**
|
|
57
68
|
* Compute the flat token the bash hook matches on. The hook recomputes the same
|
|
58
|
-
* token from the incoming tool call (`base64(
|
|
59
|
-
* encoding here must stay byte-identical to the
|
|
69
|
+
* token from the incoming tool call (`base64(key \n salient)` — see
|
|
70
|
+
* {@link toolIdentity}), so the encoding here must stay byte-identical to the
|
|
71
|
+
* hook script in hook-script.ts.
|
|
60
72
|
*/
|
|
61
|
-
export function grantToken(
|
|
62
|
-
return Buffer.from(`${
|
|
73
|
+
export function grantToken(key, salient) {
|
|
74
|
+
return Buffer.from(`${key}\n${salient}`, "utf-8").toString("base64");
|
|
63
75
|
}
|
|
64
76
|
/**
|
|
65
77
|
* Build approval grants from the pending approvals the user adjudicated and
|
|
66
|
-
* their decisions. Only APPROVE decisions produce grants.
|
|
67
|
-
*
|
|
78
|
+
* their decisions. Only APPROVE / APPROVE_ALL decisions produce grants. Each
|
|
79
|
+
* grant carries the canonical {@link ToolIdentity} (category + salient resource)
|
|
80
|
+
* so the hook allows the exact approved resource on the resumed turn.
|
|
68
81
|
*/
|
|
69
82
|
export function buildApprovalGrants(pendingApprovals, decisions) {
|
|
70
83
|
const grants = [];
|
|
@@ -77,11 +90,12 @@ export function buildApprovalGrants(pendingApprovals, decisions) {
|
|
|
77
90
|
const decision = decisions.get(pa.toolCallId);
|
|
78
91
|
if (decision !== ApprovalAction.APPROVE && decision !== ApprovalAction.APPROVE_ALL)
|
|
79
92
|
continue;
|
|
80
|
-
const
|
|
93
|
+
const id = toolIdentity(pa.toolName, pa.mcpServerSlug, parseArgs(pa.argsPreview));
|
|
81
94
|
grants.push({
|
|
82
95
|
toolName: pa.toolName,
|
|
83
96
|
mcpServerSlug: pa.mcpServerSlug,
|
|
84
|
-
|
|
97
|
+
key: id.key,
|
|
98
|
+
salient: id.salient,
|
|
85
99
|
});
|
|
86
100
|
}
|
|
87
101
|
return grants;
|
|
@@ -101,11 +115,13 @@ function parseArgs(argsPreview) {
|
|
|
101
115
|
* Build the approval state file content from merged policies and any approval
|
|
102
116
|
* grants from a previous HITL cycle.
|
|
103
117
|
*
|
|
104
|
-
* The state file
|
|
105
|
-
* - builtInGatedList: dangerous built-in tools the hook denies (unless granted)
|
|
118
|
+
* The state file carries the hook script's DYNAMIC inputs:
|
|
106
119
|
* - mcpToolPolicies: per-tool policy for MCP tools requiring approval
|
|
107
120
|
* - approvedGrants / approvedGrantTokens: tools approved in the current HITL
|
|
108
121
|
* cycle, allowed through on reinvocation
|
|
122
|
+
*
|
|
123
|
+
* The static gated built-in set and its category mapping are baked into the
|
|
124
|
+
* generated hook script (from approval-policy.ts), not carried here.
|
|
109
125
|
*/
|
|
110
126
|
export function buildApprovalState(mergedPolicies, autoApproveAll, grants) {
|
|
111
127
|
const approvedGrants = grants ?? [];
|
|
@@ -118,10 +134,9 @@ export function buildApprovalState(mergedPolicies, autoApproveAll, grants) {
|
|
|
118
134
|
}
|
|
119
135
|
return {
|
|
120
136
|
autoApproveAll,
|
|
121
|
-
builtInGatedList: getBuiltInGatedList(),
|
|
122
137
|
mcpToolPolicies,
|
|
123
138
|
approvedGrants,
|
|
124
|
-
approvedGrantTokens: approvedGrants.map((g) => grantToken(g.
|
|
139
|
+
approvedGrantTokens: approvedGrants.map((g) => grantToken(g.key, g.salient)),
|
|
125
140
|
};
|
|
126
141
|
}
|
|
127
142
|
const STATE_FILE_DIR = ".cursor/hooks";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-state.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/approval-state.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"approval-state.js","sourceRoot":"","sources":["../../../src/activities/execute-cursor/approval-state.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,30 +18,49 @@
|
|
|
18
18
|
*
|
|
19
19
|
* The script is self-contained (no Node.js required) for portability. It uses
|
|
20
20
|
* bash + grep/cut for lightweight JSON field extraction. All policy decisions
|
|
21
|
-
* are pre-computed by the runner into the state file
|
|
22
|
-
* mechanical field extraction and string
|
|
23
|
-
* authored once in TypeScript (approval-policy.ts
|
|
21
|
+
* are pre-computed by the runner into the state file (and into this generated
|
|
22
|
+
* script); the hook only performs mechanical field extraction and string
|
|
23
|
+
* lookups — the policy itself is authored once in TypeScript (approval-policy.ts
|
|
24
|
+
* / approval-state.ts).
|
|
25
|
+
*
|
|
26
|
+
* Cross-taxonomy identity (the crux):
|
|
27
|
+
* The preToolUse hook and the SDK event stream name the same operation
|
|
28
|
+
* differently — the hook receives PascalCase `tool_name` (`Write` for any file
|
|
29
|
+
* create/edit, `Shell`, `Delete`) while the stream emits lowercase `event.name`
|
|
30
|
+
* (`edit`, `shell`, `delete`). They also name the salient argument differently
|
|
31
|
+
* (`file_path` in the hook input vs `path` in the stream). So the hook and the
|
|
32
|
+
* runner cannot correlate on the raw name. Instead both reduce a tool call to a
|
|
33
|
+
* canonical identity — `base64(category \n salient)` — where `category` is the
|
|
34
|
+
* approval category (`write`/`delete`/`shell`, baked into the case statement
|
|
35
|
+
* below from approval-policy.ts) and `salient` is the resource VALUE (the file
|
|
36
|
+
* path or shell command), which is identical on both sides. The runner mirrors
|
|
37
|
+
* this exactly in approval-state.ts (toolIdentity + grantToken), so a denial
|
|
38
|
+
* recorded here correlates to the streamed tool call, and an approval grant
|
|
39
|
+
* matches the agent's re-attempt on reinvocation.
|
|
24
40
|
*
|
|
25
41
|
* Policy evaluation order (first match wins). The model is "gate the dangerous
|
|
26
42
|
* set, allow the rest" — matching the native harness and avoiding denial of
|
|
27
43
|
* auto-approved MCP tools (which are absent from mcpToolPolicies):
|
|
28
44
|
* 1. autoApproveAll → allow
|
|
29
|
-
* 2.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
45
|
+
* 2. Gated built-in (category non-empty):
|
|
46
|
+
* a. identity token in approvedGrantTokens → allow (reinvocation grant)
|
|
47
|
+
* b. otherwise → record denial, deny
|
|
48
|
+
* 3. MCP tool present in mcpToolPolicies (require-approval):
|
|
49
|
+
* a. name token in approvedGrantTokens → allow
|
|
50
|
+
* b. otherwise → record denial, deny
|
|
51
|
+
* 4. Everything else (read-only built-ins, auto-approved MCP, unknown) → allow
|
|
33
52
|
*/
|
|
34
53
|
/**
|
|
35
54
|
* Generates the bash hook script content.
|
|
36
55
|
*
|
|
37
56
|
* The script reads a JSON state file written by the cursor-runner before
|
|
38
57
|
* each agent.send() call. The state file is the single source of truth
|
|
39
|
-
* for
|
|
58
|
+
* for the dynamic approval inputs (autoApproveAll, mcpToolPolicies,
|
|
59
|
+
* approvedGrantTokens). The static policy (which built-ins are gated and their
|
|
60
|
+
* categories, and which arg fields are salient) is baked into the script at
|
|
61
|
+
* generation time from approval-policy.ts.
|
|
40
62
|
*
|
|
41
|
-
*
|
|
42
|
-
* recomputed here from the incoming tool call. The salient-arg field list is
|
|
43
|
-
* injected from SALIENT_ARG_FIELDS so the runner and the hook never disagree on
|
|
44
|
-
* which argument identifies the resource. The encoding must stay byte-identical
|
|
63
|
+
* The identity token encoding (`base64(key \n salient)`) must stay byte-identical
|
|
45
64
|
* to grantToken() in approval-state.ts.
|
|
46
65
|
*/
|
|
47
66
|
export declare function generateHookScript(stateFilePath: string, ledgerFilePath: string): string;
|