@ory/argus 0.13.3 → 0.13.5

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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "c04784bfd67acdce0133aca0ebcd335e326b9251",
4
- "commitShort": "c04784b",
3
+ "commit": "6a1250a1a231524d64aed17df86d694f829cf938",
4
+ "commitShort": "6a1250a",
5
5
  "branch": "main",
6
- "commitDate": "2026-07-14T15:46:39-07:00",
6
+ "commitDate": "2026-07-15T07:09:03-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-07-14T22:51:01.241Z"
8
+ "builtAt": "2026-07-15T14:13:28.336Z"
9
9
  }
package/dist/client.d.ts CHANGED
@@ -58,6 +58,23 @@ export declare class OryAgentClient {
58
58
  * Ory API calls. Populated by `ensureAgentIdentity`.
59
59
  */
60
60
  private _agentPrincipal;
61
+ /**
62
+ * Admin credential — an Ory Network **project API key** (`ory_pat_…`) —
63
+ * used to authenticate the Keto Permission and Relationship APIs.
64
+ *
65
+ * This is deliberately distinct from the agent principal's token. Ory
66
+ * Network's permission APIs authenticate with a project API key and
67
+ * **reject project-issued OAuth2 access tokens** (a DCR client-credentials
68
+ * token is answered with `401 "Access token is not active"`). So when the
69
+ * agent identity resolves to a DCR OAuth2 client — the default path — its
70
+ * token cannot authenticate permission checks. Holding the admin key here
71
+ * lets {@link buildApis} route Keto through it while the agent OAuth2 token
72
+ * still authenticates the OAuth2 / Frontend APIs and carries attribution.
73
+ *
74
+ * When unset, Keto calls fall back to the agent token (legacy
75
+ * single-credential behavior, unchanged for static-key deployments).
76
+ */
77
+ private _adminApiKey?;
61
78
  constructor(config: OryAgentConfig);
62
79
  /** Build a fresh set of Ory API instances using the current agent token. */
63
80
  private buildApis;
@@ -78,13 +95,25 @@ export declare class OryAgentClient {
78
95
  * Pass `{}` (or fields set to undefined) to clear.
79
96
  */
80
97
  setUserPrincipal(principal: PrincipalIdentity): void;
81
- /**
82
- * Set or update the AI agent principal. The agent's token (when
83
- * present) is used in the Authorization header for all outgoing Ory
84
- * API calls so the audit log shows "agent X acting on behalf of
85
- * user Y". Rebuilds the underlying Ory API instances when the token
86
- * actually changes so subsequent calls pick it up; otherwise leaves
87
- * the API instances alone (so test stubs survive a no-op update).
98
+ /** Whether an admin API key is set for the Keto APIs (never exposes it). */
99
+ get hasAdminApiKey(): boolean;
100
+ /**
101
+ * Set or update the admin API key (an Ory Network project API key) used to
102
+ * authenticate the Keto Permission / Relationship APIs. Rebuilds those API
103
+ * instances when the key changes; a no-op update leaves them alone so test
104
+ * stubs survive. See {@link _adminApiKey} for why Keto needs a credential
105
+ * distinct from the agent's OAuth2 token.
106
+ */
107
+ setAdminApiKey(apiKey: string | undefined): void;
108
+ /**
109
+ * Set or update the AI agent principal. The agent's token (when present)
110
+ * authenticates outgoing calls to the OAuth2 / Frontend APIs and carries
111
+ * audit attribution ("agent X acting on behalf of user Y"). Note the Keto
112
+ * Permission / Relationship APIs authenticate with {@link _adminApiKey}
113
+ * instead when one is set — Ory Network rejects OAuth2 tokens there.
114
+ * Rebuilds the underlying Ory API instances when the token actually changes
115
+ * so subsequent calls pick it up; otherwise leaves the API instances alone
116
+ * (so test stubs survive a no-op update).
88
117
  */
89
118
  setAgentPrincipal(principal: PrincipalIdentity): void;
90
119
  /**
@@ -146,6 +175,15 @@ export declare class OryAgentClient {
146
175
  * across the user/agent split.
147
176
  */
148
177
  private principalSpanAttributes;
178
+ /**
179
+ * When a Keto call is auth-rejected while it was authenticated by something
180
+ * other than an Ory Network project API key (`ory_pat_…`) — e.g. a DCR
181
+ * OAuth2 access token — the credential *type* is wrong, not merely expired.
182
+ * Ory Network's Permission / Relationship APIs only accept a project API
183
+ * key. Emit an actionable hint so the failure isn't misread as a stale
184
+ * session (the generic `session_inactive` classification of the raw 401).
185
+ */
186
+ private warnIfKetoCredentialMismatch;
149
187
  /**
150
188
  * Classify an error from any Ory API call into a structured OryError.
151
189
  */
package/dist/client.js CHANGED
@@ -42,6 +42,7 @@ const path = __importStar(require("node:path"));
42
42
  const logger_js_1 = require("./logger.js");
43
43
  const tracer_js_1 = require("./tracer.js");
44
44
  const config_js_1 = require("./config.js");
45
+ const auth_store_js_1 = require("./auth-store.js");
45
46
  const otlp_js_1 = require("./otel/otlp.js");
46
47
  class OryAgentClient {
47
48
  frontend;
@@ -63,6 +64,23 @@ class OryAgentClient {
63
64
  * Ory API calls. Populated by `ensureAgentIdentity`.
64
65
  */
65
66
  _agentPrincipal = {};
67
+ /**
68
+ * Admin credential — an Ory Network **project API key** (`ory_pat_…`) —
69
+ * used to authenticate the Keto Permission and Relationship APIs.
70
+ *
71
+ * This is deliberately distinct from the agent principal's token. Ory
72
+ * Network's permission APIs authenticate with a project API key and
73
+ * **reject project-issued OAuth2 access tokens** (a DCR client-credentials
74
+ * token is answered with `401 "Access token is not active"`). So when the
75
+ * agent identity resolves to a DCR OAuth2 client — the default path — its
76
+ * token cannot authenticate permission checks. Holding the admin key here
77
+ * lets {@link buildApis} route Keto through it while the agent OAuth2 token
78
+ * still authenticates the OAuth2 / Frontend APIs and carries attribution.
79
+ *
80
+ * When unset, Keto calls fall back to the agent token (legacy
81
+ * single-credential behavior, unchanged for static-key deployments).
82
+ */
83
+ _adminApiKey;
66
84
  constructor(config) {
67
85
  this.config = config;
68
86
  this.sessionCacheTtlMs = config.sessionCacheTtlMs ?? 60_000;
@@ -83,6 +101,13 @@ class OryAgentClient {
83
101
  // call sites keep working until they migrate to ensureAgentIdentity.
84
102
  if (config.apiKey)
85
103
  this._agentPrincipal.token = config.apiKey;
104
+ // The `apiKey` is also the admin credential for the Keto Permission /
105
+ // Relationship APIs. Seed it separately so that when `ensureAgentIdentity`
106
+ // later replaces the agent token with a DCR OAuth2 token, permission
107
+ // checks keep authenticating with the project API key rather than the
108
+ // OAuth2 token Ory Network rejects. See {@link _adminApiKey}.
109
+ if (config.apiKey)
110
+ this._adminApiKey = config.apiKey;
86
111
  const { frontend, oauth2, permission, relationship } = this.buildApis();
87
112
  this.frontend = frontend;
88
113
  this.oauth2 = oauth2;
@@ -96,11 +121,23 @@ class OryAgentClient {
96
121
  basePath: this.config.projectUrl,
97
122
  ...(accessToken ? { accessToken } : {}),
98
123
  });
124
+ // Keto (Permission + Relationship) authenticates with the admin project
125
+ // API key when one is set; otherwise it reuses the agent token (legacy
126
+ // single-credential behavior). When the two credentials are identical we
127
+ // reuse the same Configuration object so a single-credential deployment —
128
+ // and tests that stub one Configuration — see exactly one.
129
+ const ketoToken = this._adminApiKey ?? accessToken;
130
+ const ketoConfig = ketoToken === accessToken
131
+ ? apiConfig
132
+ : new client_1.Configuration({
133
+ basePath: this.config.projectUrl,
134
+ ...(ketoToken ? { accessToken: ketoToken } : {}),
135
+ });
99
136
  return {
100
137
  frontend: new client_1.FrontendApi(apiConfig),
101
138
  oauth2: new client_1.OAuth2Api(apiConfig),
102
- permission: new client_1.PermissionApi(apiConfig),
103
- relationship: new client_1.RelationshipApi(apiConfig),
139
+ permission: new client_1.PermissionApi(ketoConfig),
140
+ relationship: new client_1.RelationshipApi(ketoConfig),
104
141
  };
105
142
  }
106
143
  /** Snapshot of the current user principal. */
@@ -128,13 +165,36 @@ class OryAgentClient {
128
165
  setUserPrincipal(principal) {
129
166
  this._userPrincipal = { ...principal };
130
167
  }
168
+ /** Whether an admin API key is set for the Keto APIs (never exposes it). */
169
+ get hasAdminApiKey() {
170
+ return !!this._adminApiKey;
171
+ }
131
172
  /**
132
- * Set or update the AI agent principal. The agent's token (when
133
- * present) is used in the Authorization header for all outgoing Ory
134
- * API calls so the audit log shows "agent X acting on behalf of
135
- * user Y". Rebuilds the underlying Ory API instances when the token
136
- * actually changes so subsequent calls pick it up; otherwise leaves
137
- * the API instances alone (so test stubs survive a no-op update).
173
+ * Set or update the admin API key (an Ory Network project API key) used to
174
+ * authenticate the Keto Permission / Relationship APIs. Rebuilds those API
175
+ * instances when the key changes; a no-op update leaves them alone so test
176
+ * stubs survive. See {@link _adminApiKey} for why Keto needs a credential
177
+ * distinct from the agent's OAuth2 token.
178
+ */
179
+ setAdminApiKey(apiKey) {
180
+ if (this._adminApiKey === apiKey)
181
+ return;
182
+ this._adminApiKey = apiKey;
183
+ const { frontend, oauth2, permission, relationship } = this.buildApis();
184
+ this.frontend = frontend;
185
+ this.oauth2 = oauth2;
186
+ this.permission = permission;
187
+ this.relationship = relationship;
188
+ }
189
+ /**
190
+ * Set or update the AI agent principal. The agent's token (when present)
191
+ * authenticates outgoing calls to the OAuth2 / Frontend APIs and carries
192
+ * audit attribution ("agent X acting on behalf of user Y"). Note the Keto
193
+ * Permission / Relationship APIs authenticate with {@link _adminApiKey}
194
+ * instead when one is set — Ory Network rejects OAuth2 tokens there.
195
+ * Rebuilds the underlying Ory API instances when the token actually changes
196
+ * so subsequent calls pick it up; otherwise leaves the API instances alone
197
+ * (so test stubs survive a no-op update).
138
198
  */
139
199
  setAgentPrincipal(principal) {
140
200
  const tokenChanged = this._agentPrincipal.token !== principal.token;
@@ -320,6 +380,7 @@ class OryAgentClient {
320
380
  }
321
381
  catch (err) {
322
382
  const oryErr = this.classifyError(err);
383
+ this.warnIfKetoCredentialMismatch(oryErr);
323
384
  this.logger.error("permission.check.failed", {
324
385
  code: oryErr.code,
325
386
  status: oryErr.status,
@@ -394,6 +455,7 @@ class OryAgentClient {
394
455
  }
395
456
  catch (err) {
396
457
  const oryErr = this.classifyError(err);
458
+ this.warnIfKetoCredentialMismatch(oryErr);
397
459
  this.logger.error("permission.batch_check.failed", {
398
460
  code: oryErr.code,
399
461
  status: oryErr.status,
@@ -475,6 +537,7 @@ class OryAgentClient {
475
537
  });
476
538
  return { created: false, alreadyExisted: true };
477
539
  }
540
+ this.warnIfKetoCredentialMismatch(oryErr);
478
541
  this.logger.error("relationship.create.failed", {
479
542
  code: oryErr.code,
480
543
  status: oryErr.status,
@@ -534,6 +597,7 @@ class OryAgentClient {
534
597
  });
535
598
  return { deleted: false, notFound: true };
536
599
  }
600
+ this.warnIfKetoCredentialMismatch(oryErr);
537
601
  this.logger.error("relationship.delete.failed", {
538
602
  code: oryErr.code,
539
603
  status: oryErr.status,
@@ -560,6 +624,29 @@ class OryAgentClient {
560
624
  out.agentSubject = this._agentPrincipal.subject;
561
625
  return out;
562
626
  }
627
+ /**
628
+ * When a Keto call is auth-rejected while it was authenticated by something
629
+ * other than an Ory Network project API key (`ory_pat_…`) — e.g. a DCR
630
+ * OAuth2 access token — the credential *type* is wrong, not merely expired.
631
+ * Ory Network's Permission / Relationship APIs only accept a project API
632
+ * key. Emit an actionable hint so the failure isn't misread as a stale
633
+ * session (the generic `session_inactive` classification of the raw 401).
634
+ */
635
+ warnIfKetoCredentialMismatch(oryErr) {
636
+ if (oryErr.code !== "session_inactive" && oryErr.code !== "forbidden")
637
+ return;
638
+ const ketoToken = this._adminApiKey ?? this._agentPrincipal.token;
639
+ // A real project API key is already in use — treat as a genuine auth error.
640
+ if (typeof ketoToken === "string" && ketoToken.startsWith("ory_pat_"))
641
+ return;
642
+ this.logger.warn("permission.credential_mismatch", {
643
+ code: oryErr.code,
644
+ message: "Ory Network's Permission/Relationship APIs require a project API key " +
645
+ "(ory_pat_…); the agent's OAuth2 access token is not accepted. Create a " +
646
+ "key in the Ory Console (Project settings → API keys) and set it with " +
647
+ "`configure --api-key <key>` or the ORY_AGENT_API_KEY env var.",
648
+ });
649
+ }
563
650
  // ─── Error Classification ────────────────────────────────────────
564
651
  /**
565
652
  * Classify an error from any Ory API call into a structured OryError.
@@ -666,6 +753,7 @@ class OryAgentClient {
666
753
  "Ory integration is disabled — all hooks will pass through. " +
667
754
  "Set ORY_PROJECT_URL or run the plugin's configure command.",
668
755
  });
756
+ rehydrateUserPrincipal(client);
669
757
  attachOtlpExporterFromEnv(client, harness);
670
758
  return client;
671
759
  }
@@ -684,11 +772,45 @@ class OryAgentClient {
684
772
  projectUrlSource: resolved.projectUrlSource,
685
773
  apiKeySource: resolved.apiKeySource,
686
774
  });
775
+ rehydrateUserPrincipal(client);
687
776
  attachOtlpExporterFromEnv(client, harness);
688
777
  return client;
689
778
  }
690
779
  }
691
780
  exports.OryAgentClient = OryAgentClient;
781
+ /**
782
+ * Rehydrate the user principal from persisted OAuth2 tokens.
783
+ *
784
+ * The interactive user login ({@link ensureUserAuthenticated}) populates
785
+ * `client.userPrincipal` in memory *and* caches the tokens to the shared
786
+ * config file. But subprocess-hook harnesses (claude-code, codex,
787
+ * gemini-cli) spawn a fresh process — and thus a fresh client via
788
+ * {@link OryAgentClient.fromEnv} — for every lifecycle event. Only the
789
+ * `SessionStart` process runs the login gate, so on later events (e.g.
790
+ * `PreToolUse`) the in-memory principal is gone and permission checks
791
+ * would fall back to a per-launch `session:<id>` subject that matches no
792
+ * bootstrapped tuple.
793
+ *
794
+ * Seeding the principal from the cached tokens here makes the *stable*
795
+ * user subject available on every constructed client, independent of which
796
+ * lifecycle event is running. The live login gate still overwrites this
797
+ * with fresh values when it runs at `SessionStart`.
798
+ *
799
+ * The subject is attached whenever it is known — even for an expired token,
800
+ * since the subject is what permission checks resolve against and Keto
801
+ * checks authenticate with the admin API key, not this token. The access
802
+ * token itself is only carried forward while still valid, so nothing
803
+ * authenticates with a stale bearer.
804
+ */
805
+ function rehydrateUserPrincipal(client) {
806
+ const tokens = (0, auth_store_js_1.loadTokens)();
807
+ if (!tokens?.subject)
808
+ return;
809
+ client.setUserPrincipal({
810
+ subject: tokens.subject,
811
+ token: (0, auth_store_js_1.isExpired)(tokens) ? undefined : tokens.accessToken,
812
+ });
813
+ }
692
814
  /** Default debug-log path under the shared per-harness data dir. */
693
815
  exports.DEFAULT_DEBUG_LOG_FILENAME = "ory-agent-debug.log";
694
816
  /** Default trace (NDJSON) path under the shared per-harness data dir. */
@@ -23,7 +23,10 @@
23
23
  * rather than re-implementing the Console API: the CLI already owns Network
24
24
  * browser login + account creation, workspace/project listing, and OAuth2
25
25
  * client creation, and the client it creates is exactly the one the plugin
26
- * READMEs document for manual setup.
26
+ * READMEs document for manual setup. The one exception is minting the project
27
+ * API key that runtime permission checks need — the CLI exposes no command for
28
+ * it, so that single step calls the Console API directly, reusing the CLI's
29
+ * stored session for auth (see {@link createProjectApiKeyViaConsole}).
27
30
  */
28
31
  import { OryAgentClient } from "./client.js";
29
32
  import { ensureUserAuthenticated } from "./user-login.js";
@@ -83,6 +86,20 @@ export interface InteractiveSetupDeps {
83
86
  * failed. Defaults to a real `npm install` of {@link ORY_CLI_NPM_SPEC}.
84
87
  */
85
88
  installOryCliFn?: () => Promise<OryCliRunner | null>;
89
+ /**
90
+ * Injectable project-API-key minter. Returns the `ory_pat_…` value or null on
91
+ * any failure. Defaults to {@link createProjectApiKeyViaConsole} (Ory Console
92
+ * API, authenticated with the `ory` CLI's stored session). Tests stub this to
93
+ * avoid real HTTP.
94
+ */
95
+ createProjectApiKeyFn?: (args: CreateProjectApiKeyArgs) => Promise<string | null>;
96
+ /**
97
+ * Injectable validity probe for an already-stored project API key. Returns
98
+ * true when the key still authenticates against Keto, false when it's been
99
+ * revoked/deleted (so provisioning re-mints one). Defaults to
100
+ * {@link apiKeyAuthenticates}. Tests stub this to avoid real HTTP.
101
+ */
102
+ validateApiKeyFn?: (apiKey: string, projectUrl: string) => Promise<boolean>;
86
103
  }
87
104
  export type InteractiveSetupOutcome = "skipped_flag" | "skipped_no_tty" | "skipped_configured" | "audit_only" | "local_configured" | "network_configured" | "network_fallback";
88
105
  export interface InteractiveSetupResult {
@@ -121,3 +138,28 @@ export declare function runInteractiveSetup(binName: string, harness: string, ar
121
138
  * silently misconfigure the plugin.
122
139
  */
123
140
  export declare function projectUrlFromSlug(slug: string): string;
141
+ export interface CreateProjectApiKeyArgs {
142
+ projectId: string;
143
+ /** Human-readable label shown in the Console's API-keys list. */
144
+ name: string;
145
+ }
146
+ /**
147
+ * Mint an Ory Network **project API key** via the Console API
148
+ * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
149
+ * Returns the `ory_pat_…` value, or null on any failure (no session, non-2xx,
150
+ * missing value). Never throws — provisioning is best-effort.
151
+ *
152
+ * This is the one place the wizard reaches past the `ory` CLI to the Console
153
+ * API: the CLI (as of v1.3) exposes no project-API-key command, and Keto
154
+ * permission checks can't authenticate without one.
155
+ */
156
+ export declare function createProjectApiKeyViaConsole(args: CreateProjectApiKeyArgs): Promise<string | null>;
157
+ /**
158
+ * Best-effort check that an already-stored project API key still authenticates
159
+ * against Keto. Runs one permission check with the key: if it completes (any
160
+ * `allowed` value) the key is valid; an auth rejection (`session_inactive` /
161
+ * `forbidden` / `session_aal2_required`) means it was revoked or deleted, so we
162
+ * return false and the caller re-provisions. Transient failures (network /
163
+ * rate-limit / unknown) return true so we don't re-mint on a blip. Never throws.
164
+ */
165
+ export declare function apiKeyAuthenticates(apiKey: string, projectUrl: string): Promise<boolean>;
@@ -24,7 +24,10 @@
24
24
  * rather than re-implementing the Console API: the CLI already owns Network
25
25
  * browser login + account creation, workspace/project listing, and OAuth2
26
26
  * client creation, and the client it creates is exactly the one the plugin
27
- * READMEs document for manual setup.
27
+ * READMEs document for manual setup. The one exception is minting the project
28
+ * API key that runtime permission checks need — the CLI exposes no command for
29
+ * it, so that single step calls the Console API directly, reusing the CLI's
30
+ * stored session for auth (see {@link createProjectApiKeyViaConsole}).
28
31
  */
29
32
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
30
33
  if (k2 === undefined) k2 = k;
@@ -65,6 +68,8 @@ exports.createOryRunner = createOryRunner;
65
68
  exports.runPostInstall = runPostInstall;
66
69
  exports.runInteractiveSetup = runInteractiveSetup;
67
70
  exports.projectUrlFromSlug = projectUrlFromSlug;
71
+ exports.createProjectApiKeyViaConsole = createProjectApiKeyViaConsole;
72
+ exports.apiKeyAuthenticates = apiKeyAuthenticates;
68
73
  const node_child_process_1 = require("node:child_process");
69
74
  const fs = __importStar(require("node:fs"));
70
75
  const os = __importStar(require("node:os"));
@@ -601,6 +606,12 @@ async function configureNetwork(binName, harness, ctx) {
601
606
  if (subject) {
602
607
  await bootstrapPermissionsViaOry(runner, project.id, harness, subject, prompt);
603
608
  }
609
+ // 10. Provision the project API key runtime permission *checks* need. Same
610
+ // root cause as bootstrap's admin path — the DCR OAuth2 token can't
611
+ // authenticate Ory Network's Permission API — but this credential is used
612
+ // at runtime, so it's persisted to config. Placed last so its prompt
613
+ // doesn't consume answers meant for earlier steps.
614
+ await maybeProvisionProjectApiKey(binName, harness, project.id, projectUrl, prompt, ctx.deps.createProjectApiKeyFn ?? createProjectApiKeyViaConsole, ctx.deps.validateApiKeyFn ?? apiKeyAuthenticates);
604
615
  return {
605
616
  outcome: "network_configured",
606
617
  projectUrl,
@@ -1317,3 +1328,171 @@ function printManualOAuth2ClientHelp(binName, projectId) {
1317
1328
  ui.blank();
1318
1329
  ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --oauth2-client-id <CLIENT_ID>`);
1319
1330
  }
1331
+ /** Ory Console API base. The `ory` CLI has no project-API-key command, so key
1332
+ * provisioning talks to the Console API directly, authenticated with the CLI's
1333
+ * stored Ory Network session. Override with `ORY_CONSOLE_API_URL`. */
1334
+ const CONSOLE_API_URL = process.env.ORY_CONSOLE_API_URL?.trim() || "https://api.console.ory.sh";
1335
+ /** Path to the session the `ory` CLI persists after `ory auth`. */
1336
+ const ORY_CLI_SESSION_FILE = ".ory-cloud.json";
1337
+ /**
1338
+ * Read the Ory Network session bearer the `ory` CLI stores at
1339
+ * `~/.ory-cloud.json`. Returns null when the file is absent, unparseable, has
1340
+ * no token, or the token has expired. Never throws.
1341
+ */
1342
+ function readConsoleSessionToken() {
1343
+ try {
1344
+ const raw = fs.readFileSync(path.join(os.homedir(), ORY_CLI_SESSION_FILE), "utf-8");
1345
+ const parsed = JSON.parse(raw);
1346
+ const token = parsed.access_token?.access_token;
1347
+ if (!token)
1348
+ return null;
1349
+ const expiry = parsed.access_token?.expiry;
1350
+ if (expiry) {
1351
+ const expMs = Date.parse(expiry);
1352
+ if (Number.isFinite(expMs) && expMs <= Date.now())
1353
+ return null;
1354
+ }
1355
+ return token;
1356
+ }
1357
+ catch {
1358
+ return null;
1359
+ }
1360
+ }
1361
+ /**
1362
+ * Mint an Ory Network **project API key** via the Console API
1363
+ * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
1364
+ * Returns the `ory_pat_…` value, or null on any failure (no session, non-2xx,
1365
+ * missing value). Never throws — provisioning is best-effort.
1366
+ *
1367
+ * This is the one place the wizard reaches past the `ory` CLI to the Console
1368
+ * API: the CLI (as of v1.3) exposes no project-API-key command, and Keto
1369
+ * permission checks can't authenticate without one.
1370
+ */
1371
+ async function createProjectApiKeyViaConsole(args) {
1372
+ const token = readConsoleSessionToken();
1373
+ if (!token)
1374
+ return null;
1375
+ try {
1376
+ const res = await fetch(`${CONSOLE_API_URL}/projects/${args.projectId}/tokens`, {
1377
+ method: "POST",
1378
+ headers: {
1379
+ Authorization: `Bearer ${token}`,
1380
+ "Content-Type": "application/json",
1381
+ },
1382
+ body: JSON.stringify({ name: args.name }),
1383
+ });
1384
+ if (!res.ok)
1385
+ return null;
1386
+ const body = (await res.json());
1387
+ return typeof body.value === "string" && body.value.length > 0 ? body.value : null;
1388
+ }
1389
+ catch {
1390
+ return null;
1391
+ }
1392
+ }
1393
+ /**
1394
+ * Best-effort check that an already-stored project API key still authenticates
1395
+ * against Keto. Runs one permission check with the key: if it completes (any
1396
+ * `allowed` value) the key is valid; an auth rejection (`session_inactive` /
1397
+ * `forbidden` / `session_aal2_required`) means it was revoked or deleted, so we
1398
+ * return false and the caller re-provisions. Transient failures (network /
1399
+ * rate-limit / unknown) return true so we don't re-mint on a blip. Never throws.
1400
+ */
1401
+ async function apiKeyAuthenticates(apiKey, projectUrl) {
1402
+ try {
1403
+ // Direct construction (no fromEnv) seeds the key as both agent token and
1404
+ // admin key and skips DCR, so the probe uses exactly this key. No trace
1405
+ // file is configured, so nothing is written to disk.
1406
+ const probe = new client_js_1.OryAgentClient({ projectUrl, apiKey, harness: "install-probe" });
1407
+ await probe.checkPermission({
1408
+ namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
1409
+ object: "__ory_agent_key_probe__",
1410
+ relation: "__ory_agent_key_probe__",
1411
+ subjectId: "__ory_agent_key_probe__",
1412
+ });
1413
+ return true;
1414
+ }
1415
+ catch (err) {
1416
+ const code = err?.code;
1417
+ if (code === "session_inactive" || code === "forbidden" || code === "session_aal2_required") {
1418
+ return false;
1419
+ }
1420
+ return true;
1421
+ }
1422
+ }
1423
+ /**
1424
+ * Manual fallback steps for the project API key, shown when auto-provisioning
1425
+ * is declined or fails. The `configure --api-key` command is the stable part;
1426
+ * the Console link is a convenience.
1427
+ */
1428
+ function printManualApiKeySteps(binName, projectId) {
1429
+ ui.info("Create one in the Ory Console (Project settings → API keys):");
1430
+ ui.command(`https://console.ory.sh/projects/${projectId}/settings`);
1431
+ ui.info("then store it so permission checks authenticate with it:");
1432
+ ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --api-key <ory_pat_…>`);
1433
+ ui.hint("Observe mode works without it; enforce mode needs it to allow tools.");
1434
+ }
1435
+ /**
1436
+ * Provision the project API key that runtime Keto permission checks
1437
+ * authenticate with. The agent's DCR OAuth2 token can't — Ory Network rejects
1438
+ * it — so `enforce` mode needs a project API key. Best-effort and skippable:
1439
+ * offers to mint one via the Console API and persist it, and on decline / no
1440
+ * TTY / any failure falls back to manual guidance. No-ops when a key is already
1441
+ * configured (env or config file).
1442
+ */
1443
+ async function maybeProvisionProjectApiKey(binName, harness, projectId, projectUrl, prompt, createFn, validateFn) {
1444
+ const resolved = (0, config_js_1.resolveConfig)();
1445
+ if (resolved.apiKey) {
1446
+ // An operator-set env key is authoritative — never touch it.
1447
+ if (resolved.apiKeySource === "env")
1448
+ return;
1449
+ // A stored (config-file) key that no longer authenticates — revoked or
1450
+ // deleted — must not wedge setup: validate it and re-provision if it's
1451
+ // dead. Presence alone isn't enough (a dangling key still fails at runtime
1452
+ // with session_inactive).
1453
+ if (await validateFn(resolved.apiKey, projectUrl))
1454
+ return;
1455
+ ui.heading("Project API key");
1456
+ ui.warning("The stored project API key is no longer valid (revoked or deleted).");
1457
+ }
1458
+ else {
1459
+ ui.heading("Project API key");
1460
+ ui.info("Runtime permission checks call Ory's Permission API, which authenticates");
1461
+ ui.hint("with a project API key — the agent's OAuth2 token isn't accepted there.");
1462
+ ui.hint("(Observe mode works without it; enforce mode needs it to allow tools.)");
1463
+ }
1464
+ ui.blank();
1465
+ const answer = await prompt(ui.promptLine("Create a project API key now and store it?", { hint: "[Y/n]" }));
1466
+ // `null` = no TTY / cancelled; treat as skip so we never mint without consent.
1467
+ if (answer !== null && !isNo(answer)) {
1468
+ ui.step("Creating a project API key via the Ory Console…");
1469
+ let key = null;
1470
+ try {
1471
+ // Name each key uniquely (harness + host + timestamp) so keys are
1472
+ // distinguishable in the Console and a re-provision never collides with
1473
+ // an existing entry.
1474
+ key = await createFn({ projectId, name: projectApiKeyName(harness) });
1475
+ }
1476
+ catch {
1477
+ key = null;
1478
+ }
1479
+ if (key) {
1480
+ (0, config_js_1.saveConfig)({ apiKey: key });
1481
+ ui.success("Project API key created and stored — permission checks will use it.");
1482
+ return;
1483
+ }
1484
+ ui.warning("Couldn't create a project API key automatically (Console session may be missing or expired).");
1485
+ }
1486
+ printManualApiKeySteps(binName, projectId);
1487
+ }
1488
+ /** Distinguishable, collision-free label for a provisioned project API key. */
1489
+ function projectApiKeyName(harness) {
1490
+ let host = "unknown-host";
1491
+ try {
1492
+ host = os.hostname();
1493
+ }
1494
+ catch {
1495
+ /* keep fallback */
1496
+ }
1497
+ return `ory-agent-plugins ${harness} ${host} ${new Date().toISOString()}`;
1498
+ }
@@ -71,21 +71,6 @@ const ENV_PERMISSION_MODE = "ORY_PERMISSION_MODE";
71
71
  function resolveNamespace() {
72
72
  return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
73
73
  }
74
- /**
75
- * Apply persisted user OAuth2 tokens (if any) to the client's user
76
- * principal so downstream subject resolution and tuple writes see a
77
- * concrete identity. Mirrors what `ensureUserAuthenticated` does on a
78
- * cache hit — but works whether or not `ORY_USER_LOGIN` is enabled.
79
- */
80
- function attachCachedUserPrincipal(client) {
81
- const tokens = (0, auth_store_js_1.loadTokens)();
82
- if (!tokens || (0, auth_store_js_1.isExpired)(tokens))
83
- return;
84
- client.setUserPrincipal({
85
- subject: tokens.subject,
86
- token: tokens.accessToken,
87
- });
88
- }
89
74
  /**
90
75
  * Entry point invoked by each harness CLI's `permissions` case.
91
76
  *
@@ -178,7 +163,6 @@ async function runPermissionsStatus(binName, harness) {
178
163
  return 0;
179
164
  }
180
165
  const client = client_js_1.OryAgentClient.fromEnv(harness);
181
- attachCachedUserPrincipal(client);
182
166
  await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
183
167
  /* best-effort; status keeps working in pass-through */
184
168
  });
@@ -260,7 +244,6 @@ async function runPermissionsBootstrap(binName, harness, args) {
260
244
  return 1;
261
245
  }
262
246
  const client = client_js_1.OryAgentClient.fromEnv(harness);
263
- attachCachedUserPrincipal(client);
264
247
  await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
265
248
  /* best-effort; the write may still succeed with the API key or no auth (local Keto) */
266
249
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",