@ory/argus 1.2.4 → 1.3.1

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.
Files changed (49) hide show
  1. package/README.md +25 -0
  2. package/assets/skills/ory-build-agent/SKILL.md +1 -1
  3. package/assets/skills/ory-deploy-agent/SKILL.md +315 -0
  4. package/assets/skills/ory-deploy-openai-e2b/SKILL.md +289 -0
  5. package/assets/skills/ory-e2b-sandbox/SKILL.md +10 -6
  6. package/assets/skills/ory-temporal-worker/SKILL.md +4 -4
  7. package/dist/adapters.d.ts +11 -3
  8. package/dist/adapters.js +55 -13
  9. package/dist/agent-auth.d.ts +18 -7
  10. package/dist/agent-auth.js +120 -10
  11. package/dist/auth-store.d.ts +3 -3
  12. package/dist/auth-store.js +56 -12
  13. package/dist/auth.d.ts +1 -1
  14. package/dist/auth.js +1 -1
  15. package/dist/build-info.json +4 -4
  16. package/dist/cli.js +5 -0
  17. package/dist/client.d.ts +15 -0
  18. package/dist/client.js +24 -7
  19. package/dist/config.d.ts +23 -3
  20. package/dist/config.js +202 -18
  21. package/dist/contract-suite.d.ts +0 -8
  22. package/dist/contract-suite.js +0 -1
  23. package/dist/event-reporter.d.ts +1 -0
  24. package/dist/event-reporter.js +19 -2
  25. package/dist/hook-timeout.d.ts +2 -2
  26. package/dist/hook-timeout.js +2 -2
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +13 -6
  29. package/dist/lifecycle.js +2 -1
  30. package/dist/local/configs.d.ts +2 -4
  31. package/dist/local/configs.js +2 -4
  32. package/dist/local/manager.js +4 -4
  33. package/dist/local/seed.d.ts +4 -4
  34. package/dist/local/seed.js +8 -10
  35. package/dist/runtime-credential.d.ts +35 -0
  36. package/dist/runtime-credential.js +252 -1
  37. package/dist/runtime.d.ts +19 -12
  38. package/dist/runtime.js +69 -21
  39. package/dist/setup.js +3 -3
  40. package/dist/skills.d.ts +3 -5
  41. package/dist/skills.js +14 -5
  42. package/dist/subject.d.ts +2 -3
  43. package/dist/subject.js +10 -11
  44. package/dist/testing.d.ts +17 -0
  45. package/dist/testing.js +53 -0
  46. package/dist/tool-catalog.d.ts +12 -12
  47. package/dist/tool-catalog.js +206 -50
  48. package/dist/user-login.js +1 -1
  49. package/package.json +1 -1
package/dist/client.js CHANGED
@@ -81,6 +81,7 @@ class OryAgentClient {
81
81
  logger;
82
82
  eventReporter;
83
83
  _runtimeCredential;
84
+ _runtimeContext;
84
85
  _runtimeCredentialsBySession = new Map();
85
86
  _runtimeCredentialsByPrincipalKey = new Map();
86
87
  sessionCache = new Map();
@@ -341,6 +342,17 @@ class OryAgentClient {
341
342
  get hasRuntimeCredential() {
342
343
  return this.runtimeCredential !== undefined;
343
344
  }
345
+ /** Process-local deployment context. It is never persisted or copied to the environment. */
346
+ get runtimeContext() {
347
+ return this._runtimeContext ? { ...this._runtimeContext } : undefined;
348
+ }
349
+ get runtimeRootDelegation() {
350
+ return this._runtimeContext?.rootDelegation;
351
+ }
352
+ setRuntimeContext(context) {
353
+ this._runtimeContext = { ...context };
354
+ this.setAgentPrincipal({ subject: context.subjectId });
355
+ }
344
356
  setRuntimeCredential(credential) {
345
357
  const sessionId = this.sessionId?.trim();
346
358
  if (sessionId) {
@@ -441,7 +453,10 @@ class OryAgentClient {
441
453
  emitActivity: false,
442
454
  signal: controller.signal,
443
455
  }));
444
- if (identity.subject && identity.token) {
456
+ if (identity.runtimeCredential) {
457
+ this.rememberRuntimeCredential(identity.runtimeCredential);
458
+ }
459
+ else if (identity.subject && identity.token) {
445
460
  this._eventPrincipalsByKey.set((0, event_reporter_js_1.eventPrincipalKey)(identity.subject), {
446
461
  subject: identity.subject,
447
462
  token: identity.token,
@@ -814,28 +829,30 @@ class OryAgentClient {
814
829
  };
815
830
  const activity = this.logger.startActivity("delegation.record", { delegationKind: input.kind, ...options?.activityAttributes }, sessionId);
816
831
  const url = `${this.agentSecurityUrl.replace(/\/+$/, "")}/agent-security/v1/delegations:record`;
817
- // The user bearer authenticates who delegated; the agent or sub-agent bearer
818
- // separately proves the principal being attached to the delegation edge.
832
+ // Interactive lineage uses separate user and runtime proofs. A validated
833
+ // preprovisioned root has no user bearer; its acting runtime authenticates
834
+ // directly in Authorization.
819
835
  const userToken = this.userPrincipalForSession(sessionId).token;
820
836
  const runtimeCredential = options?.runtimeCredential ?? (options?.agentToken
821
837
  ? (0, runtime_credential_js_1.talosRuntimeCredential)(options.agentToken)
822
838
  : this.runtimeCredential);
823
839
  try {
824
- if (!userToken || !runtimeCredential) {
840
+ if (!runtimeCredential || (!userToken && !options?.preprovisionedRoot)) {
825
841
  throw httpError(403, "User token and runtime credential are required");
826
842
  }
843
+ const runtimeOnly = options?.preprovisionedRoot === true;
827
844
  const res = await (0, runtime_credential_js_1.authenticatedRuntimeRequest)(runtimeCredential, {
828
845
  url,
829
846
  init: {
830
847
  method: "POST",
831
848
  headers: {
832
849
  "content-type": "application/json",
833
- authorization: `Bearer ${userToken}`,
850
+ ...(!runtimeOnly && userToken ? { authorization: `Bearer ${userToken}` } : {}),
834
851
  },
835
852
  body: JSON.stringify(body),
836
853
  signal: options?.signal ?? AbortSignal.timeout(30_000),
837
854
  },
838
- authorizationHeader: "x-ory-agent-authorization",
855
+ authorizationHeader: runtimeOnly ? "authorization" : "x-ory-agent-authorization",
839
856
  fetch: options?.fetchImpl,
840
857
  });
841
858
  if (!res.ok) {
@@ -1104,7 +1121,7 @@ function rehydrateUserPrincipal(client) {
1104
1121
  // without persisting it — so without this branch `ORY_USER_OAUTH2_TOKEN`
1105
1122
  // authenticated the session and then silently stopped
1106
1123
  // applying on every subsequent event. On a subprocess harness that left the
1107
- // tool-call client with no user credential for delegation or DCR bootstrap.
1124
+ // tool-call client with no user credential for delegation or Talos enrollment.
1108
1125
  const env = (0, auth_store_js_1.readEnvUserCredential)();
1109
1126
  const tokens = (0, auth_store_js_1.loadTokens)();
1110
1127
  if (env.token) {
package/dist/config.d.ts CHANGED
@@ -12,7 +12,7 @@ export interface OryOAuth2Tokens {
12
12
  expiresAt: number;
13
13
  /** Subject (sub claim) the token was issued for. */
14
14
  subject?: string;
15
- /** Client ID the token was issued under. In-memory only. */
15
+ /** Client ID the token was issued under. Persisted to bind refreshes to that client. */
16
16
  clientId?: string;
17
17
  /** @deprecated Accepted in memory for older callers; never persisted. */
18
18
  idToken?: string;
@@ -104,6 +104,14 @@ 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
+ /** Opaque, administrator-provisioned root delegation node for headless agents. */
108
+ export declare const AGENT_ROOT_DELEGATION_ENV_VAR = "ORY_AGENT_ROOT_DELEGATION";
109
+ /** Validate an opaque broker node without interpreting its server-owned format. */
110
+ export declare function validateAgentRootDelegation(raw: string | undefined): {
111
+ value?: string;
112
+ warning?: string;
113
+ };
114
+ export declare function readAgentRootDelegation(env?: NodeJS.ProcessEnv, warnings?: string[]): string | undefined;
107
115
  /**
108
116
  * Credentials for the AI agent principal (machine identity). Every harness
109
117
  * session owns a distinct DCR client, and every typed sub-agent in that session
@@ -183,7 +191,7 @@ export interface OryPluginConfig {
183
191
  projectUrl?: string;
184
192
  /**
185
193
  * Canonical Ory Agent Security broker URL. Broker calls resolve this value
186
- * independently from {@link projectUrl}, which remains the OAuth2/DCR origin.
194
+ * independently from {@link projectUrl}, which remains the project and user-login origin.
187
195
  * Older configs fall back to `projectUrl` until this key is persisted.
188
196
  */
189
197
  agentSecurityUrl?: string;
@@ -250,7 +258,7 @@ export interface OryPluginConfig {
250
258
  }
251
259
  /**
252
260
  * Single OS-agnostic data directory for *all* Ory agent plugin state:
253
- * the shared config file, persisted DCR credentials, and any harness
261
+ * the shared config file, persisted legacy DCR credentials, and any harness
254
262
  * asset directories. One place per platform — never split across
255
263
  * `~/.ory/` and `~/.config/`.
256
264
  *
@@ -317,6 +325,18 @@ export declare function saveConfig(update: Partial<{
317
325
  * atomic via write-temp + rename.
318
326
  */
319
327
  export declare function mutateConfig(mutator: (current: OryPluginConfig) => OryPluginConfig | undefined): void;
328
+ export interface AsyncConfigMutation<T> {
329
+ /** New config to persist while the lock is still held. Omit for a read-only result. */
330
+ config?: OryPluginConfig;
331
+ value: T;
332
+ }
333
+ /**
334
+ * Async read-modify-write transaction using the same config.json.lock as
335
+ * mutateConfig. The lock remains held while the callback awaits, which lets
336
+ * token refresh serialize its read/check/network/write sequence across
337
+ * JavaScript and Go processes without blocking the JavaScript event loop.
338
+ */
339
+ export declare function mutateConfigAsync<T>(mutator: (current: OryPluginConfig) => Promise<AsyncConfigMutation<T>>): Promise<T>;
320
340
  /**
321
341
  * True when `value` looks like an Ory project **id** (a UUID) rather than a
322
342
  * project **slug**. Project slugs are human-readable tokens (e.g.
package/dist/config.js CHANGED
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.LOCAL_AGENT_SECURITY_URL = exports.DEFAULT_AGENT_SECURITY_URL = exports.DEFAULT_OAUTH2_CLIENT_ID = exports.SESSIONLESS_KEY = exports.UNKNOWN_HARNESS_KEY = exports.DELEGATION_NODE_HISTORY = void 0;
36
+ exports.LOCAL_AGENT_SECURITY_URL = exports.DEFAULT_AGENT_SECURITY_URL = exports.DEFAULT_OAUTH2_CLIENT_ID = exports.SESSIONLESS_KEY = exports.UNKNOWN_HARNESS_KEY = exports.AGENT_ROOT_DELEGATION_ENV_VAR = exports.DELEGATION_NODE_HISTORY = void 0;
37
+ exports.validateAgentRootDelegation = validateAgentRootDelegation;
38
+ exports.readAgentRootDelegation = readAgentRootDelegation;
37
39
  exports.getDataDir = getDataDir;
38
40
  exports.getHarnessDataDir = getHarnessDataDir;
39
41
  exports.getConfigPath = getConfigPath;
@@ -43,6 +45,7 @@ exports.registerInstalledHarness = registerInstalledHarness;
43
45
  exports.unregisterInstalledHarness = unregisterInstalledHarness;
44
46
  exports.saveConfig = saveConfig;
45
47
  exports.mutateConfig = mutateConfig;
48
+ exports.mutateConfigAsync = mutateConfigAsync;
46
49
  exports.looksLikeProjectId = looksLikeProjectId;
47
50
  exports.isSecurityConnected = isSecurityConnected;
48
51
  exports.describeSecurityGap = describeSecurityGap;
@@ -51,6 +54,7 @@ exports.configPromptMessage = configPromptMessage;
51
54
  const fs = __importStar(require("node:fs"));
52
55
  const os = __importStar(require("node:os"));
53
56
  const path = __importStar(require("node:path"));
57
+ const node_crypto_1 = require("node:crypto");
54
58
  const cli_invocation_js_1 = require("./cli-invocation.js");
55
59
  /**
56
60
  * How many sessions' delegation node ids to keep per credential. Only the
@@ -58,6 +62,29 @@ const cli_invocation_js_1 = require("./cli-invocation.js");
58
62
  * bounded so a long-lived install's config doesn't grow without limit.
59
63
  */
60
64
  exports.DELEGATION_NODE_HISTORY = 10;
65
+ /** Opaque, administrator-provisioned root delegation node for headless agents. */
66
+ exports.AGENT_ROOT_DELEGATION_ENV_VAR = "ORY_AGENT_ROOT_DELEGATION";
67
+ /** Validate an opaque broker node without interpreting its server-owned format. */
68
+ function validateAgentRootDelegation(raw) {
69
+ if (raw === undefined)
70
+ return {};
71
+ const value = raw.trim();
72
+ if (!value) {
73
+ return { warning: `${exports.AGENT_ROOT_DELEGATION_ENV_VAR} is set but empty; ignoring it.` };
74
+ }
75
+ if (Buffer.byteLength(value, "utf8") > 255 || /[\0\r\n]/.test(raw)) {
76
+ return {
77
+ warning: `${exports.AGENT_ROOT_DELEGATION_ENV_VAR} must be at most 255 UTF-8 bytes and contain no NUL or newline characters; ignoring it.`,
78
+ };
79
+ }
80
+ return { value };
81
+ }
82
+ function readAgentRootDelegation(env = process.env, warnings) {
83
+ const { value, warning } = validateAgentRootDelegation(env[exports.AGENT_ROOT_DELEGATION_ENV_VAR]);
84
+ if (warning)
85
+ warnings?.push(warning);
86
+ return value;
87
+ }
61
88
  /**
62
89
  * Key used when a credential record carries no harness of its own — only
63
90
  * reachable by migrating a config written before credentials were keyed by
@@ -78,7 +105,7 @@ exports.UNKNOWN_HARNESS_KEY = "unknown";
78
105
  exports.SESSIONLESS_KEY = "sessionless";
79
106
  /**
80
107
  * Single OS-agnostic data directory for *all* Ory agent plugin state:
81
- * the shared config file, persisted DCR credentials, and any harness
108
+ * the shared config file, persisted legacy DCR credentials, and any harness
82
109
  * asset directories. One place per platform — never split across
83
110
  * `~/.ory/` and `~/.config/`.
84
111
  *
@@ -118,8 +145,11 @@ function configFile() {
118
145
  function configLockFile() {
119
146
  return configFile() + ".lock";
120
147
  }
121
- /** A lock that is older than this is considered stale and may be broken. */
122
- const LOCK_STALE_MS = 30_000;
148
+ /** Age fallback for malformed or definitely cross-host lock owners. */
149
+ const LOCK_STALE_FALLBACK_MS = 5 * 60_000;
150
+ /** Lease used by nonce-bearing locks; async transactions refresh it. */
151
+ const LOCK_LEASE_MS = 5_000;
152
+ const LOCK_HEARTBEAT_MS = 1_000;
123
153
  /** Polling interval while waiting to acquire the lock. */
124
154
  const LOCK_POLL_MS = 25;
125
155
  /** Maximum total time to wait for the lock before giving up. */
@@ -507,6 +537,7 @@ function parseOAuth2(value) {
507
537
  refreshToken: typeof v.refreshToken === "string" ? v.refreshToken : undefined,
508
538
  expiresAt: v.expiresAt,
509
539
  subject: typeof v.subject === "string" ? v.subject : undefined,
540
+ clientId: typeof v.clientId === "string" ? v.clientId : undefined,
510
541
  };
511
542
  }
512
543
  /**
@@ -540,6 +571,25 @@ function mutateConfig(mutator) {
540
571
  releaseLock(lock);
541
572
  }
542
573
  }
574
+ /**
575
+ * Async read-modify-write transaction using the same config.json.lock as
576
+ * mutateConfig. The lock remains held while the callback awaits, which lets
577
+ * token refresh serialize its read/check/network/write sequence across
578
+ * JavaScript and Go processes without blocking the JavaScript event loop.
579
+ */
580
+ async function mutateConfigAsync(mutator) {
581
+ ensureConfigDir();
582
+ const lock = await acquireLockAsync();
583
+ try {
584
+ const mutation = await mutator(loadConfig());
585
+ if (mutation.config)
586
+ writeConfigAtomic(mutation.config);
587
+ return mutation.value;
588
+ }
589
+ finally {
590
+ releaseLock(lock);
591
+ }
592
+ }
543
593
  function mergeConfig(existing, update) {
544
594
  const result = { ...existing };
545
595
  for (const key of Object.keys(update)) {
@@ -568,23 +618,16 @@ function acquireLock() {
568
618
  for (;;) {
569
619
  try {
570
620
  const fd = fs.openSync(lockPath, "wx", 0o600);
571
- fs.writeSync(fd, String(process.pid));
572
- return { fd };
621
+ const nonce = (0, node_crypto_1.randomBytes)(16).toString("hex");
622
+ fs.writeSync(fd, configLockOwner(nonce));
623
+ return { fd, nonce };
573
624
  }
574
625
  catch (err) {
575
626
  if (err.code !== "EEXIST")
576
627
  throw err;
577
- // Lock exists — check if it's stale.
578
628
  try {
579
- const stat = fs.statSync(lockPath);
580
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
581
- // Best-effort break; if another process beats us, we'll retry.
582
- try {
583
- fs.unlinkSync(lockPath);
584
- }
585
- catch {
586
- /* ignore */
587
- }
629
+ const observed = inspectConfigLock(lockPath);
630
+ if (observed.stale && removeObservedLock(lockPath, observed)) {
588
631
  continue;
589
632
  }
590
633
  }
@@ -606,6 +649,142 @@ function acquireLock() {
606
649
  }
607
650
  }
608
651
  }
652
+ async function acquireLockAsync() {
653
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
654
+ const lockPath = configLockFile();
655
+ for (;;) {
656
+ try {
657
+ const fd = fs.openSync(lockPath, "wx", 0o600);
658
+ const nonce = (0, node_crypto_1.randomBytes)(16).toString("hex");
659
+ fs.writeSync(fd, configLockOwner(nonce));
660
+ const lock = { fd, nonce };
661
+ lock.heartbeat = setInterval(() => heartbeatLock(lock), LOCK_HEARTBEAT_MS);
662
+ lock.heartbeat.unref?.();
663
+ return lock;
664
+ }
665
+ catch (err) {
666
+ if (err.code !== "EEXIST")
667
+ throw err;
668
+ try {
669
+ const observed = inspectConfigLock(lockPath);
670
+ if (observed.stale && removeObservedLock(lockPath, observed)) {
671
+ continue;
672
+ }
673
+ }
674
+ catch {
675
+ continue;
676
+ }
677
+ if (Date.now() > deadline) {
678
+ throw new Error(`Timed out waiting for config lock at ${lockPath}. ` +
679
+ "Another process may be holding it; remove the file if you are sure no other process is running.");
680
+ }
681
+ await new Promise((resolve) => setTimeout(resolve, LOCK_POLL_MS));
682
+ }
683
+ }
684
+ }
685
+ function configLockOwner(nonce, now = Date.now()) {
686
+ return `${process.pid} ${os.hostname()} ${nonce} ${now + LOCK_LEASE_MS}`;
687
+ }
688
+ function inspectConfigLock(lockPath) {
689
+ const stat = fs.statSync(lockPath);
690
+ let raw = "";
691
+ try {
692
+ raw = fs.readFileSync(lockPath, "utf8").trim();
693
+ }
694
+ catch {
695
+ return {
696
+ stale: Date.now() - stat.mtimeMs > LOCK_STALE_FALLBACK_MS,
697
+ raw,
698
+ dev: stat.dev,
699
+ ino: stat.ino,
700
+ };
701
+ }
702
+ const [pidText, hostname, nonce, leaseText] = raw.split(/\s+/, 4);
703
+ const pid = Number.parseInt(pidText ?? "", 10);
704
+ const leaseExpiresAt = Number.parseInt(leaseText ?? "", 10);
705
+ const hasLease = Boolean(nonce) && Number.isFinite(leaseExpiresAt);
706
+ if (hasLease && Date.now() > leaseExpiresAt) {
707
+ return { stale: true, raw, dev: stat.dev, ino: stat.ino };
708
+ }
709
+ if (Number.isInteger(pid) && pid > 0 && (!hostname || hostname === os.hostname())) {
710
+ try {
711
+ process.kill(pid, 0);
712
+ return {
713
+ stale: !hasLease && Date.now() - stat.mtimeMs > LOCK_STALE_FALLBACK_MS,
714
+ raw,
715
+ dev: stat.dev,
716
+ ino: stat.ino,
717
+ };
718
+ }
719
+ catch (err) {
720
+ if (err.code === "EPERM") {
721
+ return {
722
+ stale: !hasLease && Date.now() - stat.mtimeMs > LOCK_STALE_FALLBACK_MS,
723
+ raw,
724
+ dev: stat.dev,
725
+ ino: stat.ino,
726
+ };
727
+ }
728
+ if (err.code === "ESRCH") {
729
+ return { stale: true, raw, dev: stat.dev, ino: stat.ino };
730
+ }
731
+ }
732
+ }
733
+ return {
734
+ stale: Date.now() - stat.mtimeMs > LOCK_STALE_FALLBACK_MS,
735
+ raw,
736
+ dev: stat.dev,
737
+ ino: stat.ino,
738
+ };
739
+ }
740
+ function removeObservedLock(lockPath, observed) {
741
+ try {
742
+ const stat = fs.statSync(lockPath);
743
+ if (stat.dev !== observed.dev || stat.ino !== observed.ino)
744
+ return false;
745
+ if (fs.readFileSync(lockPath, "utf8").trim() !== observed.raw)
746
+ return false;
747
+ fs.unlinkSync(lockPath);
748
+ return true;
749
+ }
750
+ catch {
751
+ return false;
752
+ }
753
+ }
754
+ function ownsLock(lock) {
755
+ if (lock.fd === null || !lock.nonce)
756
+ return false;
757
+ try {
758
+ const held = fs.fstatSync(lock.fd);
759
+ const current = fs.statSync(configLockFile());
760
+ if (held.dev !== current.dev || held.ino !== current.ino)
761
+ return false;
762
+ const [, , nonce] = fs.readFileSync(configLockFile(), "utf8").trim().split(/\s+/, 4);
763
+ return nonce === lock.nonce;
764
+ }
765
+ catch {
766
+ return false;
767
+ }
768
+ }
769
+ function heartbeatLock(lock) {
770
+ if (lock.fd === null || !lock.nonce || !ownsLock(lock)) {
771
+ if (lock.heartbeat)
772
+ clearInterval(lock.heartbeat);
773
+ lock.heartbeat = undefined;
774
+ return;
775
+ }
776
+ const record = configLockOwner(lock.nonce);
777
+ try {
778
+ fs.ftruncateSync(lock.fd, 0);
779
+ fs.writeSync(lock.fd, record, 0, "utf8");
780
+ fs.fsyncSync(lock.fd);
781
+ }
782
+ catch {
783
+ if (lock.heartbeat)
784
+ clearInterval(lock.heartbeat);
785
+ lock.heartbeat = undefined;
786
+ }
787
+ }
609
788
  /**
610
789
  * Lazily initialized buffer for `Atomics.wait`-based synchronous sleep.
611
790
  * `undefined` = not yet probed; `null` = unavailable on this runtime.
@@ -637,14 +816,18 @@ function sleepSync(ms) {
637
816
  function releaseLock(lock) {
638
817
  if (lock.fd === null)
639
818
  return; // lock-less fallback: nothing to release
819
+ if (lock.heartbeat)
820
+ clearInterval(lock.heartbeat);
821
+ lock.heartbeat = undefined;
640
822
  try {
641
- fs.closeSync(lock.fd);
823
+ if (ownsLock(lock))
824
+ fs.unlinkSync(configLockFile());
642
825
  }
643
826
  catch {
644
827
  /* ignore */
645
828
  }
646
829
  try {
647
- fs.unlinkSync(configLockFile());
830
+ fs.closeSync(lock.fd);
648
831
  }
649
832
  catch {
650
833
  /* ignore */
@@ -672,6 +855,7 @@ function compactPersistedConfig(config) {
672
855
  ...(userTokens.refreshToken ? { refreshToken: userTokens.refreshToken } : {}),
673
856
  expiresAt: userTokens.expiresAt,
674
857
  ...(userTokens.subject ? { subject: userTokens.subject } : {}),
858
+ ...(userTokens.clientId ? { clientId: userTokens.clientId } : {}),
675
859
  },
676
860
  }
677
861
  : undefined;
@@ -17,7 +17,6 @@
17
17
  * runHarnessContractSuite({
18
18
  * harness: "claude-code",
19
19
  * tool: "Bash",
20
- * sessionCanBlock: true,
21
20
  * sessionStart: async ({ client, gates }) => {
22
21
  * const out = await handleHookEvent(sessionInput(), client, gates);
23
22
  * return { blocked: out.decision === "block", reason: out.reason };
@@ -60,13 +59,6 @@ export interface HarnessContractAdapter {
60
59
  harness: string;
61
60
  /** A real, non-interactive tool from this harness's catalog. */
62
61
  tool: string;
63
- /**
64
- * @deprecated Ignored. Session start is now uniformly non-blocking on every
65
- * harness (authentication never blocks; enforcement is governed by
66
- * `permissionMode` at tool-call time). Kept optional so existing adapters
67
- * that still pass it type-check.
68
- */
69
- sessionCanBlock?: boolean;
70
62
  /** Whether the tool gate can hard-block. Defaults to true. */
71
63
  toolCanBlock?: boolean;
72
64
  /** Drive the session-start phase through the plugin's real entry point. */
@@ -18,7 +18,6 @@
18
18
  * runHarnessContractSuite({
19
19
  * harness: "claude-code",
20
20
  * tool: "Bash",
21
- * sessionCanBlock: true,
22
21
  * sessionStart: async ({ client, gates }) => {
23
22
  * const out = await handleHookEvent(sessionInput(), client, gates);
24
23
  * return { blocked: out.decision === "block", reason: out.reason };
@@ -49,6 +49,7 @@ export declare class AgentSecurityEventReporter {
49
49
  private readonly retryBaseMs;
50
50
  private readonly retryMaxMs;
51
51
  private readonly random;
52
+ private readonly reportedAuthFailures;
52
53
  constructor(options: EventReporterOptions);
53
54
  get pending(): number;
54
55
  get pendingScopes(): ReadonlyArray<PendingEventScope>;
@@ -105,6 +105,7 @@ class AgentSecurityEventReporter {
105
105
  retryBaseMs;
106
106
  retryMaxMs;
107
107
  random;
108
+ reportedAuthFailures = new Set();
108
109
  constructor(options) {
109
110
  this.options = options;
110
111
  this.fetcher = options.fetch ?? globalThis.fetch;
@@ -328,14 +329,27 @@ class AgentSecurityEventReporter {
328
329
  this.diagnostic("events.batch_rejected", new Error(`HTTP ${response.status}`));
329
330
  return "reject";
330
331
  }
331
- if (response.status === 401) {
332
+ if (response.status === 401 || response.status === 403) {
332
333
  try {
333
- if (token)
334
+ if (response.status === 401 && token) {
334
335
  this.options.onUnauthorized?.(scope.sessionId, scope.principalKey, token);
336
+ }
335
337
  }
336
338
  catch (error) {
337
339
  this.diagnostic("events.credential_invalidation_failed", error);
338
340
  }
341
+ const failureKey = `${scopeKey(scope)}:${response.status}`;
342
+ if (!this.reportedAuthFailures.has(failureKey)) {
343
+ this.reportedAuthFailures.add(failureKey);
344
+ this.options.logger?.activity("events.record", "error", {
345
+ sessionId: scope.sessionId,
346
+ attributes: {
347
+ failureType: "authentication",
348
+ reasonCode: response.status === 401 ? "session_inactive" : "forbidden",
349
+ count: batch.length,
350
+ },
351
+ });
352
+ }
339
353
  return "retain";
340
354
  }
341
355
  if (response.status === 429 || (response.status >= 500 && response.status <= 599)) {
@@ -359,6 +373,9 @@ class AgentSecurityEventReporter {
359
373
  this.diagnostic("events.invalid_response", new Error("response event IDs did not match request order"));
360
374
  return "retain";
361
375
  }
376
+ const acceptedScope = scopeKey(scope);
377
+ this.reportedAuthFailures.delete(`${acceptedScope}:401`);
378
+ this.reportedAuthFailures.delete(`${acceptedScope}:403`);
362
379
  return "accepted";
363
380
  }
364
381
  finally {
@@ -29,8 +29,8 @@
29
29
  * can be given — which is why the login window has to fit inside 120s.
30
30
  */
31
31
  /**
32
- * Wall-clock budget for a session-start hook: the interactive login, plus the
33
- * agent DCR registration and delegation record that follow it.
32
+ * Wall-clock budget for a session-start hook: the interactive login, plus Talos
33
+ * credential loading or enrollment and the delegation record that follow it.
34
34
  *
35
35
  * Generous on purpose. It is an upper bound on a hook that is *waiting*, not a
36
36
  * latency target — nothing is spinning, and the login gives up on its own well
@@ -35,8 +35,8 @@ exports.sessionStartHookTimeout = sessionStartHookTimeout;
35
35
  exports.toolHookTimeout = toolHookTimeout;
36
36
  const auth_js_1 = require("./auth.js");
37
37
  /**
38
- * Wall-clock budget for a session-start hook: the interactive login, plus the
39
- * agent DCR registration and delegation record that follow it.
38
+ * Wall-clock budget for a session-start hook: the interactive login, plus Talos
39
+ * credential loading or enrollment and the delegation record that follow it.
40
40
  *
41
41
  * Generous on purpose. It is an upper bound on a hook that is *waiting*, not a
42
42
  * latency target — nothing is spinning, and the login gives up on its own well
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, type RecordDelegationInput, type RecordDelegationResult, } from "./client.js";
2
- export { authenticatedRuntimeRequest, canonicalCredentialIdentity, canonicalProjectUrl, credentialAccount, configureSystemCaTrustForDefaultFetch, createOsTalosCredentialStore, enrollTalosChildRuntimeCredential, enrollTalosRuntimeCredential, resolveRuntimeCredential, runCredentialStoreCommand, talosRuntimeCredential, type CredentialStoreCommandOptions, type CredentialStoreCommandRunner, type ResolveRuntimeCredentialOptions, type EnrollTalosChildRuntimeCredentialOptions, type EnrollTalosRuntimeCredentialOptions, type EnrolledRuntimeCredential, type OsTalosCredentialStoreOptions, type NodeTlsCaProvider, type StoredTalosCredential, type TalosCredentialStore, type TalosCredentialIdentity, type RuntimeAuthenticatedRequest, type RuntimeAuthorizationHeader, type RuntimeCredential, type RuntimeCredentialKind, type RuntimeRequestAuthenticator, } from "./runtime-credential.js";
1
+ export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, type RecordDelegationInput, type RecordDelegationResult, type RuntimeContext, } from "./client.js";
2
+ export { authenticatedRuntimeRequest, canonicalCredentialIdentity, canonicalProjectUrl, credentialAccount, configureSystemCaTrustForDefaultFetch, createSpiffeFileRequestAuthenticator, createOsTalosCredentialStore, enrollTalosChildRuntimeCredential, enrollTalosRuntimeCredential, resolveRuntimeCredential, runCredentialStoreCommand, talosRuntimeCredential, type CredentialStoreCommandOptions, type CredentialStoreCommandRunner, type ResolveRuntimeCredentialOptions, type SpiffeFileRequestAuthenticatorOptions, type EnrollTalosChildRuntimeCredentialOptions, type EnrollTalosRuntimeCredentialOptions, type EnrolledRuntimeCredential, type OsTalosCredentialStoreOptions, type NodeTlsCaProvider, type StoredTalosCredential, type TalosCredentialStore, type TalosCredentialIdentity, type RuntimeAuthenticatedRequest, type RuntimeAuthorizationHeader, type RuntimeCredential, type RuntimeCredentialKind, type RuntimeRequestAuthenticator, } from "./runtime-credential.js";
3
3
  export { DebugLogger, ActiveActivity, redactLogData, formatLogEntry, withQuietStderr, type LogEntry, type LogLevel, type ActivityEntry, type ActivityStatus, type UnifiedLogEntry, type ActivityEnricher, type ActivitySink, } from "./logger.js";
4
4
  export { AgentSecurityEventReporter, SERVER_ACTIVITY_EVENTS, projectActivityEntry, type EventReporterOptions, type ServerActivityEvent, } from "./event-reporter.js";
5
5
  export { currentSessionContext, withSessionContext, setSessionContext, clearSessionContext, type SessionContext, } from "./context.js";
6
- export { loadConfig, saveConfig, resolveConfig, isSecurityConnected, describeSecurityGap, type SecurityConnection, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, listInstalledHarnesses, registerInstalledHarness, unregisterInstalledHarness, UNKNOWN_HARNESS_KEY, SESSIONLESS_KEY, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type OryDelegationRecord, type PermissionMode, type PermissionModeCache, } from "./config.js";
6
+ export { loadConfig, saveConfig, resolveConfig, isSecurityConnected, describeSecurityGap, readAgentRootDelegation, validateAgentRootDelegation, AGENT_ROOT_DELEGATION_ENV_VAR, type SecurityConnection, mutateConfig, mutateConfigAsync, getConfigPath, getDataDir, getHarnessDataDir, listInstalledHarnesses, registerInstalledHarness, unregisterInstalledHarness, UNKNOWN_HARNESS_KEY, SESSIONLESS_KEY, type OryPluginConfig, type AsyncConfigMutation, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type OryDelegationRecord, type PermissionMode, type PermissionModeCache, } from "./config.js";
7
7
  export { resolvePermissionMode, warmPermissionModeCache, resetPermissionModeCache, type PermissionModeSource, type ResolvedPermissionMode, type ModeSubject, } from "./permission-mode.js";
8
8
  export { pkceLogin, refreshAccessToken, detectHeadless, generateCodeVerifier, sha256Base64Url, buildAuthorizeUrl, LOOPBACK_PORTS, DEFAULT_LOGIN_TIMEOUT_MS, type PkceLoginOptions, type PkceLoginOutcome, type PkceDeclineReason, } from "./auth.js";
9
9
  export { wantsHelp, shouldPrintHelp } from "./help-cli.js";
@@ -11,7 +11,7 @@ export { loadExternalRegistrations, recordExternalRegistration, clearExternalReg
11
11
  export { SESSION_START_HOOK_WINDOW_MS, TOOL_HOOK_WINDOW_MS, HOOK_TIMEOUT_MAX_MS, sessionStartHookTimeout, toolHookTimeout, type HookTimeoutUnit, } from "./hook-timeout.js";
12
12
  export { loadTokens, saveTokens, clearTokens, isExpired, refreshAndSave, tryAcquirePkceFlightLock, clearPkceFlightLock, waitForPeerTokens, waitForPeerTokensSync, TOKEN_EXPIRY_SKEW_SEC, type PkceFlightLock, } from "./auth-store.js";
13
13
  export { ensureUserAuthenticated, ensureAuthenticated, type UserLoginDecision, type UserLoginMode, type UserLoginOptions, } from "./user-login.js";
14
- export { resolveAgentCredentials, resolveRuntimeAgentCredentials, ensureAgentIdentity, ensureSubAgentIdentity, fetchClientCredentialsToken, registerAgentClient, loadAgentDynamicCredentials, loadAgentDynamicCredentialsByHarness, saveAgentDynamicCredentials, clearAgentDynamicCredentials, latestAgentRegistration, latestSubAgentSession, countAgentSessions, claimAgentDynamicCredentials, loadSubAgentDynamicCredentials, loadAllSubAgentDynamicCredentials, loadSubAgentDynamicCredentialsByHarness, saveSubAgentDynamicCredentials, clearSubAgentDynamicCredentials, clearSubAgentDynamicCredentialsForHarness, claimSubAgentDynamicCredentials, loadDelegationNodeId, saveDelegationNodeId, loadRetiredCredentials, clearRetiredCredentials, retireAgentSessions, sessionRetention, revokeAgentDynamicClient, AGENT_TOKEN_EXPIRY_SKEW_SEC, DEFAULT_SESSION_RETENTION, type AgentRegistrationClaim, type AgentCredentials, type AgentCredentialKind, type ResolveAgentCredentialsOptions, type ResolveRuntimeAgentCredentialsOptions, type EnsureAgentIdentityOptions, type RegisterAgentClientArgs, type DelegationInput, type RevokeAgentClientResult, type SubAgentIdentity, type EnsureSubAgentIdentityOptions, } from "./agent-auth.js";
14
+ export { resolveAgentCredentials, resolvePreprovisionedRuntimeContext, resolveRuntimeAgentCredentials, ensureAgentIdentity, ensureSubAgentIdentity, fetchClientCredentialsToken, registerAgentClient, loadAgentDynamicCredentials, loadAgentDynamicCredentialsByHarness, saveAgentDynamicCredentials, clearAgentDynamicCredentials, latestAgentRegistration, latestSubAgentSession, countAgentSessions, claimAgentDynamicCredentials, loadSubAgentDynamicCredentials, loadAllSubAgentDynamicCredentials, loadSubAgentDynamicCredentialsByHarness, saveSubAgentDynamicCredentials, clearSubAgentDynamicCredentials, clearSubAgentDynamicCredentialsForHarness, claimSubAgentDynamicCredentials, loadDelegationNodeId, resolvePreprovisionedRootDelegation, saveDelegationNodeId, loadRetiredCredentials, clearRetiredCredentials, retireAgentSessions, sessionRetention, revokeAgentDynamicClient, AGENT_TOKEN_EXPIRY_SKEW_SEC, DEFAULT_SESSION_RETENTION, type AgentRegistrationClaim, type AgentCredentials, type AgentCredentialKind, type ResolveAgentCredentialsOptions, type ResolveRuntimeAgentCredentialsOptions, type EnsureAgentIdentityOptions, type RegisterAgentClientArgs, type DelegationInput, type RevokeAgentClientResult, type SubAgentIdentity, type EnsureSubAgentIdentityOptions, } from "./agent-auth.js";
15
15
  export { ensureReadCredential, resetReadCredentialBackoff, READ_CREDENTIAL_RETRY_MS, type EnsureReadCredentialOptions, type ReadCredentialOutcome, } from "./read-credential.js";
16
16
  export { clearCredentialsForUninstall, resolveInstalledHarnesses, type ClearCredentialsOptions, type ClearCredentialsResult, } from "./uninstall.js";
17
17
  export { type SessionInfo, type OAuth2TokenInfo, type PermissionCheck, type PermissionResult, type BatchPermissionResult, type OryError, type OryErrorCode, } from "./types.js";