@ory/argus 0.14.0 → 1.0.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.
- package/README.md +31 -46
- package/assets/commands/temporal-up.md +1 -1
- package/assets/skills/auth-setup/SKILL.md +1 -1
- package/assets/skills/local-dev/SKILL.md +17 -7
- package/assets/skills/ory-build-agent/SKILL.md +39 -91
- package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
- package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
- package/assets/skills/permissions-onboarding/SKILL.md +131 -104
- package/dist/adapters.d.ts +88 -32
- package/dist/adapters.js +443 -164
- package/dist/agent-auth.d.ts +226 -67
- package/dist/agent-auth.js +951 -205
- package/dist/auth-store.d.ts +37 -2
- package/dist/auth-store.js +37 -3
- package/dist/auth.d.ts +33 -4
- package/dist/auth.js +163 -20
- package/dist/bash-parser.d.ts +98 -0
- package/dist/bash-parser.js +396 -0
- package/dist/branding.d.ts +77 -16
- package/dist/branding.js +93 -23
- package/dist/build-info.json +4 -4
- package/dist/cli-invocation.d.ts +1 -1
- package/dist/cli-invocation.js +2 -1
- package/dist/cli.d.ts +20 -29
- package/dist/cli.js +271 -239
- package/dist/client.d.ts +175 -138
- package/dist/client.js +672 -391
- package/dist/config.d.ts +217 -54
- package/dist/config.js +461 -42
- package/dist/context.d.ts +10 -0
- package/dist/context.js +21 -0
- package/dist/contract-suite.d.ts +3 -5
- package/dist/contract-suite.js +75 -47
- package/dist/denial.d.ts +36 -3
- package/dist/denial.js +79 -10
- package/dist/event-reporter.d.ts +77 -0
- package/dist/event-reporter.js +776 -0
- package/dist/external-registrations-main.d.ts +10 -0
- package/dist/external-registrations-main.js +38 -0
- package/dist/external-registrations.d.ts +79 -0
- package/dist/external-registrations.js +188 -0
- package/dist/help-cli.d.ts +39 -0
- package/dist/help-cli.js +55 -0
- package/dist/hook-timeout.d.ts +64 -0
- package/dist/hook-timeout.js +88 -0
- package/dist/index.d.ts +28 -23
- package/dist/index.js +173 -58
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +38 -6
- package/dist/local/cli.js +11 -6
- package/dist/local/configs.d.ts +74 -18
- package/dist/local/configs.js +291 -84
- package/dist/local/health.js +7 -7
- package/dist/local/index.d.ts +2 -2
- package/dist/local/index.js +24 -10
- package/dist/local/manager.d.ts +20 -1
- package/dist/local/manager.js +159 -36
- package/dist/local/ports.d.ts +158 -0
- package/dist/local/ports.js +443 -0
- package/dist/local/seed.d.ts +13 -5
- package/dist/local/seed.js +62 -38
- package/dist/logger.d.ts +54 -25
- package/dist/logger.js +329 -63
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +10 -5
- package/dist/mirror-bootstrap.d.ts +48 -0
- package/dist/mirror-bootstrap.js +254 -0
- package/dist/opl.d.ts +289 -0
- package/dist/opl.js +446 -0
- package/dist/permission-mode.d.ts +87 -0
- package/dist/permission-mode.js +307 -0
- package/dist/permissions-cli.d.ts +13 -49
- package/dist/permissions-cli.js +154 -348
- package/dist/permissions.d.ts +148 -38
- package/dist/permissions.js +591 -45
- package/dist/post-install.d.ts +33 -0
- package/dist/post-install.js +127 -0
- package/dist/read-credential.d.ts +65 -0
- package/dist/read-credential.js +86 -0
- package/dist/registry/cli.js +5 -2
- package/dist/registry/config.d.ts +0 -17
- package/dist/registry/config.js +0 -23
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +2 -2
- package/dist/registry/manager.d.ts +4 -21
- package/dist/registry/manager.js +21 -37
- package/dist/runtime-credential.d.ts +140 -0
- package/dist/runtime-credential.js +572 -0
- package/dist/runtime.d.ts +413 -0
- package/dist/runtime.js +825 -0
- package/dist/setup.d.ts +23 -47
- package/dist/setup.js +59 -116
- package/dist/skills.js +0 -7
- package/dist/status-cli.d.ts +27 -11
- package/dist/status-cli.js +121 -115
- package/dist/status-data.d.ts +116 -17
- package/dist/status-data.js +121 -38
- package/dist/subject.d.ts +126 -20
- package/dist/subject.js +215 -30
- package/dist/testing.d.ts +74 -38
- package/dist/testing.js +185 -68
- package/dist/tool-catalog.d.ts +53 -11
- package/dist/tool-catalog.js +164 -13
- package/dist/tool-metadata.d.ts +7 -6
- package/dist/tool-metadata.js +6 -5
- package/dist/types.d.ts +11 -1
- package/dist/uninstall.d.ts +71 -34
- package/dist/uninstall.js +217 -85
- package/dist/user-login.d.ts +9 -10
- package/dist/user-login.js +56 -75
- package/dist/watch-cli.d.ts +6 -0
- package/dist/watch-cli.js +217 -0
- package/package.json +4 -27
- package/assets/commands/dashboard.md +0 -34
- package/dist/dashboard-cli.d.ts +0 -8
- package/dist/dashboard-cli.js +0 -70
- package/dist/dev.d.ts +0 -103
- package/dist/dev.js +0 -583
- package/dist/interactive-setup.d.ts +0 -286
- package/dist/interactive-setup.js +0 -1734
- package/dist/local/jaeger-main.d.ts +0 -13
- package/dist/local/jaeger-main.js +0 -85
- package/dist/local/jaeger.d.ts +0 -50
- package/dist/local/jaeger.js +0 -162
- package/dist/otel/exporter.d.ts +0 -17
- package/dist/otel/exporter.js +0 -12
- package/dist/otel/index.d.ts +0 -2
- package/dist/otel/index.js +0 -8
- package/dist/otel/otlp.d.ts +0 -103
- package/dist/otel/otlp.js +0 -385
- package/dist/project-api-key.d.ts +0 -69
- package/dist/project-api-key.js +0 -147
- package/dist/setup-actions.d.ts +0 -232
- package/dist/setup-actions.js +0 -507
- package/dist/tracer.d.ts +0 -190
- package/dist/tracer.js +0 -481
- package/dist/watch-sandbox.d.ts +0 -9
- package/dist/watch-sandbox.js +0 -81
- package/dist/web/api.d.ts +0 -33
- package/dist/web/api.js +0 -294
- package/dist/web/launch.d.ts +0 -11
- package/dist/web/launch.js +0 -96
- package/dist/web/server.d.ts +0 -20
- package/dist/web/server.js +0 -233
- package/dist/web/types.d.ts +0 -65
- package/dist/web/types.js +0 -2
- package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
- package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
- package/dist/webapp/favicon.ico +0 -0
- package/dist/webapp/index.html +0 -15
package/dist/agent-auth.js
CHANGED
|
@@ -4,48 +4,161 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The "agent" is the AI process making calls on behalf of the human
|
|
6
6
|
* user. Unlike the user, the agent never authenticates interactively.
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* (or an out-of-band initial access token), and persists the issued
|
|
11
|
-
* credentials so subsequent runs reuse them.
|
|
7
|
+
* The normal path resolves a noninteractive runtime credential: an injected
|
|
8
|
+
* request authenticator, `ORY_AGENT_API_KEY`, or a Talos key enrolled with the
|
|
9
|
+
* already-authenticated user. Enrolled keys remain process-local.
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 2. ORY_AGENT_CLIENT_ID +
|
|
17
|
-
* ORY_AGENT_CLIENT_SECRET — explicit static client_credentials
|
|
18
|
-
* 3. Persisted dynamic credentials — reuse a prior DCR registration
|
|
19
|
-
* 4. Fresh dynamic registration — POST /oauth2/register using the
|
|
20
|
-
* user's bearer or
|
|
21
|
-
* ORY_AGENT_REGISTRATION_TOKEN
|
|
22
|
-
* 5. ORY_API_KEY (deprecated) — alias for ORY_AGENT_API_KEY
|
|
23
|
-
* 6. <none> — fail-open; plugin runs without
|
|
24
|
-
* agent identity in audit logs
|
|
25
|
-
*
|
|
26
|
-
* Token-exchange (RFC 8693) — deriving an agent token whose `sub` is
|
|
27
|
-
* the user with an `act` claim showing the agent — is intentionally
|
|
28
|
-
* deferred.
|
|
11
|
+
* Legacy DCR and client-credentials helpers remain exported temporarily so the
|
|
12
|
+
* monorepo and existing consumers compile, but the normal gates do not select
|
|
13
|
+
* them.
|
|
29
14
|
*/
|
|
30
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = void 0;
|
|
16
|
+
exports.DEFAULT_SESSION_RETENTION = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = void 0;
|
|
17
|
+
exports.resolveRuntimeAgentCredentials = resolveRuntimeAgentCredentials;
|
|
32
18
|
exports._resetAgentCredentialsCache = _resetAgentCredentialsCache;
|
|
19
|
+
exports.invalidateAgentAccessToken = invalidateAgentAccessToken;
|
|
33
20
|
exports.loadSubAgentDynamicCredentials = loadSubAgentDynamicCredentials;
|
|
21
|
+
exports.loadAllSubAgentDynamicCredentials = loadAllSubAgentDynamicCredentials;
|
|
22
|
+
exports.loadSubAgentDynamicCredentialsByHarness = loadSubAgentDynamicCredentialsByHarness;
|
|
34
23
|
exports.saveSubAgentDynamicCredentials = saveSubAgentDynamicCredentials;
|
|
35
24
|
exports.clearSubAgentDynamicCredentials = clearSubAgentDynamicCredentials;
|
|
25
|
+
exports.clearSubAgentDynamicCredentialsForHarness = clearSubAgentDynamicCredentialsForHarness;
|
|
36
26
|
exports.loadAgentDynamicCredentials = loadAgentDynamicCredentials;
|
|
27
|
+
exports.loadAgentDynamicCredentialsByHarness = loadAgentDynamicCredentialsByHarness;
|
|
37
28
|
exports.saveAgentDynamicCredentials = saveAgentDynamicCredentials;
|
|
38
29
|
exports.clearAgentDynamicCredentials = clearAgentDynamicCredentials;
|
|
30
|
+
exports.latestAgentRegistration = latestAgentRegistration;
|
|
31
|
+
exports.latestSubAgentSession = latestSubAgentSession;
|
|
32
|
+
exports.countAgentSessions = countAgentSessions;
|
|
33
|
+
exports.loadDelegationNodeId = loadDelegationNodeId;
|
|
34
|
+
exports.saveDelegationNodeId = saveDelegationNodeId;
|
|
35
|
+
exports.loadRetiredCredentials = loadRetiredCredentials;
|
|
36
|
+
exports.clearRetiredCredentials = clearRetiredCredentials;
|
|
37
|
+
exports.sessionRetention = sessionRetention;
|
|
38
|
+
exports.retireAgentSessions = retireAgentSessions;
|
|
39
|
+
exports.claimAgentDynamicCredentials = claimAgentDynamicCredentials;
|
|
40
|
+
exports.claimSubAgentDynamicCredentials = claimSubAgentDynamicCredentials;
|
|
39
41
|
exports.revokeAgentDynamicClient = revokeAgentDynamicClient;
|
|
40
42
|
exports.registerAgentClient = registerAgentClient;
|
|
41
43
|
exports.resolveAgentCredentials = resolveAgentCredentials;
|
|
42
44
|
exports.fetchClientCredentialsToken = fetchClientCredentialsToken;
|
|
43
45
|
exports.ensureAgentIdentity = ensureAgentIdentity;
|
|
44
46
|
exports.ensureSubAgentIdentity = ensureSubAgentIdentity;
|
|
47
|
+
const promises_1 = require("node:fs/promises");
|
|
45
48
|
const node_os_1 = require("node:os");
|
|
49
|
+
const node_path_1 = require("node:path");
|
|
50
|
+
const promises_2 = require("node:timers/promises");
|
|
46
51
|
const client_1 = require("@ory/client");
|
|
47
52
|
const config_js_1 = require("./config.js");
|
|
48
53
|
const branding_js_1 = require("./branding.js");
|
|
54
|
+
const subject_js_1 = require("./subject.js");
|
|
55
|
+
const runtime_credential_js_1 = require("./runtime-credential.js");
|
|
56
|
+
const ENROLLMENT_LOCK_STALE_MS = 120_000;
|
|
57
|
+
const ENROLLMENT_LOCK_WAIT_MS = 35_000;
|
|
58
|
+
async function withEnrollmentLock(identity, work) {
|
|
59
|
+
const lockRoot = (0, node_path_1.join)((0, config_js_1.getDataDir)(), "runtime-credential-locks");
|
|
60
|
+
const lockPath = (0, node_path_1.join)(lockRoot, `${(0, runtime_credential_js_1.credentialAccount)(identity)}.lock`);
|
|
61
|
+
await (0, promises_1.mkdir)(lockRoot, { recursive: true, mode: 0o700 });
|
|
62
|
+
const deadline = Date.now() + ENROLLMENT_LOCK_WAIT_MS;
|
|
63
|
+
for (;;) {
|
|
64
|
+
try {
|
|
65
|
+
await (0, promises_1.mkdir)(lockPath, { mode: 0o700 });
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "EEXIST")
|
|
70
|
+
throw error;
|
|
71
|
+
try {
|
|
72
|
+
const info = await (0, promises_1.stat)(lockPath);
|
|
73
|
+
if (Date.now() - info.mtimeMs > ENROLLMENT_LOCK_STALE_MS) {
|
|
74
|
+
await (0, promises_1.rm)(lockPath, { recursive: true, force: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (statError) {
|
|
79
|
+
if (statError.code === "ENOENT")
|
|
80
|
+
continue;
|
|
81
|
+
throw statError;
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() >= deadline)
|
|
84
|
+
throw new Error("Timed out waiting for runtime enrollment");
|
|
85
|
+
await (0, promises_2.setTimeout)(50);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return await work();
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await (0, promises_1.rm)(lockPath, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the runtime credential used by the normal Agent Security path.
|
|
97
|
+
* This intentionally does not inspect OAuth client credentials or persisted DCR.
|
|
98
|
+
*/
|
|
99
|
+
async function resolveRuntimeAgentCredentials(options = {}) {
|
|
100
|
+
const env = options.env ?? process.env;
|
|
101
|
+
const subject = options.subject ?? (0, subject_js_1.readAgentSubjectOverride)(env, []);
|
|
102
|
+
const injected = options.runtimeCredential?.kind === "injected"
|
|
103
|
+
? options.runtimeCredential
|
|
104
|
+
: (0, runtime_credential_js_1.resolveRuntimeCredential)({
|
|
105
|
+
env: {},
|
|
106
|
+
requestAuthenticator: options.requestAuthenticator,
|
|
107
|
+
subject,
|
|
108
|
+
fetch: options.fetchImpl,
|
|
109
|
+
});
|
|
110
|
+
const configured = (0, runtime_credential_js_1.resolveRuntimeCredential)({ env, subject, fetch: options.fetchImpl });
|
|
111
|
+
const existing = injected ?? configured
|
|
112
|
+
?? (options.runtimeCredential?.kind === "talos" ? options.runtimeCredential : undefined);
|
|
113
|
+
if (existing) {
|
|
114
|
+
return {
|
|
115
|
+
kind: existing.kind,
|
|
116
|
+
runtimeCredential: existing,
|
|
117
|
+
subject: existing.subject,
|
|
118
|
+
reason: existing.kind === "talos"
|
|
119
|
+
? "Using Talos runtime credential"
|
|
120
|
+
: "Using injected runtime request authenticator",
|
|
121
|
+
warnings: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (options.projectUrl) {
|
|
125
|
+
try {
|
|
126
|
+
const store = options.credentialStore ?? (0, runtime_credential_js_1.createOsTalosCredentialStore)();
|
|
127
|
+
const stored = await store.load({
|
|
128
|
+
projectUrl: options.projectUrl,
|
|
129
|
+
harness: options.harness?.trim() || config_js_1.UNKNOWN_HARNESS_KEY,
|
|
130
|
+
sessionId: options.sessionId?.trim() || config_js_1.SESSIONLESS_KEY,
|
|
131
|
+
kind: "agent",
|
|
132
|
+
});
|
|
133
|
+
if (stored) {
|
|
134
|
+
return {
|
|
135
|
+
kind: "talos",
|
|
136
|
+
runtimeCredential: (0, runtime_credential_js_1.talosRuntimeCredential)(stored.credential, {
|
|
137
|
+
subject: stored.actorId,
|
|
138
|
+
fetch: options.fetchImpl,
|
|
139
|
+
}),
|
|
140
|
+
subject: stored.actorId,
|
|
141
|
+
reason: "Using Talos runtime credential from the OS credential store",
|
|
142
|
+
warnings: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return {
|
|
148
|
+
kind: "none",
|
|
149
|
+
reason: "No noninteractive runtime credential is configured",
|
|
150
|
+
warnings: [
|
|
151
|
+
"Could not read the Talos runtime credential from the OS credential store",
|
|
152
|
+
],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
kind: "none",
|
|
158
|
+
reason: "No noninteractive runtime credential is configured",
|
|
159
|
+
warnings: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
49
162
|
/**
|
|
50
163
|
* Skew window before nominal expiry at which a cached client_credentials
|
|
51
164
|
* token is considered stale. Matches the user-token skew in auth-store.
|
|
@@ -56,67 +169,409 @@ const ccCache = new Map();
|
|
|
56
169
|
function _resetAgentCredentialsCache() {
|
|
57
170
|
ccCache.clear();
|
|
58
171
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
172
|
+
/** Evict a bearer rejected by Agent Security without disturbing other sessions. */
|
|
173
|
+
function invalidateAgentAccessToken(rejectedToken) {
|
|
174
|
+
for (const [key, cached] of ccCache) {
|
|
175
|
+
if (cached.token === rejectedToken)
|
|
176
|
+
ccCache.delete(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ─── Credential persistence ──────────────────────────────────────────
|
|
180
|
+
//
|
|
181
|
+
// `dynamic` is keyed by harness then session. `subAgents` adds the sub-agent
|
|
182
|
+
// type below the session. Processes handling different hooks in one session
|
|
183
|
+
// converge on the same credential through the config lock; another session
|
|
184
|
+
// receives a different DCR client.
|
|
185
|
+
/** Load persisted sub-agent DCR credentials for one harness + type. */
|
|
186
|
+
function loadSubAgentDynamicCredentials(harness, sessionOrType, subAgentType) {
|
|
187
|
+
const sessionKey = subAgentType === undefined ? config_js_1.SESSIONLESS_KEY : sessionOrType;
|
|
188
|
+
const type = subAgentType ?? sessionOrType;
|
|
189
|
+
return (0, config_js_1.loadConfig)().agent?.subAgents?.[harness]?.[sessionKey]?.[type];
|
|
63
190
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
191
|
+
/**
|
|
192
|
+
* Load one harness's persisted sub-agent registrations, keyed by sub-agent type.
|
|
193
|
+
* Returns an empty object when none have been registered. Used by the status
|
|
194
|
+
* report to enumerate the sub-agent kinds this harness has resolved.
|
|
195
|
+
*/
|
|
196
|
+
function loadAllSubAgentDynamicCredentials(harness, sessionKey = config_js_1.SESSIONLESS_KEY) {
|
|
197
|
+
return { ...((0, config_js_1.loadConfig)().agent?.subAgents?.[harness]?.[sessionKey] ?? {}) };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Every persisted sub-agent registration, keyed by harness, session, then type. Used by
|
|
201
|
+
* teardown and reporting paths that must reach beyond the harness they run under.
|
|
202
|
+
*/
|
|
203
|
+
function loadSubAgentDynamicCredentialsByHarness() {
|
|
204
|
+
const stored = (0, config_js_1.loadConfig)().agent?.subAgents ?? {};
|
|
205
|
+
return Object.fromEntries(Object.entries(stored).map(([harness, bySession]) => [
|
|
206
|
+
harness,
|
|
207
|
+
Object.fromEntries(Object.entries(bySession).map(([session, byType]) => [session, { ...byType }])),
|
|
208
|
+
]));
|
|
209
|
+
}
|
|
210
|
+
function saveSubAgentDynamicCredentials(harness, sessionOrType, typeOrCreds, maybeCreds) {
|
|
211
|
+
const sessionKey = maybeCreds ? sessionOrType : config_js_1.SESSIONLESS_KEY;
|
|
212
|
+
const subAgentType = maybeCreds ? typeOrCreds : sessionOrType;
|
|
213
|
+
const creds = maybeCreds ?? typeOrCreds;
|
|
66
214
|
(0, config_js_1.mutateConfig)((current) => {
|
|
67
215
|
const agent = { ...(current.agent ?? {}) };
|
|
68
216
|
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
69
|
-
subAgents[
|
|
217
|
+
const bySession = { ...(subAgents[harness] ?? {}) };
|
|
218
|
+
bySession[sessionKey] = { ...(bySession[sessionKey] ?? {}), [subAgentType]: creds };
|
|
219
|
+
subAgents[harness] = bySession;
|
|
70
220
|
agent.subAgents = subAgents;
|
|
71
221
|
return { ...current, agent };
|
|
72
222
|
});
|
|
73
223
|
}
|
|
74
|
-
/** Remove persisted sub-agent DCR credentials for
|
|
75
|
-
function clearSubAgentDynamicCredentials(subAgentType) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
delete
|
|
83
|
-
if (Object.keys(
|
|
84
|
-
delete
|
|
224
|
+
/** Remove persisted sub-agent DCR credentials for one harness + type. */
|
|
225
|
+
function clearSubAgentDynamicCredentials(harness, sessionOrType, subAgentType) {
|
|
226
|
+
const sessionKey = subAgentType === undefined ? config_js_1.SESSIONLESS_KEY : sessionOrType;
|
|
227
|
+
const type = subAgentType ?? sessionOrType;
|
|
228
|
+
(0, config_js_1.mutateConfig)((current) => pruneAgentBlock(current, (agent) => {
|
|
229
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
230
|
+
const bySession = { ...(subAgents[harness] ?? {}) };
|
|
231
|
+
const byType = { ...(bySession[sessionKey] ?? {}) };
|
|
232
|
+
delete byType[type];
|
|
233
|
+
if (Object.keys(byType).length === 0)
|
|
234
|
+
delete bySession[sessionKey];
|
|
85
235
|
else
|
|
86
|
-
|
|
87
|
-
if (
|
|
88
|
-
delete
|
|
236
|
+
bySession[sessionKey] = byType;
|
|
237
|
+
if (Object.keys(bySession).length === 0)
|
|
238
|
+
delete subAgents[harness];
|
|
89
239
|
else
|
|
90
|
-
|
|
240
|
+
subAgents[harness] = bySession;
|
|
241
|
+
agent.subAgents = subAgents;
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
/** Remove every persisted sub-agent registration for one harness. */
|
|
245
|
+
function clearSubAgentDynamicCredentialsForHarness(harness) {
|
|
246
|
+
(0, config_js_1.mutateConfig)((current) => pruneAgentBlock(current, (agent) => {
|
|
247
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
248
|
+
delete subAgents[harness];
|
|
249
|
+
agent.subAgents = subAgents;
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
// ─── Dynamic registration persistence ────────────────────────────────
|
|
253
|
+
/** Load persisted DCR credentials for one harness session. */
|
|
254
|
+
function loadAgentDynamicCredentials(harness, sessionKey = config_js_1.SESSIONLESS_KEY) {
|
|
255
|
+
return (0, config_js_1.loadConfig)().agent?.dynamic?.[harness]?.[sessionKey];
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Every persisted agent registration, keyed by harness then session.
|
|
259
|
+
* reporting paths that must reach beyond the harness they run under.
|
|
260
|
+
*/
|
|
261
|
+
function loadAgentDynamicCredentialsByHarness() {
|
|
262
|
+
const stored = (0, config_js_1.loadConfig)().agent?.dynamic ?? {};
|
|
263
|
+
return Object.fromEntries(Object.entries(stored).map(([harness, bySession]) => [harness, { ...bySession }]));
|
|
264
|
+
}
|
|
265
|
+
function saveAgentDynamicCredentials(harness, sessionOrCreds, maybeCreds) {
|
|
266
|
+
const sessionKey = typeof sessionOrCreds === "string" ? sessionOrCreds : config_js_1.SESSIONLESS_KEY;
|
|
267
|
+
const creds = maybeCreds ?? sessionOrCreds;
|
|
268
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
269
|
+
const agent = { ...(current.agent ?? {}) };
|
|
270
|
+
const bySession = { ...(agent.dynamic?.[harness] ?? {}) };
|
|
271
|
+
bySession[sessionKey] = creds;
|
|
272
|
+
agent.dynamic = { ...(agent.dynamic ?? {}), [harness]: bySession };
|
|
273
|
+
return { ...current, agent };
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/** Remove one session, or every session when no session key is supplied. */
|
|
277
|
+
function clearAgentDynamicCredentials(harness, sessionKey) {
|
|
278
|
+
(0, config_js_1.mutateConfig)((current) => pruneAgentBlock(current, (agent) => {
|
|
279
|
+
const dynamic = { ...(agent.dynamic ?? {}) };
|
|
280
|
+
if (sessionKey === undefined)
|
|
281
|
+
delete dynamic[harness];
|
|
282
|
+
else {
|
|
283
|
+
const bySession = { ...(dynamic[harness] ?? {}) };
|
|
284
|
+
delete bySession[sessionKey];
|
|
285
|
+
if (Object.keys(bySession).length === 0)
|
|
286
|
+
delete dynamic[harness];
|
|
287
|
+
else
|
|
288
|
+
dynamic[harness] = bySession;
|
|
289
|
+
}
|
|
290
|
+
agent.dynamic = dynamic;
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
/** Most recently registered agent session for status/reporting. */
|
|
294
|
+
function latestAgentRegistration(harness) {
|
|
295
|
+
const entries = Object.entries((0, config_js_1.loadConfig)().agent?.dynamic?.[harness] ?? {}).sort(([aKey, a], [bKey, b]) => b.registeredAt - a.registeredAt || bKey.localeCompare(aKey));
|
|
296
|
+
const [sessionKey, credentials] = entries[0] ?? [];
|
|
297
|
+
return sessionKey && credentials ? { sessionKey, credentials } : undefined;
|
|
298
|
+
}
|
|
299
|
+
/** Newest session containing at least one sub-agent registration. */
|
|
300
|
+
function latestSubAgentSession(harness) {
|
|
301
|
+
return Object.entries((0, config_js_1.loadConfig)().agent?.subAgents?.[harness] ?? {})
|
|
302
|
+
.map(([sessionKey, byType]) => ({
|
|
303
|
+
sessionKey,
|
|
304
|
+
registeredAt: Math.max(...Object.values(byType).map((c) => c.registeredAt), 0),
|
|
305
|
+
}))
|
|
306
|
+
.sort((a, b) => b.registeredAt - a.registeredAt || b.sessionKey.localeCompare(a.sessionKey))[0]
|
|
307
|
+
?.sessionKey;
|
|
308
|
+
}
|
|
309
|
+
function countAgentSessions(harness) {
|
|
310
|
+
return Object.keys((0, config_js_1.loadConfig)().agent?.dynamic?.[harness] ?? {}).length;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Apply `mutate` to a copy of the agent block, then drop any map (and the block
|
|
314
|
+
* itself) left empty. Every clear path needs the same collapse, and doing it
|
|
315
|
+
* once keeps an emptied `agent: {}` from lingering in the file.
|
|
316
|
+
*/
|
|
317
|
+
function pruneAgentBlock(current, mutate) {
|
|
318
|
+
const next = { ...current };
|
|
319
|
+
if (!next.agent)
|
|
91
320
|
return next;
|
|
321
|
+
const agent = { ...next.agent };
|
|
322
|
+
mutate(agent);
|
|
323
|
+
if (agent.dynamic && Object.keys(agent.dynamic).length === 0)
|
|
324
|
+
delete agent.dynamic;
|
|
325
|
+
if (agent.subAgents && Object.keys(agent.subAgents).length === 0)
|
|
326
|
+
delete agent.subAgents;
|
|
327
|
+
if (agent.retired && agent.retired.length === 0)
|
|
328
|
+
delete agent.retired;
|
|
329
|
+
if (!agent.dynamic && !agent.subAgents && !agent.retired)
|
|
330
|
+
delete next.agent;
|
|
331
|
+
else
|
|
332
|
+
next.agent = agent;
|
|
333
|
+
return next;
|
|
334
|
+
}
|
|
335
|
+
// ─── Delegation node ids (non-secret, independent of credentials) ───
|
|
336
|
+
/**
|
|
337
|
+
* The delegation node the broker assigned to this harness's `user → agent` edge
|
|
338
|
+
* for one session, or `undefined` if that session hasn't recorded one yet.
|
|
339
|
+
*
|
|
340
|
+
* Read by the sub-agent edge to pass as `delegated_by` — which is why it is
|
|
341
|
+
* looked up by session and not just by harness: the credential is shared across
|
|
342
|
+
* sessions but the node is not, so a sub-agent must hang off *its own* run's
|
|
343
|
+
* agent node.
|
|
344
|
+
*/
|
|
345
|
+
function loadDelegationNodeId(harness, sessionKey) {
|
|
346
|
+
const config = (0, config_js_1.loadConfig)();
|
|
347
|
+
return config.delegationAnchors?.[harness]?.[sessionKey]
|
|
348
|
+
// Read the old location during the cutover; every subsequent save writes
|
|
349
|
+
// only the independent non-secret anchor map.
|
|
350
|
+
?? config.agent?.dynamic?.[harness]?.[sessionKey]?.delegationNodeIds?.[sessionKey];
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Record the broker-assigned delegation node for `(harness, session)`, trimming
|
|
354
|
+
* the map to the most recent {@link DELEGATION_NODE_HISTORY} entries.
|
|
355
|
+
*
|
|
356
|
+
* The anchor is not a credential and is safe to persist independently. A
|
|
357
|
+
* repeated idempotent edge does not rewrite the config file.
|
|
358
|
+
*/
|
|
359
|
+
function saveDelegationNodeId(harness, sessionKey, nodeId) {
|
|
360
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
361
|
+
const existing = current.delegationAnchors?.[harness] ?? {};
|
|
362
|
+
if (existing[sessionKey] === nodeId)
|
|
363
|
+
return current;
|
|
364
|
+
// Re-insert rather than spread over the existing entry: a spread keeps an
|
|
365
|
+
// already-present key in its *original* position, so a session that
|
|
366
|
+
// re-recorded a changed node id would still be pruned as if it were the
|
|
367
|
+
// oldest. Deleting first moves it to the end, where the bound never reaches.
|
|
368
|
+
const merged = { ...existing };
|
|
369
|
+
delete merged[sessionKey];
|
|
370
|
+
merged[sessionKey] = nodeId;
|
|
371
|
+
// Insertion order is oldest-first, so dropping from the front keeps the
|
|
372
|
+
// newest entries. The session just written is last and always survives.
|
|
373
|
+
const keys = Object.keys(merged);
|
|
374
|
+
const kept = keys.slice(Math.max(0, keys.length - config_js_1.DELEGATION_NODE_HISTORY));
|
|
375
|
+
return {
|
|
376
|
+
...current,
|
|
377
|
+
delegationAnchors: {
|
|
378
|
+
...(current.delegationAnchors ?? {}),
|
|
379
|
+
[harness]: Object.fromEntries(kept.map((key) => [key, merged[key]])),
|
|
380
|
+
},
|
|
381
|
+
};
|
|
92
382
|
});
|
|
93
383
|
}
|
|
94
|
-
// ───
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
function
|
|
101
|
-
(0, config_js_1.
|
|
102
|
-
|
|
103
|
-
|
|
384
|
+
// ─── Revocation drain queue ──────────────────────────────────────────
|
|
385
|
+
//
|
|
386
|
+
// Session retention atomically removes an aged parent and its typed children
|
|
387
|
+
// from the active maps and moves them to `agent.retired`. They still exist on
|
|
388
|
+
// the project, so they must be revoked rather than forgotten.
|
|
389
|
+
/** Registrations queued for best-effort revocation. */
|
|
390
|
+
function loadRetiredCredentials() {
|
|
391
|
+
return [...((0, config_js_1.loadConfig)().agent?.retired ?? [])];
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Remove `clientIds` from the drain queue. Called with the ones that were
|
|
395
|
+
* actually revoked; anything left stays queued for the next run, so a transient
|
|
396
|
+
* failure retries instead of orphaning the client.
|
|
397
|
+
*/
|
|
398
|
+
function clearRetiredCredentials(clientIds) {
|
|
399
|
+
if (clientIds.length === 0)
|
|
400
|
+
return;
|
|
401
|
+
const done = new Set(clientIds);
|
|
402
|
+
(0, config_js_1.mutateConfig)((current) => pruneAgentBlock(current, (agent) => {
|
|
403
|
+
agent.retired = (agent.retired ?? []).filter((c) => !done.has(c.clientId));
|
|
104
404
|
}));
|
|
105
405
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
406
|
+
/** Automatic retirement is opt-in because age alone cannot prove a session ended. */
|
|
407
|
+
exports.DEFAULT_SESSION_RETENTION = 0;
|
|
408
|
+
/** `0` disables automatic retirement. */
|
|
409
|
+
function sessionRetention(env = process.env) {
|
|
410
|
+
const raw = env.ORY_AGENT_SESSION_RETENTION?.trim();
|
|
411
|
+
if (!raw)
|
|
412
|
+
return exports.DEFAULT_SESSION_RETENTION;
|
|
413
|
+
if (!/^\d+$/.test(raw))
|
|
414
|
+
return exports.DEFAULT_SESSION_RETENTION;
|
|
415
|
+
const parsed = Number(raw);
|
|
416
|
+
return Number.isSafeInteger(parsed) ? parsed : exports.DEFAULT_SESSION_RETENTION;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Move aged-out agent sessions and all of their child credentials to the
|
|
420
|
+
* retirement queue atomically. They remain addressable until RFC 7592 cleanup
|
|
421
|
+
* succeeds, avoiding the orphaning behavior of the original pruning path.
|
|
422
|
+
*/
|
|
423
|
+
function retireAgentSessions(harness, opts) {
|
|
424
|
+
if (opts.keep <= 0)
|
|
425
|
+
return 0;
|
|
426
|
+
let retiredCount = 0;
|
|
427
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
428
|
+
const bySession = current.agent?.dynamic?.[harness];
|
|
429
|
+
if (!bySession)
|
|
430
|
+
return current;
|
|
431
|
+
const ordered = Object.entries(bySession).sort(([aKey, a], [bKey, b]) => b.registeredAt - a.registeredAt || bKey.localeCompare(aKey));
|
|
432
|
+
const hasProtected = !!opts.protect && Object.hasOwn(bySession, opts.protect);
|
|
433
|
+
const keepOthers = Math.max(0, opts.keep - (hasProtected ? 1 : 0));
|
|
434
|
+
const doomed = ordered.filter(([key]) => key !== opts.protect).slice(keepOthers);
|
|
435
|
+
if (doomed.length === 0)
|
|
436
|
+
return current;
|
|
437
|
+
const agent = { ...current.agent };
|
|
438
|
+
const dynamic = { ...(agent.dynamic ?? {}) };
|
|
439
|
+
const kept = { ...bySession };
|
|
440
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
441
|
+
const subBySession = { ...(subAgents[harness] ?? {}) };
|
|
442
|
+
const queue = [...(agent.retired ?? [])];
|
|
443
|
+
const queued = new Set(queue.map((c) => c.clientId));
|
|
444
|
+
for (const [sessionKey, creds] of doomed) {
|
|
445
|
+
if (!queued.has(creds.clientId))
|
|
446
|
+
queue.push({ ...creds, harness });
|
|
447
|
+
delete kept[sessionKey];
|
|
448
|
+
for (const child of Object.values(subBySession[sessionKey] ?? {})) {
|
|
449
|
+
if (!queued.has(child.clientId))
|
|
450
|
+
queue.push({ ...child, harness });
|
|
451
|
+
}
|
|
452
|
+
delete subBySession[sessionKey];
|
|
453
|
+
retiredCount++;
|
|
454
|
+
}
|
|
455
|
+
if (Object.keys(kept).length === 0)
|
|
456
|
+
delete dynamic[harness];
|
|
457
|
+
else
|
|
458
|
+
dynamic[harness] = kept;
|
|
459
|
+
if (Object.keys(subBySession).length === 0)
|
|
460
|
+
delete subAgents[harness];
|
|
461
|
+
else
|
|
462
|
+
subAgents[harness] = subBySession;
|
|
463
|
+
agent.dynamic = dynamic;
|
|
464
|
+
agent.subAgents = subAgents;
|
|
465
|
+
agent.retired = queue;
|
|
466
|
+
return { ...current, agent };
|
|
467
|
+
});
|
|
468
|
+
return retiredCount;
|
|
469
|
+
}
|
|
470
|
+
/** Retire one exact session and all children when its identity binding is stale. */
|
|
471
|
+
function retireAgentSession(harness, sessionKey) {
|
|
472
|
+
let retired = false;
|
|
108
473
|
(0, config_js_1.mutateConfig)((current) => {
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
111
|
-
return
|
|
112
|
-
const agent = { ...
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
474
|
+
const credentials = current.agent?.dynamic?.[harness]?.[sessionKey];
|
|
475
|
+
if (!credentials)
|
|
476
|
+
return current;
|
|
477
|
+
const agent = { ...current.agent };
|
|
478
|
+
const dynamic = { ...(agent.dynamic ?? {}) };
|
|
479
|
+
const bySession = { ...(dynamic[harness] ?? {}) };
|
|
480
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
481
|
+
const subBySession = { ...(subAgents[harness] ?? {}) };
|
|
482
|
+
const queue = [...(agent.retired ?? [])];
|
|
483
|
+
const queued = new Set(queue.map((item) => item.clientId));
|
|
484
|
+
if (!queued.has(credentials.clientId))
|
|
485
|
+
queue.push({ ...credentials, harness });
|
|
486
|
+
for (const child of Object.values(subBySession[sessionKey] ?? {})) {
|
|
487
|
+
if (!queued.has(child.clientId))
|
|
488
|
+
queue.push({ ...child, harness });
|
|
489
|
+
}
|
|
490
|
+
delete bySession[sessionKey];
|
|
491
|
+
delete subBySession[sessionKey];
|
|
492
|
+
if (Object.keys(bySession).length === 0)
|
|
493
|
+
delete dynamic[harness];
|
|
116
494
|
else
|
|
117
|
-
|
|
118
|
-
|
|
495
|
+
dynamic[harness] = bySession;
|
|
496
|
+
if (Object.keys(subBySession).length === 0)
|
|
497
|
+
delete subAgents[harness];
|
|
498
|
+
else
|
|
499
|
+
subAgents[harness] = subBySession;
|
|
500
|
+
agent.dynamic = dynamic;
|
|
501
|
+
agent.subAgents = subAgents;
|
|
502
|
+
agent.retired = queue;
|
|
503
|
+
retired = true;
|
|
504
|
+
return { ...current, agent };
|
|
119
505
|
});
|
|
506
|
+
return retired;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* True when `existing` is a usable peer registration that should win over a
|
|
510
|
+
* client we just minted for the same key: it is a *different* client, issued
|
|
511
|
+
* against the same project, and usable for a client_credentials grant.
|
|
512
|
+
*/
|
|
513
|
+
function isUsablePeer(existing, registered) {
|
|
514
|
+
return Boolean(existing &&
|
|
515
|
+
existing.clientId !== registered.clientId &&
|
|
516
|
+
existing.projectUrl === registered.projectUrl &&
|
|
517
|
+
existing.clientSecret &&
|
|
518
|
+
(!registered.delegation ||
|
|
519
|
+
(existing.delegation?.delegationType === registered.delegation.delegationType &&
|
|
520
|
+
existing.delegation.delegatedBy === registered.delegation.delegatedBy)));
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Persist a freshly registered agent client, unless a concurrent peer already
|
|
524
|
+
* claimed the `(projectUrl, harness)` slot.
|
|
525
|
+
*
|
|
526
|
+
* Keying credentials by harness removes the cold-start collision *between*
|
|
527
|
+
* harnesses, but two instances of the *same* harness starting at once still
|
|
528
|
+
* target one key. Both POST `/oauth2/register`, and a plain last-writer-wins
|
|
529
|
+
* save would leave one client orphaned server-side (and its delegation edge
|
|
530
|
+
* dangling). The read and the write happen inside a single `mutateConfig`, so
|
|
531
|
+
* the loser sees the winner's registration and reports it back to be revoked.
|
|
532
|
+
*/
|
|
533
|
+
function claimAgentDynamicCredentials(harness, sessionKey, registered) {
|
|
534
|
+
let claim = { credentials: registered, peer: false };
|
|
535
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
536
|
+
const existing = current.agent?.dynamic?.[harness]?.[sessionKey];
|
|
537
|
+
if (isUsablePeer(existing, registered)) {
|
|
538
|
+
claim = { credentials: existing, peer: true };
|
|
539
|
+
return current;
|
|
540
|
+
}
|
|
541
|
+
const agent = { ...(current.agent ?? {}) };
|
|
542
|
+
if (existing &&
|
|
543
|
+
existing.clientId !== registered.clientId &&
|
|
544
|
+
existing.projectUrl === registered.projectUrl) {
|
|
545
|
+
agent.retired = [
|
|
546
|
+
...(agent.retired ?? []).filter((c) => c.clientId !== existing.clientId),
|
|
547
|
+
{ ...existing, harness },
|
|
548
|
+
];
|
|
549
|
+
}
|
|
550
|
+
const bySession = { ...(agent.dynamic?.[harness] ?? {}) };
|
|
551
|
+
bySession[sessionKey] = registered;
|
|
552
|
+
agent.dynamic = { ...(agent.dynamic ?? {}), [harness]: bySession };
|
|
553
|
+
return { ...current, agent };
|
|
554
|
+
});
|
|
555
|
+
return claim;
|
|
556
|
+
}
|
|
557
|
+
/** {@link claimAgentDynamicCredentials} for a `(harness, subAgentType)` slot. */
|
|
558
|
+
function claimSubAgentDynamicCredentials(harness, sessionKey, subAgentType, registered) {
|
|
559
|
+
let claim = { credentials: registered, peer: false };
|
|
560
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
561
|
+
const existing = current.agent?.subAgents?.[harness]?.[sessionKey]?.[subAgentType];
|
|
562
|
+
if (isUsablePeer(existing, registered)) {
|
|
563
|
+
claim = { credentials: existing, peer: true };
|
|
564
|
+
return current;
|
|
565
|
+
}
|
|
566
|
+
const agent = { ...(current.agent ?? {}) };
|
|
567
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
568
|
+
const bySession = { ...(subAgents[harness] ?? {}) };
|
|
569
|
+
bySession[sessionKey] = { ...(bySession[sessionKey] ?? {}), [subAgentType]: registered };
|
|
570
|
+
subAgents[harness] = bySession;
|
|
571
|
+
agent.subAgents = subAgents;
|
|
572
|
+
return { ...current, agent };
|
|
573
|
+
});
|
|
574
|
+
return claim;
|
|
120
575
|
}
|
|
121
576
|
/**
|
|
122
577
|
* Best-effort RFC 7592 `DELETE` of a dynamically-registered OAuth2 client.
|
|
@@ -157,7 +612,7 @@ async function revokeAgentDynamicClient(creds) {
|
|
|
157
612
|
* either by the user's interactive token (the natural bootstrap) or by
|
|
158
613
|
* a pre-issued initial access token (`ORY_AGENT_REGISTRATION_TOKEN`).
|
|
159
614
|
* Throws on any non-2xx response so callers can decide whether to
|
|
160
|
-
* retry, fall back, or surface the error in
|
|
615
|
+
* retry, fall back, or surface the error in audit activity. The
|
|
161
616
|
* `HTTP <status>` error message shape is preserved so callers and tests
|
|
162
617
|
* can pattern-match on it regardless of the underlying transport.
|
|
163
618
|
*/
|
|
@@ -167,6 +622,7 @@ async function registerAgentClient(args) {
|
|
|
167
622
|
harness: args.harness,
|
|
168
623
|
host,
|
|
169
624
|
subAgentType: args.subAgentType,
|
|
625
|
+
sessionId: args.sessionId,
|
|
170
626
|
});
|
|
171
627
|
// Build the delegation anchor (if a delegator is known) and mirror it into the
|
|
172
628
|
// persisted credentials. This is the integrity anchor the Keto tuple is derived
|
|
@@ -211,7 +667,10 @@ async function registerAgentClient(args) {
|
|
|
211
667
|
// Configuration's `accessToken`. Ory projects that gate DCR behind an
|
|
212
668
|
// initial access token then reject the unauthenticated request and the
|
|
213
669
|
// agent never gets credentials. Send the bearer explicitly.
|
|
214
|
-
|
|
670
|
+
{
|
|
671
|
+
...(args.auth ? { headers: { Authorization: `Bearer ${args.auth}` } } : {}),
|
|
672
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
673
|
+
});
|
|
215
674
|
}
|
|
216
675
|
catch (err) {
|
|
217
676
|
const status = err?.response?.status;
|
|
@@ -232,35 +691,38 @@ async function registerAgentClient(args) {
|
|
|
232
691
|
registeredAt: Math.floor(Date.now() / 1000),
|
|
233
692
|
projectUrl: args.projectUrl,
|
|
234
693
|
harness: args.harness,
|
|
694
|
+
clientName: client.client_name ?? clientName,
|
|
235
695
|
...(delegation ? { delegation } : {}),
|
|
236
696
|
};
|
|
237
697
|
}
|
|
238
698
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* so the caller can fail open.
|
|
699
|
+
* Legacy OAuth/DCR resolver retained for source compatibility. Normal runtime
|
|
700
|
+
* gates use {@link resolveRuntimeAgentCredentials} instead.
|
|
242
701
|
*/
|
|
243
702
|
async function resolveAgentCredentials(options = {}) {
|
|
244
703
|
const env = options.env ?? process.env;
|
|
245
704
|
const warnings = [];
|
|
705
|
+
// Read and validate once, not at each of the three grant paths below — the
|
|
706
|
+
// rule has to be the same whichever credential slot wins (#241).
|
|
707
|
+
const subjectOverride = (0, subject_js_1.readAgentSubjectOverride)(env, warnings);
|
|
246
708
|
const fetchCcFn = options.fetchClientCredentialsTokenFn ?? fetchClientCredentialsToken;
|
|
247
709
|
const registerFn = options.registerAgentClientFn ?? registerAgentClient;
|
|
248
710
|
const loadDynamicFn = options.loadDynamicFn ?? loadAgentDynamicCredentials;
|
|
249
711
|
const saveDynamicFn = options.saveDynamicFn ?? saveAgentDynamicCredentials;
|
|
250
712
|
const clearDynamicFn = options.clearDynamicFn ?? clearAgentDynamicCredentials;
|
|
713
|
+
const revokeFn = options.revokeFn ?? revokeAgentDynamicClient;
|
|
251
714
|
const skipCache = options.skipCache ?? false;
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
// 2. Explicit static client_credentials — operator override.
|
|
715
|
+
// Credentials are keyed by harness *and session*: each installed harness owns
|
|
716
|
+
// its own agent identity, and each session of that harness gets a distinct
|
|
717
|
+
// one. A caller with no harness (an SDK embedding, a one-off script) gets the
|
|
718
|
+
// shared `unknown` slot rather than silently hijacking a harness's identity;
|
|
719
|
+
// one with no session gets the single `sessionless` identity rather than a
|
|
720
|
+
// fresh client per call.
|
|
721
|
+
const harnessKey = options.harness?.trim() || config_js_1.UNKNOWN_HARNESS_KEY;
|
|
722
|
+
const sessionKey = options.sessionId?.trim() || config_js_1.SESSIONLESS_KEY;
|
|
723
|
+
// 1. Explicit static client_credentials — operator override. This is the
|
|
724
|
+
// only non-DCR path: it still yields a real OAuth2 access token, which is
|
|
725
|
+
// what the Keto `/user` read path authenticates with.
|
|
264
726
|
const clientId = env.ORY_AGENT_CLIENT_ID;
|
|
265
727
|
const clientSecret = env.ORY_AGENT_CLIENT_SECRET;
|
|
266
728
|
if (clientId && clientSecret) {
|
|
@@ -272,21 +734,37 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
272
734
|
};
|
|
273
735
|
}
|
|
274
736
|
return await resolveClientCredentials({
|
|
275
|
-
projectUrl: options.projectUrl,
|
|
737
|
+
projectUrl: options.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl,
|
|
276
738
|
clientId,
|
|
277
739
|
clientSecret,
|
|
278
740
|
audience: env.ORY_AGENT_AUDIENCE,
|
|
279
741
|
scope: env.ORY_AGENT_SCOPE,
|
|
280
|
-
subjectOverride
|
|
742
|
+
subjectOverride,
|
|
281
743
|
kind: "client_credentials",
|
|
282
744
|
fetchFn: fetchCcFn,
|
|
283
745
|
skipCache,
|
|
284
746
|
warnings,
|
|
747
|
+
signal: options.signal,
|
|
285
748
|
});
|
|
286
749
|
}
|
|
287
|
-
//
|
|
750
|
+
// 2. Re-use the DCR client for this exact harness session. Subprocess hook
|
|
751
|
+
// processes converge here; a new session has a different slot.
|
|
288
752
|
if (options.projectUrl) {
|
|
289
|
-
|
|
753
|
+
let persisted = loadDynamicFn(harnessKey, sessionKey);
|
|
754
|
+
const expectedDelegator = options.userSubject ? `user:${options.userSubject}` : undefined;
|
|
755
|
+
if (persisted &&
|
|
756
|
+
expectedDelegator &&
|
|
757
|
+
(persisted.delegation?.delegationType !== "user-to-agent" ||
|
|
758
|
+
persisted.delegation.delegatedBy !== expectedDelegator)) {
|
|
759
|
+
warnings.push(`Persisted dynamic agent credentials belong to ${persisted.delegation?.delegatedBy ?? "an unknown user"}; registering a credential for ${expectedDelegator}.`);
|
|
760
|
+
if (options.loadDynamicFn || options.saveDynamicFn) {
|
|
761
|
+
clearDynamicFn(harnessKey, sessionKey);
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
retireAgentSession(harnessKey, sessionKey);
|
|
765
|
+
}
|
|
766
|
+
persisted = undefined;
|
|
767
|
+
}
|
|
290
768
|
if (persisted && persisted.projectUrl === options.projectUrl && persisted.clientSecret) {
|
|
291
769
|
const result = await resolveClientCredentials({
|
|
292
770
|
projectUrl: options.projectUrl,
|
|
@@ -294,11 +772,12 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
294
772
|
clientSecret: persisted.clientSecret,
|
|
295
773
|
audience: env.ORY_AGENT_AUDIENCE,
|
|
296
774
|
scope: env.ORY_AGENT_SCOPE,
|
|
297
|
-
subjectOverride
|
|
775
|
+
subjectOverride,
|
|
298
776
|
kind: "dynamic",
|
|
299
777
|
fetchFn: fetchCcFn,
|
|
300
778
|
skipCache,
|
|
301
779
|
warnings,
|
|
780
|
+
signal: options.signal,
|
|
302
781
|
});
|
|
303
782
|
if (result.kind !== "none")
|
|
304
783
|
return result;
|
|
@@ -310,7 +789,7 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
310
789
|
// blip. Keep the persisted creds and report the transient failure instead.
|
|
311
790
|
if (result.authRejected) {
|
|
312
791
|
warnings.push(`Persisted dynamic agent credentials rejected (${result.reason}); re-registering.`);
|
|
313
|
-
clearDynamicFn();
|
|
792
|
+
clearDynamicFn(harnessKey, sessionKey);
|
|
314
793
|
}
|
|
315
794
|
else {
|
|
316
795
|
warnings.push(`Persisted dynamic agent credentials temporarily unavailable (${result.reason}); keeping them for the next session.`);
|
|
@@ -318,15 +797,17 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
318
797
|
}
|
|
319
798
|
}
|
|
320
799
|
}
|
|
321
|
-
//
|
|
800
|
+
// 3. Fresh dynamic registration — bootstrap using the user's bearer
|
|
322
801
|
// or an out-of-band initial access token.
|
|
323
802
|
const iat = options.userToken ?? env.ORY_AGENT_REGISTRATION_TOKEN;
|
|
324
|
-
if (options.projectUrl && iat) {
|
|
803
|
+
if (options.allowRegistration !== false && options.projectUrl && iat) {
|
|
325
804
|
try {
|
|
326
805
|
const registered = await registerFn({
|
|
327
806
|
projectUrl: options.projectUrl,
|
|
328
807
|
auth: iat,
|
|
329
808
|
harness: options.harness,
|
|
809
|
+
sessionId: sessionKey === config_js_1.SESSIONLESS_KEY ? undefined : sessionKey,
|
|
810
|
+
signal: options.signal,
|
|
330
811
|
...(options.userSubject
|
|
331
812
|
? {
|
|
332
813
|
delegation: {
|
|
@@ -340,18 +821,34 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
340
821
|
warnings.push("Dynamic registration returned no client_secret; cannot run client_credentials grant.");
|
|
341
822
|
}
|
|
342
823
|
else {
|
|
343
|
-
|
|
824
|
+
// Double-checked registration: a concurrent instance of this same
|
|
825
|
+
// harness may have claimed the slot while our POST was in flight. The
|
|
826
|
+
// claim re-reads under the config lock; if we lost, adopt the peer's
|
|
827
|
+
// client and revoke the one we just minted so it isn't orphaned.
|
|
828
|
+
const claim = claimRegistration(harnessKey, sessionKey, registered, {
|
|
829
|
+
loadDynamicFn,
|
|
830
|
+
saveDynamicFn,
|
|
831
|
+
useSharedStore: !options.loadDynamicFn && !options.saveDynamicFn,
|
|
832
|
+
});
|
|
833
|
+
if (claim.peer) {
|
|
834
|
+
warnings.push(`A concurrent process registered agent client ${claim.credentials.clientId} for ${harnessKey}/${sessionKey} first; adopting it and revoking ${registered.clientId}.`);
|
|
835
|
+
await revokeFn(registered).catch(() => {
|
|
836
|
+
/* best-effort cleanup; a failed revoke leaves an orphan, never an error */
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const adopted = claim.credentials;
|
|
344
840
|
const result = await resolveClientCredentials({
|
|
345
841
|
projectUrl: options.projectUrl,
|
|
346
|
-
clientId:
|
|
347
|
-
clientSecret: registered.clientSecret,
|
|
842
|
+
clientId: adopted.clientId,
|
|
843
|
+
clientSecret: adopted.clientSecret ?? registered.clientSecret,
|
|
348
844
|
audience: env.ORY_AGENT_AUDIENCE,
|
|
349
845
|
scope: env.ORY_AGENT_SCOPE,
|
|
350
|
-
subjectOverride
|
|
846
|
+
subjectOverride,
|
|
351
847
|
kind: "dynamic",
|
|
352
848
|
fetchFn: fetchCcFn,
|
|
353
849
|
skipCache: true, // freshly registered, no stale cache
|
|
354
850
|
warnings,
|
|
851
|
+
signal: options.signal,
|
|
355
852
|
});
|
|
356
853
|
if (result.kind !== "none")
|
|
357
854
|
return result;
|
|
@@ -362,24 +859,32 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
362
859
|
warnings.push(`Dynamic client registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
363
860
|
}
|
|
364
861
|
}
|
|
365
|
-
// 5. Deprecated ORY_API_KEY alias.
|
|
366
|
-
const legacyApiKey = env.ORY_API_KEY;
|
|
367
|
-
if (legacyApiKey && legacyApiKey.length > 0) {
|
|
368
|
-
warnings.push("ORY_API_KEY is deprecated for agent authentication — set ORY_AGENT_API_KEY instead");
|
|
369
|
-
return {
|
|
370
|
-
kind: "api_key",
|
|
371
|
-
token: legacyApiKey,
|
|
372
|
-
subject: env.ORY_AGENT_SUBJECT_ID,
|
|
373
|
-
reason: "Resolved agent identity from ORY_API_KEY (deprecated alias)",
|
|
374
|
-
warnings,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
862
|
return {
|
|
378
863
|
kind: "none",
|
|
379
|
-
reason: "No agent credentials configured. Run with a user session (the interactive user login runs every session) or set
|
|
864
|
+
reason: "No agent credentials configured. Run with a user session (the interactive user login runs every session) or set ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET / ORY_AGENT_REGISTRATION_TOKEN.",
|
|
380
865
|
warnings,
|
|
381
866
|
};
|
|
382
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Claim the `(projectUrl, harness, session)` slot for a freshly registered client.
|
|
870
|
+
*
|
|
871
|
+
* Against the real config file this is {@link claimAgentDynamicCredentials},
|
|
872
|
+
* whose read and write share one `mutateConfig` lock — the only way the loser
|
|
873
|
+
* of a same-harness race reliably sees the winner. When the caller injected its
|
|
874
|
+
* own credential store (tests), that lock isn't ours to take, so the same
|
|
875
|
+
* check runs against the injected load/save pair instead.
|
|
876
|
+
*/
|
|
877
|
+
function claimRegistration(harness, sessionKey, registered, store) {
|
|
878
|
+
if (store.useSharedStore) {
|
|
879
|
+
return claimAgentDynamicCredentials(harness, sessionKey, registered);
|
|
880
|
+
}
|
|
881
|
+
const existing = store.loadDynamicFn(harness, sessionKey);
|
|
882
|
+
if (isUsablePeer(existing, registered)) {
|
|
883
|
+
return { credentials: existing, peer: true };
|
|
884
|
+
}
|
|
885
|
+
store.saveDynamicFn(harness, sessionKey, registered);
|
|
886
|
+
return { credentials: registered, peer: false };
|
|
887
|
+
}
|
|
383
888
|
async function resolveClientCredentials(args) {
|
|
384
889
|
const cacheKey = `${args.kind}:${args.clientId}@${args.projectUrl}`;
|
|
385
890
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
@@ -404,6 +909,7 @@ async function resolveClientCredentials(args) {
|
|
|
404
909
|
clientSecret: args.clientSecret,
|
|
405
910
|
audience: args.audience,
|
|
406
911
|
scope: args.scope,
|
|
912
|
+
signal: args.signal,
|
|
407
913
|
});
|
|
408
914
|
ccCache.set(cacheKey, {
|
|
409
915
|
token: result.accessToken,
|
|
@@ -463,6 +969,7 @@ async function fetchClientCredentialsToken(args) {
|
|
|
463
969
|
method: "POST",
|
|
464
970
|
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
465
971
|
body: body.toString(),
|
|
972
|
+
signal: args.signal,
|
|
466
973
|
});
|
|
467
974
|
if (!res.ok) {
|
|
468
975
|
const err = new Error(`HTTP ${res.status}`);
|
|
@@ -484,36 +991,48 @@ async function fetchClientCredentialsToken(args) {
|
|
|
484
991
|
* Resolve the agent's credentials and attach them to the client.
|
|
485
992
|
*
|
|
486
993
|
* Always returns the resolved credentials; never throws and never blocks.
|
|
487
|
-
* Emits exactly one `agent.auth`
|
|
994
|
+
* Emits exactly one `agent.auth` activity event so the audit trail records whether
|
|
488
995
|
* the agent identity was available for the session.
|
|
489
996
|
*/
|
|
490
997
|
async function ensureAgentIdentity(client, options = {}) {
|
|
491
|
-
|
|
492
|
-
//
|
|
493
|
-
// the
|
|
494
|
-
|
|
998
|
+
const sessionId = options.sessionId ?? client.sessionId;
|
|
999
|
+
// Agent Security isn't connected — skip credential resolution entirely
|
|
1000
|
+
// (no DCR, no token grant) and record the no-op, shaped like the user
|
|
1001
|
+
// gate's not_connected result.
|
|
1002
|
+
const resolvedConfig = (0, config_js_1.resolveConfig)();
|
|
1003
|
+
const security = resolvedConfig.security;
|
|
1004
|
+
if (!security.connected) {
|
|
495
1005
|
const creds = {
|
|
496
1006
|
kind: "none",
|
|
497
|
-
reason:
|
|
1007
|
+
reason: `Ory Agent Security is not connected (${(0, config_js_1.describeSecurityGap)(security)}); agent identity resolution is a no-op`,
|
|
498
1008
|
warnings: [],
|
|
499
1009
|
};
|
|
500
|
-
client.
|
|
501
|
-
attributes: { kind: creds.kind, reason: creds.reason,
|
|
1010
|
+
client.logger.activity("agent.auth", "skipped", {
|
|
1011
|
+
attributes: { kind: creds.kind, reason: creds.reason, securityConnected: false },
|
|
502
1012
|
});
|
|
503
1013
|
return creds;
|
|
504
1014
|
}
|
|
505
|
-
const resolveFn = options.resolveFn ??
|
|
1015
|
+
const resolveFn = options.resolveFn ?? resolveRuntimeAgentCredentials;
|
|
1016
|
+
const projectUrl = options.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
|
|
1017
|
+
let credentialStore = options.credentialStore;
|
|
1018
|
+
if (!credentialStore) {
|
|
1019
|
+
try {
|
|
1020
|
+
credentialStore = (0, runtime_credential_js_1.createOsTalosCredentialStore)();
|
|
1021
|
+
}
|
|
1022
|
+
catch {
|
|
1023
|
+
// The resolver and enrollment path report a safe warning below.
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
506
1026
|
let creds;
|
|
507
1027
|
try {
|
|
508
1028
|
creds = await resolveFn({
|
|
509
|
-
projectUrl
|
|
1029
|
+
projectUrl,
|
|
510
1030
|
env: options.env,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
harness: options.harness,
|
|
1031
|
+
signal: options.signal,
|
|
1032
|
+
runtimeCredential: client.runtimeCredential,
|
|
1033
|
+
credentialStore,
|
|
1034
|
+
harness: options.harness ?? client.harness,
|
|
1035
|
+
sessionId,
|
|
517
1036
|
});
|
|
518
1037
|
}
|
|
519
1038
|
catch (err) {
|
|
@@ -523,114 +1042,320 @@ async function ensureAgentIdentity(client, options = {}) {
|
|
|
523
1042
|
warnings: [],
|
|
524
1043
|
};
|
|
525
1044
|
}
|
|
1045
|
+
// Only the session-start gate may issue a credential. Tool/read subprocesses
|
|
1046
|
+
// can load from the OS store but must never turn a read into a write.
|
|
1047
|
+
const userToken = client.userPrincipalForSession(sessionId).token?.trim();
|
|
1048
|
+
if (creds.kind === "none" && options.trigger === undefined && userToken && projectUrl) {
|
|
1049
|
+
try {
|
|
1050
|
+
(0, runtime_credential_js_1.canonicalProjectUrl)(projectUrl);
|
|
1051
|
+
if (!credentialStore) {
|
|
1052
|
+
creds.warnings.push(`Automatic Talos enrollment is unavailable because ${process.platform} has no supported OS credential store`);
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
const enroll = options.enrollFn ?? runtime_credential_js_1.enrollTalosRuntimeCredential;
|
|
1056
|
+
const identity = {
|
|
1057
|
+
projectUrl,
|
|
1058
|
+
harness: options.harness?.trim() || client.harness || config_js_1.UNKNOWN_HARNESS_KEY,
|
|
1059
|
+
sessionId: sessionId?.trim() || config_js_1.SESSIONLESS_KEY,
|
|
1060
|
+
kind: "agent",
|
|
1061
|
+
};
|
|
1062
|
+
const enrollmentOptions = {
|
|
1063
|
+
projectUrl,
|
|
1064
|
+
agentSecurityUrl: resolvedConfig.agentSecurityUrl,
|
|
1065
|
+
userToken,
|
|
1066
|
+
name: (0, branding_js_1.agentClientName)({
|
|
1067
|
+
harness: options.harness ?? client.harness,
|
|
1068
|
+
host: (0, node_os_1.hostname)(),
|
|
1069
|
+
sessionId,
|
|
1070
|
+
}),
|
|
1071
|
+
idempotencyKey: `talos-runtime-${(0, runtime_credential_js_1.credentialAccount)(identity)}`,
|
|
1072
|
+
signal: options.signal,
|
|
1073
|
+
credentialStore,
|
|
1074
|
+
identity,
|
|
1075
|
+
};
|
|
1076
|
+
let issued = false;
|
|
1077
|
+
const enrolled = await withEnrollmentLock(identity, async () => {
|
|
1078
|
+
const peer = await credentialStore.load(identity);
|
|
1079
|
+
if (peer) {
|
|
1080
|
+
return {
|
|
1081
|
+
actorId: peer.actorId,
|
|
1082
|
+
credential: (0, runtime_credential_js_1.talosRuntimeCredential)(peer.credential, { subject: peer.actorId }),
|
|
1083
|
+
warnings: [],
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
issued = true;
|
|
1087
|
+
return enroll(enrollmentOptions);
|
|
1088
|
+
});
|
|
1089
|
+
creds = {
|
|
1090
|
+
kind: "talos",
|
|
1091
|
+
runtimeCredential: enrolled.credential,
|
|
1092
|
+
subject: enrolled.actorId,
|
|
1093
|
+
reason: issued
|
|
1094
|
+
? "Enrolled a Talos runtime credential using the authenticated user"
|
|
1095
|
+
: "Using the Talos runtime credential enrolled by a concurrent session process",
|
|
1096
|
+
warnings: [...creds.warnings, ...enrolled.warnings],
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
catch (error) {
|
|
1101
|
+
const status = typeof error === "object" && error !== null
|
|
1102
|
+
&& typeof error.status === "number"
|
|
1103
|
+
? ` (HTTP ${error.status})`
|
|
1104
|
+
: "";
|
|
1105
|
+
creds.warnings.push(`Could not enroll a Talos runtime credential${status}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (creds.runtimeCredential && (creds.kind === "talos" || creds.kind === "injected")) {
|
|
1109
|
+
client.setRuntimeCredential(creds.runtimeCredential);
|
|
1110
|
+
if (creds.subject)
|
|
1111
|
+
client.setAgentPrincipal({ subject: creds.subject }, sessionId);
|
|
1112
|
+
}
|
|
1113
|
+
else if (creds.kind !== "none") {
|
|
1114
|
+
creds = {
|
|
1115
|
+
kind: "none",
|
|
1116
|
+
reason: `Legacy ${creds.kind} credentials are not accepted for runtime authentication`,
|
|
1117
|
+
warnings: creds.warnings,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
526
1120
|
for (const warning of creds.warnings) {
|
|
527
1121
|
client.logger.warn("agent.auth.warning", { message: warning });
|
|
528
1122
|
}
|
|
529
1123
|
if (creds.kind === "none") {
|
|
530
1124
|
client.logger.warn("agent.auth.none", { reason: creds.reason });
|
|
531
1125
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
client.tracer.record("agent.auth", creds.kind === "none" ? "skipped" : "ok", {
|
|
1126
|
+
// When no credentials were resolved, leave the existing principal in place —
|
|
1127
|
+
// a caller may have set one explicitly via `setAgentPrincipal`, and "no
|
|
1128
|
+
// resolver match" should not clear it. With nothing set, the read path falls
|
|
1129
|
+
// back to the rehydrated user token (see `buildApis`).
|
|
1130
|
+
const sessionKey = sessionId?.trim() || config_js_1.SESSIONLESS_KEY;
|
|
1131
|
+
client.logger.activity("agent.auth", creds.kind === "none" ? "skipped" : "ok", {
|
|
539
1132
|
attributes: {
|
|
540
1133
|
kind: creds.kind,
|
|
541
1134
|
reason: creds.reason,
|
|
1135
|
+
sessionKey,
|
|
1136
|
+
...(options.trigger ? { trigger: options.trigger } : {}),
|
|
542
1137
|
...(creds.subject ? { subject: creds.subject } : {}),
|
|
543
1138
|
...(creds.expiresAt ? { expiresAt: creds.expiresAt } : {}),
|
|
1139
|
+
// Warnings were collected all along and never recorded, so a resolver
|
|
1140
|
+
// that half-worked looked identical to one that worked — which is what
|
|
1141
|
+
// made the rejected-DCR-metadata failure invisible in #182, and what
|
|
1142
|
+
// would hide a rejected subject override here (#241).
|
|
1143
|
+
...(creds.warnings?.length ? { warnings: creds.warnings } : {}),
|
|
544
1144
|
},
|
|
545
1145
|
});
|
|
546
1146
|
return creds;
|
|
547
1147
|
}
|
|
548
1148
|
/**
|
|
549
|
-
*
|
|
1149
|
+
* Revoke retired registrations and drop the successful ones from the queue.
|
|
550
1150
|
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1151
|
+
* Best-effort throughout: a failed revoke leaves the entry queued for the next
|
|
1152
|
+
* run (and an orphaned client that uninstall or an operator can still prune),
|
|
1153
|
+
* never an error on the session-start path.
|
|
1154
|
+
*/
|
|
1155
|
+
async function drainRetiredCredentials(client, opts) {
|
|
1156
|
+
let queued = [];
|
|
1157
|
+
try {
|
|
1158
|
+
queued = loadRetiredCredentials();
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
client.logger.warn("agent.retired.read_failed", {
|
|
1162
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1163
|
+
});
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
if (queued.length === 0)
|
|
1167
|
+
return;
|
|
1168
|
+
const revoke = opts.revokeFn ?? revokeAgentDynamicClient;
|
|
1169
|
+
const revoked = [];
|
|
1170
|
+
for (const creds of queued) {
|
|
1171
|
+
const outcome = await revoke(creds).catch(() => ({ status: "failed" }));
|
|
1172
|
+
// `deleted` (including a 404 — already gone) is done, and so is `skipped`:
|
|
1173
|
+
// that means the record carries no RFC 7592 management credentials, so no
|
|
1174
|
+
// future run could revoke it either and keeping it queued would leak a
|
|
1175
|
+
// growing list into the config file forever. Only a real failure retries.
|
|
1176
|
+
if (outcome.status !== "failed")
|
|
1177
|
+
revoked.push(creds.clientId);
|
|
1178
|
+
client.logger.debug("agent.retired.revoked", {
|
|
1179
|
+
clientId: creds.clientId,
|
|
1180
|
+
revoke: outcome.status,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
clearRetiredCredentials(revoked);
|
|
1185
|
+
}
|
|
1186
|
+
catch (err) {
|
|
1187
|
+
client.logger.warn("agent.retired.clear_failed", {
|
|
1188
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
client.logger.activity("agent.retired.drain", "ok", {
|
|
1192
|
+
attributes: { queued: queued.length, revoked: revoked.length },
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Resolve a sub-agent against the process runtime credential. The broker's
|
|
1197
|
+
* `type_name` supplies sub-agent attribution; no separate OAuth identity is
|
|
1198
|
+
* registered, loaded, minted, or persisted.
|
|
558
1199
|
*/
|
|
559
1200
|
async function ensureSubAgentIdentity(client, options) {
|
|
560
|
-
//
|
|
561
|
-
//
|
|
562
|
-
|
|
1201
|
+
// Same no-op as ensureAgentIdentity — no sub-agent DCR while Agent
|
|
1202
|
+
// Security is not connected.
|
|
1203
|
+
const resolvedConfig = (0, config_js_1.resolveConfig)();
|
|
1204
|
+
const subAgentSecurity = resolvedConfig.security;
|
|
1205
|
+
if (!subAgentSecurity.connected) {
|
|
563
1206
|
const identity = {
|
|
564
1207
|
kind: "none",
|
|
565
1208
|
subAgentType: options.subAgentType,
|
|
566
|
-
reason:
|
|
1209
|
+
reason: `Ory Agent Security is not connected (${(0, config_js_1.describeSecurityGap)(subAgentSecurity)}); sub-agent identity resolution is a no-op`,
|
|
567
1210
|
warnings: [],
|
|
568
1211
|
};
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
1212
|
+
if (options.emitActivity !== false) {
|
|
1213
|
+
client.logger.activity("agent.auth", "skipped", {
|
|
1214
|
+
attributes: {
|
|
1215
|
+
kind: "subagent_none",
|
|
1216
|
+
subAgentType: identity.subAgentType,
|
|
1217
|
+
reason: identity.reason,
|
|
1218
|
+
securityConnected: false,
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
577
1222
|
return identity;
|
|
578
1223
|
}
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
try {
|
|
586
|
-
identity = await resolveSubAgentIdentity({
|
|
1224
|
+
const projectUrl = options.projectUrl ?? resolvedConfig.projectUrl;
|
|
1225
|
+
const sessionId = options.sessionId ?? client.sessionId;
|
|
1226
|
+
const perSpawnId = options.perSpawnId?.trim();
|
|
1227
|
+
if (!projectUrl || !perSpawnId) {
|
|
1228
|
+
return {
|
|
1229
|
+
kind: "none",
|
|
587
1230
|
subAgentType: options.subAgentType,
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1231
|
+
reason: "A project and stable spawn identifier are required for a child identity",
|
|
1232
|
+
warnings: [],
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
const credentialIdentity = {
|
|
1236
|
+
projectUrl,
|
|
1237
|
+
harness: options.harness?.trim() || client.harness || config_js_1.UNKNOWN_HARNESS_KEY,
|
|
1238
|
+
sessionId: sessionId?.trim() || config_js_1.SESSIONLESS_KEY,
|
|
1239
|
+
kind: "subagent",
|
|
1240
|
+
subAgentType: options.subAgentType,
|
|
1241
|
+
perSpawnId,
|
|
1242
|
+
};
|
|
1243
|
+
let store = options.credentialStore;
|
|
1244
|
+
try {
|
|
1245
|
+
store ??= (0, runtime_credential_js_1.createOsTalosCredentialStore)();
|
|
1246
|
+
const stored = await store.load(credentialIdentity);
|
|
1247
|
+
if (stored) {
|
|
1248
|
+
const runtimeCredential = (0, runtime_credential_js_1.talosRuntimeCredential)(stored.credential, { subject: stored.actorId });
|
|
1249
|
+
return {
|
|
1250
|
+
kind: "runtime",
|
|
1251
|
+
subAgentType: options.subAgentType,
|
|
1252
|
+
subject: stored.actorId,
|
|
1253
|
+
runtimeCredential,
|
|
1254
|
+
reason: "Using the child Talos credential from the OS credential store",
|
|
1255
|
+
warnings: [],
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch {
|
|
1260
|
+
// Enrollment below reports a safe outcome if native persistence is unavailable.
|
|
1261
|
+
}
|
|
1262
|
+
let parentCredential = client.runtimeCredential;
|
|
1263
|
+
if (!parentCredential) {
|
|
1264
|
+
const parent = await resolveRuntimeAgentCredentials({
|
|
1265
|
+
projectUrl,
|
|
1266
|
+
harness: credentialIdentity.harness,
|
|
1267
|
+
sessionId: credentialIdentity.sessionId,
|
|
1268
|
+
credentialStore: store,
|
|
1269
|
+
env: options.env,
|
|
602
1270
|
});
|
|
1271
|
+
parentCredential = parent.runtimeCredential;
|
|
603
1272
|
}
|
|
604
|
-
|
|
1273
|
+
let identity;
|
|
1274
|
+
if (options.allowEnrollment !== false && parentCredential && store) {
|
|
1275
|
+
try {
|
|
1276
|
+
const enroll = options.enrollFn ?? runtime_credential_js_1.enrollTalosChildRuntimeCredential;
|
|
1277
|
+
const enrollmentOptions = {
|
|
1278
|
+
projectUrl,
|
|
1279
|
+
agentSecurityUrl: resolvedConfig.agentSecurityUrl,
|
|
1280
|
+
name: (0, branding_js_1.agentClientName)({
|
|
1281
|
+
harness: credentialIdentity.harness,
|
|
1282
|
+
host: (0, node_os_1.hostname)(),
|
|
1283
|
+
sessionId: credentialIdentity.sessionId,
|
|
1284
|
+
subAgentType: options.subAgentType,
|
|
1285
|
+
}),
|
|
1286
|
+
subAgentType: options.subAgentType,
|
|
1287
|
+
idempotencyKey: `talos-child-${(0, runtime_credential_js_1.credentialAccount)(credentialIdentity)}`,
|
|
1288
|
+
parentCredential,
|
|
1289
|
+
identity: credentialIdentity,
|
|
1290
|
+
credentialStore: store,
|
|
1291
|
+
signal: options.signal,
|
|
1292
|
+
};
|
|
1293
|
+
let issued = false;
|
|
1294
|
+
const enrolled = await withEnrollmentLock(credentialIdentity, async () => {
|
|
1295
|
+
const peer = await store.load(credentialIdentity);
|
|
1296
|
+
if (peer) {
|
|
1297
|
+
return {
|
|
1298
|
+
actorId: peer.actorId,
|
|
1299
|
+
credential: (0, runtime_credential_js_1.talosRuntimeCredential)(peer.credential, { subject: peer.actorId }),
|
|
1300
|
+
warnings: [],
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
issued = true;
|
|
1304
|
+
return enroll(enrollmentOptions);
|
|
1305
|
+
});
|
|
1306
|
+
identity = {
|
|
1307
|
+
kind: "runtime",
|
|
1308
|
+
subAgentType: options.subAgentType,
|
|
1309
|
+
subject: enrolled.actorId,
|
|
1310
|
+
runtimeCredential: enrolled.credential,
|
|
1311
|
+
reason: issued
|
|
1312
|
+
? "Enrolled a child Talos credential using the parent runtime"
|
|
1313
|
+
: "Using the child Talos credential enrolled by a concurrent process",
|
|
1314
|
+
warnings: enrolled.warnings,
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
const status = typeof error === "object" && error !== null
|
|
1319
|
+
&& typeof error.status === "number"
|
|
1320
|
+
? ` (HTTP ${error.status})`
|
|
1321
|
+
: "";
|
|
1322
|
+
identity = {
|
|
1323
|
+
kind: "none",
|
|
1324
|
+
subAgentType: options.subAgentType,
|
|
1325
|
+
reason: `Child Talos enrollment failed${status}`,
|
|
1326
|
+
warnings: [],
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
605
1331
|
identity = {
|
|
606
1332
|
kind: "none",
|
|
607
1333
|
subAgentType: options.subAgentType,
|
|
608
|
-
reason:
|
|
609
|
-
warnings,
|
|
1334
|
+
reason: "No child Talos credential is stored and parent-authenticated enrollment is unavailable",
|
|
1335
|
+
warnings: [],
|
|
610
1336
|
};
|
|
611
1337
|
}
|
|
612
|
-
|
|
613
|
-
client.logger.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
reason: identity.reason,
|
|
621
|
-
subAgentType: options.subAgentType,
|
|
1338
|
+
if (options.emitActivity !== false) {
|
|
1339
|
+
client.logger.activity("agent.auth", identity.kind === "runtime" ? "ok" : "skipped", {
|
|
1340
|
+
attributes: {
|
|
1341
|
+
kind: identity.kind === "runtime" ? "subagent_runtime" : "subagent_none",
|
|
1342
|
+
subAgentType: identity.subAgentType,
|
|
1343
|
+
reason: identity.reason,
|
|
1344
|
+
...(identity.subject ? { subject: identity.subject } : {}),
|
|
1345
|
+
},
|
|
622
1346
|
});
|
|
623
1347
|
}
|
|
624
|
-
client.tracer.record("agent.auth", identity.kind === "none" ? "skipped" : "ok", {
|
|
625
|
-
attributes: {
|
|
626
|
-
kind: identity.kind === "dynamic" ? "subagent_dynamic" : "subagent_none",
|
|
627
|
-
subAgentType: identity.subAgentType,
|
|
628
|
-
reason: identity.reason,
|
|
629
|
-
...(identity.subject ? { subject: identity.subject } : {}),
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
1348
|
return identity;
|
|
633
1349
|
}
|
|
1350
|
+
/** {@link claimRegistration} for sub-agents, against an injected store. */
|
|
1351
|
+
function claimViaInjectedSubAgentStore(harness, args, registered) {
|
|
1352
|
+
const existing = args.loadFn(harness, args.sessionKey, args.subAgentType);
|
|
1353
|
+
if (isUsablePeer(existing, registered)) {
|
|
1354
|
+
return { credentials: existing, peer: true };
|
|
1355
|
+
}
|
|
1356
|
+
args.saveFn(harness, args.sessionKey, args.subAgentType, registered);
|
|
1357
|
+
return { credentials: registered, peer: false };
|
|
1358
|
+
}
|
|
634
1359
|
async function resolveSubAgentIdentity(args) {
|
|
635
1360
|
if (!args.projectUrl) {
|
|
636
1361
|
return {
|
|
@@ -640,8 +1365,12 @@ async function resolveSubAgentIdentity(args) {
|
|
|
640
1365
|
warnings: args.warnings,
|
|
641
1366
|
};
|
|
642
1367
|
}
|
|
643
|
-
// 1. Re-use persisted credentials
|
|
644
|
-
|
|
1368
|
+
// 1. Re-use persisted credentials matching this `(projectUrl, harness,
|
|
1369
|
+
// subAgentType)` triple. Keying on the type alone would give two
|
|
1370
|
+
// harnesses' same-named sub-agents (both call theirs `Explore`) one
|
|
1371
|
+
// shared identity, and the audit trail could no longer tell them apart.
|
|
1372
|
+
const harnessKey = args.harness?.trim() || config_js_1.UNKNOWN_HARNESS_KEY;
|
|
1373
|
+
const persisted = args.loadFn(harnessKey, args.sessionKey, args.subAgentType);
|
|
645
1374
|
if (persisted && persisted.projectUrl === args.projectUrl) {
|
|
646
1375
|
return {
|
|
647
1376
|
kind: "dynamic",
|
|
@@ -664,16 +1393,20 @@ async function resolveSubAgentIdentity(args) {
|
|
|
664
1393
|
warnings: args.warnings,
|
|
665
1394
|
};
|
|
666
1395
|
}
|
|
667
|
-
// 3. Fresh DCR.
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1396
|
+
// 3. Fresh DCR. The harness and the type go over as separate fields, which is
|
|
1397
|
+
// what makes `agentClientName` emit a sub-agent name
|
|
1398
|
+
// ("… · sub-agent · <harness>/<type> @ <host>"). Folding the type into the
|
|
1399
|
+
// harness instead would register the sub-agent under an *agent* name, and
|
|
1400
|
+
// that name is the only thing tying the client back to the delegation edge
|
|
1401
|
+
// the broker keys on (harness, host, type).
|
|
672
1402
|
try {
|
|
673
1403
|
const registered = await args.registerFn({
|
|
674
1404
|
projectUrl: args.projectUrl,
|
|
675
1405
|
auth: args.iat,
|
|
676
|
-
harness:
|
|
1406
|
+
harness: args.harness,
|
|
1407
|
+
subAgentType: args.subAgentType,
|
|
1408
|
+
sessionId: args.sessionKey === config_js_1.SESSIONLESS_KEY ? undefined : args.sessionKey,
|
|
1409
|
+
signal: args.signal,
|
|
677
1410
|
...(args.delegatorSubject
|
|
678
1411
|
? {
|
|
679
1412
|
delegation: {
|
|
@@ -684,13 +1417,26 @@ async function resolveSubAgentIdentity(args) {
|
|
|
684
1417
|
}
|
|
685
1418
|
: {}),
|
|
686
1419
|
});
|
|
687
|
-
|
|
1420
|
+
// Same double-checked claim as the agent path: a concurrent process of
|
|
1421
|
+
// this harness may have registered the sub-agent while our POST was in
|
|
1422
|
+
// flight. Adopt the peer's client and revoke ours rather than orphan one.
|
|
1423
|
+
const claim = args.useSharedStore
|
|
1424
|
+
? claimSubAgentDynamicCredentials(harnessKey, args.sessionKey, args.subAgentType, registered)
|
|
1425
|
+
: claimViaInjectedSubAgentStore(harnessKey, args, registered);
|
|
1426
|
+
if (claim.peer) {
|
|
1427
|
+
args.warnings.push(`A concurrent process registered sub-agent ${args.subAgentType} (${claim.credentials.clientId}) for this session first; adopting it and revoking ${registered.clientId}.`);
|
|
1428
|
+
await args.revokeFn(registered).catch(() => {
|
|
1429
|
+
/* best-effort cleanup; a failed revoke leaves an orphan, never an error */
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
688
1432
|
return {
|
|
689
1433
|
kind: "dynamic",
|
|
690
1434
|
subAgentType: args.subAgentType,
|
|
691
|
-
subject:
|
|
692
|
-
credentials:
|
|
693
|
-
reason:
|
|
1435
|
+
subject: claim.credentials.clientId,
|
|
1436
|
+
credentials: claim.credentials,
|
|
1437
|
+
reason: claim.peer
|
|
1438
|
+
? `Adopted concurrently registered sub-agent ${args.subAgentType}`
|
|
1439
|
+
: `Registered sub-agent ${args.subAgentType} via DCR (RFC 7591)`,
|
|
694
1440
|
warnings: args.warnings,
|
|
695
1441
|
};
|
|
696
1442
|
}
|