@ory/claude-code 0.14.0 → 1.0.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/.claude-plugin/marketplace.json +2 -2
- package/README.md +71 -64
- package/dist/cli/main.js +46 -31
- package/dist/cli/setup.d.ts +39 -29
- package/dist/cli/setup.js +164 -191
- package/dist/handlers.d.ts +2 -7
- package/dist/handlers.js +157 -383
- package/dist/hook.js +40 -16
- package/package.json +9 -8
package/dist/handlers.js
CHANGED
|
@@ -2,54 +2,58 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleHookEvent = handleHookEvent;
|
|
4
4
|
const argus_1 = require("@ory/argus");
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*/
|
|
8
|
-
async function handleHookEvent(input, client, deps = {}) {
|
|
5
|
+
const HARNESS = "claude-code";
|
|
6
|
+
function handleHookEvent(input, client, deps = {}) {
|
|
9
7
|
const event = input.hook_event_name;
|
|
10
8
|
client.logger.debug("hook.received", {
|
|
11
9
|
event,
|
|
12
10
|
sessionId: input.session_id,
|
|
13
11
|
toolName: input.tool_name,
|
|
14
12
|
});
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
case "PermissionRequest":
|
|
31
|
-
return await handlePermissionRequest(input, client);
|
|
32
|
-
case "UserPromptSubmit":
|
|
33
|
-
return await handleUserPromptSubmit(input, client);
|
|
34
|
-
case "SubagentStart":
|
|
35
|
-
return await handleSubagentStart(input, client, deps);
|
|
36
|
-
case "SubagentStop":
|
|
37
|
-
return await handleSubagentStop(input, client);
|
|
38
|
-
case "SessionEnd":
|
|
39
|
-
return await handleSessionEnd(input, client);
|
|
40
|
-
default:
|
|
41
|
-
client.logger.debug("hook.passthrough", { event });
|
|
42
|
-
client.tracer.record("hook.passthrough", "skipped", {
|
|
43
|
-
attributes: { event },
|
|
44
|
-
});
|
|
45
|
-
return { continue: true };
|
|
13
|
+
return (0, argus_1.withHookContext)(client, { sessionId: input.session_id }, async () => {
|
|
14
|
+
const activeType = input.agent_type ?? input.subagent_type;
|
|
15
|
+
let runtimeCredential;
|
|
16
|
+
if (event !== "SubagentStart" && input.agent_id && activeType && (0, argus_1.isSecurityConnected)()) {
|
|
17
|
+
const identity = await (deps.subAgentGate ?? argus_1.ensureSubAgentIdentity)(client, {
|
|
18
|
+
harness: HARNESS,
|
|
19
|
+
sessionId: input.session_id,
|
|
20
|
+
subAgentType: activeType,
|
|
21
|
+
perSpawnId: input.agent_id,
|
|
22
|
+
allowEnrollment: false,
|
|
23
|
+
emitActivity: false,
|
|
24
|
+
});
|
|
25
|
+
if (identity.runtimeCredential) {
|
|
26
|
+
runtimeCredential = identity.runtimeCredential;
|
|
27
|
+
}
|
|
46
28
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
return (0, argus_1.withHookContext)(client, { runtimeCredential }, async () => {
|
|
30
|
+
switch (event) {
|
|
31
|
+
case "SessionStart":
|
|
32
|
+
return handleSessionStart(input, client, deps);
|
|
33
|
+
case "PreToolUse":
|
|
34
|
+
return handlePreToolUse(input, client, deps);
|
|
35
|
+
case "PostToolUse":
|
|
36
|
+
return handlePostToolUse(input, client);
|
|
37
|
+
case "PostToolUseFailure":
|
|
38
|
+
return handlePostToolUseFailure(input, client);
|
|
39
|
+
case "PermissionRequest":
|
|
40
|
+
return handlePermissionRequest(input, client);
|
|
41
|
+
case "UserPromptSubmit":
|
|
42
|
+
return handleUserPromptSubmit(input, client);
|
|
43
|
+
case "SubagentStart":
|
|
44
|
+
return handleSubagentStart(input, client, deps);
|
|
45
|
+
case "SubagentStop":
|
|
46
|
+
return handleSubagentStop(input, client);
|
|
47
|
+
case "SessionEnd":
|
|
48
|
+
return handleSessionEnd(input, client);
|
|
49
|
+
default:
|
|
50
|
+
client.logger.debug("hook.passthrough", { event });
|
|
51
|
+
client.logger.activity("hook.passthrough", "skipped", { attributes: { event } });
|
|
52
|
+
return { continue: true };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
51
56
|
}
|
|
52
|
-
// ─── SessionStart ───────────────────────────────────────────────────
|
|
53
57
|
async function handleSessionStart(input, client, deps) {
|
|
54
58
|
client.logger.info("lifecycle.session_start", {
|
|
55
59
|
sessionId: input.session_id,
|
|
@@ -57,148 +61,49 @@ async function handleSessionStart(input, client, deps) {
|
|
|
57
61
|
source: input.source,
|
|
58
62
|
cwd: input.cwd,
|
|
59
63
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
// Run the user login (interactive PKCE on first session, refresh
|
|
64
|
-
// when needed). This runs every session and never blocks —
|
|
65
|
-
// enforcement is governed solely by permissionMode at tool-call time.
|
|
66
|
-
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
67
|
-
await userGate(client, {
|
|
64
|
+
await (0, argus_1.sessionStart)(client, {
|
|
65
|
+
harness: HARNESS,
|
|
66
|
+
sessionId: input.session_id,
|
|
68
67
|
binName: "ory-claude",
|
|
69
|
-
|
|
68
|
+
userLogin: deps.userLogin,
|
|
69
|
+
agentGate: deps.agentGate,
|
|
70
|
+
activityAttributes: { model: input.model, source: input.source },
|
|
70
71
|
});
|
|
71
|
-
// Resolve the agent identity (machine credentials) regardless of how
|
|
72
|
-
// user auth went — it never blocks and attaches the agent's bearer
|
|
73
|
-
// token to outgoing Ory API calls so the audit trail records who
|
|
74
|
-
// acted on the user's behalf.
|
|
75
|
-
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
76
|
-
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "claude-code" });
|
|
77
|
-
// Record the user→agent delegation so the audit trail captures that
|
|
78
|
-
// this user authorized this agent for this session. Best-effort and
|
|
79
|
-
// written at most once per install: requires both principals to be
|
|
80
|
-
// populated, and any failure is logged and swallowed (fail-open —
|
|
81
|
-
// delegation tracking is for audit, not enforcement).
|
|
82
|
-
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
83
72
|
return {};
|
|
84
73
|
}
|
|
85
|
-
|
|
86
|
-
async function handlePreToolUse(input, client, deps = {}) {
|
|
74
|
+
async function handlePreToolUse(input, client, deps) {
|
|
87
75
|
const toolName = input.tool_name ?? "unknown";
|
|
88
76
|
client.logger.info("lifecycle.pre_tool_use", {
|
|
89
77
|
sessionId: input.session_id,
|
|
90
78
|
toolName,
|
|
91
79
|
toolInput: input.tool_input,
|
|
92
80
|
});
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
mcpServer: mcpTool.serverName,
|
|
119
|
-
mcpTool: mcpTool.toolName,
|
|
120
|
-
...inputSummary,
|
|
121
|
-
};
|
|
122
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
123
|
-
object: mcpTool.serverName,
|
|
124
|
-
relation: "use",
|
|
125
|
-
subjectId,
|
|
126
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
127
|
-
spanAttributes: mcpAttrs,
|
|
128
|
-
});
|
|
129
|
-
const decisionAttrs = decision.spanAttributes;
|
|
130
|
-
if (decision.kind === "allow") {
|
|
131
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
132
|
-
attributes: { ...mcpAttrs, ...decisionAttrs },
|
|
133
|
-
});
|
|
134
|
-
return {};
|
|
135
|
-
}
|
|
136
|
-
if (decision.kind === "observe") {
|
|
137
|
-
client.tracer.record("tool.block", "denied", {
|
|
138
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
139
|
-
});
|
|
140
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
141
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
|
|
142
|
-
});
|
|
143
|
-
return {};
|
|
144
|
-
}
|
|
145
|
-
client.tracer.record("tool.block", "denied", {
|
|
146
|
-
attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
147
|
-
});
|
|
148
|
-
return {
|
|
149
|
-
decision: "block",
|
|
150
|
-
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
const namespace = resolveNamespace();
|
|
154
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
155
|
-
harness: "claude-code",
|
|
156
|
-
toolName,
|
|
157
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
158
|
-
spanAttributes: { toolName },
|
|
159
|
-
});
|
|
160
|
-
// Interactive tools (AskUserQuestion, ExitPlanMode, TodoWrite, …)
|
|
161
|
-
// surface UI to the user and aren't external-system tool executions.
|
|
162
|
-
// gateToolCall has already recorded the `user.interaction` audit span;
|
|
163
|
-
// we pass through with no `tool.invoke`/`tool.block` and let the
|
|
164
|
-
// harness deliver the prompt to the user.
|
|
165
|
-
if (outcome.kind === "interactive") {
|
|
166
|
-
return {};
|
|
167
|
-
}
|
|
168
|
-
const decision = outcome;
|
|
169
|
-
if (decision.kind === "fail_open") {
|
|
170
|
-
return handlePermissionError(decision.error, toolName, client);
|
|
171
|
-
}
|
|
172
|
-
const attrs = { toolName, ...inputSummary };
|
|
173
|
-
const decisionAttrs = decision.spanAttributes;
|
|
174
|
-
if (decision.kind === "allow") {
|
|
175
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
176
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: true },
|
|
177
|
-
});
|
|
178
|
-
return {};
|
|
179
|
-
}
|
|
180
|
-
if (decision.kind === "observe") {
|
|
181
|
-
client.tracer.record("tool.block", "denied", {
|
|
182
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
|
|
183
|
-
});
|
|
184
|
-
client.tracer.record("tool.invoke", "ok", {
|
|
185
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
|
|
186
|
-
});
|
|
187
|
-
return {};
|
|
188
|
-
}
|
|
189
|
-
client.tracer.record("tool.block", "denied", {
|
|
190
|
-
attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
|
|
191
|
-
});
|
|
192
|
-
return {
|
|
193
|
-
decision: "block",
|
|
194
|
-
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
return handlePermissionError(err, toolName, client);
|
|
199
|
-
}
|
|
81
|
+
const result = await (0, argus_1.gate)(client, {
|
|
82
|
+
harness: HARNESS,
|
|
83
|
+
toolName,
|
|
84
|
+
toolArgs: input.tool_input,
|
|
85
|
+
subjectFallback: `session:${input.session_id}`,
|
|
86
|
+
mcpTool: (0, argus_1.parseClaudeCodeMcpTool)(toolName) ?? undefined,
|
|
87
|
+
principals: actingSubagentPrincipals(input, client),
|
|
88
|
+
});
|
|
89
|
+
return result.blocked
|
|
90
|
+
? { decision: "block", reason: result.denialMessage }
|
|
91
|
+
: {};
|
|
92
|
+
}
|
|
93
|
+
function actingSubagentPrincipals(input, client) {
|
|
94
|
+
const type = input.agent_type;
|
|
95
|
+
if (!input.agent_id || !type)
|
|
96
|
+
return undefined;
|
|
97
|
+
const subject = client.agentPrincipal.subject;
|
|
98
|
+
if (!subject)
|
|
99
|
+
return undefined;
|
|
100
|
+
return {
|
|
101
|
+
subAgentClientId: subject,
|
|
102
|
+
subAgentType: type,
|
|
103
|
+
perSpawnId: input.agent_id,
|
|
104
|
+
sessionId: input.session_id,
|
|
105
|
+
};
|
|
200
106
|
}
|
|
201
|
-
// ─── PostToolUse ────────────────────────────────────────────────────
|
|
202
107
|
async function handlePostToolUse(input, client) {
|
|
203
108
|
const toolName = input.tool_name ?? "unknown";
|
|
204
109
|
client.logger.info("lifecycle.post_tool_use", {
|
|
@@ -206,17 +111,14 @@ async function handlePostToolUse(input, client) {
|
|
|
206
111
|
toolName,
|
|
207
112
|
toolResponse: input.tool_response,
|
|
208
113
|
});
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
|
|
215
|
-
},
|
|
114
|
+
(0, argus_1.complete)(client, {
|
|
115
|
+
toolName,
|
|
116
|
+
input: input.tool_input,
|
|
117
|
+
output: input.tool_response,
|
|
118
|
+
extraActivityAttributes: { toolUseId: input.tool_use_id },
|
|
216
119
|
});
|
|
217
120
|
return {};
|
|
218
121
|
}
|
|
219
|
-
// ─── PostToolUseFailure ─────────────────────────────────────────────
|
|
220
122
|
async function handlePostToolUseFailure(input, client) {
|
|
221
123
|
const toolName = input.tool_name ?? "unknown";
|
|
222
124
|
client.logger.info("lifecycle.post_tool_use_failure", {
|
|
@@ -224,249 +126,121 @@ async function handlePostToolUseFailure(input, client) {
|
|
|
224
126
|
toolName,
|
|
225
127
|
toolUseId: input.tool_use_id,
|
|
226
128
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
? input.tool_error.slice(0, 500)
|
|
233
|
-
: input.tool_error
|
|
234
|
-
? "[object]"
|
|
235
|
-
: undefined,
|
|
236
|
-
...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
|
|
237
|
-
},
|
|
129
|
+
(0, argus_1.fail)(client, {
|
|
130
|
+
toolName,
|
|
131
|
+
input: input.tool_input,
|
|
132
|
+
error: input.tool_error,
|
|
133
|
+
extraActivityAttributes: { toolUseId: input.tool_use_id },
|
|
238
134
|
});
|
|
239
135
|
return {};
|
|
240
136
|
}
|
|
241
|
-
|
|
137
|
+
async function handlePermissionRequest(input, client) {
|
|
138
|
+
const toolName = input.tool_name ?? "unknown";
|
|
139
|
+
client.logger.info("lifecycle.permission_request", {
|
|
140
|
+
sessionId: input.session_id,
|
|
141
|
+
toolName,
|
|
142
|
+
toolInput: input.tool_input,
|
|
143
|
+
});
|
|
144
|
+
const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
|
|
145
|
+
const result = await (0, argus_1.decideTool)(client, {
|
|
146
|
+
harness: HARNESS,
|
|
147
|
+
toolName,
|
|
148
|
+
toolArgs: input.tool_input,
|
|
149
|
+
subjectFallback: `session:${input.session_id}`,
|
|
150
|
+
mcpTool: mcpTool ?? undefined,
|
|
151
|
+
});
|
|
152
|
+
if (result.kind === "not_connected") {
|
|
153
|
+
return permissionRequestFallback("Ory Agent Security not connected — deferring to user");
|
|
154
|
+
}
|
|
155
|
+
if (result.kind === "interactive") {
|
|
156
|
+
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
157
|
+
}
|
|
158
|
+
if (result.kind === "fail_open") {
|
|
159
|
+
return permissionRequestFallback(permissionFailureReason(result.decision));
|
|
160
|
+
}
|
|
161
|
+
if (result.kind === "deny") {
|
|
162
|
+
return permissionRequestDecision("deny", result.denialMessage ?? "Ory permission denied");
|
|
163
|
+
}
|
|
164
|
+
return permissionRequestDecision("allow", result.kind === "observe"
|
|
165
|
+
? "Ory observe mode — denied by policy but allowed through"
|
|
166
|
+
: mcpTool
|
|
167
|
+
? "Ory MCP server permission granted"
|
|
168
|
+
: "Ory permission granted");
|
|
169
|
+
}
|
|
170
|
+
function permissionRequestDecision(behavior, reason) {
|
|
171
|
+
return {
|
|
172
|
+
hookSpecificOutput: {
|
|
173
|
+
hookEventName: "PermissionRequest",
|
|
174
|
+
decision: { behavior },
|
|
175
|
+
message: reason,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function permissionRequestFallback(reason) {
|
|
180
|
+
return {
|
|
181
|
+
hookSpecificOutput: { hookEventName: "PermissionRequest", message: reason },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function permissionFailureReason(decision) {
|
|
185
|
+
if (decision.kind !== "fail_open")
|
|
186
|
+
return "Ory permission check failed";
|
|
187
|
+
const { code, message } = decision.error;
|
|
188
|
+
return code === "network_error" || code === "rate_limited"
|
|
189
|
+
? `Ory unavailable (${code}), falling back to user prompt`
|
|
190
|
+
: `Ory permission check failed: ${message}`;
|
|
191
|
+
}
|
|
242
192
|
async function handleUserPromptSubmit(input, client) {
|
|
243
193
|
client.logger.info("lifecycle.user_prompt_submit", {
|
|
244
194
|
sessionId: input.session_id,
|
|
245
195
|
promptLen: input.prompt?.length,
|
|
246
196
|
});
|
|
247
|
-
client.
|
|
197
|
+
client.logger.activity("user.prompt", "ok", {
|
|
248
198
|
attributes: { promptLen: input.prompt?.length },
|
|
249
199
|
});
|
|
250
200
|
return {};
|
|
251
201
|
}
|
|
252
|
-
// ─── SubagentStart ─────────────────────────────────────────────────
|
|
253
|
-
//
|
|
254
|
-
// Dedicated sub-agent invocation event. Replaces the legacy path of
|
|
255
|
-
// sniffing the `Task`/`Agent` tool name in PreToolUse — that still runs
|
|
256
|
-
// as a fallback for older Claude Code versions, but registerSubAgent
|
|
257
|
-
// is idempotent so duplicate registration is harmless.
|
|
258
202
|
async function handleSubagentStart(input, client, deps) {
|
|
259
203
|
const subAgentType = input.subagent_type ?? input.agent_type;
|
|
260
|
-
const agentId = input.agent_id;
|
|
261
204
|
client.logger.info("lifecycle.subagent_start", {
|
|
262
205
|
sessionId: input.session_id,
|
|
263
206
|
subAgentType,
|
|
264
|
-
agentId,
|
|
207
|
+
agentId: input.agent_id,
|
|
265
208
|
});
|
|
266
|
-
client.
|
|
267
|
-
attributes: { subAgentType, agentId },
|
|
209
|
+
client.logger.activity("subagent.start", "ok", {
|
|
210
|
+
attributes: { subAgentType, agentId: input.agent_id },
|
|
268
211
|
});
|
|
269
|
-
if (subAgentType) {
|
|
270
|
-
await
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
212
|
+
if (subAgentType && input.agent_id) {
|
|
213
|
+
await (0, argus_1.registerSubagent)(client, {
|
|
214
|
+
harness: HARNESS,
|
|
215
|
+
sessionId: input.session_id,
|
|
216
|
+
subAgentType,
|
|
217
|
+
perSpawnId: input.agent_id,
|
|
218
|
+
subAgentGate: deps.subAgentGate,
|
|
275
219
|
});
|
|
276
220
|
}
|
|
277
221
|
return {};
|
|
278
222
|
}
|
|
279
|
-
// ─── SubagentStop ──────────────────────────────────────────────────
|
|
280
223
|
async function handleSubagentStop(input, client) {
|
|
224
|
+
const subAgentType = input.subagent_type ?? input.agent_type;
|
|
281
225
|
client.logger.info("lifecycle.subagent_stop", {
|
|
282
226
|
sessionId: input.session_id,
|
|
283
|
-
subAgentType
|
|
227
|
+
subAgentType,
|
|
284
228
|
agentId: input.agent_id,
|
|
285
229
|
});
|
|
286
|
-
client.
|
|
230
|
+
client.logger.activity("subagent.stop", "ok", {
|
|
287
231
|
attributes: {
|
|
288
|
-
subAgentType
|
|
232
|
+
subAgentType,
|
|
289
233
|
agentId: input.agent_id,
|
|
290
234
|
responseLen: input.subagent_response?.length,
|
|
291
235
|
},
|
|
292
236
|
});
|
|
293
237
|
return {};
|
|
294
238
|
}
|
|
295
|
-
// ─── SessionEnd ────────────────────────────────────────────────────
|
|
296
239
|
async function handleSessionEnd(input, client) {
|
|
297
240
|
client.logger.info("lifecycle.session_end", {
|
|
298
241
|
sessionId: input.session_id,
|
|
299
242
|
reason: input.reason,
|
|
300
243
|
});
|
|
301
|
-
client.
|
|
302
|
-
attributes: { reason: input.reason },
|
|
303
|
-
});
|
|
304
|
-
return {};
|
|
305
|
-
}
|
|
306
|
-
// ─── PermissionRequest ──────────────────────────────────────────────
|
|
307
|
-
async function handlePermissionRequest(input, client) {
|
|
308
|
-
const toolName = input.tool_name ?? "unknown";
|
|
309
|
-
client.logger.info("lifecycle.permission_request", {
|
|
310
|
-
sessionId: input.session_id,
|
|
311
|
-
toolName,
|
|
312
|
-
toolInput: input.tool_input,
|
|
313
|
-
});
|
|
314
|
-
// Audit-only mode: omit `decision` so Claude falls through to its normal
|
|
315
|
-
// user-permission prompt.
|
|
316
|
-
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
317
|
-
return permissionRequestFallback("Audit-only mode — deferring to user");
|
|
318
|
-
}
|
|
319
|
-
// Interactive tools: the harness is about to surface UI to the user
|
|
320
|
-
// (AskUserQuestion, ExitPlanMode, TodoWrite, …). Trace and fall through
|
|
321
|
-
// — Ory shouldn't pre-answer a prompt that's meant for the human.
|
|
322
|
-
if ((0, argus_1.isInteractiveTool)("claude-code", toolName)) {
|
|
323
|
-
client.tracer.record("user.interaction", "ok", {
|
|
324
|
-
attributes: { harness: "claude-code", toolName, kind: "permission-request" },
|
|
325
|
-
});
|
|
326
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
327
|
-
}
|
|
328
|
-
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
329
|
-
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
330
|
-
const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
|
|
331
|
-
try {
|
|
332
|
-
if (mcpTool) {
|
|
333
|
-
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
334
|
-
subject,
|
|
335
|
-
spanAttributes: { toolName },
|
|
336
|
-
});
|
|
337
|
-
const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
|
|
338
|
-
object: mcpTool.serverName,
|
|
339
|
-
relation: "use",
|
|
340
|
-
subjectId,
|
|
341
|
-
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
342
|
-
spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
|
|
343
|
-
});
|
|
344
|
-
if (decision.kind === "deny") {
|
|
345
|
-
return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
|
|
346
|
-
}
|
|
347
|
-
return permissionRequestDecision("allow", decision.kind === "observe"
|
|
348
|
-
? "Ory observe mode — denied by policy but allowed through"
|
|
349
|
-
: "Ory MCP server permission granted");
|
|
350
|
-
}
|
|
351
|
-
const namespace = resolveNamespace();
|
|
352
|
-
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
353
|
-
harness: "claude-code",
|
|
354
|
-
toolName,
|
|
355
|
-
check: { namespace, object: toolName, relation: "use", ...subject },
|
|
356
|
-
spanAttributes: { toolName },
|
|
357
|
-
});
|
|
358
|
-
if (outcome.kind === "interactive") {
|
|
359
|
-
return permissionRequestFallback("Interactive tool — deferring to user");
|
|
360
|
-
}
|
|
361
|
-
const decision = outcome;
|
|
362
|
-
if (decision.kind === "fail_open") {
|
|
363
|
-
const code = decision.error.code;
|
|
364
|
-
const fallbackReason = code === "network_error" || code === "rate_limited"
|
|
365
|
-
? `Ory unavailable (${code}), falling back to user prompt`
|
|
366
|
-
: `Ory permission check failed: ${decision.error.message}`;
|
|
367
|
-
return permissionRequestFallback(fallbackReason);
|
|
368
|
-
}
|
|
369
|
-
if (decision.kind === "deny") {
|
|
370
|
-
return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
|
|
371
|
-
}
|
|
372
|
-
return permissionRequestDecision("allow", decision.kind === "observe"
|
|
373
|
-
? "Ory observe mode — denied by policy but allowed through"
|
|
374
|
-
: "Ory permission granted");
|
|
375
|
-
}
|
|
376
|
-
catch (err) {
|
|
377
|
-
const oryErr = err;
|
|
378
|
-
const fallbackReason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
|
|
379
|
-
? `Ory unavailable (${oryErr.code}), falling back to user prompt`
|
|
380
|
-
: `Ory permission check failed: ${oryErr.message}`;
|
|
381
|
-
return permissionRequestFallback(fallbackReason);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function permissionRequestDecision(behavior, reason) {
|
|
385
|
-
return {
|
|
386
|
-
hookSpecificOutput: {
|
|
387
|
-
hookEventName: "PermissionRequest",
|
|
388
|
-
decision: { behavior },
|
|
389
|
-
message: reason,
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
function permissionRequestFallback(reason) {
|
|
394
|
-
return {
|
|
395
|
-
hookSpecificOutput: {
|
|
396
|
-
hookEventName: "PermissionRequest",
|
|
397
|
-
message: reason,
|
|
398
|
-
},
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
// ─── Helpers ────────────────────────────────────────────────────────
|
|
402
|
-
function resolveNamespace() {
|
|
403
|
-
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Register an OAuth2 identity for a sub-agent and write the
|
|
407
|
-
* `agent → subagent` delegation tuple. Fail-open and written at most once
|
|
408
|
-
* per install, so the multiple call paths (SubagentStart event + Task-tool
|
|
409
|
-
* fallback) won't duplicate state.
|
|
410
|
-
*/
|
|
411
|
-
async function registerSubAgent(subAgentType, client, deps) {
|
|
412
|
-
const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
|
|
413
|
-
let identity;
|
|
414
|
-
try {
|
|
415
|
-
identity = await subAgentGate(client, {
|
|
416
|
-
subAgentType,
|
|
417
|
-
projectUrl: (0, argus_1.resolveConfig)().projectUrl,
|
|
418
|
-
harness: "claude-code",
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
catch (err) {
|
|
422
|
-
client.logger.warn("subagent.identity.failed", {
|
|
423
|
-
subAgentType,
|
|
424
|
-
message: err instanceof Error ? err.message : String(err),
|
|
425
|
-
});
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
if (identity.kind !== "dynamic" || !identity.subject)
|
|
429
|
-
return;
|
|
430
|
-
await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Legacy fallback for older Claude Code versions that don't fire
|
|
434
|
-
* `SubagentStart`: the sub-agent invocation appears as a `Task`/`Agent`
|
|
435
|
-
* tool call in PreToolUse with a `subagent_type` field on tool_input.
|
|
436
|
-
* `SubagentStart` is the preferred path; this is a no-op when both fire.
|
|
437
|
-
*/
|
|
438
|
-
async function maybeRegisterSubAgent(toolName, toolInput, client, deps) {
|
|
439
|
-
if (toolName !== "Task" && toolName !== "Agent")
|
|
440
|
-
return;
|
|
441
|
-
const subAgentType = typeof toolInput === "object"
|
|
442
|
-
&& toolInput !== null
|
|
443
|
-
&& typeof toolInput.subagent_type === "string"
|
|
444
|
-
? toolInput.subagent_type
|
|
445
|
-
: undefined;
|
|
446
|
-
if (!subAgentType)
|
|
447
|
-
return;
|
|
448
|
-
await registerSubAgent(subAgentType, client, deps);
|
|
449
|
-
}
|
|
450
|
-
function handlePermissionError(oryErr, toolName, client) {
|
|
451
|
-
if (oryErr.code === "network_error") {
|
|
452
|
-
client.logger.warn("permission.network_error", {
|
|
453
|
-
toolName,
|
|
454
|
-
message: "Ory unreachable, failing open",
|
|
455
|
-
});
|
|
456
|
-
return {};
|
|
457
|
-
}
|
|
458
|
-
if (oryErr.code === "rate_limited") {
|
|
459
|
-
client.logger.warn("permission.rate_limited", {
|
|
460
|
-
toolName,
|
|
461
|
-
message: "Ory rate limited, failing open",
|
|
462
|
-
});
|
|
463
|
-
return {};
|
|
464
|
-
}
|
|
465
|
-
// For other errors, also fail open but log prominently
|
|
466
|
-
client.logger.error("permission.check.error", {
|
|
467
|
-
toolName,
|
|
468
|
-
code: oryErr.code,
|
|
469
|
-
message: oryErr.message,
|
|
470
|
-
});
|
|
244
|
+
client.logger.activity("session.end", "ok", { attributes: { reason: input.reason } });
|
|
471
245
|
return {};
|
|
472
246
|
}
|