@ory/argus 1.2.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) 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/bash-parser.js +212 -47
  16. package/dist/build-info.json +4 -4
  17. package/dist/cli.js +5 -0
  18. package/dist/client.d.ts +15 -0
  19. package/dist/client.js +24 -7
  20. package/dist/config.d.ts +23 -3
  21. package/dist/config.js +202 -18
  22. package/dist/contract-suite.d.ts +0 -8
  23. package/dist/contract-suite.js +0 -1
  24. package/dist/event-reporter.d.ts +1 -0
  25. package/dist/event-reporter.js +19 -2
  26. package/dist/hook-timeout.d.ts +2 -2
  27. package/dist/hook-timeout.js +2 -2
  28. package/dist/index.d.ts +4 -4
  29. package/dist/index.js +13 -6
  30. package/dist/lifecycle.js +2 -1
  31. package/dist/local/configs.d.ts +2 -4
  32. package/dist/local/configs.js +2 -4
  33. package/dist/local/manager.js +4 -4
  34. package/dist/local/seed.d.ts +4 -4
  35. package/dist/local/seed.js +8 -10
  36. package/dist/permissions.d.ts +3 -3
  37. package/dist/permissions.js +26 -1
  38. package/dist/runtime-credential.d.ts +35 -0
  39. package/dist/runtime-credential.js +252 -1
  40. package/dist/runtime.d.ts +19 -12
  41. package/dist/runtime.js +69 -21
  42. package/dist/setup.js +3 -3
  43. package/dist/skills.d.ts +3 -5
  44. package/dist/skills.js +14 -5
  45. package/dist/subject.d.ts +2 -3
  46. package/dist/subject.js +10 -11
  47. package/dist/testing.d.ts +17 -0
  48. package/dist/testing.js +53 -0
  49. package/dist/tool-catalog.d.ts +17 -15
  50. package/dist/tool-catalog.js +239 -52
  51. package/dist/user-login.js +1 -1
  52. package/package.json +1 -1
@@ -8,10 +8,10 @@
8
8
  * redirect URIs. The launcher always runs the real PKCE browser flow; the
9
9
  * user types these credentials into the login UI to complete the flow.
10
10
  *
11
- * No agent principal is seeded: the harness self-registers a DCR OAuth2
12
- * client for the agent on first session start (using the user's bearer as
13
- * the initial access token), and permission checks are made against the
14
- * user subject — so a pre-seeded agent Kratos identity would be unused.
11
+ * No agent principal is seeded: Agent Security enrolls a Talos runtime
12
+ * credential at session start using the authenticated user's bearer, and
13
+ * permission checks are made against the user subject. A pre-seeded agent
14
+ * Kratos identity would be unused.
15
15
  *
16
16
  * Permission tuples are written as SubjectSet `User:<identity.id>` so
17
17
  * the Console's *Add relationship* dialog (which always emits SubjectSet
@@ -157,9 +157,8 @@ async function seedLocalEnvironment(namespace = "AgentTool") {
157
157
  throw new Error(`backend services not ready before seed: ${readiness.pending.join(", ")} ` +
158
158
  `did not report ready within the timeout. The stack may still be starting — retry in a moment.`);
159
159
  }
160
- // 1. User identity. No agent identity is seeded — the harness
161
- // self-registers a DCR OAuth2 client for the agent on first session
162
- // start, and permission checks target the user subject.
160
+ // 1. User identity. No agent identity is seeded; Talos enrollment is owned
161
+ // by Agent Security at session start, and checks target the user subject.
163
162
  process.stderr.write(" Creating user identity...\n");
164
163
  const userIdentity = await seedIdentity(USER_EMAIL, USER_PASSWORD, "Local User");
165
164
  process.stderr.write(` User: ${userIdentity.id} (${userIdentity.email})\n`);
@@ -168,9 +167,8 @@ async function seedLocalEnvironment(namespace = "AgentTool") {
168
167
  const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
169
168
  process.stderr.write(" Native tools: allowed unless explicitly blocked (no baseline grants)\n");
170
169
  // 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
171
- // PKCE flow has somewhere to authenticate against. The agent's
172
- // OAuth2 client is intentionally NOT pre-registered; the harness
173
- // will self-register via DCR on first session start.
170
+ // PKCE flow has somewhere to authenticate against. Agent identity uses
171
+ // Talos runtime enrollment, so no agent OAuth2 client is seeded.
174
172
  process.stderr.write(" Registering user OAuth2 client (PKCE)...\n");
175
173
  let userClient = { clientId: exports.USER_CLIENT_ID };
176
174
  try {
@@ -231,13 +231,13 @@ export interface GateToolCallArgs {
231
231
  /** Override the resolved {@link PermissionMode}. Tests use this. */
232
232
  modeOverride?: PermissionMode;
233
233
  /**
234
- * Raw shell command string when {@link toolName} is the harness's shell tool
234
+ * Raw shell command string(s) when {@link toolName} is the harness's shell tool
235
235
  * (issue #76). When present and the tool is a shell tool, the gate decomposes
236
- * the command into `ShellTool:<word>#use` sub-checks in addition to the
236
+ * each command into `ShellTool:<word>#use` sub-checks in addition to the
237
237
  * top-level tool check. Absent / undefined ⇒ decomposition is skipped and the
238
238
  * gate behaves exactly as before.
239
239
  */
240
- shellCommand?: string;
240
+ shellCommand?: string | readonly string[];
241
241
  /**
242
242
  * The acting machine principals, so an explicit block on the agent install,
243
243
  * this session, the sub-agent kind, or this spawn can deny the call. Omitted ⇒
@@ -407,11 +407,36 @@ async function decomposeAndCheck(client, args) {
407
407
  // letting the exception bubble to the harness catch, which fails open.
408
408
  let parsed;
409
409
  try {
410
- parsed = await (0, bash_parser_js_1.extractBashCommandWords)(args.shellCommand ?? "");
410
+ const commands = typeof args.shellCommand === "string"
411
+ ? [args.shellCommand]
412
+ : args.shellCommand ?? [];
413
+ const words = [];
414
+ const seen = new Set();
415
+ let tooComplex = false;
416
+ let parserUnavailable = false;
417
+ for (const command of commands) {
418
+ const result = await (0, bash_parser_js_1.extractBashCommandWords)(command);
419
+ tooComplex ||= result.tooComplex;
420
+ parserUnavailable ||= result.parserUnavailable === true;
421
+ for (const word of result.words) {
422
+ if (!seen.has(word)) {
423
+ seen.add(word);
424
+ words.push(word);
425
+ }
426
+ }
427
+ }
428
+ parsed = { words, tooComplex, parserUnavailable };
411
429
  }
412
430
  catch {
413
431
  parsed = { words: [], tooComplex: true };
414
432
  }
433
+ if (args.toolName.toLowerCase() === "powershell"
434
+ || (args.harness === "cline" && args.toolName === "run_commands" && process.platform === "win32")) {
435
+ // The bundled grammar models POSIX shell syntax, not PowerShell. Retain its
436
+ // Never retain words parsed with the incompatible grammar: PowerShell
437
+ // literals could otherwise be persisted as if they were command names.
438
+ parsed = { words: [], tooComplex: true, parserUnavailable: false };
439
+ }
415
440
  if (parsed.parserUnavailable) {
416
441
  client.logger.warn("bash.parser_unavailable", {
417
442
  harness: args.harness,
@@ -1,3 +1,5 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import * as nodeHttps from "node:https";
1
3
  /** The broker header a runtime credential must occupy for a request. */
2
4
  export type RuntimeAuthorizationHeader = "authorization" | "x-ory-agent-authorization";
3
5
  export interface RuntimeAuthenticatedRequest {
@@ -15,10 +17,37 @@ export interface RuntimeAuthenticatedRequest {
15
17
  * the request over mTLS without exposing key material to Argus.
16
18
  */
17
19
  export type RuntimeRequestAuthenticator = (request: RuntimeAuthenticatedRequest) => Promise<Response>;
20
+ export interface SpiffeFileRequestAuthenticatorOptions {
21
+ /** Atomically switched directory or symlink containing one material generation. */
22
+ generationPath?: string;
23
+ certificatePath?: string;
24
+ privateKeyPath?: string;
25
+ trustBundlePath?: string;
26
+ certificateFile?: string;
27
+ privateKeyFile?: string;
28
+ trustBundleFile?: string;
29
+ /** Optional marker read before and after the PEM files to reject mixed generations. */
30
+ generationFile?: string;
31
+ /** Exact URI SAN required on the peer leaf certificate. */
32
+ expectedServerSpiffeId?: string;
33
+ /** Exact and sole URI SAN required on the loaded client leaf certificate. */
34
+ expectedClientSpiffeId?: string;
35
+ /** Explicitly use ordinary HTTPS hostname verification instead of SPIFFE identity. */
36
+ verifyHostname?: boolean;
37
+ privateKeyPassphrase?: string;
38
+ /** @internal Injectable seam for focused transport tests. */
39
+ readFile?: typeof readFile;
40
+ /** @internal Injectable seam for focused generation-path tests. */
41
+ realpath?: typeof realpath;
42
+ /** @internal Injectable seam for focused transport tests. */
43
+ request?: typeof nodeHttps.request;
44
+ }
18
45
  export type RuntimeCredentialKind = "talos" | "injected";
19
46
  export interface RuntimeCredential {
20
47
  kind: RuntimeCredentialKind;
21
48
  subject?: string;
49
+ /** @internal Non-secret binding used to recognize the same Talos key after re-resolution. */
50
+ apiKeyFingerprint?: string;
22
51
  authenticate: RuntimeRequestAuthenticator;
23
52
  }
24
53
  export interface ResolveRuntimeCredentialOptions {
@@ -115,6 +144,12 @@ export declare function talosRuntimeCredential(apiKey: string, options?: {
115
144
  /** @internal Injectable seam for testing default-fetch TLS setup. */
116
145
  configureSystemCaTrust?: () => void;
117
146
  }): RuntimeCredential;
147
+ /**
148
+ * Build an mTLS authenticator from operator-managed SPIFFE files. The files are
149
+ * re-read for every request so atomic SVID and trust-bundle rotations are used
150
+ * without restarting the process.
151
+ */
152
+ export declare function createSpiffeFileRequestAuthenticator(options: SpiffeFileRequestAuthenticatorOptions): RuntimeRequestAuthenticator;
118
153
  /** Resolve only noninteractive runtime credentials. No persisted state is read. */
119
154
  export declare function resolveRuntimeCredential(options?: ResolveRuntimeCredentialOptions): RuntimeCredential | undefined;
120
155
  /** Explicitly enroll a Talos credential and optionally persist it in an OS store. */
@@ -40,13 +40,18 @@ exports.canonicalCredentialIdentity = canonicalCredentialIdentity;
40
40
  exports.credentialAccount = credentialAccount;
41
41
  exports.createOsTalosCredentialStore = createOsTalosCredentialStore;
42
42
  exports.talosRuntimeCredential = talosRuntimeCredential;
43
+ exports.createSpiffeFileRequestAuthenticator = createSpiffeFileRequestAuthenticator;
43
44
  exports.resolveRuntimeCredential = resolveRuntimeCredential;
44
45
  exports.enrollTalosRuntimeCredential = enrollTalosRuntimeCredential;
45
46
  exports.enrollTalosChildRuntimeCredential = enrollTalosChildRuntimeCredential;
46
47
  exports.authenticatedRuntimeRequest = authenticatedRuntimeRequest;
47
48
  const node_child_process_1 = require("node:child_process");
48
49
  const node_crypto_1 = require("node:crypto");
50
+ const promises_1 = require("node:fs/promises");
51
+ const nodeHttps = __importStar(require("node:https"));
52
+ const node_path_1 = require("node:path");
49
53
  const nodeTls = __importStar(require("node:tls"));
54
+ const SPIFFE_CREDENTIAL_METHOD = "spiffe_x509_svid";
50
55
  const CREDENTIAL_SERVICE = "com.ory.agent-security.runtime-credential";
51
56
  const MACOS_CREDENTIAL_SCRIPT = String.raw `
52
57
  ObjC.import('Foundation');
@@ -407,6 +412,7 @@ function talosRuntimeCredential(apiKey, options = {}) {
407
412
  return {
408
413
  kind: "talos",
409
414
  ...(options.subject ? { subject: options.subject } : {}),
415
+ apiKeyFingerprint: (0, node_crypto_1.createHash)("sha256").update(key).digest("hex"),
410
416
  authenticate: async ({ url, init, authorizationHeader, fetch: requestFetch, usesDefaultFetch, }) => {
411
417
  const headerName = authorizationHeader === "authorization"
412
418
  ? "authorization"
@@ -430,6 +436,205 @@ function talosRuntimeCredential(apiKey, options = {}) {
430
436
  },
431
437
  };
432
438
  }
439
+ /**
440
+ * Build an mTLS authenticator from operator-managed SPIFFE files. The files are
441
+ * re-read for every request so atomic SVID and trust-bundle rotations are used
442
+ * without restarting the process.
443
+ */
444
+ function createSpiffeFileRequestAuthenticator(options) {
445
+ const read = options.readFile ?? promises_1.readFile;
446
+ const resolveRealpath = options.realpath ?? promises_1.realpath;
447
+ const requestHttps = options.request ?? nodeHttps.request;
448
+ const expectedServerSpiffeId = options.expectedServerSpiffeId
449
+ ? canonicalSpiffeId(options.expectedServerSpiffeId)
450
+ : undefined;
451
+ const expectedClientSpiffeId = options.expectedClientSpiffeId
452
+ ? canonicalSpiffeId(options.expectedClientSpiffeId, "expectedClientSpiffeId")
453
+ : undefined;
454
+ if (Boolean(expectedServerSpiffeId) === Boolean(options.verifyHostname)) {
455
+ throw new Error("SPIFFE file authentication requires exactly one of expectedServerSpiffeId or verifyHostname");
456
+ }
457
+ const materialPaths = async () => {
458
+ if (options.generationPath) {
459
+ const generation = await resolveRealpath(options.generationPath);
460
+ const files = [
461
+ options.certificateFile ?? "svid.pem",
462
+ options.privateKeyFile ?? "svid-key.pem",
463
+ options.trustBundleFile ?? "bundle.pem",
464
+ options.generationFile,
465
+ ];
466
+ const paths = files.map((file) => {
467
+ if (file === undefined)
468
+ return undefined;
469
+ if ((0, node_path_1.isAbsolute)(file)) {
470
+ throw new Error("SPIFFE generation file names must be relative paths");
471
+ }
472
+ const path = (0, node_path_1.resolve)(generation, file);
473
+ const fromGeneration = (0, node_path_1.relative)(generation, path);
474
+ if (fromGeneration === ".." || fromGeneration.startsWith(`..${node_path_1.sep}`)) {
475
+ throw new Error("SPIFFE generation file names must remain within the generation path");
476
+ }
477
+ return path;
478
+ });
479
+ return paths;
480
+ }
481
+ if (options.generationFile) {
482
+ throw new Error("SPIFFE generationFile requires generationPath");
483
+ }
484
+ if (!options.certificatePath || !options.privateKeyPath || !options.trustBundlePath) {
485
+ throw new Error("SPIFFE file authentication requires generationPath or all three material paths");
486
+ }
487
+ return [options.certificatePath, options.privateKeyPath, options.trustBundlePath];
488
+ };
489
+ const isKeyMismatch = (error) => {
490
+ if (!(error instanceof Error))
491
+ return false;
492
+ const code = error.code;
493
+ return code === "ERR_OSSL_X509_KEY_VALUES_MISMATCH"
494
+ || /key values mismatch/i.test(error.message);
495
+ };
496
+ return async ({ url, init }) => {
497
+ const target = new URL(url);
498
+ if (target.protocol !== "https:") {
499
+ throw new Error("SPIFFE file authentication requires an HTTPS URL");
500
+ }
501
+ if (target.username || target.password) {
502
+ throw new Error("SPIFFE file authentication does not accept URL credentials");
503
+ }
504
+ const normalized = new Request(target, init);
505
+ const body = normalized.body === null
506
+ ? undefined
507
+ : Buffer.from(await normalized.arrayBuffer());
508
+ const send = async () => {
509
+ let snapshot;
510
+ for (let attempt = 0; attempt < 2; attempt += 1) {
511
+ const [certificatePath, privateKeyPath, trustBundlePath, generationFile] = await materialPaths();
512
+ const generationBefore = generationFile ? await read(generationFile) : undefined;
513
+ const [cert, key, ca] = await Promise.all([
514
+ read(certificatePath),
515
+ read(privateKeyPath),
516
+ read(trustBundlePath),
517
+ ]);
518
+ const generationAfter = generationFile ? await read(generationFile) : undefined;
519
+ if (!generationBefore || (generationAfter && generationBefore.equals(generationAfter))) {
520
+ if (expectedClientSpiffeId) {
521
+ validateClientCertificate(cert, expectedClientSpiffeId);
522
+ }
523
+ snapshot = [cert, key, ca];
524
+ break;
525
+ }
526
+ }
527
+ if (!snapshot)
528
+ throw new Error("SPIFFE material generation changed while loading files");
529
+ const [cert, key, ca] = snapshot;
530
+ return await new Promise((resolveResponse, reject) => {
531
+ const outgoing = requestHttps(target, {
532
+ method: normalized.method,
533
+ headers: Object.fromEntries(normalized.headers),
534
+ cert,
535
+ key,
536
+ ...(expectedServerSpiffeId ? { ca } : {}),
537
+ ...(options.privateKeyPassphrase ? { passphrase: options.privateKeyPassphrase } : {}),
538
+ minVersion: "TLSv1.3",
539
+ rejectUnauthorized: true,
540
+ ...(expectedServerSpiffeId ? {
541
+ // Never reuse a connection authenticated under another expected workload ID.
542
+ agent: false,
543
+ checkServerIdentity: (_hostname, certificate) => {
544
+ if (!peerHasExactUriSan(certificate.subjectaltname, expectedServerSpiffeId)) {
545
+ return new Error(`Peer certificate does not contain expected SPIFFE URI SAN ${expectedServerSpiffeId}`);
546
+ }
547
+ return undefined;
548
+ },
549
+ } : {}),
550
+ signal: init.signal ?? undefined,
551
+ }, (incoming) => {
552
+ const chunks = [];
553
+ incoming.on("data", (chunk) => {
554
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
555
+ });
556
+ incoming.on("end", () => {
557
+ const headers = new Headers();
558
+ for (let index = 0; index < incoming.rawHeaders.length; index += 2) {
559
+ headers.append(incoming.rawHeaders[index], incoming.rawHeaders[index + 1]);
560
+ }
561
+ resolveResponse(new Response(Buffer.concat(chunks), {
562
+ status: incoming.statusCode ?? 500,
563
+ statusText: incoming.statusMessage,
564
+ headers,
565
+ }));
566
+ });
567
+ incoming.on("error", reject);
568
+ });
569
+ outgoing.on("error", reject);
570
+ if (body)
571
+ outgoing.write(body);
572
+ outgoing.end();
573
+ });
574
+ };
575
+ try {
576
+ return await send();
577
+ }
578
+ catch (error) {
579
+ if (!isKeyMismatch(error))
580
+ throw error;
581
+ return await send();
582
+ }
583
+ };
584
+ }
585
+ function canonicalSpiffeId(value, name = "expectedServerSpiffeId") {
586
+ const parsed = new URL(value);
587
+ if (parsed.protocol !== "spiffe:"
588
+ || !parsed.hostname
589
+ || parsed.username
590
+ || parsed.password
591
+ || parsed.port
592
+ || parsed.search
593
+ || parsed.hash) {
594
+ throw new Error(`${name} must be a valid SPIFFE ID`);
595
+ }
596
+ return value;
597
+ }
598
+ function validateClientCertificate(certificate, expectedSpiffeId) {
599
+ let leaf;
600
+ try {
601
+ leaf = new node_crypto_1.X509Certificate(certificate);
602
+ }
603
+ catch {
604
+ throw new Error("SPIFFE client certificate is not a valid X.509 certificate");
605
+ }
606
+ const uriSans = uriSansFromSubjectAltName(leaf.subjectAltName);
607
+ if (uriSans.length !== 1 || uriSans[0] !== expectedSpiffeId) {
608
+ throw new Error(`Client certificate must contain only the expected SPIFFE URI SAN ${expectedSpiffeId}`);
609
+ }
610
+ const now = Date.now();
611
+ if (now < Date.parse(leaf.validFrom) || now > Date.parse(leaf.validTo)) {
612
+ throw new Error("SPIFFE client certificate is not currently valid");
613
+ }
614
+ }
615
+ function uriSansFromSubjectAltName(subjectAltName) {
616
+ if (!subjectAltName)
617
+ return [];
618
+ const result = [];
619
+ for (const entry of subjectAltName.match(/(?:^|, )URI:(?:"(?:\\.|[^"\\])*"|.*?)(?=, [A-Za-z0-9.]+:|$)/g) ?? []) {
620
+ const encoded = entry.replace(/^, /, "").slice(4);
621
+ if (!encoded.startsWith('"')) {
622
+ result.push(encoded);
623
+ continue;
624
+ }
625
+ try {
626
+ result.push(JSON.parse(encoded));
627
+ }
628
+ catch {
629
+ // Malformed SANs cannot satisfy an exact SPIFFE identity match.
630
+ }
631
+ }
632
+ return result;
633
+ }
634
+ function peerHasExactUriSan(subjectAltName, expected) {
635
+ const uriSans = uriSansFromSubjectAltName(subjectAltName);
636
+ return uriSans.length === 1 && uriSans[0] === expected;
637
+ }
433
638
  /** Resolve only noninteractive runtime credentials. No persisted state is read. */
434
639
  function resolveRuntimeCredential(options = {}) {
435
640
  if (options.requestAuthenticator) {
@@ -439,11 +644,57 @@ function resolveRuntimeCredential(options = {}) {
439
644
  authenticate: options.requestAuthenticator,
440
645
  };
441
646
  }
442
- const apiKey = (options.env ?? process.env).ORY_AGENT_API_KEY?.trim();
647
+ const env = options.env ?? process.env;
648
+ if (env.ORY_AGENT_CREDENTIAL_METHOD?.trim() === SPIFFE_CREDENTIAL_METHOD) {
649
+ const expectedClientSpiffeId = requiredEnv(env, "ORY_AGENT_SPIFFE_ID");
650
+ const generationPath = env.ORY_AGENT_SPIFFE_GENERATION_PATH?.trim();
651
+ const certificatePath = env.ORY_AGENT_SPIFFE_SVID_PATH?.trim();
652
+ const privateKeyPath = env.ORY_AGENT_SPIFFE_KEY_PATH?.trim();
653
+ const trustBundlePath = env.ORY_AGENT_SPIFFE_TRUST_BUNDLE_PATH?.trim();
654
+ const generationFile = env.ORY_SPIFFE_GENERATION_FILE?.trim();
655
+ const directPaths = [certificatePath, privateKeyPath, trustBundlePath];
656
+ if (generationPath && directPaths.some(Boolean)) {
657
+ throw new Error("SPIFFE configuration must use either ORY_AGENT_SPIFFE_GENERATION_PATH or direct material paths");
658
+ }
659
+ if (!generationPath && !directPaths.every(Boolean)) {
660
+ throw new Error("SPIFFE configuration requires ORY_AGENT_SPIFFE_GENERATION_PATH or all three direct material paths");
661
+ }
662
+ if (!generationPath && generationFile) {
663
+ throw new Error("ORY_SPIFFE_GENERATION_FILE requires ORY_AGENT_SPIFFE_GENERATION_PATH");
664
+ }
665
+ const expectedServerSpiffeId = env.ORY_AGENT_SPIFFE_SERVER_ID?.trim();
666
+ return {
667
+ kind: "injected",
668
+ ...(options.subject ? { subject: options.subject } : {}),
669
+ authenticate: createSpiffeFileRequestAuthenticator({
670
+ ...(generationPath ? {
671
+ generationPath,
672
+ ...(generationFile
673
+ ? { generationFile }
674
+ : {}),
675
+ } : {
676
+ certificatePath: certificatePath,
677
+ privateKeyPath: privateKeyPath,
678
+ trustBundlePath: trustBundlePath,
679
+ }),
680
+ expectedClientSpiffeId,
681
+ ...(expectedServerSpiffeId
682
+ ? { expectedServerSpiffeId }
683
+ : { verifyHostname: true }),
684
+ }),
685
+ };
686
+ }
687
+ const apiKey = env.ORY_AGENT_API_KEY?.trim();
443
688
  if (!apiKey)
444
689
  return undefined;
445
690
  return talosRuntimeCredential(apiKey, { subject: options.subject, fetch: options.fetch });
446
691
  }
692
+ function requiredEnv(env, name) {
693
+ const value = env[name]?.trim();
694
+ if (!value)
695
+ throw new Error(`SPIFFE configuration requires ${name}`);
696
+ return value;
697
+ }
447
698
  /** Explicitly enroll a Talos credential and optionally persist it in an OS store. */
448
699
  async function enrollTalosRuntimeCredential(options) {
449
700
  let response;
package/dist/runtime.d.ts CHANGED
@@ -26,10 +26,10 @@
26
26
  * wired where, so `status` can report the runtime a harness will actually
27
27
  * load, upgrades can rewire, and unreferenced stores can be pruned.
28
28
  *
29
- * A **linked** runtime (`linkedRuntime`) points the shim straight at a
30
- * workspace build instead of a materialized store. That is the development
31
- * loop: `pnpm build` + relaunch, with no publish, no version bump, and no
32
- * registry involved.
29
+ * A **linked** runtime (`linkedRuntime`) points the shim straight at an
30
+ * explicit prebuilt package directory instead of a materialized store. This
31
+ * supports both administrator-managed runtimes and the workspace development
32
+ * loop (`pnpm build` + relaunch), with no registry involved.
33
33
  *
34
34
  * ## The shim must fail open
35
35
  *
@@ -137,8 +137,8 @@ export declare function materializeRuntime(opts: {
137
137
  installer?: NpmInstaller;
138
138
  }): RuntimeTarget;
139
139
  /**
140
- * Use an already-built package directory as the runtime — the development
141
- * path (`install --link <dir>`), where the workspace build *is* the artifact.
140
+ * Use an explicit prebuilt package directory as the runtime, including a
141
+ * managed installation or workspace build.
142
142
  *
143
143
  * Validates that the directory looks like a built package so a typo surfaces
144
144
  * at install time instead of as hooks that silently never run.
@@ -206,13 +206,19 @@ export declare function writeMcpShim(opts: {
206
206
  target: RuntimeTarget;
207
207
  installCommand: string;
208
208
  }): string | undefined;
209
+ /**
210
+ * Resolve the Node executable persisted in subprocess hook and MCP config.
211
+ * Managed installations can pin an administrator-approved absolute path;
212
+ * ordinary installations retain the portable `node` command lookup.
213
+ */
214
+ export declare function resolvePluginNodePath(env?: NodeJS.ProcessEnv): string;
209
215
  /**
210
216
  * The hook command a harness config should carry: `node "<shim>"`.
211
217
  *
212
- * Quoted because a data dir can contain spaces, and several harnesses run the
213
- * command through a shell.
218
+ * Paths are shell-quoted because managed executables and data dirs can contain
219
+ * spaces or shell metacharacters, and several harnesses run this as a string.
214
220
  */
215
- export declare function hookCommand(shimPath: string): string;
221
+ export declare function hookCommand(shimPath: string, nodePath?: string): string;
216
222
  /**
217
223
  * True when a command in a harness config is one of ours.
218
224
  *
@@ -225,7 +231,7 @@ export declare function hookCommand(shimPath: string): string;
225
231
  */
226
232
  export declare function isOryRuntimeCommand(command: string | undefined): boolean;
227
233
  /** The MCP server registration a harness config should carry. */
228
- export declare function mcpCommand(shimPath: string): {
234
+ export declare function mcpCommand(shimPath: string, nodePath?: string): {
229
235
  command: string;
230
236
  args: string[];
231
237
  };
@@ -296,8 +302,9 @@ export declare function pruneRuntimeStores(): string[];
296
302
  * `ORY_PLUGIN_RUNTIME_DIR` → link that directory
297
303
  * otherwise → materialize the CLI's own version
298
304
  *
299
- * The flag and the env var support explicit source installs that point at an
300
- * already-built workspace package instead of materializing from a registry.
305
+ * The flag and the env var support explicit prebuilt runtime installs,
306
+ * including administrator-managed package directories and workspace builds,
307
+ * instead of materializing from a registry.
301
308
  *
302
309
  * The flag deliberately requires an explicit directory. A bare "link whatever
303
310
  * package I am running from" would resolve to npx's ephemeral cache dir under