@ory/cursor 1.5.3 → 1.7.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 +26 -7
- package/dist/cli/setup.js +19 -15
- package/dist/handlers.js +85 -13
- package/dist/types.d.ts +10 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -104,17 +104,36 @@ Then a denied tool is actually blocked (`preToolUse` returns `permission: "deny"
|
|
|
104
104
|
| Cursor hook | What the plugin does |
|
|
105
105
|
|---|---|
|
|
106
106
|
| `sessionStart` | User + agent auth gates, user→agent delegation (never blocks) |
|
|
107
|
-
| `preToolUse` | The permission gate — blocks with `permission: "deny"` in enforce mode |
|
|
107
|
+
| `preToolUse` | The permission gate for native tools — blocks with `permission: "deny"` in enforce mode |
|
|
108
|
+
| `beforeMCPExecution` | The permission gate for MCP tools — checks `McpServer` and `McpTool` and blocks with `permission: "deny"` in enforce mode |
|
|
108
109
|
| `postToolUse` / `postToolUseFailure` | Record `tool.complete` / `tool.fail` activity |
|
|
109
110
|
| `beforeSubmitPrompt` | Record `user.prompt` activity (never blocks the human) |
|
|
110
111
|
| `subagentStart` / `subagentStop` | Spawn-scoped child Talos identity + delegation activity |
|
|
111
112
|
|
|
112
|
-
The fine-grained `beforeShellExecution` / `
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
The fine-grained `beforeShellExecution` / `beforeReadFile` hooks are **not**
|
|
114
|
+
registered: `preToolUse` already gates those categories, so registering them
|
|
115
|
+
would only spawn extra hook subprocesses without adding any enforcement. (When
|
|
116
|
+
they do fire, the handler records activity only.)
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
### MCP servers
|
|
119
|
+
|
|
120
|
+
`preToolUse` names an MCP tool `MCP:<tool>` without its server, so the plugin
|
|
121
|
+
gates MCP calls in `beforeMCPExecution`, which [Cursor documents](https://cursor.com/docs/hooks)
|
|
122
|
+
with the server's `mcp.json` key (`mcp_server_name`), the server's own
|
|
123
|
+
`tool_name`, and its stdio launch `command` or HTTP/SSE `url`. Each call checks
|
|
124
|
+
`McpServer:<server>` and `McpTool:<server>:<tool>`; Agent Security resolves the
|
|
125
|
+
reported key to a cataloged server, helped by hashed fingerprints of the
|
|
126
|
+
launched package, container image, or URL host (raw commands, arguments, and
|
|
127
|
+
URLs never leave the machine). `preToolUse` still checks `MCP:<tool>` as a
|
|
128
|
+
native tool, as it did before MCP gating, but records no activity, so one call
|
|
129
|
+
is counted once.
|
|
130
|
+
|
|
131
|
+
Cursor documents that `beforeMCPExecution` does not run in cloud agents, and
|
|
132
|
+
that cloud agents load only project hooks, not `~/.cursor/hooks.json`. With a
|
|
133
|
+
`--project` install, MCP calls a cloud agent makes get only that native
|
|
134
|
+
`MCP:<tool>` check, so per-server MCP levels do not apply to them.
|
|
135
|
+
|
|
136
|
+
Both gates are registered fail-closed: if the hook can't produce a verdict, Cursor blocks the tool (exit code 2), so a crashed or timed-out gate can't silently defeat enforce mode. The audit/identity hooks stay fail-open — none of them gate a tool, and session start must never block. Re-run `install` after upgrading to register `beforeMCPExecution`.
|
|
118
137
|
|
|
119
138
|
## Connect to Ory Agent Security
|
|
120
139
|
|
|
@@ -136,7 +155,7 @@ npx -y -p @ory/cursor ory-cursor configure \
|
|
|
136
155
|
|
|
137
156
|
**Ory Network or OEL.** Either works. For an Ory Network project the URL is `https://<slug>.projects.oryapis.com`; for a self-hosted **Ory Enterprise License** deployment, point `--project-url` at that deployment's base URL and use the sign-in client id from its Agent Security configuration. Everything downstream — sign-in, checks, delegation — is identical.
|
|
138
157
|
|
|
139
|
-
**There is nothing else for you to create.** The sign-in client, permission model, native-tool blocks, MCP
|
|
158
|
+
**There is nothing else for you to create.** The sign-in client, permission model, native-tool blocks, MCP server access, and observe/enforce posture are provisioned in the Console by someone with project access. At session start the plugin loads or enrolls Cursor's Talos runtime credential in the OS credential store; tool paths only read it. Installing needs no workspace privilege.
|
|
140
159
|
|
|
141
160
|
Sign-in runs at the start of every session and never blocks — a declined, skipped, or timed-out login simply leaves that session without a user identity.
|
|
142
161
|
|
package/dist/cli/setup.js
CHANGED
|
@@ -93,7 +93,7 @@ function writeJson(file, value) {
|
|
|
93
93
|
/**
|
|
94
94
|
* Resolve this install's runtime and write its shims.
|
|
95
95
|
*
|
|
96
|
-
* Cursor's `preToolUse`
|
|
96
|
+
* Cursor's `preToolUse` and `beforeMCPExecution` gates are registered `failClosed: true` (see
|
|
97
97
|
* {@link oryHooksBlock}), so a hook that cannot answer blocks every tool. That
|
|
98
98
|
* makes the shim's runtime-missing path load-bearing: it has to emit an
|
|
99
99
|
* explicit allow, not just exit 0.
|
|
@@ -110,21 +110,24 @@ function wireCursorRuntime(args) {
|
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Build the Ory hooks block. Only the load-bearing events are registered:
|
|
113
|
-
* `preToolUse` is the permission gate
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
113
|
+
* `preToolUse` is the permission gate for native tools and `beforeMCPExecution`
|
|
114
|
+
* the gate for MCP tools; the rest are audit/identity. `beforeMCPExecution` is
|
|
115
|
+
* the only hook that reports an MCP call's server (its `mcp.json` key and launch
|
|
116
|
+
* command or URL) — `preToolUse` sees just `MCP:<tool>` and defers those calls
|
|
117
|
+
* to it. The other fine-grained shell/file hooks are intentionally omitted —
|
|
118
|
+
* `preToolUse` already covers those categories, and registering them would
|
|
119
|
+
* spawn a hook subprocess per file read / shell line for no added enforcement.
|
|
117
120
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
121
|
+
* Both gates are registered `failClosed: true`. Cursor's hook default is
|
|
122
|
+
* fail-open: a crashed / timed-out / malformed-JSON hook lets the tool through.
|
|
123
|
+
* For a hook that makes the permission decision that would silently defeat
|
|
124
|
+
* `enforce` mode (the whole point of the gate), so we opt it into fail-closed —
|
|
125
|
+
* if the gate can't produce a verdict, Cursor blocks the tool. The gate still
|
|
126
|
+
* fails *open* on the paths Ory treats as non-blocking (network / rate-limit),
|
|
127
|
+
* because those are handled inside the hook, which exits 0 with an allow
|
|
128
|
+
* verdict; `failClosed` only governs the hook process itself failing to answer.
|
|
129
|
+
* The audit/identity hooks stay fail-open — none of them gate a tool, and
|
|
130
|
+
* session start must never block.
|
|
128
131
|
*/
|
|
129
132
|
function oryHooksBlock(command) {
|
|
130
133
|
const entry = (timeout, failClosed = false) => [
|
|
@@ -134,6 +137,7 @@ function oryHooksBlock(command) {
|
|
|
134
137
|
sessionStart: entry(SESSION_START_TIMEOUT_SEC),
|
|
135
138
|
sessionEnd: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
136
139
|
preToolUse: entry(TOOL_HOOK_TIMEOUT_SEC, true),
|
|
140
|
+
beforeMCPExecution: entry(TOOL_HOOK_TIMEOUT_SEC, true),
|
|
137
141
|
postToolUse: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
138
142
|
postToolUseFailure: entry(TOOL_HOOK_TIMEOUT_SEC),
|
|
139
143
|
beforeSubmitPrompt: entry(TOOL_HOOK_TIMEOUT_SEC),
|
package/dist/handlers.js
CHANGED
|
@@ -52,16 +52,19 @@ async function handleHookEvent(input, client, deps = {}) {
|
|
|
52
52
|
return await handleAudit(input, client, "compaction", {
|
|
53
53
|
trigger: input.trigger,
|
|
54
54
|
});
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
55
|
+
// MCP calls are gated here, not in `preToolUse`: only this hook carries
|
|
56
|
+
// the server's `mcp.json` key and how the server is launched or reached.
|
|
57
|
+
case "beforeMCPExecution":
|
|
58
|
+
return await handleBeforeMcpExecution(input, client);
|
|
59
|
+
// The remaining fine-grained hooks are activity-only — `preToolUse`
|
|
60
|
+
// already ran the permission gate for the corresponding tool category,
|
|
61
|
+
// so gating here would double-count.
|
|
58
62
|
case "beforeShellExecution":
|
|
59
63
|
case "afterShellExecution":
|
|
60
64
|
return await handleAudit(input, client, "user.interaction", {
|
|
61
65
|
kind: event,
|
|
62
66
|
hasCommand: input.command !== undefined,
|
|
63
67
|
});
|
|
64
|
-
case "beforeMCPExecution":
|
|
65
68
|
case "afterMCPExecution":
|
|
66
69
|
return await handleAudit(input, client, "user.interaction", {
|
|
67
70
|
kind: event,
|
|
@@ -106,10 +109,34 @@ async function handlePreToolUse(input, client) {
|
|
|
106
109
|
toolName,
|
|
107
110
|
toolInput: input.tool_input,
|
|
108
111
|
});
|
|
112
|
+
// `preToolUse` names an MCP tool `MCP:<tool>` with no server. Locally the
|
|
113
|
+
// install's `beforeMCPExecution` fires for the same call with the server's
|
|
114
|
+
// `mcp.json` key and owns the MCP decision and its activity. Cloud agents do
|
|
115
|
+
// not run `beforeMCPExecution`, so the native check still applies here, as
|
|
116
|
+
// it did before MCP gating existed, but without recording the call twice.
|
|
117
|
+
if (isMcpPreToolUse(toolName)) {
|
|
118
|
+
const session = sessionKey(input);
|
|
119
|
+
const result = await (0, argus_1.decideTool)(client, {
|
|
120
|
+
harness: HARNESS,
|
|
121
|
+
toolName,
|
|
122
|
+
toolArgs: input.tool_input,
|
|
123
|
+
subjectFallback: session ? `session:${session}` : undefined,
|
|
124
|
+
principals: subAgentPrincipals(input),
|
|
125
|
+
});
|
|
126
|
+
if (result.blocked) {
|
|
127
|
+
const reason = result.denialMessage;
|
|
128
|
+
return { permission: "deny", agent_message: reason, user_message: reason };
|
|
129
|
+
}
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
109
132
|
// Only offer a `session:` fallback subject when Cursor actually sent a
|
|
110
133
|
// session — interpolating an absent one yields the literal
|
|
111
134
|
// `session:undefined`, a subject that matches nothing yet looks like a real
|
|
112
135
|
// one in the audit trail.
|
|
136
|
+
return gateCursorTool(input, client, toolName, input.tool_input);
|
|
137
|
+
}
|
|
138
|
+
/** The acting sub-agent's principals, when Cursor identifies one. */
|
|
139
|
+
function subAgentPrincipals(input) {
|
|
113
140
|
const session = sessionKey(input);
|
|
114
141
|
const subAgentType = input.subagent_type;
|
|
115
142
|
const subAgent = subAgentType
|
|
@@ -117,19 +144,20 @@ async function handlePreToolUse(input, client) {
|
|
|
117
144
|
? (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, session, subAgentType)
|
|
118
145
|
: (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, subAgentType)
|
|
119
146
|
: undefined;
|
|
147
|
+
return subAgent
|
|
148
|
+
? { subAgentClientId: subAgent.clientId, subAgentType, perSpawnId: input.subagent_id, sessionId: session }
|
|
149
|
+
: undefined;
|
|
150
|
+
}
|
|
151
|
+
/** Gate one tool call, as the acting sub-agent when Cursor identifies it. */
|
|
152
|
+
async function gateCursorTool(input, client, toolName, toolArgs, mcpTool) {
|
|
153
|
+
const session = sessionKey(input);
|
|
120
154
|
const result = await (0, argus_1.gate)(client, {
|
|
121
155
|
harness: HARNESS,
|
|
122
156
|
toolName,
|
|
123
|
-
toolArgs
|
|
157
|
+
toolArgs,
|
|
124
158
|
subjectFallback: session ? `session:${session}` : undefined,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
subAgentClientId: subAgent.clientId,
|
|
128
|
-
subAgentType,
|
|
129
|
-
perSpawnId: input.subagent_id,
|
|
130
|
-
sessionId: session,
|
|
131
|
-
}
|
|
132
|
-
: undefined,
|
|
159
|
+
...(mcpTool ? { mcpTool } : {}),
|
|
160
|
+
principals: subAgentPrincipals(input),
|
|
133
161
|
});
|
|
134
162
|
if (result.blocked) {
|
|
135
163
|
const reason = result.denialMessage;
|
|
@@ -137,6 +165,50 @@ async function handlePreToolUse(input, client) {
|
|
|
137
165
|
}
|
|
138
166
|
return {};
|
|
139
167
|
}
|
|
168
|
+
// ─── beforeMCPExecution — the MCP permission gate ───────────────────
|
|
169
|
+
/** Cursor's `preToolUse` / `postToolUse` name for an MCP tool: `MCP:<tool>`. */
|
|
170
|
+
const MCP_TOOL_PREFIX = "MCP:";
|
|
171
|
+
function isMcpPreToolUse(toolName) {
|
|
172
|
+
return toolName.startsWith(MCP_TOOL_PREFIX);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Gate an MCP call with the server Cursor reports: `mcp_server_name` is the
|
|
176
|
+
* server's `mcp.json` key and `tool_name` the server's own tool name. The
|
|
177
|
+
* launch command (stdio) or URL (HTTP/SSE) becomes hashed fingerprints, so a
|
|
178
|
+
* cataloged server is recognized whatever the user named it locally.
|
|
179
|
+
*/
|
|
180
|
+
async function handleBeforeMcpExecution(input, client) {
|
|
181
|
+
const serverName = input.mcp_server_name?.trim() ?? "";
|
|
182
|
+
const mcpToolName = input.tool_name ?? "";
|
|
183
|
+
client.logger.info("lifecycle.before_mcp_execution", {
|
|
184
|
+
sessionId: sessionKey(input),
|
|
185
|
+
mcpServer: serverName,
|
|
186
|
+
mcpTool: mcpToolName,
|
|
187
|
+
});
|
|
188
|
+
const fingerprints = (0, argus_1.mcpServerFingerprints)({
|
|
189
|
+
url: input.url ?? input.mcp_server_url,
|
|
190
|
+
command: input.command,
|
|
191
|
+
});
|
|
192
|
+
// A missing server key still reaches Agent Security, which treats an
|
|
193
|
+
// unrecognized server as uncatalogued rather than letting it pass unchecked.
|
|
194
|
+
const mcpTool = {
|
|
195
|
+
serverName: serverName || "unknown",
|
|
196
|
+
toolName: mcpToolName,
|
|
197
|
+
...(fingerprints.length > 0 ? { fingerprints } : {}),
|
|
198
|
+
};
|
|
199
|
+
return gateCursorTool(input, client, `${MCP_TOOL_PREFIX}${mcpToolName || "unknown"}`, parseToolInput(input.tool_input), mcpTool);
|
|
200
|
+
}
|
|
201
|
+
/** `beforeMCPExecution` sends the arguments as a JSON string. */
|
|
202
|
+
function parseToolInput(toolInput) {
|
|
203
|
+
if (typeof toolInput !== "string")
|
|
204
|
+
return toolInput;
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(toolInput);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
140
212
|
// ─── postToolUse ────────────────────────────────────────────────────
|
|
141
213
|
async function handlePostToolUse(input, client) {
|
|
142
214
|
const toolName = input.tool_name ?? "unknown";
|
package/dist/types.d.ts
CHANGED
|
@@ -30,7 +30,11 @@ export interface CursorHookInput {
|
|
|
30
30
|
reason?: string;
|
|
31
31
|
duration_ms?: number;
|
|
32
32
|
tool_name?: string;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The tool arguments. An object on `preToolUse`; a JSON-encoded string on
|
|
35
|
+
* `beforeMCPExecution`.
|
|
36
|
+
*/
|
|
37
|
+
tool_input?: Record<string, unknown> | string;
|
|
34
38
|
/** postToolUse — JSON-stringified tool output. */
|
|
35
39
|
tool_output?: string;
|
|
36
40
|
tool_use_id?: string;
|
|
@@ -41,8 +45,12 @@ export interface CursorHookInput {
|
|
|
41
45
|
command?: string;
|
|
42
46
|
sandbox?: boolean;
|
|
43
47
|
output?: string;
|
|
44
|
-
/**
|
|
48
|
+
/** The server's key in `mcp.json` (for example `linear`). */
|
|
49
|
+
mcp_server_name?: string;
|
|
50
|
+
/** Remote server URL — present only for HTTP/SSE servers. */
|
|
45
51
|
url?: string;
|
|
52
|
+
/** Same as `url`; present only for HTTP/SSE servers. */
|
|
53
|
+
mcp_server_url?: string;
|
|
46
54
|
file_path?: string;
|
|
47
55
|
edits?: Array<{
|
|
48
56
|
old_string?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/cursor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Ory plugin for Cursor: scaffolding rules, a local Ory instance, and authentication, authorization, and audit for every tool call",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/ory/ory-agent-plugins/tree/main/packages/cursor",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
],
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"reova": "^0.7.0",
|
|
60
|
-
"@ory/argus": "1.
|
|
60
|
+
"@ory/argus": "1.7.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"typescript": "^
|
|
63
|
+
"typescript": "^7.0.2"
|
|
64
64
|
},
|
|
65
65
|
"engines": {
|
|
66
66
|
"node": ">=22"
|