@ory/claude-code 0.1.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 +13 -0
- package/.claude-plugin/plugin.json +10 -0
- package/.mcp.json +12 -0
- package/README.md +101 -0
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.js +131 -0
- package/dist/cli/setup.d.ts +28 -0
- package/dist/cli/setup.js +297 -0
- package/dist/handlers.d.ts +14 -0
- package/dist/handlers.js +525 -0
- package/dist/hook.d.ts +9 -0
- package/dist/hook.js +76 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/settings.d.ts +12 -0
- package/dist/settings.js +67 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +2 -0
- package/hooks/hooks.json +104 -0
- package/package.json +86 -0
package/dist/handlers.js
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleHookEvent = handleHookEvent;
|
|
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 = {}) {
|
|
9
|
+
const event = input.hook_event_name;
|
|
10
|
+
client.logger.debug("hook.received", {
|
|
11
|
+
event,
|
|
12
|
+
sessionId: input.session_id,
|
|
13
|
+
toolName: input.tool_name,
|
|
14
|
+
});
|
|
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 };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
client.tracer.clearContext();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ─── SessionStart ───────────────────────────────────────────────────
|
|
53
|
+
async function handleSessionStart(input, client, deps) {
|
|
54
|
+
client.logger.info("lifecycle.session_start", {
|
|
55
|
+
sessionId: input.session_id,
|
|
56
|
+
model: input.model,
|
|
57
|
+
source: input.source,
|
|
58
|
+
cwd: input.cwd,
|
|
59
|
+
});
|
|
60
|
+
client.tracer.record("session.start", "ok", {
|
|
61
|
+
attributes: { model: input.model, source: input.source },
|
|
62
|
+
});
|
|
63
|
+
// Run the user auth gate (interactive PKCE on first session, refresh
|
|
64
|
+
// when needed). When ORY_AUTH_GATE is unset this is a no-op and we
|
|
65
|
+
// fall through to the legacy verify path below.
|
|
66
|
+
const userGate = deps.authGate ?? argus_1.ensureUserAuthenticated;
|
|
67
|
+
const decision = await userGate(client, {
|
|
68
|
+
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:
|
|
80
|
+
// requires both principals to be populated, and any failure is logged
|
|
81
|
+
// and swallowed (fail-open — delegation tracking is for audit, not
|
|
82
|
+
// enforcement).
|
|
83
|
+
await recordUserDelegatesAgent(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.",
|
|
118
|
+
});
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
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 = {}) {
|
|
163
|
+
const toolName = input.tool_name ?? "unknown";
|
|
164
|
+
client.logger.info("lifecycle.pre_tool_use", {
|
|
165
|
+
sessionId: input.session_id,
|
|
166
|
+
toolName,
|
|
167
|
+
toolInput: input.tool_input,
|
|
168
|
+
});
|
|
169
|
+
const inputSummary = (0, argus_1.summarizeToolInput)(toolName, input.tool_input);
|
|
170
|
+
// If the agent is invoking a sub-agent (Claude Code's "Task"/"Agent"
|
|
171
|
+
// tool with a `subagent_type`), resolve a distinct OAuth2 identity for
|
|
172
|
+
// that sub-agent via DCR and record the agent→sub-agent delegation
|
|
173
|
+
// tuple. Best-effort — failures here never block the actual tool call.
|
|
174
|
+
await maybeRegisterSubAgent(toolName, input.tool_input, client, deps);
|
|
175
|
+
// In audit-only mode, log the invocation but skip permission checks
|
|
176
|
+
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
177
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
178
|
+
attributes: { toolName, ...inputSummary },
|
|
179
|
+
});
|
|
180
|
+
return {};
|
|
181
|
+
}
|
|
182
|
+
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
183
|
+
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
184
|
+
const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
|
|
185
|
+
try {
|
|
186
|
+
if (mcpTool) {
|
|
187
|
+
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
188
|
+
subject,
|
|
189
|
+
spanAttributes: { toolName },
|
|
190
|
+
});
|
|
191
|
+
if (!mcpResult.allowed) {
|
|
192
|
+
client.tracer.record("tool.block", "denied", {
|
|
193
|
+
attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary, ...(0, argus_1.alertAttributes)(true) },
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
decision: "block",
|
|
197
|
+
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
201
|
+
attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary },
|
|
202
|
+
});
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
const namespace = resolveNamespace();
|
|
206
|
+
const result = await client.checkPermission({
|
|
207
|
+
namespace,
|
|
208
|
+
object: toolName,
|
|
209
|
+
relation: "use",
|
|
210
|
+
...subject,
|
|
211
|
+
}, { spanAttributes: { toolName } });
|
|
212
|
+
if (!result.allowed) {
|
|
213
|
+
client.tracer.record("tool.block", "denied", {
|
|
214
|
+
attributes: { toolName, ...inputSummary, allowed: result.allowed, ...(0, argus_1.alertAttributes)(true) },
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
decision: "block",
|
|
218
|
+
reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
222
|
+
attributes: { toolName, ...inputSummary, allowed: result.allowed },
|
|
223
|
+
});
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return handlePermissionError(err, toolName, client);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ─── PostToolUse ────────────────────────────────────────────────────
|
|
231
|
+
async function handlePostToolUse(input, client) {
|
|
232
|
+
const toolName = input.tool_name ?? "unknown";
|
|
233
|
+
client.logger.info("lifecycle.post_tool_use", {
|
|
234
|
+
sessionId: input.session_id,
|
|
235
|
+
toolName,
|
|
236
|
+
toolResponse: input.tool_response,
|
|
237
|
+
});
|
|
238
|
+
client.tracer.record("tool.complete", "ok", {
|
|
239
|
+
attributes: {
|
|
240
|
+
toolName,
|
|
241
|
+
toolUseId: input.tool_use_id,
|
|
242
|
+
...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
|
|
243
|
+
...(0, argus_1.summarizeToolOutput)(toolName, input.tool_response),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
return {};
|
|
247
|
+
}
|
|
248
|
+
// ─── PostToolUseFailure ─────────────────────────────────────────────
|
|
249
|
+
async function handlePostToolUseFailure(input, client) {
|
|
250
|
+
const toolName = input.tool_name ?? "unknown";
|
|
251
|
+
client.logger.info("lifecycle.post_tool_use_failure", {
|
|
252
|
+
sessionId: input.session_id,
|
|
253
|
+
toolName,
|
|
254
|
+
toolUseId: input.tool_use_id,
|
|
255
|
+
});
|
|
256
|
+
client.tracer.record("tool.fail", "error", {
|
|
257
|
+
attributes: {
|
|
258
|
+
toolName,
|
|
259
|
+
toolUseId: input.tool_use_id,
|
|
260
|
+
toolError: typeof input.tool_error === "string"
|
|
261
|
+
? input.tool_error.slice(0, 500)
|
|
262
|
+
: input.tool_error
|
|
263
|
+
? "[object]"
|
|
264
|
+
: undefined,
|
|
265
|
+
...(0, argus_1.summarizeToolInput)(toolName, input.tool_input),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
return {};
|
|
269
|
+
}
|
|
270
|
+
// ─── UserPromptSubmit ──────────────────────────────────────────────
|
|
271
|
+
async function handleUserPromptSubmit(input, client) {
|
|
272
|
+
client.logger.info("lifecycle.user_prompt_submit", {
|
|
273
|
+
sessionId: input.session_id,
|
|
274
|
+
promptLen: input.prompt?.length,
|
|
275
|
+
});
|
|
276
|
+
client.tracer.record("user.prompt", "ok", {
|
|
277
|
+
attributes: { promptLen: input.prompt?.length },
|
|
278
|
+
});
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
// ─── SubagentStart ─────────────────────────────────────────────────
|
|
282
|
+
//
|
|
283
|
+
// Dedicated sub-agent invocation event. Replaces the legacy path of
|
|
284
|
+
// sniffing the `Task`/`Agent` tool name in PreToolUse — that still runs
|
|
285
|
+
// as a fallback for older Claude Code versions, but registerSubAgent
|
|
286
|
+
// is idempotent so duplicate registration is harmless.
|
|
287
|
+
async function handleSubagentStart(input, client, deps) {
|
|
288
|
+
const subAgentType = input.subagent_type ?? input.agent_type;
|
|
289
|
+
const agentId = input.agent_id;
|
|
290
|
+
client.logger.info("lifecycle.subagent_start", {
|
|
291
|
+
sessionId: input.session_id,
|
|
292
|
+
subAgentType,
|
|
293
|
+
agentId,
|
|
294
|
+
});
|
|
295
|
+
client.tracer.record("subagent.start", "ok", {
|
|
296
|
+
attributes: { subAgentType, agentId },
|
|
297
|
+
});
|
|
298
|
+
if (subAgentType) {
|
|
299
|
+
await registerSubAgent(subAgentType, client, deps);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
client.logger.debug("subagent_start.no_type", {
|
|
303
|
+
message: "SubagentStart event without subagent_type — skipping DCR.",
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
// ─── SubagentStop ──────────────────────────────────────────────────
|
|
309
|
+
async function handleSubagentStop(input, client) {
|
|
310
|
+
client.logger.info("lifecycle.subagent_stop", {
|
|
311
|
+
sessionId: input.session_id,
|
|
312
|
+
subAgentType: input.subagent_type ?? input.agent_type,
|
|
313
|
+
agentId: input.agent_id,
|
|
314
|
+
});
|
|
315
|
+
client.tracer.record("subagent.stop", "ok", {
|
|
316
|
+
attributes: {
|
|
317
|
+
subAgentType: input.subagent_type ?? input.agent_type,
|
|
318
|
+
agentId: input.agent_id,
|
|
319
|
+
responseLen: input.subagent_response?.length,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
// ─── SessionEnd ────────────────────────────────────────────────────
|
|
325
|
+
async function handleSessionEnd(input, client) {
|
|
326
|
+
client.logger.info("lifecycle.session_end", {
|
|
327
|
+
sessionId: input.session_id,
|
|
328
|
+
reason: input.reason,
|
|
329
|
+
});
|
|
330
|
+
client.tracer.record("session.end", "ok", {
|
|
331
|
+
attributes: { reason: input.reason },
|
|
332
|
+
});
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
// ─── PermissionRequest ──────────────────────────────────────────────
|
|
336
|
+
async function handlePermissionRequest(input, client) {
|
|
337
|
+
const toolName = input.tool_name ?? "unknown";
|
|
338
|
+
client.logger.info("lifecycle.permission_request", {
|
|
339
|
+
sessionId: input.session_id,
|
|
340
|
+
toolName,
|
|
341
|
+
toolInput: input.tool_input,
|
|
342
|
+
});
|
|
343
|
+
// Audit-only mode: omit `decision` so Claude falls through to its normal
|
|
344
|
+
// user-permission prompt.
|
|
345
|
+
if ((0, argus_1.resolveConfig)().auditOnly) {
|
|
346
|
+
return permissionRequestFallback("Audit-only mode — deferring to user");
|
|
347
|
+
}
|
|
348
|
+
const subject = (0, argus_1.resolveUserSubject)(client, `session:${input.session_id}`);
|
|
349
|
+
const subjectId = (0, argus_1.subjectLabel)(subject);
|
|
350
|
+
const mcpTool = (0, argus_1.parseClaudeCodeMcpTool)(toolName);
|
|
351
|
+
try {
|
|
352
|
+
if (mcpTool) {
|
|
353
|
+
const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
|
|
354
|
+
subject,
|
|
355
|
+
spanAttributes: { toolName },
|
|
356
|
+
});
|
|
357
|
+
return permissionRequestDecision(mcpResult.allowed ? "allow" : "deny", mcpResult.allowed
|
|
358
|
+
? "Ory MCP server permission granted"
|
|
359
|
+
: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
|
|
360
|
+
}
|
|
361
|
+
const namespace = resolveNamespace();
|
|
362
|
+
const result = await client.checkPermission({
|
|
363
|
+
namespace,
|
|
364
|
+
object: toolName,
|
|
365
|
+
relation: "use",
|
|
366
|
+
...subject,
|
|
367
|
+
}, { spanAttributes: { toolName } });
|
|
368
|
+
return permissionRequestDecision(result.allowed ? "allow" : "deny", result.allowed
|
|
369
|
+
? "Ory permission granted"
|
|
370
|
+
: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
const oryErr = err;
|
|
374
|
+
const fallbackReason = oryErr.code === "network_error" || oryErr.code === "rate_limited"
|
|
375
|
+
? `Ory unavailable (${oryErr.code}), falling back to user prompt`
|
|
376
|
+
: `Ory permission check failed: ${oryErr.message}`;
|
|
377
|
+
return permissionRequestFallback(fallbackReason);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function permissionRequestDecision(behavior, reason) {
|
|
381
|
+
return {
|
|
382
|
+
hookSpecificOutput: {
|
|
383
|
+
hookEventName: "PermissionRequest",
|
|
384
|
+
decision: { behavior },
|
|
385
|
+
message: reason,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function permissionRequestFallback(reason) {
|
|
390
|
+
return {
|
|
391
|
+
hookSpecificOutput: {
|
|
392
|
+
hookEventName: "PermissionRequest",
|
|
393
|
+
message: reason,
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// ─── Helpers ────────────────────────────────────────────────────────
|
|
398
|
+
function resolveNamespace() {
|
|
399
|
+
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Write the user→agent delegation tuple. Idempotent and fail-open:
|
|
403
|
+
* requires both principal subjects to be populated; any error (including
|
|
404
|
+
* unconfigured projectUrl, which manifests as a network_error) is logged
|
|
405
|
+
* and swallowed. The actual permission enforcement is unchanged — this
|
|
406
|
+
* tuple is purely audit-trail data.
|
|
407
|
+
*/
|
|
408
|
+
async function recordUserDelegatesAgent(client) {
|
|
409
|
+
const user = client.userPrincipal.subject;
|
|
410
|
+
const agent = client.agentPrincipal.subject;
|
|
411
|
+
if (!user || !agent) {
|
|
412
|
+
client.logger.debug("delegation.skip", {
|
|
413
|
+
reason: "missing principal",
|
|
414
|
+
hasUser: !!user,
|
|
415
|
+
hasAgent: !!agent,
|
|
416
|
+
});
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
await client.createRelationship({
|
|
421
|
+
namespace: resolveNamespace(),
|
|
422
|
+
object: `agent:${agent}`,
|
|
423
|
+
relation: "delegate",
|
|
424
|
+
subjectId: `user:${user}`,
|
|
425
|
+
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
const oryErr = err;
|
|
429
|
+
client.logger.warn("delegation.user_to_agent.failed", {
|
|
430
|
+
code: oryErr.code,
|
|
431
|
+
message: oryErr.message,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Register an OAuth2 identity for a sub-agent and write the
|
|
437
|
+
* `agent → subagent` delegation tuple. Fail-open and idempotent:
|
|
438
|
+
* `createRelationship` absorbs 409s, so multiple call paths
|
|
439
|
+
* (SubagentStart event + Task-tool fallback) won't duplicate state.
|
|
440
|
+
*/
|
|
441
|
+
async function registerSubAgent(subAgentType, client, deps) {
|
|
442
|
+
const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
|
|
443
|
+
let identity;
|
|
444
|
+
try {
|
|
445
|
+
identity = await subAgentGate(client, {
|
|
446
|
+
subAgentType,
|
|
447
|
+
projectUrl: (0, argus_1.resolveConfig)().projectUrl,
|
|
448
|
+
harness: "claude-code",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
client.logger.warn("subagent.identity.failed", {
|
|
453
|
+
subAgentType,
|
|
454
|
+
message: err instanceof Error ? err.message : String(err),
|
|
455
|
+
});
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (identity.kind !== "dynamic" || !identity.subject)
|
|
459
|
+
return;
|
|
460
|
+
const agent = client.agentPrincipal.subject;
|
|
461
|
+
if (!agent) {
|
|
462
|
+
client.logger.debug("delegation.skip", {
|
|
463
|
+
reason: "agent principal not populated",
|
|
464
|
+
subAgentType,
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await client.createRelationship({
|
|
470
|
+
namespace: resolveNamespace(),
|
|
471
|
+
object: `subagent:${identity.subject}`,
|
|
472
|
+
relation: "delegate",
|
|
473
|
+
subjectId: `agent:${agent}`,
|
|
474
|
+
}, { spanAttributes: { delegation: "agent-to-subagent", subAgentType } });
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
const oryErr = err;
|
|
478
|
+
client.logger.warn("delegation.agent_to_subagent.failed", {
|
|
479
|
+
subAgentType,
|
|
480
|
+
code: oryErr.code,
|
|
481
|
+
message: oryErr.message,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Legacy fallback for older Claude Code versions that don't fire
|
|
487
|
+
* `SubagentStart`: the sub-agent invocation appears as a `Task`/`Agent`
|
|
488
|
+
* tool call in PreToolUse with a `subagent_type` field on tool_input.
|
|
489
|
+
* `SubagentStart` is the preferred path; this is a no-op when both fire.
|
|
490
|
+
*/
|
|
491
|
+
async function maybeRegisterSubAgent(toolName, toolInput, client, deps) {
|
|
492
|
+
if (toolName !== "Task" && toolName !== "Agent")
|
|
493
|
+
return;
|
|
494
|
+
const subAgentType = typeof toolInput === "object"
|
|
495
|
+
&& toolInput !== null
|
|
496
|
+
&& typeof toolInput.subagent_type === "string"
|
|
497
|
+
? toolInput.subagent_type
|
|
498
|
+
: undefined;
|
|
499
|
+
if (!subAgentType)
|
|
500
|
+
return;
|
|
501
|
+
await registerSubAgent(subAgentType, client, deps);
|
|
502
|
+
}
|
|
503
|
+
function handlePermissionError(oryErr, toolName, client) {
|
|
504
|
+
if (oryErr.code === "network_error") {
|
|
505
|
+
client.logger.warn("permission.network_error", {
|
|
506
|
+
toolName,
|
|
507
|
+
message: "Ory unreachable, failing open",
|
|
508
|
+
});
|
|
509
|
+
return {};
|
|
510
|
+
}
|
|
511
|
+
if (oryErr.code === "rate_limited") {
|
|
512
|
+
client.logger.warn("permission.rate_limited", {
|
|
513
|
+
toolName,
|
|
514
|
+
message: "Ory rate limited, failing open",
|
|
515
|
+
});
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
// For other errors, also fail open but log prominently
|
|
519
|
+
client.logger.error("permission.check.error", {
|
|
520
|
+
toolName,
|
|
521
|
+
code: oryErr.code,
|
|
522
|
+
message: oryErr.message,
|
|
523
|
+
});
|
|
524
|
+
return {};
|
|
525
|
+
}
|
package/dist/hook.d.ts
ADDED
package/dist/hook.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code hook entry point.
|
|
5
|
+
* This script is invoked by Claude Code for each hook event.
|
|
6
|
+
* Input: JSON on stdin
|
|
7
|
+
* Output: JSON on stdout
|
|
8
|
+
* Exit code 2 = block the action
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
const argus_1 = require("@ory/argus");
|
|
12
|
+
const handlers_js_1 = require("./handlers.js");
|
|
13
|
+
/**
|
|
14
|
+
* Read all of stdin as a string.
|
|
15
|
+
*
|
|
16
|
+
* Uses event listeners instead of `for await` so that we don't depend on the
|
|
17
|
+
* parent process closing stdin (sending EOF). If no EOF arrives, we resolve
|
|
18
|
+
* after a short idle gap once data has been received.
|
|
19
|
+
*/
|
|
20
|
+
function readStdin() {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let resolved = false;
|
|
24
|
+
function done() {
|
|
25
|
+
if (!resolved) {
|
|
26
|
+
resolved = true;
|
|
27
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
process.stdin.on("data", (chunk) => {
|
|
31
|
+
chunks.push(chunk);
|
|
32
|
+
// Reset idle timer on every chunk. Once data stops arriving for 100 ms
|
|
33
|
+
// we assume the full payload has been delivered.
|
|
34
|
+
clearTimeout(idleTimer);
|
|
35
|
+
idleTimer = setTimeout(done, 100);
|
|
36
|
+
});
|
|
37
|
+
process.stdin.on("end", done);
|
|
38
|
+
process.stdin.on("error", (err) => {
|
|
39
|
+
if (!resolved) {
|
|
40
|
+
resolved = true;
|
|
41
|
+
reject(err);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
let idleTimer;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const client = argus_1.OryAgentClient.fromEnv("claude-code");
|
|
49
|
+
const raw = await readStdin();
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
client.logger.error("hook.stdin.parse_failed", { raw: raw.slice(0, 200) });
|
|
56
|
+
await client.tracer.shutdown();
|
|
57
|
+
process.exit(0); // Don't block on parse errors
|
|
58
|
+
}
|
|
59
|
+
const output = await (0, handlers_js_1.handleHookEvent)(input, client);
|
|
60
|
+
if (output.decision === "block") {
|
|
61
|
+
process.stdout.write(JSON.stringify(output));
|
|
62
|
+
await client.tracer.shutdown();
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
if (output.hookSpecificOutput ||
|
|
66
|
+
output.updatedInput ||
|
|
67
|
+
output.decision === "approve") {
|
|
68
|
+
process.stdout.write(JSON.stringify(output));
|
|
69
|
+
}
|
|
70
|
+
await client.tracer.shutdown();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
main().catch((err) => {
|
|
74
|
+
process.stderr.write(`[ory-agent] fatal: ${err}\n`);
|
|
75
|
+
process.exit(0); // Fail open
|
|
76
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSettings = exports.handleHookEvent = void 0;
|
|
4
|
+
var handlers_js_1 = require("./handlers.js");
|
|
5
|
+
Object.defineProperty(exports, "handleHookEvent", { enumerable: true, get: function () { return handlers_js_1.handleHookEvent; } });
|
|
6
|
+
var settings_js_1 = require("./settings.js");
|
|
7
|
+
Object.defineProperty(exports, "generateSettings", { enumerable: true, get: function () { return settings_js_1.generateSettings; } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate the .claude/settings.local.json that configures Ory hooks.
|
|
3
|
+
*/
|
|
4
|
+
export declare function generateSettings(hookScriptPath: string): object;
|
|
5
|
+
/**
|
|
6
|
+
* Generate settings assuming the plugin is installed via npm/pnpm.
|
|
7
|
+
*/
|
|
8
|
+
export declare function generateInstalledSettings(): object;
|
|
9
|
+
/**
|
|
10
|
+
* Generate settings pointing to a local dev build.
|
|
11
|
+
*/
|
|
12
|
+
export declare function generateDevSettings(packageDir: string): object;
|