@ory/openclaw 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/plugin.js CHANGED
@@ -2,20 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createOryPlugin = createOryPlugin;
4
4
  const argus_1 = require("@ory/argus");
5
- /**
6
- * Create the Ory plugin entry for OpenClaw.
7
- *
8
- * OpenClaw plugins export { id, name, register(api) } where register
9
- * receives a PluginApi for registering hooks. The `before_tool_call`
10
- * hook can return `{ block: true }` to deny tool execution — this
11
- * makes permission checks enforceable, unlike harnesses that can only
12
- * log denials.
13
- *
14
- * The `session_start` hook returns `Promise<void>` so user login is
15
- * advisory at session start: the flow still runs to emit the audit span,
16
- * refresh tokens, and prompt for the project URL when interactive, but
17
- * cannot hard-block the session.
18
- */
5
+ const HARNESS = "openclaw";
19
6
  function createOryPlugin(clientOrConfig, deps = {}) {
20
7
  return {
21
8
  id: "ory-agent-security",
@@ -24,309 +11,91 @@ function createOryPlugin(clientOrConfig, deps = {}) {
24
11
  const client = clientOrConfig instanceof argus_1.OryAgentClient
25
12
  ? clientOrConfig
26
13
  : clientOrConfig
27
- ? new argus_1.OryAgentClient({
28
- ...clientOrConfig,
29
- harness: "openclaw",
30
- })
31
- : argus_1.OryAgentClient.fromEnv("openclaw");
32
- api.registerHook("session_start", createSessionStartHandler(client, deps), { name: "ory.session-verify", description: "Verify session via Ory Identities" });
33
- api.registerHook("before_agent_run", createBeforeAgentRunHandler(client, deps), { name: "ory.user-login", description: "Block runs when the user login denies" });
34
- api.registerHook("before_tool_call", createBeforeToolCallHandler(client), { name: "ory.permission-check", description: "Check tool permissions via Ory Permissions" });
35
- api.registerHook("after_tool_call", createAfterToolCallHandler(client), { name: "ory.trace-complete", description: "Record tool completion trace" });
36
- api.registerHook("subagent_spawning", createSubagentSpawningHandler(client, deps), { name: "ory.subagent-register", description: "Register Ory identity and write delegation tuple for spawned sub-agent" });
37
- api.registerHook("subagent_spawned", createSubagentSpawnedHandler(client), { name: "ory.subagent-spawned", description: "Record sub-agent spawn audit span" });
38
- api.registerHook("subagent_ended", createSubagentEndedHandler(client), { name: "ory.subagent-ended", description: "Record sub-agent termination audit span" });
39
- },
40
- };
41
- }
42
- // ─── session_start ─────────────────────────────────────────────────
43
- function createSessionStartHandler(client, deps) {
44
- return async (ctx) => {
45
- client.logger.info("lifecycle.session_start", {
46
- sessionId: ctx.sessionId,
47
- model: ctx.model,
48
- });
49
- // Set trace context for this session
50
- client.tracer.setContext({
51
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionId),
52
- sessionId: ctx.sessionId,
53
- });
54
- // Run the user login. session_start cannot hard-block in OpenClaw
55
- // (handler returns void), so allowBlock is false — it still emits
56
- // the user.auth audit span, refreshes tokens, and prompts for the
57
- // project URL when interactive. When ORY_USER_LOGIN is unset this is
58
- // a no-op (mode === "disabled") and we fall through to the legacy
59
- // verification path.
60
- const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
61
- const decision = await userGate(client, {
62
- binName: "ory-openclaw",
63
- harness: "openclaw",
64
- allowBlock: false,
65
- });
66
- // Resolve the agent identity (machine credentials). Never blocks;
67
- // attaches the agent's bearer token to outgoing Ory API calls.
68
- const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
69
- await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "openclaw" });
70
- // Record the user→agent delegation so the audit trail captures that
71
- // this user authorized this agent for this session. Best-effort and
72
- // written at most once per install: requires both principals to be
73
- // populated, and any failure is logged and swallowed (fail-open —
74
- // delegation tracking is for audit, not enforcement).
75
- await (0, argus_1.writeUserDelegatesAgent)(client);
76
- if (decision.mode !== "disabled") {
77
- return;
78
- }
79
- const modelAttr = ctx.model ? { model: ctx.model } : {};
80
- const resolved = (0, argus_1.resolveConfig)();
81
- if (resolved.auditOnly) {
82
- client.logger.info("config.audit_only", {
83
- message: "Audit-only mode enabled. Auth and permission checks are disabled.",
14
+ ? new argus_1.OryAgentClient({ ...clientOrConfig, harness: HARNESS })
15
+ : argus_1.OryAgentClient.fromEnv(HARNESS);
16
+ const subagents = new Map();
17
+ api.registerHook("session_start", createSessionStartHandler(client, deps), {
18
+ name: "ory.session-verify",
19
+ description: "Verify session via Ory Identities",
84
20
  });
85
- client.tracer.record("session.start", "ok", {
86
- attributes: { sessionId: ctx.sessionId, mode: "audit-only", ...modelAttr },
21
+ api.registerHook("before_agent_run", createBeforeAgentRunHandler(client, deps), {
22
+ name: "ory.user-login",
23
+ description: "Block runs when the user login denies",
87
24
  });
88
- return;
89
- }
90
- if (!resolved.projectUrl) {
91
- client.logger.warn("config.not_configured", {
92
- message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
93
- "Run 'npx ory-openclaw configure' to connect to an Ory project.",
25
+ api.registerHook("before_tool_call", createBeforeToolCallHandler(client, subagents), {
26
+ name: "ory.permission-check",
27
+ description: "Check tool permissions via Ory Permissions",
94
28
  });
95
- client.tracer.record("session.start", "skipped", {
96
- attributes: { reason: "not_configured", ...modelAttr },
29
+ api.registerHook("after_tool_call", createAfterToolCallHandler(client), {
30
+ name: "ory.activity-complete",
31
+ description: "Record tool completion activity",
97
32
  });
98
- return;
99
- }
100
- const sessionToken = process.env.ORY_SESSION_TOKEN;
101
- const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
102
- if (sessionToken) {
103
- await verifySessionToken(client, sessionToken, ctx.sessionId, ctx.model);
104
- return;
105
- }
106
- if (oauth2Token) {
107
- await verifyOAuth2Token(client, oauth2Token, ctx.sessionId, ctx.model);
108
- return;
109
- }
110
- client.logger.warn("session.no_credentials", {
111
- message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
112
- "Skipping authentication.",
113
- });
114
- client.tracer.record("session.start", "skipped", {
115
- attributes: { reason: "no_credentials", ...modelAttr },
116
- });
117
- };
118
- }
119
- async function verifySessionToken(client, sessionToken, sessionId, model) {
120
- const modelAttr = model ? { model } : {};
121
- try {
122
- const session = await client.verifySession(sessionToken);
123
- if (!session.active) {
124
- client.logger.warn("session.inactive", {
125
- message: "Ory session is not active. Re-authenticate to enable auth checks.",
33
+ api.registerHook("subagent_spawning", createSubagentSpawningHandler(client, deps, subagents), {
34
+ name: "ory.subagent-register",
35
+ description: "Register Ory identity and write delegation tuple for spawned sub-agent",
126
36
  });
127
- client.tracer.record("session.start", "skipped", {
128
- attributes: { reason: "session_inactive", sessionId, ...modelAttr },
37
+ api.registerHook("subagent_spawned", createSubagentSpawnedHandler(client), {
38
+ name: "ory.subagent-spawned",
39
+ description: "Record sub-agent spawn activity",
129
40
  });
130
- }
131
- else {
132
- client.tracer.record("session.start", "ok", {
133
- attributes: {
134
- sessionId,
135
- identityId: session.identityId,
136
- aal: session.authenticatorAssuranceLevel,
137
- ...modelAttr,
138
- },
41
+ api.registerHook("subagent_ended", createSubagentEndedHandler(client, subagents), {
42
+ name: "ory.subagent-ended",
43
+ description: "Record sub-agent termination activity",
139
44
  });
140
- }
141
- }
142
- catch (err) {
143
- const oryErr = err;
144
- client.logger.warn("session.verify_failed", {
145
- code: isOryError(err) ? oryErr.code : "unknown",
146
- message: err instanceof Error ? err.message : String(err),
147
- });
148
- client.tracer.record("session.start", "error", {
149
- attributes: { error: isOryError(err) ? oryErr.code : "unknown", ...modelAttr },
150
- });
151
- }
45
+ },
46
+ };
152
47
  }
153
- async function verifyOAuth2Token(client, oauth2Token, sessionId, model) {
154
- const modelAttr = model ? { model } : {};
155
- try {
156
- const tokenInfo = await client.introspectToken(oauth2Token);
157
- if (!tokenInfo.active) {
158
- client.logger.warn("oauth2.token_inactive", {
159
- message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
160
- });
161
- client.tracer.record("session.start", "skipped", {
162
- attributes: { reason: "token_inactive", sessionId, ...modelAttr },
163
- });
164
- return;
165
- }
166
- client.logger.info("oauth2.session_authenticated", {
167
- clientId: tokenInfo.clientId,
168
- subject: tokenInfo.subject,
169
- });
170
- client.tracer.record("session.start", "ok", {
171
- attributes: {
172
- sessionId,
173
- clientId: tokenInfo.clientId,
174
- subject: tokenInfo.subject,
175
- ...modelAttr,
176
- },
177
- });
178
- }
179
- catch (err) {
180
- const oryErr = err;
181
- client.logger.warn("oauth2.introspect_failed", {
182
- code: isOryError(err) ? oryErr.code : "unknown",
183
- message: err instanceof Error ? err.message : String(err),
48
+ function createSessionStartHandler(client, deps) {
49
+ return (ctx) => (0, argus_1.withHookContext)(client, { sessionId: ctx.sessionId }, async () => {
50
+ client.logger.info("lifecycle.session_start", {
51
+ sessionId: ctx.sessionId,
52
+ model: ctx.model,
184
53
  });
185
- client.tracer.record("session.start", "error", {
186
- attributes: { error: isOryError(err) ? oryErr.code : "unknown", ...modelAttr },
54
+ await (0, argus_1.sessionStart)(client, {
55
+ harness: HARNESS,
56
+ binName: "ory-openclaw",
57
+ userLogin: deps.userLogin,
58
+ agentGate: deps.agentGate,
59
+ activityAttributes: { model: ctx.model },
187
60
  });
188
- }
61
+ });
189
62
  }
190
- // ─── before_tool_call ──────────────────────────────────────────────
191
- function createBeforeToolCallHandler(client) {
192
- return async (ctx) => {
63
+ function createBeforeToolCallHandler(client, subagents) {
64
+ return (ctx) => (0, argus_1.withHookContext)(client, { sessionId: ctx.sessionId }, async () => {
193
65
  client.logger.info("lifecycle.before_tool_call", {
194
66
  toolId: ctx.toolId,
195
67
  toolName: ctx.toolName,
196
68
  sessionId: ctx.sessionId,
197
69
  callId: ctx.callId,
198
70
  });
199
- // Set trace context for this tool invocation
200
- client.tracer.setContext({
201
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionId),
202
- sessionId: ctx.sessionId,
71
+ const result = await (0, argus_1.gate)(client, {
72
+ harness: HARNESS,
73
+ toolName: ctx.toolId,
74
+ toolArgs: ctx.args,
75
+ subjectFallback: ctx.sessionId ? `session:${ctx.sessionId}` : "agent:openclaw",
76
+ canBlock: true,
77
+ mcpTool: (0, argus_1.parseMcpToolGeneric)(ctx.toolId) ?? undefined,
78
+ principals: openClawSubagentPrincipals(ctx, subagents),
203
79
  });
204
- const inputSummary = (0, argus_1.summarizeToolInput)(ctx.toolId, ctx.args);
205
- // In audit-only mode, log the invocation but skip permission checks
206
- if ((0, argus_1.resolveConfig)().auditOnly) {
207
- client.tracer.record("tool.invoke", "ok", {
208
- attributes: { toolName: ctx.toolId, ...inputSummary },
209
- });
210
- return;
211
- }
212
- // If Ory is not configured, pass through
213
- if (!(0, argus_1.resolveConfig)().projectUrl) {
214
- client.tracer.record("tool.invoke", "skipped", {
215
- attributes: { toolName: ctx.toolId, reason: "not_configured", ...inputSummary },
216
- });
217
- return;
218
- }
219
- const subject = (0, argus_1.resolveUserSubject)(client, ctx.sessionId ? `session:${ctx.sessionId}` : "agent:openclaw");
220
- const subjectId = (0, argus_1.subjectLabel)(subject);
221
- const mcpTool = (0, argus_1.parseMcpToolGeneric)(ctx.toolId);
222
- try {
223
- if (mcpTool) {
224
- const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
225
- subject,
226
- spanAttributes: { toolName: ctx.toolId },
227
- });
228
- const mcpAttrs = {
229
- toolName: ctx.toolId,
230
- mcpServer: mcpTool.serverName,
231
- mcpTool: mcpTool.toolName,
232
- ...inputSummary,
233
- };
234
- const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
235
- object: mcpTool.serverName,
236
- relation: "use",
237
- subjectId,
238
- ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
239
- spanAttributes: mcpAttrs,
240
- });
241
- const decisionAttrs = decision.spanAttributes;
242
- if (decision.kind === "allow") {
243
- client.tracer.record("tool.invoke", "ok", {
244
- attributes: { ...mcpAttrs, ...decisionAttrs },
245
- });
246
- return;
247
- }
248
- if (decision.kind === "observe") {
249
- client.tracer.record("tool.block", "denied", {
250
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
251
- });
252
- client.tracer.record("tool.invoke", "ok", {
253
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, observed: true },
254
- });
255
- return;
256
- }
257
- client.tracer.record("tool.block", "denied", {
258
- attributes: { ...mcpAttrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
259
- });
260
- const blockReason = (0, argus_1.formatDenialMessage)({
261
- tool: ctx.toolId,
262
- subjectId,
263
- mcp: mcpTool,
264
- });
265
- client.logger.warn("tool.denied", {
266
- toolId: ctx.toolId,
267
- subjectId,
268
- message: blockReason,
269
- });
270
- return { block: true, blockReason };
271
- }
272
- const namespace = resolveNamespace();
273
- const outcome = await (0, argus_1.gateToolCall)(client, {
274
- harness: "openclaw",
275
- toolName: ctx.toolId,
276
- check: { namespace, object: ctx.toolId, relation: "use", ...subject },
277
- spanAttributes: { toolName: ctx.toolId },
278
- });
279
- // Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
280
- // the user.interaction span is already recorded; pass through.
281
- if (outcome.kind === "interactive") {
282
- return;
283
- }
284
- const decision = outcome;
285
- if (decision.kind === "fail_open") {
286
- handlePermissionError(decision.error, ctx.toolId, client);
287
- return;
288
- }
289
- const attrs = { toolName: ctx.toolId, ...inputSummary };
290
- const decisionAttrs = decision.spanAttributes;
291
- if (decision.kind === "allow") {
292
- client.tracer.record("tool.invoke", "ok", {
293
- attributes: { ...attrs, ...decisionAttrs, allowed: true },
294
- });
295
- return;
296
- }
297
- if (decision.kind === "observe") {
298
- client.tracer.record("tool.block", "denied", {
299
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
300
- });
301
- client.tracer.record("tool.invoke", "ok", {
302
- attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
303
- });
304
- return;
305
- }
306
- client.tracer.record("tool.block", "denied", {
307
- attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
308
- });
309
- const blockReason = (0, argus_1.formatDenialMessage)({
310
- tool: ctx.toolId,
311
- subjectId,
312
- namespace,
313
- });
314
- client.logger.warn("tool.denied", {
315
- toolId: ctx.toolId,
316
- subjectId,
317
- message: blockReason,
318
- });
319
- return { block: true, blockReason };
320
- }
321
- catch (err) {
322
- handlePermissionError(err, ctx.toolId, client);
323
- // Fail open — allow the tool call
80
+ if (result.blocked) {
81
+ return { block: true, blockReason: result.denialMessage };
324
82
  }
83
+ });
84
+ }
85
+ function openClawSubagentPrincipals(ctx, subagents) {
86
+ const childSession = ctx.sessionKey ?? ctx.sessionId;
87
+ const subagent = subagents.get(childSession);
88
+ if (!subagent)
89
+ return undefined;
90
+ return {
91
+ subAgentClientId: subagent.clientId,
92
+ subAgentType: subagent.type,
93
+ perSpawnId: childSession,
94
+ sessionId: subagent.parentSession,
325
95
  };
326
96
  }
327
- // ─── after_tool_call ───────────────────────────────────────────────
328
97
  function createAfterToolCallHandler(client) {
329
- return async (ctx) => {
98
+ return (ctx) => (0, argus_1.withHookContext)(client, { sessionId: ctx.sessionId }, async () => {
330
99
  client.logger.info("lifecycle.after_tool_call", {
331
100
  toolId: ctx.toolId,
332
101
  toolName: ctx.toolName,
@@ -335,71 +104,40 @@ function createAfterToolCallHandler(client) {
335
104
  durationMs: ctx.durationMs,
336
105
  hasError: !!ctx.error,
337
106
  });
338
- // Set trace context for this tool completion
339
- client.tracer.setContext({
340
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionId),
341
- sessionId: ctx.sessionId,
342
- });
343
- const status = ctx.error ? "error" : "ok";
344
- client.tracer.record("tool.complete", status, {
345
- attributes: {
107
+ if (ctx.error)
108
+ client.logger.debug("lifecycle.after_tool_call.error", { error: ctx.error });
109
+ if (ctx.error) {
110
+ (0, argus_1.fail)(client, {
346
111
  toolName: ctx.toolId,
347
- ...(0, argus_1.summarizeToolInput)(ctx.toolId, ctx.args),
348
- ...(0, argus_1.summarizeToolOutput)(ctx.toolId, ctx.result),
349
- durationMs: ctx.durationMs,
350
- ...(ctx.error ? { error: ctx.error } : {}),
351
- },
352
- });
353
- };
354
- }
355
- // ─── before_agent_run ──────────────────────────────────────────────
356
- //
357
- // Real per-turn enforcement gate. `session_start` cannot hard-block in
358
- // OpenClaw (handler returns void), so the user login runs there in
359
- // advisory mode. `before_agent_run` is the first hook OpenClaw exposes
360
- // that can return a block decision on the user prompt — we re-run the
361
- // login here so denied users can't drive the agent. `ensureUserAuthenticated`
362
- // reuses persisted tokens, so per-turn calls are non-interactive after
363
- // the first session.
364
- function createBeforeAgentRunHandler(client, deps) {
365
- return async (ctx) => {
366
- client.tracer.setContext({
367
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionKey ?? ctx.sessionId ?? "openclaw"),
368
- sessionId: ctx.sessionKey ?? ctx.sessionId,
369
- });
370
- const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
371
- const decision = await userGate(client, {
372
- binName: "ory-openclaw",
373
- harness: "openclaw",
374
- allowBlock: true,
375
- });
376
- if (!decision.proceed) {
377
- client.logger.warn("before_agent_run.blocked", {
378
- reason: decision.reason,
379
- mode: decision.mode,
112
+ input: ctx.args,
113
+ error: ctx.error,
114
+ extraActivityAttributes: { durationMs: ctx.durationMs },
380
115
  });
381
- return {
382
- outcome: "block",
383
- reason: decision.reason ?? "User authentication required",
384
- message: decision.reason,
385
- };
386
116
  }
387
- // Disabled gate or successful auth: let the run proceed.
388
- return;
389
- };
117
+ else {
118
+ (0, argus_1.complete)(client, {
119
+ toolName: ctx.toolId,
120
+ input: ctx.args,
121
+ output: ctx.result,
122
+ extraActivityAttributes: { durationMs: ctx.durationMs },
123
+ });
124
+ }
125
+ });
390
126
  }
391
- // ─── subagent_spawning ─────────────────────────────────────────────
392
- //
393
- // Fires before a child agent run starts. We resolve a distinct OAuth2
394
- // identity for the sub-agent (DCR) and write the agent→sub-agent
395
- // delegation tuple. Best-effort: failures here never abort the child run.
396
- function createSubagentSpawningHandler(client, deps) {
397
- return async (ctx) => {
127
+ // OpenClaw refreshes the interactive user credential before every model turn.
128
+ function createBeforeAgentRunHandler(client, deps) {
129
+ return (ctx) => (0, argus_1.withHookContext)(client, {
130
+ sessionId: ctx.sessionKey ?? ctx.sessionId,
131
+ }, async () => {
132
+ const userLogin = deps.userLogin ?? argus_1.ensureUserAuthenticated;
133
+ await userLogin(client, { binName: "ory-openclaw", harness: HARNESS });
134
+ });
135
+ }
136
+ function createSubagentSpawningHandler(client, deps, subagents) {
137
+ return (ctx) => (0, argus_1.withHookContext)(client, {
138
+ sessionId: ctx.sessionKey,
139
+ }, async () => {
398
140
  const subAgentType = ctx.agentId ?? ctx.label;
399
- client.tracer.setContext({
400
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionKey ?? ctx.childSessionKey ?? "openclaw"),
401
- sessionId: ctx.sessionKey,
402
- });
403
141
  client.logger.info("lifecycle.subagent_spawning", {
404
142
  agentId: ctx.agentId,
405
143
  label: ctx.label,
@@ -407,7 +145,7 @@ function createSubagentSpawningHandler(client, deps) {
407
145
  mode: ctx.mode,
408
146
  requester: ctx.requester,
409
147
  });
410
- client.tracer.record("subagent.start", "ok", {
148
+ client.logger.activity("subagent.prepare", "ok", {
411
149
  attributes: {
412
150
  subAgentType,
413
151
  agentId: ctx.agentId,
@@ -418,44 +156,40 @@ function createSubagentSpawningHandler(client, deps) {
418
156
  });
419
157
  if (!subAgentType) {
420
158
  client.logger.debug("subagent.no_type", {
421
- message: "subagent_spawning event without agentId/label skipping DCR.",
159
+ message: "subagent_spawning event without agentId/label; skipping DCR.",
422
160
  });
423
161
  return;
424
162
  }
425
- const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
426
- let identity;
427
- try {
428
- identity = await subAgentGate(client, {
429
- subAgentType,
430
- projectUrl: (0, argus_1.resolveConfig)().projectUrl,
431
- harness: "openclaw",
432
- });
433
- }
434
- catch (err) {
435
- client.logger.warn("subagent.identity.failed", {
436
- subAgentType,
437
- message: err instanceof Error ? err.message : String(err),
438
- });
439
- return;
163
+ await (0, argus_1.registerSubagent)(client, {
164
+ harness: HARNESS,
165
+ subAgentType,
166
+ subAgentGate: deps.subAgentGate,
167
+ perSpawnId: ctx.childSessionKey,
168
+ });
169
+ if (ctx.childSessionKey) {
170
+ const credential = ctx.sessionKey
171
+ ? (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, ctx.sessionKey, subAgentType)
172
+ : (0, argus_1.loadSubAgentDynamicCredentials)(HARNESS, subAgentType);
173
+ if (credential) {
174
+ subagents.set(ctx.childSessionKey, {
175
+ clientId: credential.clientId,
176
+ type: subAgentType,
177
+ parentSession: ctx.sessionKey,
178
+ });
179
+ }
440
180
  }
441
- if (identity.kind !== "dynamic" || !identity.subject)
442
- return;
443
- await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
444
- };
181
+ });
445
182
  }
446
- // ─── subagent_spawned ──────────────────────────────────────────────
447
183
  function createSubagentSpawnedHandler(client) {
448
- return async (ctx) => {
449
- client.tracer.setContext({
450
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionKey ?? ctx.childSessionKey ?? "openclaw"),
451
- sessionId: ctx.sessionKey,
452
- });
184
+ return (ctx) => (0, argus_1.withHookContext)(client, {
185
+ sessionId: ctx.sessionKey,
186
+ }, async () => {
453
187
  client.logger.info("lifecycle.subagent_spawned", {
454
188
  agentId: ctx.agentId,
455
189
  childRunId: ctx.childRunId,
456
190
  childSessionKey: ctx.childSessionKey,
457
191
  });
458
- client.tracer.record("subagent.start", "ok", {
192
+ client.logger.activity("subagent.start", "ok", {
459
193
  attributes: {
460
194
  subAgentType: ctx.agentId ?? ctx.label,
461
195
  agentId: ctx.agentId,
@@ -463,21 +197,21 @@ function createSubagentSpawnedHandler(client) {
463
197
  stage: "spawned",
464
198
  },
465
199
  });
466
- };
200
+ });
467
201
  }
468
- // ─── subagent_ended ────────────────────────────────────────────────
469
- function createSubagentEndedHandler(client) {
470
- return async (ctx) => {
471
- client.tracer.setContext({
472
- traceId: (0, argus_1.deriveTraceId)(ctx.sessionKey ?? ctx.childSessionKey ?? "openclaw"),
473
- sessionId: ctx.sessionKey,
474
- });
202
+ function createSubagentEndedHandler(client, subagents) {
203
+ return (ctx) => (0, argus_1.withHookContext)(client, {
204
+ sessionId: ctx.sessionKey,
205
+ }, async () => {
206
+ const childSessionKey = ctx.targetSessionKey ?? ctx.childSessionKey;
207
+ if (childSessionKey)
208
+ subagents.delete(childSessionKey);
475
209
  client.logger.info("lifecycle.subagent_ended", {
476
210
  agentId: ctx.agentId,
477
211
  childRunId: ctx.childRunId,
478
212
  reason: ctx.reason,
479
213
  });
480
- client.tracer.record("subagent.stop", "ok", {
214
+ client.logger.activity("subagent.stop", "ok", {
481
215
  attributes: {
482
216
  subAgentType: ctx.agentId ?? ctx.label,
483
217
  agentId: ctx.agentId,
@@ -485,41 +219,5 @@ function createSubagentEndedHandler(client) {
485
219
  reason: ctx.reason,
486
220
  },
487
221
  });
488
- };
489
- }
490
- // ─── Helpers ───────────────────────────────────────────────────────
491
- function resolveNamespace() {
492
- return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
493
- }
494
- function isOryError(err) {
495
- return (typeof err === "object" &&
496
- err !== null &&
497
- "code" in err &&
498
- "message" in err);
499
- }
500
- function handlePermissionError(err, toolId, client) {
501
- if (isOryError(err)) {
502
- if (err.code === "network_error" || err.code === "rate_limited") {
503
- client.logger.warn("permission.fallback", {
504
- toolId,
505
- code: err.code,
506
- message: "Failing open",
507
- });
508
- return;
509
- }
510
- client.logger.error("permission.check.error", {
511
- toolId,
512
- code: err.code,
513
- message: err.message,
514
- });
515
- }
516
- else {
517
- client.logger.error("permission.check.error", {
518
- toolId,
519
- error: err instanceof Error ? err.message : String(err),
520
- });
521
- }
522
- // Fail-open is log-and-allow: no tool.invoke span, matching the core
523
- // gate() vocabulary. The errored permission.check span carries the audit
524
- // trail for the failed check itself.
222
+ });
525
223
  }