@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.
- package/README.md +25 -0
- package/assets/skills/ory-build-agent/SKILL.md +1 -1
- package/assets/skills/ory-deploy-agent/SKILL.md +315 -0
- package/assets/skills/ory-deploy-openai-e2b/SKILL.md +289 -0
- package/assets/skills/ory-e2b-sandbox/SKILL.md +10 -6
- package/assets/skills/ory-temporal-worker/SKILL.md +4 -4
- package/dist/adapters.d.ts +11 -3
- package/dist/adapters.js +55 -13
- package/dist/agent-auth.d.ts +18 -7
- package/dist/agent-auth.js +120 -10
- package/dist/auth-store.d.ts +3 -3
- package/dist/auth-store.js +56 -12
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/bash-parser.js +212 -47
- package/dist/build-info.json +4 -4
- package/dist/cli.js +5 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.js +24 -7
- package/dist/config.d.ts +23 -3
- package/dist/config.js +202 -18
- package/dist/contract-suite.d.ts +0 -8
- package/dist/contract-suite.js +0 -1
- package/dist/event-reporter.d.ts +1 -0
- package/dist/event-reporter.js +19 -2
- package/dist/hook-timeout.d.ts +2 -2
- package/dist/hook-timeout.js +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +13 -6
- package/dist/lifecycle.js +2 -1
- package/dist/local/configs.d.ts +2 -4
- package/dist/local/configs.js +2 -4
- package/dist/local/manager.js +4 -4
- package/dist/local/seed.d.ts +4 -4
- package/dist/local/seed.js +8 -10
- package/dist/permissions.d.ts +3 -3
- package/dist/permissions.js +26 -1
- package/dist/runtime-credential.d.ts +35 -0
- package/dist/runtime-credential.js +252 -1
- package/dist/runtime.d.ts +19 -12
- package/dist/runtime.js +69 -21
- package/dist/setup.js +3 -3
- package/dist/skills.d.ts +3 -5
- package/dist/skills.js +14 -5
- package/dist/subject.d.ts +2 -3
- package/dist/subject.js +10 -11
- package/dist/testing.d.ts +17 -0
- package/dist/testing.js +53 -0
- package/dist/tool-catalog.d.ts +17 -15
- package/dist/tool-catalog.js +239 -52
- package/dist/user-login.js +1 -1
- package/package.json +1 -1
package/dist/local/seed.js
CHANGED
|
@@ -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:
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
161
|
-
//
|
|
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.
|
|
172
|
-
//
|
|
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 {
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 ⇒
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
141
|
-
*
|
|
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
|
-
*
|
|
213
|
-
*
|
|
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
|
|
300
|
-
*
|
|
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
|