@ory/claude-code 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 +10 -7
- package/dist/cli/main.js +7 -0
- package/dist/cli/setup.js +41 -3
- package/dist/handlers.js +12 -145
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -21,12 +21,14 @@ Run one command. It installs the plugin and walks you through connecting:
|
|
|
21
21
|
npx -y -p @ory/claude-code ory-claude install
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
It opens a guided setup **in your browser** where you pick how to connect — the default takes just a click:
|
|
25
25
|
|
|
26
26
|
- **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.
|
|
27
27
|
- **Local** — run a complete Ory on your laptop with Docker. No account, no signup, no keys. Great for trying it out.
|
|
28
28
|
- **Audit-only** — skip Ory entirely and just log what Claude does.
|
|
29
29
|
|
|
30
|
+
> No browser available (CI, SSH, headless)? The same walkthrough runs right in your terminal instead — or force it with `--no-web`.
|
|
31
|
+
|
|
30
32
|
That's it. Confirm everything landed with:
|
|
31
33
|
|
|
32
34
|
```bash
|
|
@@ -52,6 +54,7 @@ If Ory is ever unreachable, the plugin gets out of the way and lets Claude keep
|
|
|
52
54
|
Everything the plugin does is observable out of the box — no configuration required:
|
|
53
55
|
|
|
54
56
|
- **Status at a glance.** `npx -y -p @ory/claude-code ory-claude status` shows what's configured, who's signed in, how many built-in tools your permissions cover, and the most recent tool-call activity.
|
|
57
|
+
- **Live dashboard.** `npx -y -p @ory/claude-code ory-claude 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.
|
|
55
58
|
- **Live traces.** Every tool call is recorded as an OpenTelemetry-style span. Watch them stream as the agent works:
|
|
56
59
|
|
|
57
60
|
```bash
|
|
@@ -69,7 +72,7 @@ When the watch-mode logs look right, turn on blocking with one command (setup al
|
|
|
69
72
|
npx -y -p @ory/claude-code ory-claude permissions enforce
|
|
70
73
|
```
|
|
71
74
|
|
|
72
|
-
Now a denied tool is actually blocked and Claude 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 Claude in chat, e.g. *"grant me use of the Bash tool."*
|
|
75
|
+
Now a denied tool is actually blocked and Claude 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 Claude in chat, e.g. *"grant me use of the Bash tool."*
|
|
73
76
|
|
|
74
77
|
## Also: add login to your own app
|
|
75
78
|
|
|
@@ -84,8 +87,7 @@ The guided setup covers most people. For scripted or CI setups, or to point at a
|
|
|
84
87
|
```bash
|
|
85
88
|
npx -y -p @ory/claude-code ory-claude configure \
|
|
86
89
|
--project-url https://<slug>.projects.oryapis.com \
|
|
87
|
-
--oauth2-client-id <login client id>
|
|
88
|
-
--user-login
|
|
90
|
+
--oauth2-client-id <login client id>
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Claude'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`.
|
|
@@ -97,7 +99,7 @@ The guided setup normally does this. To do it yourself, create a **public** OAut
|
|
|
97
99
|
|
|
98
100
|
```bash
|
|
99
101
|
ory create oauth2-client --project <project-id> \
|
|
100
|
-
--name "
|
|
102
|
+
--name "Ory Agent Security · user login (PKCE)" \
|
|
101
103
|
--grant-type authorization_code,refresh_token \
|
|
102
104
|
--response-type code \
|
|
103
105
|
--scope openid,offline_access \
|
|
@@ -117,11 +119,12 @@ With nothing configured, the plugin still loads and runs in **pass-through mode*
|
|
|
117
119
|
## Commands
|
|
118
120
|
|
|
119
121
|
```
|
|
120
|
-
ory-claude install | uninstall Install/remove; --reconfigure re-runs setup, --no-configure skips it
|
|
122
|
+
ory-claude install | uninstall Install/remove; --reconfigure re-runs setup, --no-web forces the terminal wizard, --no-configure skips it
|
|
121
123
|
ory-claude status Show configuration, identities, permission coverage, recent activity
|
|
124
|
+
ory-claude dashboard Open the live browser dashboard (status + service health + Change stack)
|
|
122
125
|
ory-claude watch Tail the live trace stream (OTel spans)
|
|
123
126
|
ory-claude permissions <cmd> status | bootstrap | observe (watch) | enforce (block)
|
|
124
|
-
ory-claude configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --
|
|
127
|
+
ory-claude configure <flags> Point at a project by hand (--project-url, --oauth2-client-id, --audit-only)
|
|
125
128
|
ory-claude agent <status|unregister> Manage Claude's own auto-created identity
|
|
126
129
|
ory-claude local <up|down|status|…> Run / manage a local Ory in Docker
|
|
127
130
|
```
|
package/dist/cli/main.js
CHANGED
|
@@ -98,6 +98,12 @@ function main() {
|
|
|
98
98
|
case "watch":
|
|
99
99
|
(0, argus_1.runWatchCommand)("claude-code", args);
|
|
100
100
|
break;
|
|
101
|
+
case "dashboard":
|
|
102
|
+
(0, argus_1.runDashboardCommand)("ory-claude", "claude-code", args).then((code) => process.exit(code), (err) => {
|
|
103
|
+
console.error(err.message ?? err);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
101
107
|
case "version":
|
|
102
108
|
case "--version":
|
|
103
109
|
case "-v":
|
|
@@ -155,6 +161,7 @@ Commands:
|
|
|
155
161
|
agent <cmd> Manage the agent's OAuth2 (DCR) identity (status, unregister)
|
|
156
162
|
permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
|
|
157
163
|
status Show plugin status and configuration
|
|
164
|
+
dashboard Open a live web dashboard (stack health + configuration)
|
|
158
165
|
watch [trace-file] Tail the trace stream (OTel spans) live
|
|
159
166
|
version Show version and the ory-agent-plugins build commit
|
|
160
167
|
local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
|
package/dist/cli/setup.js
CHANGED
|
@@ -108,8 +108,18 @@ const RENDER_OPTS = {
|
|
|
108
108
|
* fetches everything directly from the GitHub marketplace.
|
|
109
109
|
*/
|
|
110
110
|
const PERSISTENT_DIR = (0, argus_1.getHarnessDataDir)("claude-code");
|
|
111
|
-
/**
|
|
112
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Hook command for a `--from-source` install fetched via npx. The plugin lives
|
|
113
|
+
* in an ephemeral `_npx` cache dir (cleaned between runs), so the hook must
|
|
114
|
+
* re-resolve `@ory/claude-code` at runtime rather than point at a soon-to-vanish
|
|
115
|
+
* path. `--from-source` is dev-only and always paired with the local Verdaccio
|
|
116
|
+
* registry, so the hook's npx is pinned to it (with `--min-release-age=0` so a
|
|
117
|
+
* just-published dev version isn't filtered out). Without the explicit
|
|
118
|
+
* `--registry`, a normally-launched `claude` (i.e. not via the dev launcher,
|
|
119
|
+
* which sets `npm_config_registry`) would resolve `@ory/claude-code` from
|
|
120
|
+
* npmjs.org and run the last *published* release instead of your local build.
|
|
121
|
+
*/
|
|
122
|
+
const HOOK_CMD_FROM_SOURCE_REMOTE = `npx -y --registry ${argus_1.REGISTRY_URL} --min-release-age=0 -p @ory/claude-code ory-claude-hook`;
|
|
113
123
|
/**
|
|
114
124
|
* Detect if we're running from a temporary npx/pnpm-dlx cache directory
|
|
115
125
|
* rather than a proper local or global npm installation. Only consulted on
|
|
@@ -145,6 +155,25 @@ function assemblePluginDir(hookCommand) {
|
|
|
145
155
|
});
|
|
146
156
|
}
|
|
147
157
|
}
|
|
158
|
+
// Stamp the assembled plugin manifest with this package's version. The
|
|
159
|
+
// committed manifest carries a frozen placeholder version, and the `claude`
|
|
160
|
+
// CLI keys its cached component inventory (commands, skills) on the plugin
|
|
161
|
+
// version — so without this, a release that adds a command would keep the
|
|
162
|
+
// stale command set until the user manually removed the plugin. Tracking the
|
|
163
|
+
// package version means every release advertises a new plugin version and
|
|
164
|
+
// Claude Code refreshes.
|
|
165
|
+
const manifestPath = path.join(PERSISTENT_DIR, ".claude-plugin", "plugin.json");
|
|
166
|
+
try {
|
|
167
|
+
const pkgVersion = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
168
|
+
if (pkgVersion && fs.existsSync(manifestPath)) {
|
|
169
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
170
|
+
manifest.version = pkgVersion;
|
|
171
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
/* best-effort — a missing/unreadable manifest just keeps its default version */
|
|
176
|
+
}
|
|
148
177
|
// Skills — materialized fresh from the shared core templates.
|
|
149
178
|
const skillsDir = path.join(PERSISTENT_DIR, "skills");
|
|
150
179
|
fs.rmSync(skillsDir, { recursive: true, force: true });
|
|
@@ -318,10 +347,19 @@ function installFromSource(scope) {
|
|
|
318
347
|
const remote = isRemoteContext();
|
|
319
348
|
console.log("Assembling Ory plugin (skills, commands, hooks, MCP) at:");
|
|
320
349
|
console.log(` ${PERSISTENT_DIR}`);
|
|
321
|
-
const pluginRoot = assemblePluginDir(remote ?
|
|
350
|
+
const pluginRoot = assemblePluginDir(remote ? HOOK_CMD_FROM_SOURCE_REMOTE : localHookCommand());
|
|
322
351
|
const marketplaceName = readLocalMarketplaceName();
|
|
323
352
|
const pluginName = readLocalPluginName();
|
|
324
353
|
console.log(`Source: ${pluginName}@${marketplaceName} (from ${pluginRoot})`);
|
|
354
|
+
// Force a clean slate before re-adding. The `claude` CLI caches an
|
|
355
|
+
// already-installed plugin by version and won't re-read its command/skill
|
|
356
|
+
// files on a same-version re-install — so a freshly-assembled command (e.g.
|
|
357
|
+
// `dashboard`) would be missing after a republish. Removing the plugin and
|
|
358
|
+
// its marketplace first guarantees the current command set is picked up on
|
|
359
|
+
// every dev iteration. Best-effort and quiet: a not-yet-installed plugin
|
|
360
|
+
// just no-ops here.
|
|
361
|
+
runClaude(["plugin", "uninstall", pluginName, "--scope", scope]);
|
|
362
|
+
runClaude(["plugin", "marketplace", "remove", marketplaceName]);
|
|
325
363
|
console.log("Adding local Ory plugin marketplace...");
|
|
326
364
|
const addResult = runClaude([
|
|
327
365
|
"plugin",
|
package/dist/handlers.js
CHANGED
|
@@ -61,13 +61,12 @@ async function handleSessionStart(input, client, deps) {
|
|
|
61
61
|
attributes: { model: input.model, source: input.source },
|
|
62
62
|
});
|
|
63
63
|
// Run the user login (interactive PKCE on first session, refresh
|
|
64
|
-
// when needed).
|
|
65
|
-
//
|
|
64
|
+
// when needed). This runs every session and never blocks —
|
|
65
|
+
// enforcement is governed solely by permissionMode at tool-call time.
|
|
66
66
|
const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
|
|
67
|
-
|
|
67
|
+
await userGate(client, {
|
|
68
68
|
binName: "ory-claude",
|
|
69
69
|
harness: "claude-code",
|
|
70
|
-
allowBlock: true,
|
|
71
70
|
});
|
|
72
71
|
// Resolve the agent identity (machine credentials) regardless of how
|
|
73
72
|
// user auth went — it never blocks and attaches the agent's bearer
|
|
@@ -76,88 +75,13 @@ async function handleSessionStart(input, client, deps) {
|
|
|
76
75
|
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
77
76
|
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "claude-code" });
|
|
78
77
|
// Record the user→agent delegation so the audit trail captures that
|
|
79
|
-
// this user authorized this agent for this session. Best-effort
|
|
80
|
-
//
|
|
81
|
-
// and swallowed (fail-open —
|
|
82
|
-
// enforcement).
|
|
83
|
-
await
|
|
84
|
-
if (!decision.proceed) {
|
|
85
|
-
return { decision: "block", reason: decision.reason };
|
|
86
|
-
}
|
|
87
|
-
if (decision.mode !== "disabled") {
|
|
88
|
-
return {};
|
|
89
|
-
}
|
|
90
|
-
const resolved = (0, argus_1.resolveConfig)();
|
|
91
|
-
if (resolved.auditOnly) {
|
|
92
|
-
client.logger.info("config.audit_only", {
|
|
93
|
-
message: "Audit-only mode enabled. Auth and permission checks are disabled.",
|
|
94
|
-
});
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
|
-
if (!resolved.projectUrl) {
|
|
98
|
-
client.logger.warn("config.not_configured", {
|
|
99
|
-
message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
|
|
100
|
-
"Run 'npx ory-claude configure' to connect to an Ory project.",
|
|
101
|
-
});
|
|
102
|
-
return {};
|
|
103
|
-
}
|
|
104
|
-
// Try session token first, then OAuth2 token
|
|
105
|
-
const sessionToken = process.env.ORY_SESSION_TOKEN;
|
|
106
|
-
const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
|
|
107
|
-
if (sessionToken) {
|
|
108
|
-
await verifySessionToken(sessionToken, client);
|
|
109
|
-
return {};
|
|
110
|
-
}
|
|
111
|
-
if (oauth2Token) {
|
|
112
|
-
await verifyOAuth2Token(oauth2Token, client);
|
|
113
|
-
return {};
|
|
114
|
-
}
|
|
115
|
-
client.logger.warn("session.no_credentials", {
|
|
116
|
-
message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
|
|
117
|
-
"Skipping authentication.",
|
|
118
|
-
});
|
|
78
|
+
// this user authorized this agent for this session. Best-effort and
|
|
79
|
+
// written at most once per install: requires both principals to be
|
|
80
|
+
// populated, and any failure is logged and swallowed (fail-open —
|
|
81
|
+
// delegation tracking is for audit, not enforcement).
|
|
82
|
+
await (0, argus_1.writeUserDelegatesAgent)(client);
|
|
119
83
|
return {};
|
|
120
84
|
}
|
|
121
|
-
async function verifySessionToken(token, client) {
|
|
122
|
-
try {
|
|
123
|
-
const session = await client.verifySession(token);
|
|
124
|
-
if (!session.active) {
|
|
125
|
-
client.logger.warn("session.inactive", {
|
|
126
|
-
message: "Ory session is not active. Re-authenticate to enable auth checks.",
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
const oryErr = err;
|
|
132
|
-
client.logger.warn("session.verify_failed", {
|
|
133
|
-
code: oryErr.code,
|
|
134
|
-
message: oryErr.message,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
async function verifyOAuth2Token(token, client) {
|
|
139
|
-
try {
|
|
140
|
-
const tokenInfo = await client.introspectToken(token);
|
|
141
|
-
if (!tokenInfo.active) {
|
|
142
|
-
client.logger.warn("oauth2.token_inactive", {
|
|
143
|
-
message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
|
|
144
|
-
});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
client.logger.info("oauth2.session_authenticated", {
|
|
148
|
-
clientId: tokenInfo.clientId,
|
|
149
|
-
subject: tokenInfo.subject,
|
|
150
|
-
scope: tokenInfo.scope,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
const oryErr = err;
|
|
155
|
-
client.logger.warn("oauth2.introspect_failed", {
|
|
156
|
-
code: oryErr.code,
|
|
157
|
-
message: oryErr.message,
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
85
|
// ─── PreToolUse ─────────────────────────────────────────────────────
|
|
162
86
|
async function handlePreToolUse(input, client, deps = {}) {
|
|
163
87
|
const toolName = input.tool_name ?? "unknown";
|
|
@@ -478,45 +402,11 @@ function permissionRequestFallback(reason) {
|
|
|
478
402
|
function resolveNamespace() {
|
|
479
403
|
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
480
404
|
}
|
|
481
|
-
/**
|
|
482
|
-
* Write the user→agent delegation tuple. Idempotent and fail-open:
|
|
483
|
-
* requires both principal subjects to be populated; any error (including
|
|
484
|
-
* unconfigured projectUrl, which manifests as a network_error) is logged
|
|
485
|
-
* and swallowed. The actual permission enforcement is unchanged — this
|
|
486
|
-
* tuple is purely audit-trail data.
|
|
487
|
-
*/
|
|
488
|
-
async function recordUserDelegatesAgent(client) {
|
|
489
|
-
const user = client.userPrincipal.subject;
|
|
490
|
-
const agent = client.agentPrincipal.subject;
|
|
491
|
-
if (!user || !agent) {
|
|
492
|
-
client.logger.debug("delegation.skip", {
|
|
493
|
-
reason: "missing principal",
|
|
494
|
-
hasUser: !!user,
|
|
495
|
-
hasAgent: !!agent,
|
|
496
|
-
});
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
try {
|
|
500
|
-
await client.createRelationship({
|
|
501
|
-
namespace: resolveNamespace(),
|
|
502
|
-
object: `agent:${agent}`,
|
|
503
|
-
relation: "delegate",
|
|
504
|
-
subjectId: `user:${user}`,
|
|
505
|
-
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
506
|
-
}
|
|
507
|
-
catch (err) {
|
|
508
|
-
const oryErr = err;
|
|
509
|
-
client.logger.warn("delegation.user_to_agent.failed", {
|
|
510
|
-
code: oryErr.code,
|
|
511
|
-
message: oryErr.message,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
405
|
/**
|
|
516
406
|
* Register an OAuth2 identity for a sub-agent and write the
|
|
517
|
-
* `agent → subagent` delegation tuple. Fail-open and
|
|
518
|
-
*
|
|
519
|
-
*
|
|
407
|
+
* `agent → subagent` delegation tuple. Fail-open and written at most once
|
|
408
|
+
* per install, so the multiple call paths (SubagentStart event + Task-tool
|
|
409
|
+
* fallback) won't duplicate state.
|
|
520
410
|
*/
|
|
521
411
|
async function registerSubAgent(subAgentType, client, deps) {
|
|
522
412
|
const subAgentGate = deps.subAgentGate ?? argus_1.ensureSubAgentIdentity;
|
|
@@ -537,30 +427,7 @@ async function registerSubAgent(subAgentType, client, deps) {
|
|
|
537
427
|
}
|
|
538
428
|
if (identity.kind !== "dynamic" || !identity.subject)
|
|
539
429
|
return;
|
|
540
|
-
|
|
541
|
-
if (!agent) {
|
|
542
|
-
client.logger.debug("delegation.skip", {
|
|
543
|
-
reason: "agent principal not populated",
|
|
544
|
-
subAgentType,
|
|
545
|
-
});
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
try {
|
|
549
|
-
await client.createRelationship({
|
|
550
|
-
namespace: resolveNamespace(),
|
|
551
|
-
object: `subagent:${identity.subject}`,
|
|
552
|
-
relation: "delegate",
|
|
553
|
-
subjectId: `agent:${agent}`,
|
|
554
|
-
}, { spanAttributes: { delegation: "agent-to-subagent", subAgentType } });
|
|
555
|
-
}
|
|
556
|
-
catch (err) {
|
|
557
|
-
const oryErr = err;
|
|
558
|
-
client.logger.warn("delegation.agent_to_subagent.failed", {
|
|
559
|
-
subAgentType,
|
|
560
|
-
code: oryErr.code,
|
|
561
|
-
message: oryErr.message,
|
|
562
|
-
});
|
|
563
|
-
}
|
|
430
|
+
await (0, argus_1.writeAgentDelegatesSubagent)(client, identity.subject, subAgentType);
|
|
564
431
|
}
|
|
565
432
|
/**
|
|
566
433
|
* Legacy fallback for older Claude Code versions that don't fire
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/claude-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Ory plugin for Claude Code: 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://github.com/ory/claude-plugins/tree/master/plugins/ory-agent-plugin",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
],
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"reo-census": "^1.2.8",
|
|
79
|
-
"@ory/argus": "0.
|
|
79
|
+
"@ory/argus": "0.14.0"
|
|
80
80
|
},
|
|
81
81
|
"engines": {
|
|
82
82
|
"node": ">=22"
|