@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/README.md +67 -102
- package/dist/cli/main.d.ts +0 -1
- package/dist/cli/main.js +51 -13
- package/dist/cli/plugin-config.d.ts +12 -8
- package/dist/cli/plugin-config.js +22 -16
- package/dist/cli/setup.js +24 -5
- package/dist/plugin.d.ts +1 -19
- package/dist/plugin.js +127 -429
- package/dist/types.d.ts +8 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +9 -8
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
96
|
-
|
|
29
|
+
api.registerHook("after_tool_call", createAfterToolCallHandler(client), {
|
|
30
|
+
name: "ory.activity-complete",
|
|
31
|
+
description: "Record tool completion activity",
|
|
97
32
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
128
|
-
|
|
37
|
+
api.registerHook("subagent_spawned", createSubagentSpawnedHandler(client), {
|
|
38
|
+
name: "ory.subagent-spawned",
|
|
39
|
+
description: "Record sub-agent spawn activity",
|
|
129
40
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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 (
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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.
|
|
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
|
|
159
|
+
message: "subagent_spawning event without agentId/label; skipping DCR.",
|
|
422
160
|
});
|
|
423
161
|
return;
|
|
424
162
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
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
|
|
449
|
-
|
|
450
|
-
|
|
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.
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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.
|
|
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
|
}
|