@ory/argus 0.1.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 +134 -0
- package/assets/commands/local-down.md +19 -0
- package/assets/commands/local-up.md +27 -0
- package/assets/skills/auth-setup/SKILL.md +279 -0
- package/assets/skills/local-dev/SKILL.md +206 -0
- package/assets/skills/login-flow/SKILL.md +383 -0
- package/assets/skills/social-login/SKILL.md +312 -0
- package/dist/agent-auth.d.ts +204 -0
- package/dist/agent-auth.js +553 -0
- package/dist/auth-gate.d.ts +71 -0
- package/dist/auth-gate.js +308 -0
- package/dist/auth-store.d.ts +75 -0
- package/dist/auth-store.js +261 -0
- package/dist/auth.d.ts +93 -0
- package/dist/auth.js +323 -0
- package/dist/cli.d.ts +73 -0
- package/dist/cli.js +484 -0
- package/dist/client.d.ts +158 -0
- package/dist/client.js +679 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.js +344 -0
- package/dist/denial.d.ts +79 -0
- package/dist/denial.js +103 -0
- package/dist/dev.d.ts +95 -0
- package/dist/dev.js +514 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +137 -0
- package/dist/local/cli.d.ts +12 -0
- package/dist/local/cli.js +95 -0
- package/dist/local/configs.d.ts +89 -0
- package/dist/local/configs.js +634 -0
- package/dist/local/health.d.ts +32 -0
- package/dist/local/health.js +65 -0
- package/dist/local/index.d.ts +6 -0
- package/dist/local/index.js +38 -0
- package/dist/local/jaeger-main.d.ts +13 -0
- package/dist/local/jaeger-main.js +85 -0
- package/dist/local/jaeger.d.ts +50 -0
- package/dist/local/jaeger.js +162 -0
- package/dist/local/main.d.ts +7 -0
- package/dist/local/main.js +14 -0
- package/dist/local/manager.d.ts +45 -0
- package/dist/local/manager.js +676 -0
- package/dist/local/seed.d.ts +71 -0
- package/dist/local/seed.js +237 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.js +139 -0
- package/dist/mcp.d.ts +76 -0
- package/dist/mcp.js +122 -0
- package/dist/otel/exporter.d.ts +17 -0
- package/dist/otel/exporter.js +12 -0
- package/dist/otel/index.d.ts +2 -0
- package/dist/otel/index.js +8 -0
- package/dist/otel/otlp-http.d.ts +116 -0
- package/dist/otel/otlp-http.js +322 -0
- package/dist/registry/cli.d.ts +12 -0
- package/dist/registry/cli.js +76 -0
- package/dist/registry/config.d.ts +23 -0
- package/dist/registry/config.js +80 -0
- package/dist/registry/index.d.ts +3 -0
- package/dist/registry/index.js +21 -0
- package/dist/registry/main.d.ts +7 -0
- package/dist/registry/main.js +14 -0
- package/dist/registry/manager.d.ts +38 -0
- package/dist/registry/manager.js +674 -0
- package/dist/setup.d.ts +118 -0
- package/dist/setup.js +398 -0
- package/dist/skills.d.ts +78 -0
- package/dist/skills.js +264 -0
- package/dist/subject.d.ts +43 -0
- package/dist/subject.js +55 -0
- package/dist/tool-metadata.d.ts +41 -0
- package/dist/tool-metadata.js +127 -0
- package/dist/tracer.d.ts +172 -0
- package/dist/tracer.js +452 -0
- package/dist/types.d.ts +57 -0
- package/dist/types.js +3 -0
- package/dist/watch-sandbox.d.ts +9 -0
- package/dist/watch-sandbox.js +81 -0
- package/package.json +79 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Agent identity resolution.
|
|
4
|
+
*
|
|
5
|
+
* The "agent" is the AI process making calls on behalf of the human
|
|
6
|
+
* user. Unlike the user, the agent never authenticates interactively.
|
|
7
|
+
* The default path is **OAuth2 Dynamic Client Registration (RFC 7591)**:
|
|
8
|
+
* on first run the agent registers itself with the project's
|
|
9
|
+
* `/oauth2/register` endpoint, authenticated by the user's bearer token
|
|
10
|
+
* (or an out-of-band initial access token), and persists the issued
|
|
11
|
+
* credentials so subsequent runs reuse them.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order:
|
|
14
|
+
*
|
|
15
|
+
* 1. ORY_AGENT_API_KEY — explicit static bearer
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = void 0;
|
|
32
|
+
exports._resetAgentCredentialsCache = _resetAgentCredentialsCache;
|
|
33
|
+
exports.loadSubAgentDynamicCredentials = loadSubAgentDynamicCredentials;
|
|
34
|
+
exports.saveSubAgentDynamicCredentials = saveSubAgentDynamicCredentials;
|
|
35
|
+
exports.clearSubAgentDynamicCredentials = clearSubAgentDynamicCredentials;
|
|
36
|
+
exports.loadAgentDynamicCredentials = loadAgentDynamicCredentials;
|
|
37
|
+
exports.saveAgentDynamicCredentials = saveAgentDynamicCredentials;
|
|
38
|
+
exports.clearAgentDynamicCredentials = clearAgentDynamicCredentials;
|
|
39
|
+
exports.registerAgentClient = registerAgentClient;
|
|
40
|
+
exports.resolveAgentCredentials = resolveAgentCredentials;
|
|
41
|
+
exports.fetchClientCredentialsToken = fetchClientCredentialsToken;
|
|
42
|
+
exports.ensureAgentIdentity = ensureAgentIdentity;
|
|
43
|
+
exports.ensureSubAgentIdentity = ensureSubAgentIdentity;
|
|
44
|
+
const node_os_1 = require("node:os");
|
|
45
|
+
const client_1 = require("@ory/client");
|
|
46
|
+
const config_js_1 = require("./config.js");
|
|
47
|
+
/**
|
|
48
|
+
* Skew window before nominal expiry at which a cached client_credentials
|
|
49
|
+
* token is considered stale. Matches the user-token skew in auth-store.
|
|
50
|
+
*/
|
|
51
|
+
exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = 60;
|
|
52
|
+
const ccCache = new Map();
|
|
53
|
+
/** Drop all cached agent client_credentials tokens. Tests only. */
|
|
54
|
+
function _resetAgentCredentialsCache() {
|
|
55
|
+
ccCache.clear();
|
|
56
|
+
}
|
|
57
|
+
// ─── Sub-agent persistence ───────────────────────────────────────────
|
|
58
|
+
/** Load persisted sub-agent DCR credentials keyed by sub-agent type. */
|
|
59
|
+
function loadSubAgentDynamicCredentials(subAgentType) {
|
|
60
|
+
return (0, config_js_1.loadConfig)().agent?.subAgents?.[subAgentType];
|
|
61
|
+
}
|
|
62
|
+
/** Persist sub-agent DCR credentials under the given type key. */
|
|
63
|
+
function saveSubAgentDynamicCredentials(subAgentType, creds) {
|
|
64
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
65
|
+
const agent = { ...(current.agent ?? {}) };
|
|
66
|
+
const subAgents = { ...(agent.subAgents ?? {}) };
|
|
67
|
+
subAgents[subAgentType] = creds;
|
|
68
|
+
agent.subAgents = subAgents;
|
|
69
|
+
return { ...current, agent };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Remove persisted sub-agent DCR credentials for the given type. */
|
|
73
|
+
function clearSubAgentDynamicCredentials(subAgentType) {
|
|
74
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
75
|
+
const next = { ...current };
|
|
76
|
+
if (!next.agent?.subAgents)
|
|
77
|
+
return next;
|
|
78
|
+
const agent = { ...next.agent };
|
|
79
|
+
const subAgents = { ...agent.subAgents };
|
|
80
|
+
delete subAgents[subAgentType];
|
|
81
|
+
if (Object.keys(subAgents).length === 0)
|
|
82
|
+
delete agent.subAgents;
|
|
83
|
+
else
|
|
84
|
+
agent.subAgents = subAgents;
|
|
85
|
+
if (!agent.dynamic && !agent.subAgents)
|
|
86
|
+
delete next.agent;
|
|
87
|
+
else
|
|
88
|
+
next.agent = agent;
|
|
89
|
+
return next;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// ─── Dynamic registration persistence ────────────────────────────────
|
|
93
|
+
/** Load persisted DCR credentials from the shared config file. */
|
|
94
|
+
function loadAgentDynamicCredentials() {
|
|
95
|
+
return (0, config_js_1.loadConfig)().agent?.dynamic;
|
|
96
|
+
}
|
|
97
|
+
/** Persist DCR credentials. Existing credentials are replaced atomically. */
|
|
98
|
+
function saveAgentDynamicCredentials(creds) {
|
|
99
|
+
(0, config_js_1.mutateConfig)((current) => ({
|
|
100
|
+
...current,
|
|
101
|
+
agent: { ...(current.agent ?? {}), dynamic: creds },
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/** Remove persisted DCR credentials (e.g. after revocation or unregister). */
|
|
105
|
+
function clearAgentDynamicCredentials() {
|
|
106
|
+
(0, config_js_1.mutateConfig)((current) => {
|
|
107
|
+
const next = { ...current };
|
|
108
|
+
if (!next.agent)
|
|
109
|
+
return next;
|
|
110
|
+
const agent = { ...next.agent };
|
|
111
|
+
delete agent.dynamic;
|
|
112
|
+
if (Object.keys(agent).length === 0)
|
|
113
|
+
delete next.agent;
|
|
114
|
+
else
|
|
115
|
+
next.agent = agent;
|
|
116
|
+
return next;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Register a new OAuth2 client via RFC 7591. Authorization is carried
|
|
121
|
+
* either by the user's interactive token (the natural bootstrap) or by
|
|
122
|
+
* a pre-issued initial access token (`ORY_AGENT_REGISTRATION_TOKEN`).
|
|
123
|
+
* Throws on any non-2xx response so callers can decide whether to
|
|
124
|
+
* retry, fall back, or surface the error in the audit span. The
|
|
125
|
+
* `HTTP <status>` error message shape is preserved so callers and tests
|
|
126
|
+
* can pattern-match on it regardless of the underlying transport.
|
|
127
|
+
*/
|
|
128
|
+
async function registerAgentClient(args) {
|
|
129
|
+
const host = (args.hostnameFn ?? node_os_1.hostname)() || "unknown-host";
|
|
130
|
+
const clientName = args.harness
|
|
131
|
+
? `ory-agent-${args.harness}@${host}`
|
|
132
|
+
: `ory-agent@${host}`;
|
|
133
|
+
const oidc = new client_1.OidcApi(new client_1.Configuration({
|
|
134
|
+
basePath: args.projectUrl,
|
|
135
|
+
accessToken: args.auth,
|
|
136
|
+
}));
|
|
137
|
+
let response;
|
|
138
|
+
try {
|
|
139
|
+
response = await oidc.createOidcDynamicClient({
|
|
140
|
+
oAuth2Client: {
|
|
141
|
+
client_name: clientName,
|
|
142
|
+
grant_types: ["client_credentials"],
|
|
143
|
+
response_types: ["token"],
|
|
144
|
+
scope: "openid offline_access",
|
|
145
|
+
token_endpoint_auth_method: "client_secret_post",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const status = err?.response?.status;
|
|
151
|
+
if (typeof status === "number") {
|
|
152
|
+
throw new Error(`HTTP ${status}`);
|
|
153
|
+
}
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
const client = response.data;
|
|
157
|
+
if (!client.client_id || client.client_id.length === 0) {
|
|
158
|
+
throw new Error("Registration response missing client_id");
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
clientId: client.client_id,
|
|
162
|
+
clientSecret: client.client_secret,
|
|
163
|
+
registrationAccessToken: client.registration_access_token,
|
|
164
|
+
registrationClientUri: client.registration_client_uri,
|
|
165
|
+
registeredAt: Math.floor(Date.now() / 1000),
|
|
166
|
+
projectUrl: args.projectUrl,
|
|
167
|
+
harness: args.harness,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the agent's credentials from environment configuration and
|
|
172
|
+
* persisted state. Never throws — failures collapse to `kind: "none"`
|
|
173
|
+
* so the caller can fail open.
|
|
174
|
+
*/
|
|
175
|
+
async function resolveAgentCredentials(options = {}) {
|
|
176
|
+
const env = options.env ?? process.env;
|
|
177
|
+
const warnings = [];
|
|
178
|
+
const fetchCcFn = options.fetchClientCredentialsTokenFn ?? fetchClientCredentialsToken;
|
|
179
|
+
const registerFn = options.registerAgentClientFn ?? registerAgentClient;
|
|
180
|
+
const loadDynamicFn = options.loadDynamicFn ?? loadAgentDynamicCredentials;
|
|
181
|
+
const saveDynamicFn = options.saveDynamicFn ?? saveAgentDynamicCredentials;
|
|
182
|
+
const clearDynamicFn = options.clearDynamicFn ?? clearAgentDynamicCredentials;
|
|
183
|
+
const skipCache = options.skipCache ?? false;
|
|
184
|
+
// 1. Explicit static API key — operator override.
|
|
185
|
+
const apiKey = env.ORY_AGENT_API_KEY;
|
|
186
|
+
if (apiKey && apiKey.length > 0) {
|
|
187
|
+
return {
|
|
188
|
+
kind: "api_key",
|
|
189
|
+
token: apiKey,
|
|
190
|
+
subject: env.ORY_AGENT_SUBJECT_ID,
|
|
191
|
+
reason: "Resolved agent identity from ORY_AGENT_API_KEY",
|
|
192
|
+
warnings,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// 2. Explicit static client_credentials — operator override.
|
|
196
|
+
const clientId = env.ORY_AGENT_CLIENT_ID;
|
|
197
|
+
const clientSecret = env.ORY_AGENT_CLIENT_SECRET;
|
|
198
|
+
if (clientId && clientSecret) {
|
|
199
|
+
if (!options.projectUrl) {
|
|
200
|
+
return {
|
|
201
|
+
kind: "none",
|
|
202
|
+
reason: "ORY_AGENT_CLIENT_ID is set but ORY_PROJECT_URL is missing; cannot run client_credentials grant",
|
|
203
|
+
warnings,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return await resolveClientCredentials({
|
|
207
|
+
projectUrl: options.projectUrl,
|
|
208
|
+
clientId,
|
|
209
|
+
clientSecret,
|
|
210
|
+
audience: env.ORY_AGENT_AUDIENCE,
|
|
211
|
+
scope: env.ORY_AGENT_SCOPE,
|
|
212
|
+
subjectOverride: env.ORY_AGENT_SUBJECT_ID,
|
|
213
|
+
kind: "client_credentials",
|
|
214
|
+
fetchFn: fetchCcFn,
|
|
215
|
+
skipCache,
|
|
216
|
+
warnings,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// 3. Persisted dynamic registration — re-use a prior DCR run.
|
|
220
|
+
if (options.projectUrl) {
|
|
221
|
+
const persisted = loadDynamicFn();
|
|
222
|
+
if (persisted && persisted.projectUrl === options.projectUrl && persisted.clientSecret) {
|
|
223
|
+
const result = await resolveClientCredentials({
|
|
224
|
+
projectUrl: options.projectUrl,
|
|
225
|
+
clientId: persisted.clientId,
|
|
226
|
+
clientSecret: persisted.clientSecret,
|
|
227
|
+
audience: env.ORY_AGENT_AUDIENCE,
|
|
228
|
+
scope: env.ORY_AGENT_SCOPE,
|
|
229
|
+
subjectOverride: env.ORY_AGENT_SUBJECT_ID,
|
|
230
|
+
kind: "dynamic",
|
|
231
|
+
fetchFn: fetchCcFn,
|
|
232
|
+
skipCache,
|
|
233
|
+
warnings,
|
|
234
|
+
});
|
|
235
|
+
if (result.kind !== "none")
|
|
236
|
+
return result;
|
|
237
|
+
// Server rejected the persisted creds — clear and fall through to
|
|
238
|
+
// a fresh registration attempt.
|
|
239
|
+
warnings.push(`Persisted dynamic agent credentials rejected (${result.reason}); re-registering.`);
|
|
240
|
+
clearDynamicFn();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 4. Fresh dynamic registration — bootstrap using the user's bearer
|
|
244
|
+
// or an out-of-band initial access token.
|
|
245
|
+
const iat = options.userToken ?? env.ORY_AGENT_REGISTRATION_TOKEN;
|
|
246
|
+
if (options.projectUrl && iat) {
|
|
247
|
+
try {
|
|
248
|
+
const registered = await registerFn({
|
|
249
|
+
projectUrl: options.projectUrl,
|
|
250
|
+
auth: iat,
|
|
251
|
+
harness: options.harness,
|
|
252
|
+
});
|
|
253
|
+
if (!registered.clientSecret) {
|
|
254
|
+
warnings.push("Dynamic registration returned no client_secret; cannot run client_credentials grant.");
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
saveDynamicFn(registered);
|
|
258
|
+
const result = await resolveClientCredentials({
|
|
259
|
+
projectUrl: options.projectUrl,
|
|
260
|
+
clientId: registered.clientId,
|
|
261
|
+
clientSecret: registered.clientSecret,
|
|
262
|
+
audience: env.ORY_AGENT_AUDIENCE,
|
|
263
|
+
scope: env.ORY_AGENT_SCOPE,
|
|
264
|
+
subjectOverride: env.ORY_AGENT_SUBJECT_ID,
|
|
265
|
+
kind: "dynamic",
|
|
266
|
+
fetchFn: fetchCcFn,
|
|
267
|
+
skipCache: true, // freshly registered, no stale cache
|
|
268
|
+
warnings,
|
|
269
|
+
});
|
|
270
|
+
if (result.kind !== "none")
|
|
271
|
+
return result;
|
|
272
|
+
warnings.push(`Token grant failed immediately after registration (${result.reason}).`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
warnings.push(`Dynamic client registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// 5. Deprecated ORY_API_KEY alias.
|
|
280
|
+
const legacyApiKey = env.ORY_API_KEY;
|
|
281
|
+
if (legacyApiKey && legacyApiKey.length > 0) {
|
|
282
|
+
warnings.push("ORY_API_KEY is deprecated for agent authentication — set ORY_AGENT_API_KEY instead");
|
|
283
|
+
return {
|
|
284
|
+
kind: "api_key",
|
|
285
|
+
token: legacyApiKey,
|
|
286
|
+
subject: env.ORY_AGENT_SUBJECT_ID,
|
|
287
|
+
reason: "Resolved agent identity from ORY_API_KEY (deprecated alias)",
|
|
288
|
+
warnings,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
kind: "none",
|
|
293
|
+
reason: "No agent credentials configured. Run with a user session (ORY_AUTH_GATE=1) or set ORY_AGENT_API_KEY / ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET / ORY_AGENT_REGISTRATION_TOKEN.",
|
|
294
|
+
warnings,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function resolveClientCredentials(args) {
|
|
298
|
+
const cacheKey = `${args.kind}:${args.clientId}@${args.projectUrl}`;
|
|
299
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
300
|
+
const reasonNoun = args.kind === "dynamic" ? "dynamic" : "static";
|
|
301
|
+
if (!args.skipCache) {
|
|
302
|
+
const cached = ccCache.get(cacheKey);
|
|
303
|
+
if (cached && cached.expiresAt - exports.AGENT_TOKEN_EXPIRY_SKEW_SEC > nowSec) {
|
|
304
|
+
return {
|
|
305
|
+
kind: args.kind,
|
|
306
|
+
token: cached.token,
|
|
307
|
+
subject: args.subjectOverride ?? cached.subject,
|
|
308
|
+
expiresAt: cached.expiresAt,
|
|
309
|
+
reason: `Re-using cached agent ${reasonNoun} token`,
|
|
310
|
+
warnings: args.warnings,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const result = await args.fetchFn({
|
|
316
|
+
projectUrl: args.projectUrl,
|
|
317
|
+
clientId: args.clientId,
|
|
318
|
+
clientSecret: args.clientSecret,
|
|
319
|
+
audience: args.audience,
|
|
320
|
+
scope: args.scope,
|
|
321
|
+
});
|
|
322
|
+
ccCache.set(cacheKey, {
|
|
323
|
+
token: result.accessToken,
|
|
324
|
+
subject: args.subjectOverride ?? args.clientId,
|
|
325
|
+
expiresAt: result.expiresAt,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
kind: args.kind,
|
|
329
|
+
token: result.accessToken,
|
|
330
|
+
subject: args.subjectOverride ?? args.clientId,
|
|
331
|
+
expiresAt: result.expiresAt,
|
|
332
|
+
reason: `Fetched agent token via OAuth2 client_credentials grant (${reasonNoun})`,
|
|
333
|
+
warnings: args.warnings,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
return {
|
|
338
|
+
kind: "none",
|
|
339
|
+
reason: `${reasonNoun} client_credentials grant failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
340
|
+
warnings: args.warnings,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Kept as raw fetch (rather than @ory/client's OAuth2Api.oauth2TokenExchange)
|
|
345
|
+
// because the SDK method's signature does not expose client_secret, audience,
|
|
346
|
+
// or scope — all of which this grant needs. Ory's own SDK docstring on that
|
|
347
|
+
// method says it should not be used via the SDK.
|
|
348
|
+
/** POST /oauth2/token with grant_type=client_credentials. */
|
|
349
|
+
async function fetchClientCredentialsToken(args) {
|
|
350
|
+
const body = new URLSearchParams({
|
|
351
|
+
grant_type: "client_credentials",
|
|
352
|
+
client_id: args.clientId,
|
|
353
|
+
client_secret: args.clientSecret,
|
|
354
|
+
});
|
|
355
|
+
if (args.scope)
|
|
356
|
+
body.set("scope", args.scope);
|
|
357
|
+
if (args.audience)
|
|
358
|
+
body.set("audience", args.audience);
|
|
359
|
+
const tokenUrl = new URL("/oauth2/token", args.projectUrl).toString();
|
|
360
|
+
const res = await fetch(tokenUrl, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
363
|
+
body: body.toString(),
|
|
364
|
+
});
|
|
365
|
+
if (!res.ok) {
|
|
366
|
+
throw new Error(`HTTP ${res.status}`);
|
|
367
|
+
}
|
|
368
|
+
const json = (await res.json());
|
|
369
|
+
const accessToken = typeof json.access_token === "string" ? json.access_token : "";
|
|
370
|
+
if (!accessToken)
|
|
371
|
+
throw new Error("Token response missing access_token");
|
|
372
|
+
const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 3600;
|
|
373
|
+
return {
|
|
374
|
+
accessToken,
|
|
375
|
+
expiresAt: Math.floor(Date.now() / 1000) + expiresIn,
|
|
376
|
+
scope: typeof json.scope === "string" ? json.scope : undefined,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Resolve the agent's credentials and attach them to the client.
|
|
381
|
+
*
|
|
382
|
+
* Always returns the resolved credentials; never throws and never blocks.
|
|
383
|
+
* Emits exactly one `agent.auth` span so the audit trail records whether
|
|
384
|
+
* the agent identity was available for the session.
|
|
385
|
+
*/
|
|
386
|
+
async function ensureAgentIdentity(client, options = {}) {
|
|
387
|
+
const resolveFn = options.resolveFn ?? resolveAgentCredentials;
|
|
388
|
+
let creds;
|
|
389
|
+
try {
|
|
390
|
+
creds = await resolveFn({
|
|
391
|
+
projectUrl: options.projectUrl,
|
|
392
|
+
env: options.env,
|
|
393
|
+
// The user gate populates userPrincipal.token on successful login;
|
|
394
|
+
// we hand it to the resolver so DCR can use it as the IAT.
|
|
395
|
+
userToken: client.userPrincipal.token,
|
|
396
|
+
harness: options.harness,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
creds = {
|
|
401
|
+
kind: "none",
|
|
402
|
+
reason: `Agent credential resolution threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
403
|
+
warnings: [],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
for (const warning of creds.warnings) {
|
|
407
|
+
client.logger.warn("agent.auth.warning", { message: warning });
|
|
408
|
+
}
|
|
409
|
+
if (creds.kind === "none") {
|
|
410
|
+
client.logger.warn("agent.auth.none", { reason: creds.reason });
|
|
411
|
+
}
|
|
412
|
+
if (creds.token) {
|
|
413
|
+
client.setAgentPrincipal({ token: creds.token, subject: creds.subject });
|
|
414
|
+
}
|
|
415
|
+
// When no credentials were resolved, leave the existing principal in
|
|
416
|
+
// place. The constructor may have seeded one from `config.apiKey` for
|
|
417
|
+
// backward compat, and "no resolver match" should not clear it.
|
|
418
|
+
client.tracer.record("agent.auth", creds.kind === "none" ? "skipped" : "ok", {
|
|
419
|
+
attributes: {
|
|
420
|
+
kind: creds.kind,
|
|
421
|
+
reason: creds.reason,
|
|
422
|
+
...(creds.subject ? { subject: creds.subject } : {}),
|
|
423
|
+
...(creds.expiresAt ? { expiresAt: creds.expiresAt } : {}),
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
return creds;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Resolve a sub-agent identity via OAuth2 Dynamic Client Registration.
|
|
430
|
+
*
|
|
431
|
+
* Reuses persisted credentials when the `(projectUrl, subAgentType)`
|
|
432
|
+
* pair matches; otherwise registers a fresh client with
|
|
433
|
+
* `client_name = ory-subagent-<type>@<host>` and persists the result.
|
|
434
|
+
* Never throws and never blocks — on any failure the identity collapses
|
|
435
|
+
* to `kind: "none"` and the plugin continues without it. Emits exactly
|
|
436
|
+
* one `agent.auth` span so the audit trail records each sub-agent
|
|
437
|
+
* resolution.
|
|
438
|
+
*/
|
|
439
|
+
async function ensureSubAgentIdentity(client, options) {
|
|
440
|
+
const env = options.env ?? process.env;
|
|
441
|
+
const registerFn = options.registerAgentClientFn ?? registerAgentClient;
|
|
442
|
+
const loadFn = options.loadFn ?? loadSubAgentDynamicCredentials;
|
|
443
|
+
const saveFn = options.saveFn ?? saveSubAgentDynamicCredentials;
|
|
444
|
+
const warnings = [];
|
|
445
|
+
let identity;
|
|
446
|
+
try {
|
|
447
|
+
identity = await resolveSubAgentIdentity({
|
|
448
|
+
subAgentType: options.subAgentType,
|
|
449
|
+
projectUrl: options.projectUrl,
|
|
450
|
+
harness: options.harness,
|
|
451
|
+
iat: options.iat
|
|
452
|
+
?? client.agentPrincipal.token
|
|
453
|
+
?? client.userPrincipal.token
|
|
454
|
+
?? env.ORY_AGENT_REGISTRATION_TOKEN,
|
|
455
|
+
registerFn,
|
|
456
|
+
loadFn,
|
|
457
|
+
saveFn,
|
|
458
|
+
warnings,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
identity = {
|
|
463
|
+
kind: "none",
|
|
464
|
+
subAgentType: options.subAgentType,
|
|
465
|
+
reason: `Sub-agent identity resolution threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
466
|
+
warnings,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
for (const warning of identity.warnings) {
|
|
470
|
+
client.logger.warn("agent.auth.warning", {
|
|
471
|
+
message: warning,
|
|
472
|
+
subAgentType: options.subAgentType,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (identity.kind === "none") {
|
|
476
|
+
client.logger.warn("agent.auth.none", {
|
|
477
|
+
reason: identity.reason,
|
|
478
|
+
subAgentType: options.subAgentType,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
client.tracer.record("agent.auth", identity.kind === "none" ? "skipped" : "ok", {
|
|
482
|
+
attributes: {
|
|
483
|
+
kind: identity.kind === "dynamic" ? "subagent_dynamic" : "subagent_none",
|
|
484
|
+
subAgentType: identity.subAgentType,
|
|
485
|
+
reason: identity.reason,
|
|
486
|
+
...(identity.subject ? { subject: identity.subject } : {}),
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
return identity;
|
|
490
|
+
}
|
|
491
|
+
async function resolveSubAgentIdentity(args) {
|
|
492
|
+
if (!args.projectUrl) {
|
|
493
|
+
return {
|
|
494
|
+
kind: "none",
|
|
495
|
+
subAgentType: args.subAgentType,
|
|
496
|
+
reason: "ORY_PROJECT_URL is missing; cannot register sub-agent identity",
|
|
497
|
+
warnings: args.warnings,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
// 1. Re-use persisted credentials if they match the current project URL.
|
|
501
|
+
const persisted = args.loadFn(args.subAgentType);
|
|
502
|
+
if (persisted && persisted.projectUrl === args.projectUrl) {
|
|
503
|
+
return {
|
|
504
|
+
kind: "dynamic",
|
|
505
|
+
subAgentType: args.subAgentType,
|
|
506
|
+
subject: persisted.clientId,
|
|
507
|
+
credentials: persisted,
|
|
508
|
+
reason: `Re-using persisted sub-agent registration for ${args.subAgentType}`,
|
|
509
|
+
warnings: args.warnings,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// 2. Need an IAT to register. The parent agent's token is preferred;
|
|
513
|
+
// the user's token is the fallback. Neither available ⇒ no identity.
|
|
514
|
+
if (!args.iat) {
|
|
515
|
+
return {
|
|
516
|
+
kind: "none",
|
|
517
|
+
subAgentType: args.subAgentType,
|
|
518
|
+
reason: "No initial access token available for sub-agent registration "
|
|
519
|
+
+ "(neither agent nor user principal is populated, and "
|
|
520
|
+
+ "ORY_AGENT_REGISTRATION_TOKEN is unset).",
|
|
521
|
+
warnings: args.warnings,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// 3. Fresh DCR. client_name is namespaced under sub-agent so audit logs
|
|
525
|
+
// can distinguish it from the top-level agent registration.
|
|
526
|
+
const subHarness = args.harness
|
|
527
|
+
? `${args.harness}-subagent-${args.subAgentType}`
|
|
528
|
+
: `subagent-${args.subAgentType}`;
|
|
529
|
+
try {
|
|
530
|
+
const registered = await args.registerFn({
|
|
531
|
+
projectUrl: args.projectUrl,
|
|
532
|
+
auth: args.iat,
|
|
533
|
+
harness: subHarness,
|
|
534
|
+
});
|
|
535
|
+
args.saveFn(args.subAgentType, registered);
|
|
536
|
+
return {
|
|
537
|
+
kind: "dynamic",
|
|
538
|
+
subAgentType: args.subAgentType,
|
|
539
|
+
subject: registered.clientId,
|
|
540
|
+
credentials: registered,
|
|
541
|
+
reason: `Registered sub-agent ${args.subAgentType} via DCR (RFC 7591)`,
|
|
542
|
+
warnings: args.warnings,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
return {
|
|
547
|
+
kind: "none",
|
|
548
|
+
subAgentType: args.subAgentType,
|
|
549
|
+
reason: `Sub-agent DCR failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
550
|
+
warnings: args.warnings,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User authentication gate. Orchestrates the "first interaction must
|
|
3
|
+
* authenticate the human user" requirement across all plugins:
|
|
4
|
+
*
|
|
5
|
+
* 1. Resolve config (env + ~/.config/ory-agent-plugins/config.json).
|
|
6
|
+
* 2. If no project URL, prompt for one when a TTY is available;
|
|
7
|
+
* otherwise skip with an audit span.
|
|
8
|
+
* 3. If a session/oauth2 token is already in env, short-circuit ok.
|
|
9
|
+
* 4. If persisted tokens are valid, return ok.
|
|
10
|
+
* 5. If they are expired with a refresh_token, refresh.
|
|
11
|
+
* 6. Otherwise launch the PKCE browser login. The flow prints the
|
|
12
|
+
* authorize URL to stderr and waits on a loopback callback, so a
|
|
13
|
+
* missing TTY is fine — users without an interactive terminal can
|
|
14
|
+
* paste the URL into any browser that can reach 127.0.0.1. Only
|
|
15
|
+
* truly unattended (CI=true) environments short-circuit early.
|
|
16
|
+
*
|
|
17
|
+
* Every terminal path emits exactly one `user.auth` trace span so that
|
|
18
|
+
* the audit trail is complete regardless of outcome.
|
|
19
|
+
*
|
|
20
|
+
* The whole gate is a no-op (mode `disabled`) unless the
|
|
21
|
+
* `ORY_AUTH_GATE` env var is set to `1`/`true` — phased rollout.
|
|
22
|
+
*
|
|
23
|
+
* This gate authenticates the *user* (the human at the keyboard). The
|
|
24
|
+
* separate agent identity (the AI process making the calls) is resolved
|
|
25
|
+
* non-interactively via env-configured machine credentials and is not
|
|
26
|
+
* handled here — see `ensureAgentIdentity` (forthcoming).
|
|
27
|
+
*/
|
|
28
|
+
import { OryAgentClient } from "./client.js";
|
|
29
|
+
import { type OryOAuth2Tokens } from "./config.js";
|
|
30
|
+
import { promptForProjectUrl } from "./cli.js";
|
|
31
|
+
import { pkceLogin } from "./auth.js";
|
|
32
|
+
export type AuthGateMode = "disabled" | "audit_only" | "env_token" | "ok" | "refreshed" | "skipped" | "declined" | "error";
|
|
33
|
+
export interface AuthGateOptions {
|
|
34
|
+
/** Bin name used in user-facing prompts (e.g. "ory-claude"). */
|
|
35
|
+
binName: string;
|
|
36
|
+
/** Logical harness name (used by the tracer). */
|
|
37
|
+
harness: string;
|
|
38
|
+
/** Whether the caller can hard-block on auth failure. */
|
|
39
|
+
allowBlock: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Optional override of the PKCE login function — used by tests and by
|
|
42
|
+
* harnesses that want to plug in a different flow.
|
|
43
|
+
*/
|
|
44
|
+
loginFn?: typeof pkceLogin;
|
|
45
|
+
/** Override TTY detection (for tests). */
|
|
46
|
+
isTtyAvailableFn?: () => boolean;
|
|
47
|
+
/** Override the prompt-for-URL step (for tests). */
|
|
48
|
+
promptForProjectUrlFn?: typeof promptForProjectUrl;
|
|
49
|
+
}
|
|
50
|
+
export interface AuthGateDecision {
|
|
51
|
+
/** Whether the caller should let the session proceed. */
|
|
52
|
+
proceed: boolean;
|
|
53
|
+
/** Stable mode identifier for telemetry and tests. */
|
|
54
|
+
mode: AuthGateMode;
|
|
55
|
+
/** Human-readable reason; safe to surface to the operator. */
|
|
56
|
+
reason: string;
|
|
57
|
+
/** Subject (sub claim) when authenticated. */
|
|
58
|
+
subject?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The entry point that every plugin's session-start handler should call
|
|
62
|
+
* to authenticate the human user. Always returns a decision; never throws.
|
|
63
|
+
*/
|
|
64
|
+
export declare function ensureUserAuthenticated(client: OryAgentClient, options: AuthGateOptions): Promise<AuthGateDecision>;
|
|
65
|
+
/** Re-export so callers don't need to import auth.ts directly. */
|
|
66
|
+
export type { OryOAuth2Tokens };
|
|
67
|
+
/**
|
|
68
|
+
* @deprecated Renamed to `ensureUserAuthenticated`. The old name will be
|
|
69
|
+
* removed in a future release. The behaviour is unchanged.
|
|
70
|
+
*/
|
|
71
|
+
export declare const ensureAuthenticated: typeof ensureUserAuthenticated;
|