@ory/argus 0.11.0 → 0.12.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
@@ -1,6 +1,6 @@
1
1
  # Ory Argus: Agent and Developer Experience
2
2
 
3
- The core API behind every Ory Agent Plugin and Extension. Argus wraps [Ory Identities](https://www.ory.com/ory-ecosystem), [Ory Permissions](https://ory.com/permissions), MCP authorization, and distributed tracing into a single client. Each of the harness packages (`@ory/claude-code`, `@ory/codex`, `@ory/gemini-cli`, `@ory/openclaw`, `@ory/opencode`) is a thin adapter that maps one harness's hook contract onto Argus.
3
+ The core API behind every Ory Agent Plugin and Extension. Argus wraps [Ory Identities](https://www.ory.com/ory-ecosystem), [Ory Permissions](https://ory.com/permissions), MCP authorization, and distributed tracing into a single client. Each harness package (`@ory/claude-code`, `@ory/codex`, `@ory/gemini-cli`, and the rest — see the [root README](../../README.md) for the full list) is a thin adapter that maps one harness's hook contract onto Argus.
4
4
 
5
5
  Argus is also published on its own so you can build new harness plugins or extensions, embed Ory into a custom agent runtime, or instrument any SDK that exposes event lifecycle hooks for session start, tool execution, and tool completion.
6
6
 
@@ -88,6 +88,29 @@ export declare function loadAgentDynamicCredentials(): OryAgentDynamicCredential
88
88
  export declare function saveAgentDynamicCredentials(creds: OryAgentDynamicCredentials): void;
89
89
  /** Remove persisted DCR credentials (e.g. after revocation or unregister). */
90
90
  export declare function clearAgentDynamicCredentials(): void;
91
+ /** Outcome of a best-effort RFC 7592 client revocation. */
92
+ export interface RevokeAgentClientResult {
93
+ /**
94
+ * `deleted` — the server-side client was removed (or was already gone);
95
+ * `skipped` — no management credentials were persisted, so we couldn't
96
+ * try; `failed` — the DELETE was attempted but the server rejected it.
97
+ */
98
+ status: "deleted" | "skipped" | "failed";
99
+ /** HTTP status when the server responded. */
100
+ httpStatus?: number;
101
+ /** Human-readable detail for warnings (present on `skipped` / `failed`). */
102
+ message?: string;
103
+ }
104
+ /**
105
+ * Best-effort RFC 7592 `DELETE` of a dynamically-registered OAuth2 client.
106
+ *
107
+ * Requires the registration management URI and access token to have been
108
+ * persisted at registration time; otherwise returns `{ status: "skipped" }`.
109
+ * Never throws — a missing client (404) counts as `deleted`, and any other
110
+ * network/HTTP failure is captured in the result so callers can warn and
111
+ * still clear local state.
112
+ */
113
+ export declare function revokeAgentDynamicClient(creds: OryAgentDynamicCredentials): Promise<RevokeAgentClientResult>;
91
114
  export interface RegisterAgentClientArgs {
92
115
  projectUrl: string;
93
116
  /** Bearer used as the initial access token (RFC 7591 §3). */
@@ -36,6 +36,7 @@ exports.clearSubAgentDynamicCredentials = clearSubAgentDynamicCredentials;
36
36
  exports.loadAgentDynamicCredentials = loadAgentDynamicCredentials;
37
37
  exports.saveAgentDynamicCredentials = saveAgentDynamicCredentials;
38
38
  exports.clearAgentDynamicCredentials = clearAgentDynamicCredentials;
39
+ exports.revokeAgentDynamicClient = revokeAgentDynamicClient;
39
40
  exports.registerAgentClient = registerAgentClient;
40
41
  exports.resolveAgentCredentials = resolveAgentCredentials;
41
42
  exports.fetchClientCredentialsToken = fetchClientCredentialsToken;
@@ -116,6 +117,40 @@ function clearAgentDynamicCredentials() {
116
117
  return next;
117
118
  });
118
119
  }
120
+ /**
121
+ * Best-effort RFC 7592 `DELETE` of a dynamically-registered OAuth2 client.
122
+ *
123
+ * Requires the registration management URI and access token to have been
124
+ * persisted at registration time; otherwise returns `{ status: "skipped" }`.
125
+ * Never throws — a missing client (404) counts as `deleted`, and any other
126
+ * network/HTTP failure is captured in the result so callers can warn and
127
+ * still clear local state.
128
+ */
129
+ async function revokeAgentDynamicClient(creds) {
130
+ if (!creds.registrationClientUri || !creds.registrationAccessToken) {
131
+ return {
132
+ status: "skipped",
133
+ message: "no registration_access_token persisted",
134
+ };
135
+ }
136
+ const oidc = new client_1.OidcApi(new client_1.Configuration({
137
+ basePath: creds.projectUrl,
138
+ accessToken: creds.registrationAccessToken,
139
+ }));
140
+ try {
141
+ await oidc.deleteOidcDynamicClient({ id: creds.clientId });
142
+ return { status: "deleted" };
143
+ }
144
+ catch (err) {
145
+ const httpStatus = err
146
+ ?.response?.status;
147
+ // Already gone server-side — treat as success.
148
+ if (httpStatus === 404)
149
+ return { status: "deleted", httpStatus };
150
+ const message = err instanceof Error ? err.message : String(err);
151
+ return { status: "failed", httpStatus, message };
152
+ }
153
+ }
119
154
  /**
120
155
  * Register a new OAuth2 client via RFC 7591. Authorization is carried
121
156
  * either by the user's interactive token (the natural bootstrap) or by
@@ -144,7 +179,13 @@ async function registerAgentClient(args) {
144
179
  scope: "openid offline_access",
145
180
  token_endpoint_auth_method: "client_secret_post",
146
181
  },
147
- });
182
+ },
183
+ // The generated SDK declares no security scheme for the public DCR create
184
+ // operation (`/oauth2/register`), so it never attaches the bearer from the
185
+ // Configuration's `accessToken`. Ory projects that gate DCR behind an
186
+ // initial access token then reject the unauthenticated request and the
187
+ // agent never gets credentials. Send the bearer explicitly.
188
+ args.auth ? { headers: { Authorization: `Bearer ${args.auth}` } } : undefined);
148
189
  }
149
190
  catch (err) {
150
191
  const status = err?.response?.status;
package/dist/auth.js CHANGED
@@ -103,6 +103,10 @@ async function pkceLogin(options) {
103
103
  server.close();
104
104
  if (result.kind !== "ok")
105
105
  return result;
106
+ // Mirror the browser success page on the terminal so it's unambiguous the
107
+ // flow finished and the browser tab can be closed — the user's attention is
108
+ // often still on the browser at this point.
109
+ process.stderr.write("Signed in. You can close the browser tab — continuing here…\n\n");
106
110
  try {
107
111
  const tokens = await exchangeCodeForTokens({
108
112
  projectUrl: options.projectUrl,
@@ -203,18 +207,18 @@ function waitForCallback(server, opts) {
203
207
  }
204
208
  const error = url.searchParams.get("error");
205
209
  if (error) {
206
- respond(res, 400, "Authentication declined. You may close this window.");
210
+ respondHtml(res, 400, resultPage("cancelled"));
207
211
  settle({ kind: "declined", reason: "user_denied" });
208
212
  return;
209
213
  }
210
214
  const state = url.searchParams.get("state");
211
215
  const code = url.searchParams.get("code");
212
216
  if (!code || state !== opts.expectedState) {
213
- respond(res, 400, "State mismatch.");
217
+ respondHtml(res, 400, resultPage("mismatch"));
214
218
  settle({ kind: "declined", reason: "state_mismatch" });
215
219
  return;
216
220
  }
217
- respond(res, 200, "Sign-in complete. You may close this window and return to your terminal.");
221
+ respondHtml(res, 200, resultPage("success"));
218
222
  settle({ kind: "ok", code });
219
223
  };
220
224
  server.on("request", onRequest);
@@ -230,6 +234,75 @@ function respond(res, status, body) {
230
234
  res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
231
235
  res.end(body + "\n");
232
236
  }
237
+ function respondHtml(res, status, html) {
238
+ res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
239
+ res.end(html);
240
+ }
241
+ /**
242
+ * The page the browser lands on after the OAuth2 redirect. The whole point is
243
+ * to make the next action unambiguous: on success it tells the user, in plain
244
+ * words, that they can close the tab and return to the terminal (the #1
245
+ * source of "is it done? do I close this?" confusion). Self-contained HTML —
246
+ * inline styles only, Ory-branded (Indigo #4F46E5), no external requests — so
247
+ * it renders identically offline and on the loopback origin.
248
+ */
249
+ function resultPage(kind) {
250
+ const view = {
251
+ success: {
252
+ accent: "#4F46E5", // Ory Indigo
253
+ icon: "&#10003;", // ✓
254
+ title: "Signed in to Ory",
255
+ body: "You can <strong>close this tab</strong> and return to your terminal — setup continues automatically.",
256
+ },
257
+ cancelled: {
258
+ accent: "#F43F5E", // Ory Rose
259
+ icon: "&#10005;", // ✕
260
+ title: "Sign-in cancelled",
261
+ body: "No changes were made. <strong>Close this tab</strong> and return to your terminal to try again.",
262
+ },
263
+ mismatch: {
264
+ accent: "#F43F5E",
265
+ icon: "!",
266
+ title: "Sign-in could not be verified",
267
+ body: "The login response failed a security check. <strong>Close this tab</strong> and re-run the login from your terminal.",
268
+ },
269
+ }[kind];
270
+ return `<!doctype html>
271
+ <html lang="en">
272
+ <head>
273
+ <meta charset="utf-8">
274
+ <meta name="viewport" content="width=device-width, initial-scale=1">
275
+ <title>${view.title}</title>
276
+ <style>
277
+ :root { color-scheme: light dark; }
278
+ html, body { height: 100%; margin: 0; }
279
+ body {
280
+ display: flex; align-items: center; justify-content: center;
281
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
282
+ background: #0F172A; color: #E2E8F0;
283
+ }
284
+ .card { max-width: 26rem; padding: 2.5rem 2rem; text-align: center; }
285
+ .badge {
286
+ width: 4rem; height: 4rem; border-radius: 9999px; margin: 0 auto 1.25rem;
287
+ display: flex; align-items: center; justify-content: center;
288
+ font-size: 2rem; line-height: 1; font-weight: 700;
289
+ background: ${view.accent}22; color: ${view.accent};
290
+ }
291
+ h1 { font-size: 1.35rem; margin: 0 0 .5rem; color: #F8FAFC; }
292
+ p { margin: 0; line-height: 1.55; color: #94A3B8; }
293
+ strong { color: #E2E8F0; font-weight: 600; }
294
+ </style>
295
+ </head>
296
+ <body>
297
+ <div class="card">
298
+ <div class="badge">${view.icon}</div>
299
+ <h1>${view.title}</h1>
300
+ <p>${view.body}</p>
301
+ </div>
302
+ </body>
303
+ </html>
304
+ `;
305
+ }
233
306
  // ─── Browser launch ───────────────────────────────────────────────────
234
307
  async function launchBrowser(url, override) {
235
308
  if (override) {
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Helper for rendering copy-pasteable `npx` invocations in CLI banners,
3
+ * help text, and prompts.
4
+ *
5
+ * Every harness plugin ships a CLI bin named `ory-<short>` (e.g.
6
+ * `ory-claude`, `ory-gemini`). Those bins only resolve on `PATH` when the
7
+ * package is already installed. In the common install flow the user runs
8
+ * `npx @ory/<harness> install` — a one-shot npx invocation that leaves
9
+ * nothing on `PATH` — so a follow-up banner that says `npx ory-claude
10
+ * permissions bootstrap` fails: npm can't find a package literally named
11
+ * `ory-claude` and errors out.
12
+ *
13
+ * The always-resolvable form is `npx -y -p @ory/<harness> ory-<short>
14
+ * <args>`, which is exactly what the READMEs document. {@link oryNpx}
15
+ * renders that prefix for a given bin name so every banner is
16
+ * copy-paste-safe from a clean shell.
17
+ */
18
+ /**
19
+ * Render a copy-pasteable `npx` invocation prefix for a harness bin.
20
+ *
21
+ * Returns `npx -y -p <package> <binName>` when the bin's package is
22
+ * known, so the command resolves from a clean shell. Each package ships
23
+ * sibling `-setup` and `-hook` bins alongside its main `ory-<short>` bin;
24
+ * those normalize to the same package. Falls back to the bare `npx
25
+ * <binName>` for unknown bins (preserving prior behavior) — that only
26
+ * resolves when the bin is already on `PATH`, so keep
27
+ * {@link BIN_TO_PACKAGE} in sync with the shipped bins.
28
+ *
29
+ * @example
30
+ * oryNpx("ory-claude") // "npx -y -p @ory/claude-code ory-claude"
31
+ * oryNpx("ory-claude-setup") // "npx -y -p @ory/claude-code ory-claude-setup"
32
+ * `${oryNpx("ory-claude")} permissions status`
33
+ */
34
+ export declare function oryNpx(binName: string): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * Helper for rendering copy-pasteable `npx` invocations in CLI banners,
4
+ * help text, and prompts.
5
+ *
6
+ * Every harness plugin ships a CLI bin named `ory-<short>` (e.g.
7
+ * `ory-claude`, `ory-gemini`). Those bins only resolve on `PATH` when the
8
+ * package is already installed. In the common install flow the user runs
9
+ * `npx @ory/<harness> install` — a one-shot npx invocation that leaves
10
+ * nothing on `PATH` — so a follow-up banner that says `npx ory-claude
11
+ * permissions bootstrap` fails: npm can't find a package literally named
12
+ * `ory-claude` and errors out.
13
+ *
14
+ * The always-resolvable form is `npx -y -p @ory/<harness> ory-<short>
15
+ * <args>`, which is exactly what the READMEs document. {@link oryNpx}
16
+ * renders that prefix for a given bin name so every banner is
17
+ * copy-paste-safe from a clean shell.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.oryNpx = oryNpx;
21
+ /**
22
+ * Maps each harness CLI bin name to its npm package. Keyed by the bin
23
+ * name callers pass into the shared CLI helpers (always `ory-<short>`).
24
+ * When adding a new harness plugin, add its bin here so its banners emit
25
+ * a resolvable command.
26
+ */
27
+ const BIN_TO_PACKAGE = {
28
+ "ory-claude": "@ory/claude-code",
29
+ "ory-codex": "@ory/codex",
30
+ "ory-gemini": "@ory/gemini-cli",
31
+ "ory-openclaw": "@ory/openclaw",
32
+ "ory-opencode": "@ory/opencode",
33
+ "ory-continue": "@ory/continue",
34
+ "ory-goose": "@ory/goose",
35
+ "ory-cline": "@ory/cline",
36
+ "ory-amp": "@ory/amp",
37
+ "ory-pi": "@ory/pi",
38
+ "ory-antigravity": "@ory/antigravity",
39
+ };
40
+ /**
41
+ * Render a copy-pasteable `npx` invocation prefix for a harness bin.
42
+ *
43
+ * Returns `npx -y -p <package> <binName>` when the bin's package is
44
+ * known, so the command resolves from a clean shell. Each package ships
45
+ * sibling `-setup` and `-hook` bins alongside its main `ory-<short>` bin;
46
+ * those normalize to the same package. Falls back to the bare `npx
47
+ * <binName>` for unknown bins (preserving prior behavior) — that only
48
+ * resolves when the bin is already on `PATH`, so keep
49
+ * {@link BIN_TO_PACKAGE} in sync with the shipped bins.
50
+ *
51
+ * @example
52
+ * oryNpx("ory-claude") // "npx -y -p @ory/claude-code ory-claude"
53
+ * oryNpx("ory-claude-setup") // "npx -y -p @ory/claude-code ory-claude-setup"
54
+ * `${oryNpx("ory-claude")} permissions status`
55
+ */
56
+ function oryNpx(binName) {
57
+ const base = binName.replace(/-(setup|hook)$/, "");
58
+ const pkg = BIN_TO_PACKAGE[base];
59
+ return pkg ? `npx -y -p ${pkg} ${binName}` : `npx ${binName}`;
60
+ }
package/dist/cli.d.ts CHANGED
@@ -22,22 +22,32 @@ export declare function printOryConfig(): void;
22
22
  */
23
23
  export declare function printEnvironment(): void;
24
24
  /**
25
- * Print the last 5 debug log entries if ORY_AGENT_LOG_FILE is set and exists.
25
+ * Print the last 5 debug log entries. When `harness` is given the path is
26
+ * resolved the same way the plugin does at runtime — the default per-harness
27
+ * data-dir location when `ORY_AGENT_LOG_FILE` is unset — so `status` finds the
28
+ * log without the reader hand-setting env vars. The debug log only exists when
29
+ * a session ran with `ORY_AGENT_DEBUG=true`; when it doesn't, this prints a
30
+ * one-line pointer on how to turn it on rather than staying silent.
26
31
  */
27
- export declare function printLogTail(): void;
32
+ export declare function printLogTail(harness?: string): void;
28
33
  /**
29
34
  * Print the "Configure your Ory credentials" help text.
30
35
  */
31
36
  export declare function printEnvHelp(binName: string): void;
32
37
  /**
33
- * Print the last 5 trace entries if ORY_AGENT_TRACE_FILE is set and exists.
38
+ * Print the last 5 trace spans. When `harness` is given the path is resolved
39
+ * the same way the plugin does at runtime — the default per-harness data-dir
40
+ * location when `ORY_AGENT_TRACE_FILE` is unset — so `status` shows recent
41
+ * activity out of the box (traces are written on every recorded span). When no
42
+ * spans have been recorded yet, prints a one-line pointer at the trace file so
43
+ * the reader knows where to look (and how to watch it live).
34
44
  */
35
- export declare function printTraceTail(): void;
45
+ export declare function printTraceTail(harness?: string): void;
36
46
  /**
37
47
  * Run the live trace watcher. Tails the NDJSON trace file and prints
38
48
  * formatted spans to stdout as they arrive. Blocks until interrupted.
39
49
  */
40
- export declare function runWatchCommand(args: string[]): void;
50
+ export declare function runWatchCommand(harness: string, args: string[]): void;
41
51
  /**
42
52
  * Returns true if `/dev/tty` can be opened for reading. Use this before
43
53
  * attempting to prompt the user — the harness has already taken stdin so