@ory/argus 1.5.1 → 1.5.3

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
@@ -112,8 +112,11 @@ Headless deployments normally supply only `ORY_AGENT_API_KEY` (or an injected ru
112
112
  authenticator such as SPIFFE) plus `ORY_PROJECT_URL` and `ORY_AGENT_SECURITY_URL`.
113
113
  Before PKCE, Argus resolves the administrator-provisioned subject and opaque root from
114
114
  Agent Security, keeps them in process memory, and reuses the root unchanged for child
115
- lineage. `ORY_AGENT_SUBJECT_ID` and `ORY_AGENT_ROOT_DELEGATION` remain compatibility
116
- overrides; a complete legacy package skips the context request.
115
+ lineage. Subprocess hooks restore that context from a non-secret, credential-bound
116
+ per-session record in `config.json` instead of requesting it per tool call. Without a
117
+ user, tools are checked as the deployment owner. `ORY_AGENT_SUBJECT_ID` and
118
+ `ORY_AGENT_ROOT_DELEGATION` remain compatibility overrides; a complete legacy package
119
+ skips the context request.
117
120
 
118
121
  ### Logger
119
122
 
package/dist/adapters.js CHANGED
@@ -119,6 +119,15 @@ async function sessionStart(client, opts) {
119
119
  fetchImpl: opts.runtimeContextFetch,
120
120
  });
121
121
  rootDelegation = context?.rootDelegation;
122
+ // Subprocess harnesses gate tools in later processes; let them restore
123
+ // this context instead of resolving it again on every tool call.
124
+ if (context) {
125
+ (0, agent_auth_js_1.saveRuntimeContext)(client, context, {
126
+ harness: opts.harness,
127
+ sessionId: opts.sessionId ?? client.sessionId,
128
+ projectUrl,
129
+ });
130
+ }
122
131
  }
123
132
  catch (err) {
124
133
  client.logger.warn("agent.runtime_context.failed", describeThrown(err));
@@ -290,6 +299,13 @@ async function writeUserDelegatesAgent(client, harness, sessionId, env = process
290
299
  async function registerSubagent(client, opts) {
291
300
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
292
301
  const subAgentGate = opts.subAgentGate ?? agent_auth_js_1.ensureSubAgentIdentity;
302
+ // A child of a headless deployment hangs off the deployment root; a
303
+ // subprocess restores it from the session's persisted runtime context.
304
+ await (0, agent_auth_js_1.ensureRuntimeContext)(client, {
305
+ harness: opts.harness,
306
+ sessionId: opts.sessionId,
307
+ env: opts.env,
308
+ });
293
309
  let identity;
294
310
  try {
295
311
  identity = await subAgentGate(client, {
@@ -403,6 +419,12 @@ function gateActivityAttributes(opts) {
403
419
  * separately reports actual execution; {@link gate} adds the standard events.
404
420
  */
405
421
  async function decideTool(client, opts) {
422
+ if ((0, config_js_1.isSecurityConnected)()) {
423
+ // A subprocess hook of a headless deployment starts without the context
424
+ // its session-start process resolved. Restore it (or resolve it once) so
425
+ // the deployment owner, not a session placeholder, is checked (#338).
426
+ await (0, agent_auth_js_1.ensureRuntimeContext)(client, { harness: opts.harness, allowResolve: true });
427
+ }
406
428
  const namespace = resolveNamespace();
407
429
  const subjectRef = (0, subject_js_1.resolveUserSubject)(client, opts.subjectFallback);
408
430
  const label = (0, subject_js_1.subjectLabel)(subjectRef);
@@ -25,6 +25,51 @@ export interface ResolvePreprovisionedRuntimeContextOptions {
25
25
  }
26
26
  /** Resolve missing headless deployment metadata using the existing runtime proof. */
27
27
  export declare function resolvePreprovisionedRuntimeContext(client: OryAgentClient, options: ResolvePreprovisionedRuntimeContextOptions): Promise<RuntimeContext | undefined>;
28
+ /**
29
+ * SHA-256 binding of a runtime credential for persisted runtime context. It is
30
+ * derived from the credential's existing non-secret fingerprint and is never
31
+ * the secret itself. Credentials without a stable fingerprint (programmatic
32
+ * injected authenticators) are not persisted.
33
+ */
34
+ export declare function runtimeCredentialBinding(credential: RuntimeCredential | undefined): string | undefined;
35
+ export interface SaveRuntimeContextOptions {
36
+ harness: string;
37
+ sessionId?: string;
38
+ /** Project the context was resolved for. Defaults to the client's project. */
39
+ projectUrl?: string;
40
+ }
41
+ /** Persist a resolved runtime context for later hook processes of the same session. */
42
+ export declare function saveRuntimeContext(client: OryAgentClient, context: RuntimeContext, options: SaveRuntimeContextOptions): boolean;
43
+ export interface RestoreRuntimeContextOptions {
44
+ harness: string;
45
+ sessionId?: string;
46
+ /** Authoritative agent type required of the context. Defaults to `harness`. */
47
+ agentType?: string;
48
+ }
49
+ /**
50
+ * Restore a persisted runtime context into a fresh client. Loads only: it
51
+ * succeeds when the persisted entry is bound to the active runtime credential,
52
+ * the client's project and Agent Security URLs, and the expected agent type.
53
+ */
54
+ export declare function restoreRuntimeContext(client: OryAgentClient, options: RestoreRuntimeContextOptions): RuntimeContext | undefined;
55
+ export interface EnsureRuntimeContextOptions extends RestoreRuntimeContextOptions {
56
+ env?: NodeJS.ProcessEnv;
57
+ fetchImpl?: typeof fetch;
58
+ /** Allow one bounded runtime-context request when nothing is persisted. */
59
+ allowResolve?: boolean;
60
+ timeoutMs?: number;
61
+ }
62
+ /**
63
+ * Make the headless deployment context available to a tool or event path.
64
+ *
65
+ * Restores the session's persisted context (no network). When none exists —
66
+ * for example a harness that skipped its session-start hook — and
67
+ * `allowResolve` is set, it resolves the context once with a bounded request
68
+ * and persists it so later hook processes only load. A failed resolution is
69
+ * not retried by the same client for {@link LAZY_RUNTIME_CONTEXT_RETRY_MS}.
70
+ * Never throws.
71
+ */
72
+ export declare function ensureRuntimeContext(client: OryAgentClient, options: EnsureRuntimeContextOptions): Promise<RuntimeContext | undefined>;
28
73
  /**
29
74
  * Delegation context handed to {@link registerAgentClient} so the freshly
30
75
  * minted OAuth2 client is stamped with its delegator at the identity layer.
@@ -356,6 +401,8 @@ export interface EnsureAgentIdentityOptions {
356
401
  * What prompted this resolution, recorded on the `agent.auth` activity event. Absent for
357
402
  * the session-start gate; `read_credential` when the tool gate resolved the
358
403
  * identity because it needed a credential to authenticate a permission read.
404
+ * A triggered resolution that cleanly loads a stored credential records no
405
+ * activity; only its failures and warnings do.
359
406
  */
360
407
  trigger?: string;
361
408
  }
@@ -363,8 +410,10 @@ export interface EnsureAgentIdentityOptions {
363
410
  * Resolve the agent's credentials and attach them to the client.
364
411
  *
365
412
  * Always returns the resolved credentials; never throws and never blocks.
366
- * Emits exactly one `agent.auth` activity event so the audit trail records whether
367
- * the agent identity was available for the session.
413
+ * Emits one `agent.auth` activity event so the audit trail records whether the
414
+ * agent identity was available for the session. A `trigger`ed (tool-path)
415
+ * resolution emits it only when the credential is missing or resolved with
416
+ * warnings; a clean reload of the stored credential is logged at debug level.
368
417
  */
369
418
  export declare function ensureAgentIdentity(client: OryAgentClient, options?: EnsureAgentIdentityOptions): Promise<AgentCredentials>;
370
419
  /**
@@ -16,6 +16,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.DEFAULT_SESSION_RETENTION = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = void 0;
17
17
  exports.resolvePreprovisionedRootDelegation = resolvePreprovisionedRootDelegation;
18
18
  exports.resolvePreprovisionedRuntimeContext = resolvePreprovisionedRuntimeContext;
19
+ exports.runtimeCredentialBinding = runtimeCredentialBinding;
20
+ exports.saveRuntimeContext = saveRuntimeContext;
21
+ exports.restoreRuntimeContext = restoreRuntimeContext;
22
+ exports.ensureRuntimeContext = ensureRuntimeContext;
19
23
  exports.resolveRuntimeAgentCredentials = resolveRuntimeAgentCredentials;
20
24
  exports._resetAgentCredentialsCache = _resetAgentCredentialsCache;
21
25
  exports.invalidateAgentAccessToken = invalidateAgentAccessToken;
@@ -46,6 +50,7 @@ exports.resolveAgentCredentials = resolveAgentCredentials;
46
50
  exports.fetchClientCredentialsToken = fetchClientCredentialsToken;
47
51
  exports.ensureAgentIdentity = ensureAgentIdentity;
48
52
  exports.ensureSubAgentIdentity = ensureSubAgentIdentity;
53
+ const node_crypto_1 = require("node:crypto");
49
54
  const promises_1 = require("node:fs/promises");
50
55
  const node_os_1 = require("node:os");
51
56
  const node_path_1 = require("node:path");
@@ -53,6 +58,7 @@ const promises_2 = require("node:timers/promises");
53
58
  const client_1 = require("@ory/client");
54
59
  const config_js_1 = require("./config.js");
55
60
  const branding_js_1 = require("./branding.js");
61
+ const context_js_1 = require("./context.js");
56
62
  const subject_js_1 = require("./subject.js");
57
63
  const runtime_credential_js_1 = require("./runtime-credential.js");
58
64
  const ENROLLMENT_LOCK_STALE_MS = 120_000;
@@ -147,12 +153,192 @@ async function resolvePreprovisionedRuntimeContext(client, options) {
147
153
  if (context.agentType !== options.expectedAgentType) {
148
154
  throw new Error("Runtime context agent type does not match the adapter type");
149
155
  }
156
+ activateRuntimeContext(client, credential, context);
157
+ return context;
158
+ }
159
+ function activateRuntimeContext(client, credential, context) {
150
160
  const contextualCredential = { ...credential, subject: context.subjectId };
151
161
  validatedRuntimeSubjects.add(contextualCredential);
152
162
  client.setRuntimeCredential(contextualCredential);
153
163
  client.setRuntimeContext(context);
164
+ }
165
+ // ─── Persisted runtime context (subprocess hooks) ────────────────────
166
+ //
167
+ // Subprocess harnesses run every hook event in a new process, so the context
168
+ // resolved at session start would otherwise exist only in the session-start
169
+ // process. The non-secret context is persisted per (harness, session) and bound
170
+ // to a fingerprint of the runtime credential plus the canonical project and
171
+ // Agent Security URLs; a rotated key or another project never reuses it.
172
+ const LAZY_RUNTIME_CONTEXT_TIMEOUT_MS = 10_000;
173
+ const LAZY_RUNTIME_CONTEXT_RETRY_MS = 60_000;
174
+ const lazyRuntimeContextAttempts = new WeakMap();
175
+ /**
176
+ * SHA-256 binding of a runtime credential for persisted runtime context. It is
177
+ * derived from the credential's existing non-secret fingerprint and is never
178
+ * the secret itself. Credentials without a stable fingerprint (programmatic
179
+ * injected authenticators) are not persisted.
180
+ */
181
+ function runtimeCredentialBinding(credential) {
182
+ let source;
183
+ if (credential?.kind === "talos" && credential.apiKeyFingerprint) {
184
+ source = `talos\0${credential.apiKeyFingerprint}`;
185
+ }
186
+ else if (credential?.kind === "injected" && credential.spiffeIdFingerprint) {
187
+ source = `spiffe\0${credential.spiffeIdFingerprint}`;
188
+ }
189
+ return source ? (0, node_crypto_1.createHash)("sha256").update(source).digest("hex") : undefined;
190
+ }
191
+ function canonicalUrlOrUndefined(url) {
192
+ if (!url)
193
+ return undefined;
194
+ try {
195
+ return (0, runtime_credential_js_1.canonicalProjectUrl)(url);
196
+ }
197
+ catch {
198
+ return undefined;
199
+ }
200
+ }
201
+ /** Persist a resolved runtime context for later hook processes of the same session. */
202
+ function saveRuntimeContext(client, context, options) {
203
+ const credential = runtimeCredentialBinding(client.rootRuntimeCredential);
204
+ const projectUrl = canonicalUrlOrUndefined(options.projectUrl ?? client.projectUrl);
205
+ const agentSecurityUrl = canonicalUrlOrUndefined(client.agentSecurityUrl);
206
+ if (!credential || !projectUrl || !agentSecurityUrl)
207
+ return false;
208
+ const sessionKey = options.sessionId?.trim() || config_js_1.SESSIONLESS_KEY;
209
+ const entry = {
210
+ credential,
211
+ projectUrl,
212
+ agentSecurityUrl,
213
+ subjectId: context.subjectId,
214
+ rootDelegation: context.rootDelegation,
215
+ deploymentId: context.deploymentId,
216
+ agentType: context.agentType,
217
+ };
218
+ (0, config_js_1.mutateConfig)((current) => {
219
+ const existing = current.runtimeContexts?.[options.harness] ?? {};
220
+ const previous = existing[sessionKey];
221
+ if (previous && JSON.stringify(previous) === JSON.stringify(entry))
222
+ return current;
223
+ const merged = { ...existing };
224
+ delete merged[sessionKey];
225
+ merged[sessionKey] = entry;
226
+ const keys = Object.keys(merged);
227
+ const kept = keys.slice(Math.max(0, keys.length - config_js_1.DELEGATION_NODE_HISTORY));
228
+ return {
229
+ ...current,
230
+ runtimeContexts: {
231
+ ...(current.runtimeContexts ?? {}),
232
+ [options.harness]: Object.fromEntries(kept.map((key) => [key, merged[key]])),
233
+ },
234
+ };
235
+ });
236
+ return true;
237
+ }
238
+ /**
239
+ * Restore a persisted runtime context into a fresh client. Loads only: it
240
+ * succeeds when the persisted entry is bound to the active runtime credential,
241
+ * the client's project and Agent Security URLs, and the expected agent type.
242
+ */
243
+ function restoreRuntimeContext(client, options) {
244
+ const active = client.runtimeContext;
245
+ if (active)
246
+ return active;
247
+ const credential = client.rootRuntimeCredential;
248
+ const binding = runtimeCredentialBinding(credential);
249
+ if (!credential || !binding)
250
+ return undefined;
251
+ const sessionKey = (options.sessionId ?? client.sessionId)?.trim() || config_js_1.SESSIONLESS_KEY;
252
+ const entry = (0, config_js_1.loadConfig)().runtimeContexts?.[options.harness]?.[sessionKey];
253
+ if (!entry ||
254
+ entry.credential !== binding ||
255
+ entry.projectUrl !== canonicalUrlOrUndefined(client.projectUrl) ||
256
+ entry.agentSecurityUrl !== canonicalUrlOrUndefined(client.agentSecurityUrl) ||
257
+ entry.agentType !== (options.agentType ?? options.harness)) {
258
+ return undefined;
259
+ }
260
+ let context;
261
+ try {
262
+ context = parseRuntimeContext({
263
+ agent_type: entry.agentType,
264
+ authenticated: true,
265
+ deployment_id: entry.deploymentId,
266
+ root_delegation: entry.rootDelegation,
267
+ subject_id: entry.subjectId,
268
+ });
269
+ }
270
+ catch {
271
+ return undefined;
272
+ }
273
+ activateRuntimeContext(client, credential, context);
154
274
  return context;
155
275
  }
276
+ /**
277
+ * True when a hook may act for a headless deployment owner: an
278
+ * administrator-supplied runtime credential (`ORY_AGENT_API_KEY` or an injected
279
+ * authenticator) is configured, no user principal is known, and no complete
280
+ * environment package already identifies the runtime. A Talos credential the
281
+ * plugin enrolled for an interactive user never qualifies.
282
+ */
283
+ function runtimeOwnerContextEligible(client, env) {
284
+ if (client.userPrincipal.subject || env.ORY_USER_SUBJECT_ID?.trim())
285
+ return false;
286
+ if (resolvePreprovisionedRootDelegation(env, undefined, client.runtimeCredential))
287
+ return false;
288
+ return Boolean(env.ORY_AGENT_API_KEY?.trim()) || client.rootRuntimeCredential?.kind === "injected";
289
+ }
290
+ /**
291
+ * Make the headless deployment context available to a tool or event path.
292
+ *
293
+ * Restores the session's persisted context (no network). When none exists —
294
+ * for example a harness that skipped its session-start hook — and
295
+ * `allowResolve` is set, it resolves the context once with a bounded request
296
+ * and persists it so later hook processes only load. A failed resolution is
297
+ * not retried by the same client for {@link LAZY_RUNTIME_CONTEXT_RETRY_MS}.
298
+ * Never throws.
299
+ */
300
+ async function ensureRuntimeContext(client, options) {
301
+ const active = client.runtimeContext;
302
+ if (active)
303
+ return active;
304
+ const env = options.env ?? process.env;
305
+ try {
306
+ if (!runtimeOwnerContextEligible(client, env))
307
+ return undefined;
308
+ const restored = restoreRuntimeContext(client, options);
309
+ // Resolve only as the root runtime: an acting child credential must not
310
+ // stand in for its deployment's root.
311
+ if (restored || !options.allowResolve || (0, context_js_1.currentSessionContext)().runtimeCredential) {
312
+ return restored;
313
+ }
314
+ const sessionKey = (options.sessionId ?? client.sessionId)?.trim() || config_js_1.SESSIONLESS_KEY;
315
+ const attempts = lazyRuntimeContextAttempts.get(client) ?? new Map();
316
+ lazyRuntimeContextAttempts.set(client, attempts);
317
+ const lastAttempt = attempts.get(sessionKey);
318
+ if (lastAttempt !== undefined && Date.now() - lastAttempt < LAZY_RUNTIME_CONTEXT_RETRY_MS) {
319
+ return undefined;
320
+ }
321
+ attempts.set(sessionKey, Date.now());
322
+ const context = await resolvePreprovisionedRuntimeContext(client, {
323
+ projectUrl: client.projectUrl,
324
+ expectedAgentType: options.agentType ?? options.harness,
325
+ env,
326
+ fetchImpl: options.fetchImpl,
327
+ signal: AbortSignal.timeout(options.timeoutMs ?? LAZY_RUNTIME_CONTEXT_TIMEOUT_MS),
328
+ });
329
+ if (context) {
330
+ attempts.delete(sessionKey);
331
+ saveRuntimeContext(client, context, { harness: options.harness, sessionId: sessionKey });
332
+ }
333
+ return context;
334
+ }
335
+ catch (err) {
336
+ client.logger.warn("agent.runtime_context.unavailable", {
337
+ message: err instanceof Error ? err.message : String(err),
338
+ });
339
+ return undefined;
340
+ }
341
+ }
156
342
  async function withEnrollmentLock(identity, work) {
157
343
  const lockRoot = (0, node_path_1.join)((0, config_js_1.getDataDir)(), "runtime-credential-locks");
158
344
  const lockPath = (0, node_path_1.join)(lockRoot, `${(0, runtime_credential_js_1.credentialAccount)(identity)}.lock`);
@@ -1102,8 +1288,10 @@ async function fetchClientCredentialsToken(args) {
1102
1288
  * Resolve the agent's credentials and attach them to the client.
1103
1289
  *
1104
1290
  * Always returns the resolved credentials; never throws and never blocks.
1105
- * Emits exactly one `agent.auth` activity event so the audit trail records whether
1106
- * the agent identity was available for the session.
1291
+ * Emits one `agent.auth` activity event so the audit trail records whether the
1292
+ * agent identity was available for the session. A `trigger`ed (tool-path)
1293
+ * resolution emits it only when the credential is missing or resolved with
1294
+ * warnings; a clean reload of the stored credential is logged at debug level.
1107
1295
  */
1108
1296
  async function ensureAgentIdentity(client, options = {}) {
1109
1297
  const sessionId = options.sessionId ?? client.sessionId;
@@ -1239,6 +1427,19 @@ async function ensureAgentIdentity(client, options = {}) {
1239
1427
  // resolver match" should not clear it. With nothing set, the read path falls
1240
1428
  // back to the rehydrated user token (see `buildApis`).
1241
1429
  const sessionKey = sessionId?.trim() || config_js_1.SESSIONLESS_KEY;
1430
+ // Subprocess harnesses start a fresh process for every hook, so a tool-path
1431
+ // resolution that merely reloads the stored credential would otherwise record
1432
+ // one `agent.auth` per tool call. Only the session-start gate, a failure, or a
1433
+ // partial resolution is an authentication outcome worth auditing.
1434
+ if (options.trigger && creds.kind !== "none" && !creds.warnings?.length) {
1435
+ client.logger.debug("agent.auth.loaded", {
1436
+ kind: creds.kind,
1437
+ sessionKey,
1438
+ trigger: options.trigger,
1439
+ ...(creds.subject ? { subject: creds.subject } : {}),
1440
+ });
1441
+ return creds;
1442
+ }
1242
1443
  client.logger.activity("agent.auth", creds.kind === "none" ? "skipped" : "ok", {
1243
1444
  attributes: {
1244
1445
  kind: creds.kind,
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "e0d37342a77d3ffe2922926142992924b126154e",
4
- "commitShort": "e0d37342",
3
+ "commit": "6d5a4dd670531925d1a0807d25406ccc4db24bf5",
4
+ "commitShort": "6d5a4dd6",
5
5
  "branch": "main",
6
- "commitDate": "2026-10-21T08:37:08-07:00",
6
+ "commitDate": "2026-10-22T14:21:25-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-10-21T15:41:02.900Z"
8
+ "builtAt": "2026-10-22T21:25:21.096Z"
9
9
  }
package/dist/client.d.ts CHANGED
@@ -140,6 +140,8 @@ export declare class OryAgentClient {
140
140
  constructor(config: OryAgentConfig);
141
141
  /** Build a fresh set of Ory API instances using the current agent token. */
142
142
  private buildApis;
143
+ /** Ory project URL this client sends with Agent Security requests. */
144
+ get projectUrl(): string;
143
145
  /** Canonical broker origin, with the shipped project URL fallback applied. */
144
146
  get agentSecurityUrl(): string;
145
147
  private buildBrokerPermissionTransport;
@@ -154,9 +156,22 @@ export declare class OryAgentClient {
154
156
  get agentPrincipal(): PrincipalIdentity;
155
157
  /** Active noninteractive broker credential. It is process memory only. */
156
158
  get runtimeCredential(): RuntimeCredential | undefined;
159
+ /** The session's root broker credential, ignoring an ambient child credential. */
160
+ get rootRuntimeCredential(): RuntimeCredential | undefined;
157
161
  get hasRuntimeCredential(): boolean;
158
- /** Process-local deployment context. It is never persisted or copied to the environment. */
162
+ /**
163
+ * Active deployment context. It is never copied to the environment; a
164
+ * subprocess harness restores it from the credential-bound, non-secret
165
+ * per-session record written at session start (see `restoreRuntimeContext`).
166
+ */
159
167
  get runtimeContext(): RuntimeContext | undefined;
168
+ /**
169
+ * True when this client runs as an administrator-preprovisioned runtime whose
170
+ * owner only the broker knows: a resolved runtime context, or a complete
171
+ * `ORY_AGENT_ROOT_DELEGATION` + `ORY_AGENT_SUBJECT_ID` environment package
172
+ * with its runtime credential.
173
+ */
174
+ get actsForRuntimeOwner(): boolean;
160
175
  get runtimeRootDelegation(): string | undefined;
161
176
  setRuntimeContext(context: RuntimeContext): void;
162
177
  setRuntimeCredential(credential: RuntimeCredential | undefined): void;
package/dist/client.js CHANGED
@@ -180,6 +180,10 @@ class OryAgentClient {
180
180
  permission: this.buildBrokerPermissionTransport(),
181
181
  };
182
182
  }
183
+ /** Ory project URL this client sends with Agent Security requests. */
184
+ get projectUrl() {
185
+ return this.config.projectUrl;
186
+ }
183
187
  /** Canonical broker origin, with the shipped project URL fallback applied. */
184
188
  get agentSecurityUrl() {
185
189
  return this.config.agentSecurityUrl ?? this.config.projectUrl;
@@ -268,7 +272,7 @@ class OryAgentClient {
268
272
  }
269
273
  // A preprovisioned runtime with no local user: the broker derives the
270
274
  // owner from the authenticated runtime, and no owner ID is sent.
271
- if (userSubject === undefined && subject.object === subject_js_1.RUNTIME_OWNER_SUBJECT && this._runtimeContext) {
275
+ if (userSubject === undefined && subject.object === subject_js_1.RUNTIME_OWNER_SUBJECT && this.actsForRuntimeOwner) {
272
276
  return { kind: "owner", namespace: subject.namespace };
273
277
  }
274
278
  throw httpError(403, "Permission owner does not match the authenticated user");
@@ -331,9 +335,11 @@ class OryAgentClient {
331
335
  }
332
336
  /** Active noninteractive broker credential. It is process memory only. */
333
337
  get runtimeCredential() {
338
+ return (0, context_js_1.currentSessionContext)().runtimeCredential ?? this.rootRuntimeCredential;
339
+ }
340
+ /** The session's root broker credential, ignoring an ambient child credential. */
341
+ get rootRuntimeCredential() {
334
342
  const context = (0, context_js_1.currentSessionContext)();
335
- if (context.runtimeCredential)
336
- return context.runtimeCredential;
337
343
  if (context.sessionId) {
338
344
  const credential = this._runtimeCredentialsBySession.get(context.sessionId);
339
345
  if (credential)
@@ -341,7 +347,7 @@ class OryAgentClient {
341
347
  }
342
348
  if (this._runtimeCredential)
343
349
  return this._runtimeCredential;
344
- const principal = this.agentPrincipal;
350
+ const principal = this.agentPrincipalForSession(this.sessionId);
345
351
  return principal.token
346
352
  ? (0, runtime_credential_js_1.talosRuntimeCredential)(principal.token, { subject: principal.subject })
347
353
  : undefined;
@@ -349,10 +355,25 @@ class OryAgentClient {
349
355
  get hasRuntimeCredential() {
350
356
  return this.runtimeCredential !== undefined;
351
357
  }
352
- /** Process-local deployment context. It is never persisted or copied to the environment. */
358
+ /**
359
+ * Active deployment context. It is never copied to the environment; a
360
+ * subprocess harness restores it from the credential-bound, non-secret
361
+ * per-session record written at session start (see `restoreRuntimeContext`).
362
+ */
353
363
  get runtimeContext() {
354
364
  return this._runtimeContext ? { ...this._runtimeContext } : undefined;
355
365
  }
366
+ /**
367
+ * True when this client runs as an administrator-preprovisioned runtime whose
368
+ * owner only the broker knows: a resolved runtime context, or a complete
369
+ * `ORY_AGENT_ROOT_DELEGATION` + `ORY_AGENT_SUBJECT_ID` environment package
370
+ * with its runtime credential.
371
+ */
372
+ get actsForRuntimeOwner() {
373
+ if (this._runtimeContext)
374
+ return true;
375
+ return (0, agent_auth_js_1.resolvePreprovisionedRootDelegation)(process.env, undefined, this.runtimeCredential) !== undefined;
376
+ }
356
377
  get runtimeRootDelegation() {
357
378
  return this._runtimeContext?.rootDelegation;
358
379
  }
package/dist/config.d.ts CHANGED
@@ -104,6 +104,20 @@ export interface OryAgentDynamicCredentials {
104
104
  * bounded so a long-lived install's config doesn't grow without limit.
105
105
  */
106
106
  export declare const DELEGATION_NODE_HISTORY = 10;
107
+ /**
108
+ * A runtime context resolved for one headless session, persisted without any
109
+ * secret. `credential` is a SHA-256 binding of the runtime credential (never the
110
+ * credential itself); `projectUrl` and `agentSecurityUrl` are canonical URLs.
111
+ */
112
+ export interface PersistedRuntimeContext {
113
+ credential: string;
114
+ projectUrl: string;
115
+ agentSecurityUrl: string;
116
+ subjectId: string;
117
+ rootDelegation: string;
118
+ deploymentId: string;
119
+ agentType: string;
120
+ }
107
121
  /** Opaque, administrator-provisioned root delegation node for headless agents. */
108
122
  export declare const AGENT_ROOT_DELEGATION_ENV_VAR = "ORY_AGENT_ROOT_DELEGATION";
109
123
  /** Validate an opaque broker node without interpreting its server-owned format. */
@@ -213,6 +227,14 @@ export interface OryPluginConfig {
213
227
  workspaceName?: string;
214
228
  /** Opaque broker node ids keyed by harness and session. Contains no credentials. */
215
229
  delegationAnchors?: Record<string, Record<string, string>>;
230
+ /**
231
+ * Non-secret headless deployment context keyed by harness and session, so a
232
+ * later subprocess hook of the same session can act for the deployment owner
233
+ * without a runtime-context request per tool call. Each entry is bound to a
234
+ * fingerprint of the runtime credential and to the project and Agent Security
235
+ * URLs it was resolved for; a different credential or project never reuses it.
236
+ */
237
+ runtimeContexts?: Record<string, Record<string, PersistedRuntimeContext>>;
216
238
  /**
217
239
  * Public OAuth2 client id used by the user PKCE browser flow when
218
240
  * the interactive user login runs (every session). The client must be registered ahead of time
package/dist/config.js CHANGED
@@ -62,6 +62,46 @@ const cli_invocation_js_1 = require("./cli-invocation.js");
62
62
  * bounded so a long-lived install's config doesn't grow without limit.
63
63
  */
64
64
  exports.DELEGATION_NODE_HISTORY = 10;
65
+ const PERSISTED_RUNTIME_CONTEXT_KEYS = [
66
+ "agentSecurityUrl",
67
+ "agentType",
68
+ "credential",
69
+ "deploymentId",
70
+ "projectUrl",
71
+ "rootDelegation",
72
+ "subjectId",
73
+ ];
74
+ function parsePersistedRuntimeContext(value) {
75
+ if (!value || typeof value !== "object" || Array.isArray(value))
76
+ return undefined;
77
+ const raw = value;
78
+ const entry = {};
79
+ for (const key of PERSISTED_RUNTIME_CONTEXT_KEYS) {
80
+ const field = raw[key];
81
+ if (typeof field !== "string" || !field.trim())
82
+ return undefined;
83
+ entry[key] = field;
84
+ }
85
+ return entry;
86
+ }
87
+ function parseRuntimeContexts(value) {
88
+ if (!value || typeof value !== "object" || Array.isArray(value))
89
+ return undefined;
90
+ const result = {};
91
+ for (const [harness, rawSessions] of Object.entries(value)) {
92
+ if (!rawSessions || typeof rawSessions !== "object" || Array.isArray(rawSessions))
93
+ continue;
94
+ const sessions = {};
95
+ for (const [session, rawEntry] of Object.entries(rawSessions)) {
96
+ const entry = parsePersistedRuntimeContext(rawEntry);
97
+ if (entry)
98
+ sessions[session] = entry;
99
+ }
100
+ if (Object.keys(sessions).length > 0)
101
+ result[harness] = sessions;
102
+ }
103
+ return Object.keys(result).length > 0 ? result : undefined;
104
+ }
65
105
  /** Opaque, administrator-provisioned root delegation node for headless agents. */
66
106
  exports.AGENT_ROOT_DELEGATION_ENV_VAR = "ORY_AGENT_ROOT_DELEGATION";
67
107
  /** Validate an opaque broker node without interpreting its server-owned format. */
@@ -197,6 +237,7 @@ function loadConfig() {
197
237
  : undefined,
198
238
  installedHarnesses: parseInstalledHarnesses(parsed.installedHarnesses),
199
239
  delegationAnchors: parseDelegationAnchors(parsed.delegationAnchors),
240
+ runtimeContexts: parseRuntimeContexts(parsed.runtimeContexts),
200
241
  user: parseUserCredentials(parsed),
201
242
  agent: parseAgentCredentials(parsed),
202
243
  };
@@ -928,6 +969,7 @@ function compactPersistedConfig(config) {
928
969
  ? { installedHarnesses: config.installedHarnesses }
929
970
  : {}),
930
971
  ...(config.delegationAnchors ? { delegationAnchors: config.delegationAnchors } : {}),
972
+ ...(config.runtimeContexts ? { runtimeContexts: config.runtimeContexts } : {}),
931
973
  ...(user ? { user } : {}),
932
974
  ...(agent ? { agent } : {}),
933
975
  };
@@ -48,6 +48,8 @@ export interface RuntimeCredential {
48
48
  subject?: string;
49
49
  /** @internal Non-secret binding used to recognize the same Talos key after re-resolution. */
50
50
  apiKeyFingerprint?: string;
51
+ /** @internal Non-secret binding of an environment-configured SPIFFE workload identity. */
52
+ spiffeIdFingerprint?: string;
51
53
  authenticate: RuntimeRequestAuthenticator;
52
54
  }
53
55
  export interface ResolveRuntimeCredentialOptions {
@@ -666,6 +666,7 @@ function resolveRuntimeCredential(options = {}) {
666
666
  return {
667
667
  kind: "injected",
668
668
  ...(options.subject ? { subject: options.subject } : {}),
669
+ spiffeIdFingerprint: (0, node_crypto_1.createHash)("sha256").update(expectedClientSpiffeId).digest("hex"),
669
670
  authenticate: createSpiffeFileRequestAuthenticator({
670
671
  ...(generationPath ? {
671
672
  generationPath,
package/dist/subject.d.ts CHANGED
@@ -61,8 +61,8 @@ export declare function runWithUserSubject<T>(subject: string | undefined, fn: (
61
61
  * Resolve the user subject for permission checks. Prefers a per-call
62
62
  * override installed via {@link runWithUserSubject}, then the user
63
63
  * login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
64
- * legacy `ORY_AGENT_SUBJECT_ID`, then the deployment owner of a preprovisioned
65
- * runtime ({@link RUNTIME_OWNER_SUBJECT}), then the caller-supplied `fallback`
64
+ * deployment owner of a preprovisioned runtime ({@link RUNTIME_OWNER_SUBJECT}),
65
+ * then the legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
66
66
  * (typically `session:<id>`).
67
67
  *
68
68
  * Always returns a SubjectSet. A resolved principal is addressed under the
package/dist/subject.js CHANGED
@@ -80,8 +80,8 @@ function subjectSet(namespace, object) {
80
80
  * Resolve the user subject for permission checks. Prefers a per-call
81
81
  * override installed via {@link runWithUserSubject}, then the user
82
82
  * login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
83
- * legacy `ORY_AGENT_SUBJECT_ID`, then the deployment owner of a preprovisioned
84
- * runtime ({@link RUNTIME_OWNER_SUBJECT}), then the caller-supplied `fallback`
83
+ * deployment owner of a preprovisioned runtime ({@link RUNTIME_OWNER_SUBJECT}),
84
+ * then the legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
85
85
  * (typically `session:<id>`).
86
86
  *
87
87
  * Always returns a SubjectSet. A resolved principal is addressed under the
@@ -92,22 +92,26 @@ function subjectSet(namespace, object) {
92
92
  * marker that must never match a stored tuple.
93
93
  */
94
94
  function resolveUserSubject(client, fallback) {
95
- const principal = perCallUserSubject.getStore()
96
- ?? client.userPrincipal.subject
97
- ?? process.env.ORY_USER_SUBJECT_ID
98
- // Validated, not read raw: the same value feeds the agent subject, so a
99
- // value rejected there must not slip in here (#241).
100
- ?? readAgentSubjectOverride();
101
95
  const ns = process.env.ORY_USER_SUBJECT_NAMESPACE?.trim()
102
96
  || client.userSubjectNamespace
103
97
  || exports.DEFAULT_USER_SUBJECT_NAMESPACE;
98
+ const principal = perCallUserSubject.getStore()
99
+ ?? client.userPrincipal.subject
100
+ ?? process.env.ORY_USER_SUBJECT_ID;
104
101
  if (principal)
105
102
  return subjectSet(ns, principal);
106
103
  // A preprovisioned runtime acts for its deployment's owner, whom only the
107
104
  // broker knows. Without this, a headless deployment resolves no subject and
108
- // every native tool is denied in enforce mode (#338).
109
- if (client.runtimeContext)
105
+ // every native tool is denied in enforce mode (#338). Its
106
+ // `ORY_AGENT_SUBJECT_ID` names the runtime Agent, never a user, so the legacy
107
+ // fallback below does not apply to it.
108
+ if (client.actsForRuntimeOwner)
110
109
  return subjectSet(ns, exports.RUNTIME_OWNER_SUBJECT);
110
+ // Validated, not read raw: the same value feeds the agent subject, so a
111
+ // value rejected there must not slip in here (#241).
112
+ const legacyAgentSubject = readAgentSubjectOverride();
113
+ if (legacyAgentSubject)
114
+ return subjectSet(ns, legacyAgentSubject);
111
115
  // No principal — use the caller's fallback. A `session:<id>` fallback is
112
116
  // addressed under the Session namespace so it is a well-formed SubjectSet that
113
117
  // could match a Session-scoped grant, rather than being wrapped under `User`.
package/dist/uninstall.js CHANGED
@@ -229,6 +229,14 @@ async function clearCredentialsForUninstall(options = {}) {
229
229
  else
230
230
  delete next.delegationAnchors;
231
231
  }
232
+ if (current.runtimeContexts?.[harness]) {
233
+ const contexts = { ...current.runtimeContexts };
234
+ delete contexts[harness];
235
+ if (Object.keys(contexts).length > 0)
236
+ next.runtimeContexts = contexts;
237
+ else
238
+ delete next.runtimeContexts;
239
+ }
232
240
  return next;
233
241
  });
234
242
  log(`Removed the ${harness} agent identities. Shared Ory configuration is kept for: ${remainingHarnesses.join(", ")}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
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",