@ory/openclaw 0.13.8 → 0.14.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 CHANGED
@@ -51,12 +51,14 @@ Run one command. It installs the plugin and walks you through connecting:
51
51
  npx -y -p @ory/openclaw ory-openclaw install
52
52
  ```
53
53
 
54
- OpenClaw loads plugins that run inside its own process, so the installer registers the plugin in `.openclaw/config.json` (along with its skills and the Ory MCP server), then walks you through connecting. You'll be asked how you want to connect — **press Enter for the default**:
54
+ OpenClaw loads plugins that run inside its own process, so the installer registers the plugin in `.openclaw/config.json` (along with its skills and the Ory MCP server), then opens a guided setup **in your browser** where you pick how to connect — the default takes just a click:
55
55
 
56
56
  - **Ory Network** *(default)* — sign in, or create a free account, in your browser. The project, keys, permissions, and login are all set up for you. Nothing to configure by hand.
57
57
  - **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
58
58
  - **Audit-only** — skip Ory entirely and just log what OpenClaw does.
59
59
 
60
+ > No browser available (CI, SSH, headless)? The same walkthrough runs right in your terminal instead — or force it with `--no-web`.
61
+
60
62
  That's it. Confirm everything landed with:
61
63
 
62
64
  ```bash
@@ -99,6 +101,7 @@ If Ory is ever unreachable, the plugin gets out of the way and lets OpenClaw kee
99
101
  Everything the plugin does is observable out of the box — no configuration required:
100
102
 
101
103
  - **Status at a glance.** `npx -y -p @ory/openclaw ory-openclaw status` shows what's configured, who's signed in, how many built-in tools your permissions cover, and the most recent tool-call activity.
104
+ - **Live dashboard.** `npx -y -p @ory/openclaw ory-openclaw dashboard` opens the same picture in your browser — configuration, identities, permission coverage, Ory service health, and the latest tool-call activity, all refreshing live. From there you can flip **enforcement** on or off, toggle **user login**, and use **Change stack** to reconnect to a different Ory — no CLI required.
102
105
  - **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
103
106
 
104
107
  ```bash
@@ -116,7 +119,7 @@ When the watch-mode logs look right, turn on blocking with one command (setup al
116
119
  npx -y -p @ory/openclaw ory-openclaw permissions enforce
117
120
  ```
118
121
 
119
- Now a denied tool is actually blocked and OpenClaw shows why. Go back to watch mode anytime with `permissions observe`. Use `permissions status` to see what's covered and `permissions bootstrap` to (re-)grant the built-in tools — or just ask OpenClaw in chat, e.g. *"grant me use of the execute_command tool."*
122
+ Now a denied tool is actually blocked and OpenClaw shows why. Go back to watch mode anytime with `permissions observe`. Prefer clicking? The dashboard has the same **Enforce** switch — flip it on, or back to watch mode, without touching the CLI. Use `permissions status` to see what's covered and `permissions bootstrap` to (re-)grant the built-in tools — or just ask OpenClaw in chat, e.g. *"grant me use of the execute_command tool."*
120
123
 
121
124
  ## Also: add login to your own app
122
125
 
@@ -131,8 +134,7 @@ The guided setup covers most people. For scripted or CI setups, or to point at a
131
134
  ```bash
132
135
  npx -y -p @ory/openclaw ory-openclaw configure \
133
136
  --project-url https://<slug>.projects.oryapis.com \
134
- --oauth2-client-id <sign-in client id> \
135
- --user-login
137
+ --oauth2-client-id <sign-in client id>
136
138
  ```
137
139
 
138
140
  OpenClaw's own identity registers itself automatically on first run — nothing to create. The `--oauth2-client-id` is the one piece browser sign-in needs; the guided setup makes it for you, or see below to do it by hand. For logging-only with no checks, use `--audit-only`.
@@ -144,7 +146,7 @@ The guided setup normally does this. To do it yourself, create a **public** OAut
144
146
 
145
147
  ```bash
146
148
  ory create oauth2-client --project <project-id> \
147
- --name "ory-agent-plugin" \
149
+ --name "Ory Agent Security · user login (PKCE)" \
148
150
  --grant-type authorization_code,refresh_token \
149
151
  --response-type code \
150
152
  --scope openid,offline_access \
@@ -164,11 +166,12 @@ With nothing configured, the plugin still loads and runs in **pass-through mode*
164
166
  ## Commands
165
167
 
166
168
  ```
167
- ory-openclaw install | uninstall Install/remove; --reconfigure re-runs setup, --no-configure skips it
169
+ ory-openclaw install | uninstall Install/remove; --reconfigure re-runs setup, --no-web forces the terminal wizard, --no-configure skips it
168
170
  ory-openclaw status Show configuration, identities, permission coverage, recent activity
171
+ ory-openclaw dashboard Open the live browser dashboard (status + service health + Change stack)
169
172
  ory-openclaw watch Tail the live trace stream (OTel spans)
170
173
  ory-openclaw permissions <cmd> status | bootstrap | observe (watch) | enforce (block)
171
- ory-openclaw configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --user-login, --audit-only)
174
+ ory-openclaw configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --audit-only)
172
175
  ory-openclaw agent <status|unregister> Manage OpenClaw's own auto-created identity
173
176
  ory-openclaw local <up|down|status|…> Run / manage a local Ory in Docker
174
177
  ory-openclaw watch [<trace-file>] Live-tail the trace of decisions as they happen
package/dist/cli/main.js CHANGED
@@ -99,6 +99,12 @@ function main() {
99
99
  case "watch":
100
100
  (0, argus_1.runWatchCommand)("openclaw", args);
101
101
  break;
102
+ case "dashboard":
103
+ (0, argus_1.runDashboardCommand)("ory-openclaw", "openclaw", args).then((code) => process.exit(code), (err) => {
104
+ console.error(err.message ?? err);
105
+ process.exit(1);
106
+ });
107
+ break;
102
108
  case "version":
103
109
  case "--version":
104
110
  case "-v":
@@ -190,6 +196,7 @@ Commands:
190
196
  agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
191
197
  permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
192
198
  status [--project-dir <path>] Show plugin configuration and status
199
+ dashboard Open the Ory Agent dashboard (system health + configuration)
193
200
  watch [trace-file] Watch live trace output
194
201
  version Show version and the ory-agent-plugins build commit
195
202
  local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
package/dist/plugin.d.ts CHANGED
@@ -17,10 +17,9 @@ export interface CreateOryPluginDeps {
17
17
  * makes permission checks enforceable, unlike harnesses that can only
18
18
  * log denials.
19
19
  *
20
- * The `session_start` hook returns `Promise<void>` so user login is
21
- * advisory at session start: the flow still runs to emit the audit span,
22
- * refresh tokens, and prompt for the project URL when interactive, but
23
- * cannot hard-block the session.
20
+ * The user login runs every session but never blocks: it emits the audit
21
+ * span, refreshes tokens, and prompts for the project URL when interactive.
22
+ * Enforcement is governed solely by permissionMode at tool-call time.
24
23
  */
25
24
  export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
26
25
  projectUrl: string;
package/dist/plugin.js CHANGED
@@ -11,10 +11,9 @@ const argus_1 = require("@ory/argus");
11
11
  * makes permission checks enforceable, unlike harnesses that can only
12
12
  * log denials.
13
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.
14
+ * The user login runs every session but never blocks: it emits the audit
15
+ * span, refreshes tokens, and prompts for the project URL when interactive.
16
+ * Enforcement is governed solely by permissionMode at tool-call time.
18
17
  */
19
18
  function createOryPlugin(clientOrConfig, deps = {}) {
20
19
  return {
@@ -51,142 +50,28 @@ function createSessionStartHandler(client, deps) {
51
50
  traceId: (0, argus_1.deriveTraceId)(ctx.sessionId),
52
51
  sessionId: ctx.sessionId,
53
52
  });
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.
53
+ // Run the user login every session. Authentication is non-blocking:
54
+ // the flow emits the user.auth audit span, refreshes tokens, and
55
+ // prompts for the project URL when interactive, but never prevents the
56
+ // session from starting enforcement is governed solely by
57
+ // permissionMode at tool-call time.
60
58
  const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
61
- const decision = await userGate(client, {
59
+ await userGate(client, {
62
60
  binName: "ory-openclaw",
63
61
  harness: "openclaw",
64
- allowBlock: false,
65
62
  });
66
63
  // Resolve the agent identity (machine credentials). Never blocks;
67
64
  // attaches the agent's bearer token to outgoing Ory API calls.
68
65
  const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
69
66
  await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "openclaw" });
70
67
  // Record the user→agent delegation so the audit trail captures that
71
- // this user authorized this agent for this session. Best-effort:
72
- // requires both principals to be populated, and any failure is logged
73
- // and swallowed (fail-open — delegation tracking is for audit, not
74
- // enforcement).
75
- await recordUserDelegatesAgent(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.",
84
- });
85
- client.tracer.record("session.start", "ok", {
86
- attributes: { sessionId: ctx.sessionId, mode: "audit-only", ...modelAttr },
87
- });
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.",
94
- });
95
- client.tracer.record("session.start", "skipped", {
96
- attributes: { reason: "not_configured", ...modelAttr },
97
- });
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
- });
68
+ // this user authorized this agent for this session. Best-effort and
69
+ // written at most once per install: requires both principals to be
70
+ // populated, and any failure is logged and swallowed (fail-open —
71
+ // delegation tracking is for audit, not enforcement).
72
+ await (0, argus_1.writeUserDelegatesAgent)(client);
117
73
  };
118
74
  }
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.",
126
- });
127
- client.tracer.record("session.start", "skipped", {
128
- attributes: { reason: "session_inactive", sessionId, ...modelAttr },
129
- });
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
- },
139
- });
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
- }
152
- }
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),
184
- });
185
- client.tracer.record("session.start", "error", {
186
- attributes: { error: isOryError(err) ? oryErr.code : "unknown", ...modelAttr },
187
- });
188
- }
189
- }
190
75
  // ─── before_tool_call ──────────────────────────────────────────────
191
76
  function createBeforeToolCallHandler(client) {
192
77
  return async (ctx) => {
@@ -354,37 +239,25 @@ function createAfterToolCallHandler(client) {
354
239
  }
355
240
  // ─── before_agent_run ──────────────────────────────────────────────
356
241
  //
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.
242
+ // Per-turn user login refresh. The user login runs at `session_start` and
243
+ // again here; it never blocks (authentication is non-blocking enforcement
244
+ // is governed by permissionMode at tool-call time). `ensureUserAuthenticated`
245
+ // reuses persisted tokens, so per-turn calls are non-interactive after the
246
+ // first session.
364
247
  function createBeforeAgentRunHandler(client, deps) {
365
248
  return async (ctx) => {
366
249
  client.tracer.setContext({
367
250
  traceId: (0, argus_1.deriveTraceId)(ctx.sessionKey ?? ctx.sessionId ?? "openclaw"),
368
251
  sessionId: ctx.sessionKey ?? ctx.sessionId,
369
252
  });
253
+ // Per-turn user-login refresh. Non-blocking: it reuses persisted tokens
254
+ // (non-interactive after the first session) and never stops the run —
255
+ // enforcement is governed by permissionMode at tool-call time.
370
256
  const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
371
- const decision = await userGate(client, {
257
+ await userGate(client, {
372
258
  binName: "ory-openclaw",
373
259
  harness: "openclaw",
374
- allowBlock: true,
375
260
  });
376
- if (!decision.proceed) {
377
- client.logger.warn("before_agent_run.blocked", {
378
- reason: decision.reason,
379
- mode: decision.mode,
380
- });
381
- return {
382
- outcome: "block",
383
- reason: decision.reason ?? "User authentication required",
384
- message: decision.reason,
385
- };
386
- }
387
- // Disabled gate or successful auth: let the run proceed.
388
261
  return;
389
262
  };
390
263
  }
@@ -440,30 +313,7 @@ function createSubagentSpawningHandler(client, deps) {
440
313
  }
441
314
  if (identity.kind !== "dynamic" || !identity.subject)
442
315
  return;
443
- const agent = client.agentPrincipal.subject;
444
- if (!agent) {
445
- client.logger.debug("delegation.skip", {
446
- reason: "agent principal not populated",
447
- subAgentType,
448
- });
449
- return;
450
- }
451
- try {
452
- await client.createRelationship({
453
- namespace: resolveNamespace(),
454
- object: `subagent:${identity.subject}`,
455
- relation: "delegate",
456
- subjectId: `agent:${agent}`,
457
- }, { spanAttributes: { delegation: "agent-to-subagent", subAgentType } });
458
- }
459
- catch (err) {
460
- const oryErr = err;
461
- client.logger.warn("delegation.agent_to_subagent.failed", {
462
- subAgentType,
463
- code: oryErr.code,
464
- message: oryErr.message,
465
- });
466
- }
316
+ await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
467
317
  };
468
318
  }
469
319
  // ─── subagent_spawned ──────────────────────────────────────────────
@@ -514,40 +364,6 @@ function createSubagentEndedHandler(client) {
514
364
  function resolveNamespace() {
515
365
  return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
516
366
  }
517
- /**
518
- * Write the user→agent delegation tuple. Idempotent and fail-open:
519
- * requires both principal subjects to be populated; any error (including
520
- * an unconfigured projectUrl, which manifests as a network_error) is
521
- * logged and swallowed. Purely audit-trail data — does not affect
522
- * enforcement.
523
- */
524
- async function recordUserDelegatesAgent(client) {
525
- const user = client.userPrincipal.subject;
526
- const agent = client.agentPrincipal.subject;
527
- if (!user || !agent) {
528
- client.logger.debug("delegation.skip", {
529
- reason: "missing principal",
530
- hasUser: !!user,
531
- hasAgent: !!agent,
532
- });
533
- return;
534
- }
535
- try {
536
- await client.createRelationship({
537
- namespace: resolveNamespace(),
538
- object: `agent:${agent}`,
539
- relation: "delegate",
540
- subjectId: `user:${user}`,
541
- }, { spanAttributes: { delegation: "user-to-agent" } });
542
- }
543
- catch (err) {
544
- const oryErr = err;
545
- client.logger.warn("delegation.user_to_agent.failed", {
546
- code: oryErr.code,
547
- message: oryErr.message,
548
- });
549
- }
550
- }
551
367
  function isOryError(err) {
552
368
  return (typeof err === "object" &&
553
369
  err !== null &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/openclaw",
3
- "version": "0.13.8",
3
+ "version": "0.14.0",
4
4
  "description": "Ory plugin for OpenClaw: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",
@@ -66,7 +66,7 @@
66
66
  ],
67
67
  "dependencies": {
68
68
  "reo-census": "^1.2.8",
69
- "@ory/argus": "0.13.8"
69
+ "@ory/argus": "0.14.0"
70
70
  },
71
71
  "engines": {
72
72
  "node": ">=22"