@ory/claude-code 0.13.9 → 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/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
- * Route a Claude Code hook event to the appropriate Ory integration.
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
- // Set trace context so all spans within this hook invocation correlate
16
- client.tracer.setContext({
17
- traceId: (0, argus_1.deriveTraceId)(input.session_id),
18
- sessionId: input.session_id,
19
- });
20
- try {
21
- switch (event) {
22
- case "SessionStart":
23
- return await handleSessionStart(input, client, deps);
24
- case "PreToolUse":
25
- return await handlePreToolUse(input, client, deps);
26
- case "PostToolUse":
27
- return await handlePostToolUse(input, client);
28
- case "PostToolUseFailure":
29
- return await handlePostToolUseFailure(input, client);
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
- finally {
49
- client.tracer.clearContext();
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,224 +61,49 @@ async function handleSessionStart(input, client, deps) {
57
61
  source: input.source,
58
62
  cwd: input.cwd,
59
63
  });
60
- client.tracer.record("session.start", "ok", {
61
- attributes: { model: input.model, source: input.source },
62
- });
63
- // Run the user login (interactive PKCE on first session, refresh
64
- // when needed). When ORY_USER_LOGIN is unset this is a no-op and we
65
- // fall through to the legacy verify path below.
66
- const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
67
- const decision = await userGate(client, {
64
+ await (0, argus_1.sessionStart)(client, {
65
+ harness: HARNESS,
66
+ sessionId: input.session_id,
68
67
  binName: "ory-claude",
69
- harness: "claude-code",
70
- allowBlock: true,
71
- });
72
- // Resolve the agent identity (machine credentials) regardless of how
73
- // user auth went — it never blocks and attaches the agent's bearer
74
- // token to outgoing Ory API calls so the audit trail records who
75
- // acted on the user's behalf.
76
- const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
77
- await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "claude-code" });
78
- // Record the user→agent delegation so the audit trail captures that
79
- // this user authorized this agent for this session. Best-effort and
80
- // written at most once per install: requires both principals to be
81
- // populated, and any failure is logged and swallowed (fail-open —
82
- // delegation tracking is for audit, not enforcement).
83
- await (0, argus_1.writeUserDelegatesAgent)(client);
84
- if (!decision.proceed) {
85
- return { decision: "block", reason: decision.reason };
86
- }
87
- if (decision.mode !== "disabled") {
88
- return {};
89
- }
90
- const resolved = (0, argus_1.resolveConfig)();
91
- if (resolved.auditOnly) {
92
- client.logger.info("config.audit_only", {
93
- message: "Audit-only mode enabled. Auth and permission checks are disabled.",
94
- });
95
- return {};
96
- }
97
- if (!resolved.projectUrl) {
98
- client.logger.warn("config.not_configured", {
99
- message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
100
- "Run 'npx ory-claude configure' to connect to an Ory project.",
101
- });
102
- return {};
103
- }
104
- // Try session token first, then OAuth2 token
105
- const sessionToken = process.env.ORY_SESSION_TOKEN;
106
- const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
107
- if (sessionToken) {
108
- await verifySessionToken(sessionToken, client);
109
- return {};
110
- }
111
- if (oauth2Token) {
112
- await verifyOAuth2Token(oauth2Token, client);
113
- return {};
114
- }
115
- client.logger.warn("session.no_credentials", {
116
- message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
117
- "Skipping authentication.",
68
+ userLogin: deps.userLogin,
69
+ agentGate: deps.agentGate,
70
+ activityAttributes: { model: input.model, source: input.source },
118
71
  });
119
72
  return {};
120
73
  }
121
- async function verifySessionToken(token, client) {
122
- try {
123
- const session = await client.verifySession(token);
124
- if (!session.active) {
125
- client.logger.warn("session.inactive", {
126
- message: "Ory session is not active. Re-authenticate to enable auth checks.",
127
- });
128
- }
129
- }
130
- catch (err) {
131
- const oryErr = err;
132
- client.logger.warn("session.verify_failed", {
133
- code: oryErr.code,
134
- message: oryErr.message,
135
- });
136
- }
137
- }
138
- async function verifyOAuth2Token(token, client) {
139
- try {
140
- const tokenInfo = await client.introspectToken(token);
141
- if (!tokenInfo.active) {
142
- client.logger.warn("oauth2.token_inactive", {
143
- message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
144
- });
145
- return;
146
- }
147
- client.logger.info("oauth2.session_authenticated", {
148
- clientId: tokenInfo.clientId,
149
- subject: tokenInfo.subject,
150
- scope: tokenInfo.scope,
151
- });
152
- }
153
- catch (err) {
154
- const oryErr = err;
155
- client.logger.warn("oauth2.introspect_failed", {
156
- code: oryErr.code,
157
- message: oryErr.message,
158
- });
159
- }
160
- }
161
- // ─── PreToolUse ─────────────────────────────────────────────────────
162
- async function handlePreToolUse(input, client, deps = {}) {
74
+ async function handlePreToolUse(input, client, deps) {
163
75
  const toolName = input.tool_name ?? "unknown";
164
76
  client.logger.info("lifecycle.pre_tool_use", {
165
77
  sessionId: input.session_id,
166
78
  toolName,
167
79
  toolInput: input.tool_input,
168
80
  });
169
- const inputSummary = (0, argus_1.summarizeToolInput)(toolName, input.tool_input);
170
- // In audit-only mode, log the invocation but skip permission checks —
171
- // and skip sub-agent DCR/delegation writes too (Ory is disabled entirely).
172
- if ((0, argus_1.resolveConfig)().auditOnly) {
173
- client.tracer.record("tool.invoke", "ok", {
174
- attributes: { toolName, ...inputSummary },
175
- });
176
- return {};
177
- }
178
- // If the agent is invoking a sub-agent (Claude Code's "Task"/"Agent"
179
- // tool with a `subagent_type`), resolve a distinct OAuth2 identity for
180
- // that sub-agent via DCR and record the agent→sub-agent delegation
181
- // tuple. Best-effort — failures here never block the actual tool call.
182
- await maybeRegisterSubAgent(toolName, input.tool_input, client, deps);
183
- const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
184
- const subjectId = (0, argus_1.subjectLabel)(subject);
185
- const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
186
- try {
187
- if (mcpTool) {
188
- const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
189
- subject,
190
- spanAttributes: { toolName },
191
- });
192
- const mcpAttrs = {
193
- toolName,
194
- mcpServer: mcpTool.serverName,
195
- mcpTool: mcpTool.toolName,
196
- ...inputSummary,
197
- };
198
- const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
199
- object: mcpTool.serverName,
200
- relation: "use",
201
- subjectId,
202
- ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
203
- spanAttributes: mcpAttrs,
204
- });
205
- const decisionAttrs = decision.spanAttributes;
206
- if (decision.kind === "allow") {
207
- client.tracer.record("tool.invoke", "ok", {
208
- attributes: { ...mcpAttrs, ...decisionAttrs },
209
- });
210
- return {};
211
- }
212
- if (decision.kind === "observe") {
213
- client.tracer.record("tool.block", "denied", {
214
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
215
- });
216
- client.tracer.record("tool.invoke", "ok", {
217
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
218
- });
219
- return {};
220
- }
221
- client.tracer.record("tool.block", "denied", {
222
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
223
- });
224
- return {
225
- decision: "block",
226
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
227
- };
228
- }
229
- const namespace = resolveNamespace();
230
- const outcome = await (0, argus_1.gateToolCall)(client, {
231
- harness: "claude-code",
232
- toolName,
233
- check: { namespace, object: toolName, relation: "use", ...subject },
234
- spanAttributes: { toolName },
235
- });
236
- // Interactive tools (AskUserQuestion, ExitPlanMode, TodoWrite, …)
237
- // surface UI to the user and aren't external-system tool executions.
238
- // gateToolCall has already recorded the `user.interaction` audit span;
239
- // we pass through with no `tool.invoke`/`tool.block` and let the
240
- // harness deliver the prompt to the user.
241
- if (outcome.kind === "interactive") {
242
- return {};
243
- }
244
- const decision = outcome;
245
- if (decision.kind === "fail_open") {
246
- return handlePermissionError(decision.error, toolName, client);
247
- }
248
- const attrs = { toolName, ...inputSummary };
249
- const decisionAttrs = decision.spanAttributes;
250
- if (decision.kind === "allow") {
251
- client.tracer.record("tool.invoke", "ok", {
252
- attributes: { ...attrs, ...decisionAttrs, allowed: true },
253
- });
254
- return {};
255
- }
256
- if (decision.kind === "observe") {
257
- client.tracer.record("tool.block", "denied", {
258
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
259
- });
260
- client.tracer.record("tool.invoke", "ok", {
261
- attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
262
- });
263
- return {};
264
- }
265
- client.tracer.record("tool.block", "denied", {
266
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
267
- });
268
- return {
269
- decision: "block",
270
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
271
- };
272
- }
273
- catch (err) {
274
- return handlePermissionError(err, toolName, client);
275
- }
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
+ };
276
106
  }
277
- // ─── PostToolUse ────────────────────────────────────────────────────
278
107
  async function handlePostToolUse(input, client) {
279
108
  const toolName = input.tool_name ?? "unknown";
280
109
  client.logger.info("lifecycle.post_tool_use", {
@@ -282,17 +111,14 @@ async function handlePostToolUse(input, client) {
282
111
  toolName,
283
112
  toolResponse: input.tool_response,
284
113
  });
285
- client.tracer.record("tool.complete", "ok", {
286
- attributes: {
287
- toolName,
288
- toolUseId: input.tool_use_id,
289
- ...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
290
- ...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
291
- },
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 },
292
119
  });
293
120
  return {};
294
121
  }
295
- // ─── PostToolUseFailure ─────────────────────────────────────────────
296
122
  async function handlePostToolUseFailure(input, client) {
297
123
  const toolName = input.tool_name ?? "unknown";
298
124
  client.logger.info("lifecycle.post_tool_use_failure", {
@@ -300,249 +126,121 @@ async function handlePostToolUseFailure(input, client) {
300
126
  toolName,
301
127
  toolUseId: input.tool_use_id,
302
128
  });
303
- client.tracer.record("tool.fail", "error", {
304
- attributes: {
305
- toolName,
306
- toolUseId: input.tool_use_id,
307
- toolError: typeof input.tool_error === "string"
308
- ? input.tool_error.slice(0, 500)
309
- : input.tool_error
310
- ? "[object]"
311
- : undefined,
312
- ...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
313
- },
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 },
314
134
  });
315
135
  return {};
316
136
  }
317
- // ─── UserPromptSubmit ──────────────────────────────────────────────
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
+ }
318
192
  async function handleUserPromptSubmit(input, client) {
319
193
  client.logger.info("lifecycle.user_prompt_submit", {
320
194
  sessionId: input.session_id,
321
195
  promptLen: input.prompt?.length,
322
196
  });
323
- client.tracer.record("user.prompt", "ok", {
197
+ client.logger.activity("user.prompt", "ok", {
324
198
  attributes: { promptLen: input.prompt?.length },
325
199
  });
326
200
  return {};
327
201
  }
328
- // ─── SubagentStart ─────────────────────────────────────────────────
329
- //
330
- // Dedicated sub-agent invocation event. Replaces the legacy path of
331
- // sniffing the `Task`/`Agent` tool name in PreToolUse — that still runs
332
- // as a fallback for older Claude Code versions, but registerSubAgent
333
- // is idempotent so duplicate registration is harmless.
334
202
  async function handleSubagentStart(input, client, deps) {
335
203
  const subAgentType = input.subagent_type ?? input.agent_type;
336
- const agentId = input.agent_id;
337
204
  client.logger.info("lifecycle.subagent_start", {
338
205
  sessionId: input.session_id,
339
206
  subAgentType,
340
- agentId,
207
+ agentId: input.agent_id,
341
208
  });
342
- client.tracer.record("subagent.start", "ok", {
343
- attributes: { subAgentType, agentId },
209
+ client.logger.activity("subagent.start", "ok", {
210
+ attributes: { subAgentType, agentId: input.agent_id },
344
211
  });
345
- if (subAgentType) {
346
- await registerSubAgent(subAgentType, client, deps);
347
- }
348
- else {
349
- client.logger.debug("subagent_start.no_type", {
350
- message: "SubagentStart event without subagent_type — skipping DCR.",
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,
351
219
  });
352
220
  }
353
221
  return {};
354
222
  }
355
- // ─── SubagentStop ──────────────────────────────────────────────────
356
223
  async function handleSubagentStop(input, client) {
224
+ const subAgentType = input.subagent_type ?? input.agent_type;
357
225
  client.logger.info("lifecycle.subagent_stop", {
358
226
  sessionId: input.session_id,
359
- subAgentType: input.subagent_type ?? input.agent_type,
227
+ subAgentType,
360
228
  agentId: input.agent_id,
361
229
  });
362
- client.tracer.record("subagent.stop", "ok", {
230
+ client.logger.activity("subagent.stop", "ok", {
363
231
  attributes: {
364
- subAgentType: input.subagent_type ?? input.agent_type,
232
+ subAgentType,
365
233
  agentId: input.agent_id,
366
234
  responseLen: input.subagent_response?.length,
367
235
  },
368
236
  });
369
237
  return {};
370
238
  }
371
- // ─── SessionEnd ────────────────────────────────────────────────────
372
239
  async function handleSessionEnd(input, client) {
373
240
  client.logger.info("lifecycle.session_end", {
374
241
  sessionId: input.session_id,
375
242
  reason: input.reason,
376
243
  });
377
- client.tracer.record("session.end", "ok", {
378
- attributes: { reason: input.reason },
379
- });
380
- return {};
381
- }
382
- // ─── PermissionRequest ──────────────────────────────────────────────
383
- async function handlePermissionRequest(input, client) {
384
- const toolName = input.tool_name ?? "unknown";
385
- client.logger.info("lifecycle.permission_request", {
386
- sessionId: input.session_id,
387
- toolName,
388
- toolInput: input.tool_input,
389
- });
390
- // Audit-only mode: omit `decision` so Claude falls through to its normal
391
- // user-permission prompt.
392
- if ((0, argus_1.resolveConfig)().auditOnly) {
393
- return permissionRequestFallback("Audit-only mode — deferring to user");
394
- }
395
- // Interactive tools: the harness is about to surface UI to the user
396
- // (AskUserQuestion, ExitPlanMode, TodoWrite, …). Trace and fall through
397
- // — Ory shouldn't pre-answer a prompt that's meant for the human.
398
- if ((0, argus_1.isInteractiveTool)("claude-code", toolName)) {
399
- client.tracer.record("user.interaction", "ok", {
400
- attributes: { harness: "claude-code", toolName, kind: "permission-request" },
401
- });
402
- return permissionRequestFallback("Interactive tool — deferring to user");
403
- }
404
- const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
405
- const subjectId = (0, argus_1.subjectLabel)(subject);
406
- const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
407
- try {
408
- if (mcpTool) {
409
- const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
410
- subject,
411
- spanAttributes: { toolName },
412
- });
413
- const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
414
- object: mcpTool.serverName,
415
- relation: "use",
416
- subjectId,
417
- ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
418
- spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
419
- });
420
- if (decision.kind === "deny") {
421
- return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
422
- }
423
- return permissionRequestDecision("allow", decision.kind === "observe"
424
- ? "Ory observe mode — denied by policy but allowed through"
425
- : "Ory MCP server permission granted");
426
- }
427
- const namespace = resolveNamespace();
428
- const outcome = await (0, argus_1.gateToolCall)(client, {
429
- harness: "claude-code",
430
- toolName,
431
- check: { namespace, object: toolName, relation: "use", ...subject },
432
- spanAttributes: { toolName },
433
- });
434
- if (outcome.kind === "interactive") {
435
- return permissionRequestFallback("Interactive tool — deferring to user");
436
- }
437
- const decision = outcome;
438
- if (decision.kind === "fail_open") {
439
- const code = decision.error.code;
440
- const fallbackReason = code === "network_error" || code === "rate_limited"
441
- ? `Ory unavailable (${code}), falling back to user prompt`
442
- : `Ory permission check failed: ${decision.error.message}`;
443
- return permissionRequestFallback(fallbackReason);
444
- }
445
- if (decision.kind === "deny") {
446
- return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
447
- }
448
- return permissionRequestDecision("allow", decision.kind === "observe"
449
- ? "Ory observe mode — denied by policy but allowed through"
450
- : "Ory permission granted");
451
- }
452
- catch (err) {
453
- const oryErr = err;
454
- const fallbackReason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
455
- ? `Ory unavailable (${oryErr.code}), falling back to user prompt`
456
- : `Ory permission check failed: ${oryErr.message}`;
457
- return permissionRequestFallback(fallbackReason);
458
- }
459
- }
460
- function permissionRequestDecision(behavior, reason) {
461
- return {
462
- hookSpecificOutput: {
463
- hookEventName: "PermissionRequest",
464
- decision: { behavior },
465
- message: reason,
466
- },
467
- };
468
- }
469
- function permissionRequestFallback(reason) {
470
- return {
471
- hookSpecificOutput: {
472
- hookEventName: "PermissionRequest",
473
- message: reason,
474
- },
475
- };
476
- }
477
- // ─── Helpers ────────────────────────────────────────────────────────
478
- function resolveNamespace() {
479
- return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
480
- }
481
- /**
482
- * Register an OAuth2 identity for a sub-agent and write the
483
- * `agent → subagent` delegation tuple. Fail-open and written at most once
484
- * per install, so the multiple call paths (SubagentStart event + Task-tool
485
- * fallback) won't duplicate state.
486
- */
487
- async function registerSubAgent(subAgentType, client, deps) {
488
- const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
489
- let identity;
490
- try {
491
- identity = await subAgentGate(client, {
492
- subAgentType,
493
- projectUrl: (0, argus_1.resolveConfig)().projectUrl,
494
- harness: "claude-code",
495
- });
496
- }
497
- catch (err) {
498
- client.logger.warn("subagent.identity.failed", {
499
- subAgentType,
500
- message: err instanceof Error ? err.message : String(err),
501
- });
502
- return;
503
- }
504
- if (identity.kind !== "dynamic" || !identity.subject)
505
- return;
506
- await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
507
- }
508
- /**
509
- * Legacy fallback for older Claude Code versions that don't fire
510
- * `SubagentStart`: the sub-agent invocation appears as a `Task`/`Agent`
511
- * tool call in PreToolUse with a `subagent_type` field on tool_input.
512
- * `SubagentStart` is the preferred path; this is a no-op when both fire.
513
- */
514
- async function maybeRegisterSubAgent(toolName, toolInput, client, deps) {
515
- if (toolName !== "Task" && toolName !== "Agent")
516
- return;
517
- const subAgentType = typeof toolInput === "object"
518
- && toolInput !== null
519
- && typeof toolInput.subagent_type === "string"
520
- ? toolInput.subagent_type
521
- : undefined;
522
- if (!subAgentType)
523
- return;
524
- await registerSubAgent(subAgentType, client, deps);
525
- }
526
- function handlePermissionError(oryErr, toolName, client) {
527
- if (oryErr.code === "network_error") {
528
- client.logger.warn("permission.network_error", {
529
- toolName,
530
- message: "Ory unreachable, failing open",
531
- });
532
- return {};
533
- }
534
- if (oryErr.code === "rate_limited") {
535
- client.logger.warn("permission.rate_limited", {
536
- toolName,
537
- message: "Ory rate limited, failing open",
538
- });
539
- return {};
540
- }
541
- // For other errors, also fail open but log prominently
542
- client.logger.error("permission.check.error", {
543
- toolName,
544
- code: oryErr.code,
545
- message: oryErr.message,
546
- });
244
+ client.logger.activity("session.end", "ok", { attributes: { reason: input.reason } });
547
245
  return {};
548
246
  }