@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/.claude-plugin/marketplace.json +2 -2
- package/README.md +71 -61
- package/dist/cli/main.js +46 -24
- package/dist/cli/setup.d.ts +39 -29
- package/dist/cli/setup.js +191 -180
- package/dist/handlers.d.ts +2 -7
- package/dist/handlers.js +157 -459
- 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,224 +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). 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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
227
|
+
subAgentType,
|
|
360
228
|
agentId: input.agent_id,
|
|
361
229
|
});
|
|
362
|
-
client.
|
|
230
|
+
client.logger.activity("subagent.stop", "ok", {
|
|
363
231
|
attributes: {
|
|
364
|
-
subAgentType
|
|
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.
|
|
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
|
}
|