@ory/argus 0.13.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +31 -45
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +43 -97
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -18
  7. package/assets/skills/ory-temporal-worker/SKILL.md +38 -42
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +93 -30
  10. package/dist/adapters.js +464 -136
  11. package/dist/agent-auth.d.ts +258 -68
  12. package/dist/agent-auth.js +998 -202
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +40 -4
  16. package/dist/auth.js +247 -19
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +128 -0
  20. package/dist/branding.js +151 -0
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -278
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +249 -57
  29. package/dist/config.js +486 -62
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +8 -8
  33. package/dist/contract-suite.js +88 -69
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +31 -19
  47. package/dist/index.js +182 -31
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.d.ts +14 -0
  54. package/dist/local/health.js +50 -4
  55. package/dist/local/index.d.ts +2 -2
  56. package/dist/local/index.js +24 -10
  57. package/dist/local/manager.d.ts +20 -1
  58. package/dist/local/manager.js +160 -39
  59. package/dist/local/ports.d.ts +158 -0
  60. package/dist/local/ports.js +443 -0
  61. package/dist/local/seed.d.ts +22 -25
  62. package/dist/local/seed.js +88 -56
  63. package/dist/logger.d.ts +54 -25
  64. package/dist/logger.js +329 -63
  65. package/dist/mcp.d.ts +2 -2
  66. package/dist/mcp.js +10 -5
  67. package/dist/mirror-bootstrap.d.ts +48 -0
  68. package/dist/mirror-bootstrap.js +254 -0
  69. package/dist/opl.d.ts +289 -0
  70. package/dist/opl.js +446 -0
  71. package/dist/permission-mode.d.ts +87 -0
  72. package/dist/permission-mode.js +307 -0
  73. package/dist/permissions-cli.d.ts +13 -49
  74. package/dist/permissions-cli.js +154 -348
  75. package/dist/permissions.d.ts +148 -38
  76. package/dist/permissions.js +591 -45
  77. package/dist/post-install.d.ts +33 -0
  78. package/dist/post-install.js +127 -0
  79. package/dist/read-credential.d.ts +65 -0
  80. package/dist/read-credential.js +86 -0
  81. package/dist/registry/cli.js +5 -2
  82. package/dist/registry/config.d.ts +0 -17
  83. package/dist/registry/config.js +0 -23
  84. package/dist/registry/index.d.ts +1 -1
  85. package/dist/registry/index.js +2 -2
  86. package/dist/registry/manager.d.ts +4 -21
  87. package/dist/registry/manager.js +83 -55
  88. package/dist/runtime-credential.d.ts +140 -0
  89. package/dist/runtime-credential.js +572 -0
  90. package/dist/runtime.d.ts +408 -0
  91. package/dist/runtime.js +748 -0
  92. package/dist/setup.d.ts +23 -28
  93. package/dist/setup.js +57 -84
  94. package/dist/status-cli.d.ts +29 -13
  95. package/dist/status-cli.js +124 -144
  96. package/dist/status-data.d.ts +195 -0
  97. package/dist/status-data.js +333 -0
  98. package/dist/status-system.d.ts +24 -0
  99. package/dist/status-system.js +56 -0
  100. package/dist/subject.d.ts +126 -20
  101. package/dist/subject.js +215 -30
  102. package/dist/testing.d.ts +74 -38
  103. package/dist/testing.js +185 -68
  104. package/dist/tool-catalog.d.ts +53 -11
  105. package/dist/tool-catalog.js +164 -13
  106. package/dist/tool-metadata.d.ts +7 -6
  107. package/dist/tool-metadata.js +6 -5
  108. package/dist/types.d.ts +11 -1
  109. package/dist/uninstall.d.ts +74 -19
  110. package/dist/uninstall.js +224 -49
  111. package/dist/user-login.d.ts +22 -16
  112. package/dist/user-login.js +67 -96
  113. package/dist/watch-cli.d.ts +6 -0
  114. package/dist/watch-cli.js +217 -0
  115. package/package.json +3 -11
  116. package/dist/dev.d.ts +0 -103
  117. package/dist/dev.js +0 -584
  118. package/dist/interactive-setup.d.ts +0 -165
  119. package/dist/interactive-setup.js +0 -1546
  120. package/dist/local/jaeger-main.d.ts +0 -13
  121. package/dist/local/jaeger-main.js +0 -85
  122. package/dist/local/jaeger.d.ts +0 -50
  123. package/dist/local/jaeger.js +0 -162
  124. package/dist/otel/exporter.d.ts +0 -17
  125. package/dist/otel/exporter.js +0 -12
  126. package/dist/otel/index.d.ts +0 -2
  127. package/dist/otel/index.js +0 -8
  128. package/dist/otel/otlp.d.ts +0 -103
  129. package/dist/otel/otlp.js +0 -385
  130. package/dist/tracer.d.ts +0 -190
  131. package/dist/tracer.js +0 -481
  132. package/dist/watch-sandbox.d.ts +0 -9
  133. package/dist/watch-sandbox.js +0 -81
package/dist/config.js CHANGED
@@ -33,20 +33,49 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.LOCAL_AGENT_SECURITY_URL = exports.DEFAULT_AGENT_SECURITY_URL = exports.DEFAULT_OAUTH2_CLIENT_ID = exports.SESSIONLESS_KEY = exports.UNKNOWN_HARNESS_KEY = exports.DELEGATION_NODE_HISTORY = void 0;
36
37
  exports.getDataDir = getDataDir;
37
38
  exports.getHarnessDataDir = getHarnessDataDir;
38
39
  exports.getConfigPath = getConfigPath;
39
40
  exports.loadConfig = loadConfig;
40
- exports.parseUserLoginEnv = parseUserLoginEnv;
41
+ exports.listInstalledHarnesses = listInstalledHarnesses;
42
+ exports.registerInstalledHarness = registerInstalledHarness;
43
+ exports.unregisterInstalledHarness = unregisterInstalledHarness;
41
44
  exports.saveConfig = saveConfig;
42
45
  exports.mutateConfig = mutateConfig;
43
46
  exports.looksLikeProjectId = looksLikeProjectId;
47
+ exports.isSecurityConnected = isSecurityConnected;
48
+ exports.describeSecurityGap = describeSecurityGap;
44
49
  exports.resolveConfig = resolveConfig;
45
50
  exports.configPromptMessage = configPromptMessage;
46
51
  const fs = __importStar(require("node:fs"));
47
52
  const os = __importStar(require("node:os"));
48
53
  const path = __importStar(require("node:path"));
49
54
  const cli_invocation_js_1 = require("./cli-invocation.js");
55
+ /**
56
+ * How many sessions' delegation node ids to keep per credential. Only the
57
+ * current session's is ever read; the rest are kept as a small audit tail and
58
+ * bounded so a long-lived install's config doesn't grow without limit.
59
+ */
60
+ exports.DELEGATION_NODE_HISTORY = 10;
61
+ /**
62
+ * Key used when a credential record carries no harness of its own — only
63
+ * reachable by migrating a config written before credentials were keyed by
64
+ * harness, where the `harness` field had never been persisted. Such a record
65
+ * is never re-used (no harness matches it), but it stays addressable so
66
+ * uninstall can still revoke the server-side client rather than orphaning it.
67
+ */
68
+ exports.UNKNOWN_HARNESS_KEY = "unknown";
69
+ /**
70
+ * Session key for a caller with no session concept — an Agent SDK integration
71
+ * embedded in a long-running service, or a one-off script. Such a caller gets
72
+ * one stable identity under this key rather than a new client per call, which
73
+ * is the only sensible reading of "per session" when there are no sessions.
74
+ * Credentials migrated from a config written before session keying also land
75
+ * here: the session they belonged to is not recoverable, but the record stays
76
+ * addressable so uninstall can revoke it.
77
+ */
78
+ exports.SESSIONLESS_KEY = "sessionless";
50
79
  /**
51
80
  * Single OS-agnostic data directory for *all* Ory agent plugin state:
52
81
  * the shared config file, persisted DCR credentials, and any harness
@@ -117,17 +146,27 @@ function loadConfig() {
117
146
  const parsed = JSON.parse(raw);
118
147
  return {
119
148
  projectUrl: typeof parsed.projectUrl === "string" ? parsed.projectUrl : undefined,
149
+ agentSecurityUrl: typeof parsed.agentSecurityUrl === "string" ? parsed.agentSecurityUrl : undefined,
120
150
  projectId: typeof parsed.projectId === "string" && parsed.projectId.trim()
121
151
  ? parsed.projectId.trim()
122
152
  : undefined,
123
- apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
153
+ projectName: typeof parsed.projectName === "string" && parsed.projectName.trim()
154
+ ? parsed.projectName.trim()
155
+ : undefined,
156
+ workspaceName: typeof parsed.workspaceName === "string" && parsed.workspaceName.trim()
157
+ ? parsed.workspaceName.trim()
158
+ : undefined,
124
159
  oauth2ClientId: typeof parsed.oauth2ClientId === "string" ? parsed.oauth2ClientId : undefined,
125
- auditOnly: parsed.auditOnly === true ? true : undefined,
126
- permissionMode: parsePermissionMode(parsed.permissionMode),
127
- userLogin: typeof parsed.userLogin === "boolean" ? parsed.userLogin : undefined,
160
+ // NOTE: a `dxOnly` key written by an older release is deliberately not
161
+ // parsed. Whether Agent Security runs is derived from the presence of
162
+ // `projectUrl` + `oauth2ClientId` (see resolveConfig), never from a
163
+ // stored flag — so the stale key is simply dropped on the next write.
164
+ permissionModeCache: parsePermissionModeCache(parsed),
128
165
  userSubjectNamespace: typeof parsed.userSubjectNamespace === "string" && parsed.userSubjectNamespace.trim()
129
166
  ? parsed.userSubjectNamespace.trim()
130
167
  : undefined,
168
+ installedHarnesses: parseInstalledHarnesses(parsed.installedHarnesses),
169
+ delegationAnchors: parseDelegationAnchors(parsed.delegationAnchors),
131
170
  user: parseUserCredentials(parsed),
132
171
  agent: parseAgentCredentials(parsed),
133
172
  };
@@ -136,47 +175,195 @@ function loadConfig() {
136
175
  return {};
137
176
  }
138
177
  }
178
+ function parseDelegationAnchors(value) {
179
+ if (!value || typeof value !== "object" || Array.isArray(value))
180
+ return undefined;
181
+ const result = {};
182
+ for (const [harness, rawSessions] of Object.entries(value)) {
183
+ if (!rawSessions || typeof rawSessions !== "object" || Array.isArray(rawSessions))
184
+ continue;
185
+ const sessions = {};
186
+ for (const [session, nodeId] of Object.entries(rawSessions)) {
187
+ if (typeof nodeId === "string" && nodeId.length > 0)
188
+ sessions[session] = nodeId;
189
+ }
190
+ if (Object.keys(sessions).length > 0)
191
+ result[harness] = sessions;
192
+ }
193
+ return Object.keys(result).length > 0 ? result : undefined;
194
+ }
139
195
  function parsePermissionMode(value) {
140
196
  return value === "observe" || value === "enforce" ? value : undefined;
141
197
  }
142
- /** Env values that turn the interactive user login on. */
143
- const USER_LOGIN_ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
144
198
  /**
145
- * Read `ORY_USER_LOGIN` from the environment. Returns `true`/`false` when
146
- * the var is set (any recognized truthy value enables it; anything else
147
- * disables it), or `undefined` when the var is absent/empty so callers
148
- * fall back to the config file. Centralized here so the user gate, status
149
- * CLI, and configure command resolve it identically.
199
+ * Parse the persisted {@link PermissionModeCache}. Also migrates a legacy
200
+ * top-level `permissionMode` string (the old user-set field) into the cache so
201
+ * an upgrade preserves the last posture until the first server read overwrites
202
+ * it (`fetchedAt: 0` marks it as never-server-confirmed).
150
203
  */
151
- function parseUserLoginEnv() {
152
- const raw = process.env.ORY_USER_LOGIN;
153
- if (raw === undefined || raw.trim() === "")
154
- return undefined;
155
- return USER_LOGIN_ENABLED_VALUES.has(raw.trim().toLowerCase());
204
+ function parsePermissionModeCache(parsed) {
205
+ const raw = parsed.permissionModeCache;
206
+ if (raw && typeof raw === "object") {
207
+ const obj = raw;
208
+ const mode = parsePermissionMode(obj.mode);
209
+ if (mode) {
210
+ const fetchedAt = typeof obj.fetchedAt === "number" ? obj.fetchedAt : 0;
211
+ const scopes = {};
212
+ if (obj.scopes && typeof obj.scopes === "object") {
213
+ for (const [key, value] of Object.entries(obj.scopes)) {
214
+ if (!value || typeof value !== "object")
215
+ continue;
216
+ const entry = value;
217
+ const entryMode = parsePermissionMode(entry.mode);
218
+ if (!entryMode)
219
+ continue;
220
+ scopes[key] = {
221
+ mode: entryMode,
222
+ fetchedAt: typeof entry.fetchedAt === "number" ? entry.fetchedAt : 0,
223
+ };
224
+ }
225
+ }
226
+ return {
227
+ mode,
228
+ fetchedAt,
229
+ ...(typeof obj.projectUrl === "string" ? { projectUrl: obj.projectUrl } : {}),
230
+ ...(Object.keys(scopes).length ? { scopes } : {}),
231
+ };
232
+ }
233
+ }
234
+ const legacy = parsePermissionMode(parsed.permissionMode);
235
+ if (legacy)
236
+ return { mode: legacy, fetchedAt: 0 };
237
+ return undefined;
156
238
  }
157
239
  function parseAgentCredentials(parsed) {
158
240
  const raw = parsed.agent;
159
241
  if (!raw || typeof raw !== "object")
160
242
  return undefined;
161
243
  const block = raw;
162
- const dynamic = parseAgentDynamicCredentials(block.dynamic);
244
+ const retired = parseRetiredCredentials(block.retired);
245
+ const dynamic = parseAgentDynamicByHarness(block.dynamic);
163
246
  const subAgents = parseSubAgentCredentials(block.subAgents);
164
- if (!dynamic && !subAgents)
247
+ if (!dynamic && !subAgents && retired.length === 0)
165
248
  return undefined;
166
249
  return {
167
250
  ...(dynamic ? { dynamic } : {}),
168
251
  ...(subAgents ? { subAgents } : {}),
252
+ ...(retired.length > 0 ? { retired } : {}),
253
+ };
254
+ }
255
+ function parseRetiredCredentials(value) {
256
+ if (!Array.isArray(value))
257
+ return [];
258
+ const out = [];
259
+ for (const entry of value) {
260
+ const creds = parseAgentDynamicCredentials(entry);
261
+ if (creds)
262
+ out.push(creds);
263
+ }
264
+ return out;
265
+ }
266
+ /**
267
+ * True when `value` is a credential record itself rather than a map of them.
268
+ * A credential record always carries a string `clientId`; a keyed map never
269
+ * does. This is what lets the parser accept both the current harness-keyed
270
+ * shape and the pre-migration flat shape without a version field.
271
+ */
272
+ function looksLikeCredentialRecord(value) {
273
+ if (!value || typeof value !== "object")
274
+ return false;
275
+ return typeof value.clientId === "string";
276
+ }
277
+ /**
278
+ * Parse `agent.dynamic` into `{ [harness]: { [session]: creds } }`.
279
+ *
280
+ * Three shapes have existed, and each is recognizable without a version field
281
+ * because only a credential record carries a string `clientId`:
282
+ *
283
+ * 1. one bare record — pre-harness keying
284
+ * 2. `{ [harness]: creds }` — install-scoped release
285
+ * 3. `{ [harness]: { [session]: creds } }` — current
286
+ *
287
+ * Records without a known session migrate to {@link SESSIONLESS_KEY}.
288
+ */
289
+ function parseAgentDynamicByHarness(value) {
290
+ if (!value || typeof value !== "object")
291
+ return undefined;
292
+ const result = {};
293
+ const put = (harness, session, creds) => {
294
+ (result[harness] ??= {})[session] = creds;
169
295
  };
296
+ // Shape 1: a single bare record, re-keyed under the harness it names.
297
+ if (looksLikeCredentialRecord(value)) {
298
+ const legacy = parseAgentDynamicCredentials(value);
299
+ if (!legacy)
300
+ return undefined;
301
+ put(legacy.harness ?? exports.UNKNOWN_HARNESS_KEY, exports.SESSIONLESS_KEY, legacy);
302
+ return result;
303
+ }
304
+ for (const [harness, raw] of Object.entries(value)) {
305
+ // Shape 2: harness → one install-scoped record.
306
+ if (looksLikeCredentialRecord(raw)) {
307
+ const creds = parseAgentDynamicCredentials(raw);
308
+ if (creds)
309
+ put(harness, exports.SESSIONLESS_KEY, creds);
310
+ continue;
311
+ }
312
+ // Shape 3: harness → session → record.
313
+ if (!raw || typeof raw !== "object")
314
+ continue;
315
+ for (const [session, entry] of Object.entries(raw)) {
316
+ const creds = parseAgentDynamicCredentials(entry);
317
+ if (creds)
318
+ put(harness, session, creds);
319
+ }
320
+ }
321
+ return Object.keys(result).length > 0 ? result : undefined;
170
322
  }
323
+ /**
324
+ * Parse `agent.subAgents` into
325
+ * `{ [harness]: { [session]: { [type]: creds } } }`.
326
+ *
327
+ * As with `agent.dynamic`, three shapes have existed:
328
+ *
329
+ * 1. `{ [type]: creds }` — pre-harness keying
330
+ * 2. `{ [harness]: { [type]: creds } }` — install-scoped release
331
+ * 3. `{ [harness]: { [session]: { [type]: creds } } }` — current
332
+ */
171
333
  function parseSubAgentCredentials(value) {
172
334
  if (!value || typeof value !== "object")
173
335
  return undefined;
174
- const entries = value;
175
336
  const result = {};
176
- for (const [key, raw] of Object.entries(entries)) {
177
- const creds = parseAgentDynamicCredentials(raw);
178
- if (creds)
179
- result[key] = creds;
337
+ const put = (harness, session, subAgentType, creds) => {
338
+ ((result[harness] ??= {})[session] ??= {})[subAgentType] = creds;
339
+ };
340
+ for (const [key, raw] of Object.entries(value)) {
341
+ // Shape 1: `key` is the sub-agent type; the harness comes from the record.
342
+ if (looksLikeCredentialRecord(raw)) {
343
+ const creds = parseAgentDynamicCredentials(raw);
344
+ if (creds)
345
+ put(creds.harness ?? exports.UNKNOWN_HARNESS_KEY, exports.SESSIONLESS_KEY, key, creds);
346
+ continue;
347
+ }
348
+ if (!raw || typeof raw !== "object")
349
+ continue;
350
+ for (const [inner, entry] of Object.entries(raw)) {
351
+ // Shape 2: `inner` is the sub-agent type, sitting directly under harness.
352
+ if (looksLikeCredentialRecord(entry)) {
353
+ const creds = parseAgentDynamicCredentials(entry);
354
+ if (creds)
355
+ put(key, exports.SESSIONLESS_KEY, inner, creds);
356
+ continue;
357
+ }
358
+ // Shape 3: `inner` is the session.
359
+ if (!entry || typeof entry !== "object")
360
+ continue;
361
+ for (const [subAgentType, leaf] of Object.entries(entry)) {
362
+ const creds = parseAgentDynamicCredentials(leaf);
363
+ if (creds)
364
+ put(key, inner, subAgentType, creds);
365
+ }
366
+ }
180
367
  }
181
368
  return Object.keys(result).length > 0 ? result : undefined;
182
369
  }
@@ -197,8 +384,104 @@ function parseAgentDynamicCredentials(value) {
197
384
  registeredAt,
198
385
  projectUrl: v.projectUrl,
199
386
  harness: typeof v.harness === "string" ? v.harness : undefined,
387
+ ...(parseDelegationRecord(v.delegation)
388
+ ? { delegation: parseDelegationRecord(v.delegation) }
389
+ : {}),
390
+ ...(parseDelegationNodeIds(v.delegationNodeIds, v.delegationNodeId)
391
+ ? { delegationNodeIds: parseDelegationNodeIds(v.delegationNodeIds, v.delegationNodeId) }
392
+ : {}),
200
393
  };
201
394
  }
395
+ /**
396
+ * Parse the per-session delegation node map, migrating the single
397
+ * `delegationNodeId` field earlier releases wrote.
398
+ *
399
+ * A migrated scalar lands under {@link SESSIONLESS_KEY}: the session it was
400
+ * recorded in was never persisted alongside it, and guessing would attribute a
401
+ * sub-agent to the wrong run. Landing it there means the next session records
402
+ * its own node id rather than inheriting a stale one — the correct outcome,
403
+ * since the node id is cheap to re-obtain (the broker's write is idempotent).
404
+ */
405
+ function parseDelegationNodeIds(value, legacy) {
406
+ const result = {};
407
+ if (typeof legacy === "string" && legacy.length > 0)
408
+ result[exports.SESSIONLESS_KEY] = legacy;
409
+ if (value && typeof value === "object" && !Array.isArray(value)) {
410
+ for (const [session, nodeId] of Object.entries(value)) {
411
+ if (typeof nodeId === "string" && nodeId.length > 0)
412
+ result[session] = nodeId;
413
+ }
414
+ }
415
+ return Object.keys(result).length > 0 ? result : undefined;
416
+ }
417
+ function parseDelegationRecord(value) {
418
+ if (!value || typeof value !== "object")
419
+ return undefined;
420
+ const v = value;
421
+ if (typeof v.delegatedBy !== "string" || v.delegatedBy.length === 0)
422
+ return undefined;
423
+ if (v.delegationType !== "user-to-agent" && v.delegationType !== "agent-to-subagent") {
424
+ return undefined;
425
+ }
426
+ return {
427
+ delegatedBy: v.delegatedBy,
428
+ delegationType: v.delegationType,
429
+ ...(typeof v.harness === "string" ? { harness: v.harness } : {}),
430
+ ...(typeof v.subAgentType === "string" ? { subAgentType: v.subAgentType } : {}),
431
+ delegatedAt: typeof v.delegatedAt === "string" ? v.delegatedAt : "",
432
+ };
433
+ }
434
+ function parseInstalledHarnesses(value) {
435
+ if (!Array.isArray(value))
436
+ return undefined;
437
+ const names = normalizeHarnessList(value.filter((v) => typeof v === "string"));
438
+ return names.length > 0 ? names : undefined;
439
+ }
440
+ /** De-duplicate, trim, and sort a harness list so the file stays stable. */
441
+ function normalizeHarnessList(names) {
442
+ return [...new Set(names.map((n) => n.trim()).filter((n) => n.length > 0))].sort();
443
+ }
444
+ /**
445
+ * The harness plugins recorded as installed against this shared config.
446
+ * Returns an empty array when nothing has been registered yet (which, on a
447
+ * config written before the registry existed, is not the same as "nothing is
448
+ * installed" — callers that must not over-read that should reconcile against
449
+ * the runtime manifest and persisted credentials; see
450
+ * `resolveInstalledHarnesses` in `uninstall.ts`).
451
+ */
452
+ function listInstalledHarnesses() {
453
+ return loadConfig().installedHarnesses ?? [];
454
+ }
455
+ /**
456
+ * Record a harness plugin as installed. Idempotent — installing twice leaves
457
+ * one entry. Returns the full list after the addition.
458
+ */
459
+ function registerInstalledHarness(harness) {
460
+ let result = [];
461
+ mutateConfig((current) => {
462
+ result = normalizeHarnessList([...(current.installedHarnesses ?? []), harness]);
463
+ return { ...current, installedHarnesses: result };
464
+ });
465
+ return result;
466
+ }
467
+ /**
468
+ * Forget a harness plugin. Returns the harnesses still installed afterwards —
469
+ * what the uninstall path uses to decide whether the shared config is now
470
+ * unreferenced and safe to wipe.
471
+ */
472
+ function unregisterInstalledHarness(harness) {
473
+ let result = [];
474
+ mutateConfig((current) => {
475
+ result = normalizeHarnessList(current.installedHarnesses ?? []).filter((h) => h !== harness.trim());
476
+ const next = { ...current };
477
+ if (result.length > 0)
478
+ next.installedHarnesses = result;
479
+ else
480
+ delete next.installedHarnesses;
481
+ return next;
482
+ });
483
+ return result;
484
+ }
202
485
  function parseUserCredentials(parsed) {
203
486
  // Prefer the new nested shape; fall back to the legacy root-level
204
487
  // `oauth2` block so configs written before the user/agent split still
@@ -224,9 +507,6 @@ function parseOAuth2(value) {
224
507
  refreshToken: typeof v.refreshToken === "string" ? v.refreshToken : undefined,
225
508
  expiresAt: v.expiresAt,
226
509
  subject: typeof v.subject === "string" ? v.subject : undefined,
227
- clientId: typeof v.clientId === "string" ? v.clientId : undefined,
228
- idToken: typeof v.idToken === "string" ? v.idToken : undefined,
229
- scope: typeof v.scope === "string" ? v.scope : undefined,
230
510
  };
231
511
  }
232
512
  /**
@@ -253,7 +533,8 @@ function mutateConfig(mutator) {
253
533
  try {
254
534
  const existing = loadConfig();
255
535
  const next = mutator(existing);
256
- writeConfigAtomic(next);
536
+ if (next)
537
+ writeConfigAtomic(next);
257
538
  }
258
539
  finally {
259
540
  releaseLock(lock);
@@ -372,9 +653,101 @@ function releaseLock(lock) {
372
653
  function writeConfigAtomic(config) {
373
654
  const target = configFile();
374
655
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
375
- fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
656
+ fs.writeFileSync(tmp, JSON.stringify(compactPersistedConfig(config), null, 2) + "\n", {
657
+ mode: 0o600,
658
+ });
376
659
  fs.renameSync(tmp, target);
377
660
  }
661
+ /**
662
+ * Keep only state that cannot be reconstructed safely in a later process.
663
+ * This is the one persistence boundary, so old verbose records are compacted
664
+ * by any subsequent config mutation regardless of which subsystem writes.
665
+ */
666
+ function compactPersistedConfig(config) {
667
+ const userTokens = config.user?.oauth2;
668
+ const user = userTokens
669
+ ? {
670
+ oauth2: {
671
+ accessToken: userTokens.accessToken,
672
+ ...(userTokens.refreshToken ? { refreshToken: userTokens.refreshToken } : {}),
673
+ expiresAt: userTokens.expiresAt,
674
+ ...(userTokens.subject ? { subject: userTokens.subject } : {}),
675
+ },
676
+ }
677
+ : undefined;
678
+ const active = (creds) => ({
679
+ clientId: creds.clientId,
680
+ ...(creds.clientSecret ? { clientSecret: creds.clientSecret } : {}),
681
+ ...(creds.registrationAccessToken
682
+ ? { registrationAccessToken: creds.registrationAccessToken }
683
+ : {}),
684
+ ...(creds.registrationClientUri
685
+ ? { registrationClientUri: creds.registrationClientUri }
686
+ : {}),
687
+ registeredAt: creds.registeredAt,
688
+ projectUrl: creds.projectUrl,
689
+ ...(creds.delegation
690
+ ? {
691
+ delegation: {
692
+ delegatedBy: creds.delegation.delegatedBy,
693
+ delegationType: creds.delegation.delegationType,
694
+ },
695
+ }
696
+ : {}),
697
+ ...(creds.delegationNodeIds
698
+ ? { delegationNodeIds: creds.delegationNodeIds }
699
+ : {}),
700
+ });
701
+ const dynamic = config.agent?.dynamic
702
+ ? Object.fromEntries(Object.entries(config.agent.dynamic).map(([harness, sessions]) => [
703
+ harness,
704
+ Object.fromEntries(Object.entries(sessions).map(([session, creds]) => [session, active(creds)])),
705
+ ]))
706
+ : undefined;
707
+ const subAgents = config.agent?.subAgents
708
+ ? Object.fromEntries(Object.entries(config.agent.subAgents).map(([harness, sessions]) => [
709
+ harness,
710
+ Object.fromEntries(Object.entries(sessions).map(([session, byType]) => [
711
+ session,
712
+ Object.fromEntries(Object.entries(byType).map(([type, creds]) => [type, active(creds)])),
713
+ ])),
714
+ ]))
715
+ : undefined;
716
+ const retired = config.agent?.retired?.map((creds) => ({
717
+ clientId: creds.clientId,
718
+ projectUrl: creds.projectUrl,
719
+ ...(creds.harness ? { harness: creds.harness } : {}),
720
+ ...(creds.registrationAccessToken
721
+ ? { registrationAccessToken: creds.registrationAccessToken }
722
+ : {}),
723
+ ...(creds.registrationClientUri
724
+ ? { registrationClientUri: creds.registrationClientUri }
725
+ : {}),
726
+ }));
727
+ const agent = dynamic || subAgents || retired?.length
728
+ ? {
729
+ ...(dynamic ? { dynamic } : {}),
730
+ ...(subAgents ? { subAgents } : {}),
731
+ ...(retired?.length ? { retired } : {}),
732
+ }
733
+ : undefined;
734
+ return {
735
+ ...(config.projectUrl ? { projectUrl: config.projectUrl } : {}),
736
+ ...(config.agentSecurityUrl ? { agentSecurityUrl: config.agentSecurityUrl } : {}),
737
+ ...(config.projectId ? { projectId: config.projectId } : {}),
738
+ ...(config.oauth2ClientId ? { oauth2ClientId: config.oauth2ClientId } : {}),
739
+ ...(config.permissionModeCache ? { permissionModeCache: config.permissionModeCache } : {}),
740
+ ...(config.userSubjectNamespace
741
+ ? { userSubjectNamespace: config.userSubjectNamespace }
742
+ : {}),
743
+ ...(config.installedHarnesses?.length
744
+ ? { installedHarnesses: config.installedHarnesses }
745
+ : {}),
746
+ ...(config.delegationAnchors ? { delegationAnchors: config.delegationAnchors } : {}),
747
+ ...(user ? { user } : {}),
748
+ ...(agent ? { agent } : {}),
749
+ };
750
+ }
378
751
  /** Matches an Ory project UUID (the value `ORY_PROJECT_ID` carries). */
379
752
  const PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
380
753
  /**
@@ -390,6 +763,47 @@ const PROJECT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]
390
763
  function looksLikeProjectId(value) {
391
764
  return PROJECT_ID_RE.test(value.trim());
392
765
  }
766
+ /** Reserved public PKCE client provisioned by Ory Agent Security setup. */
767
+ exports.DEFAULT_OAUTH2_CLIENT_ID = "ory-agent-security-login";
768
+ exports.DEFAULT_AGENT_SECURITY_URL = "https://agents.console.ory.com";
769
+ exports.LOCAL_AGENT_SECURITY_URL = "https://agents.console.ory:8080";
770
+ function resolveSecurityConnection(projectUrl, agentSecurityUrl, oauth2ClientId) {
771
+ const missing = [];
772
+ if (!projectUrl?.trim())
773
+ missing.push("projectUrl");
774
+ if (!agentSecurityUrl?.trim())
775
+ missing.push("agentSecurityUrl");
776
+ if (!oauth2ClientId?.trim())
777
+ missing.push("oauth2ClientId");
778
+ return { connected: missing.length === 0, missing };
779
+ }
780
+ /**
781
+ * The one predicate the gates ask: is Agent Security connected?
782
+ *
783
+ * Shorthand for `resolveConfig().security.connected`, provided so the many
784
+ * call sites that only need the boolean read as a single question rather than
785
+ * a property walk. When this is false the plugin still runs — it just logs activity
786
+ * locally and gates nothing.
787
+ */
788
+ function isSecurityConnected() {
789
+ return resolveConfig().security.connected;
790
+ }
791
+ /** The CLI flag / env var a user sets for each required value. */
792
+ const SECURITY_VALUE_LABELS = {
793
+ projectUrl: "--project-url (ORY_PROJECT_URL)",
794
+ agentSecurityUrl: "--agent-security-url (ORY_AGENT_SECURITY_URL)",
795
+ oauth2ClientId: "--oauth2-client-id (ORY_OAUTH2_CLIENT_ID)",
796
+ };
797
+ /**
798
+ * Render why Agent Security isn't running, e.g.
799
+ * `"missing --oauth2-client-id (ORY_OAUTH2_CLIENT_ID)"`. Returns undefined when
800
+ * it *is* connected, so callers can `?? "connected"` at the display site.
801
+ */
802
+ function describeSecurityGap(security = resolveConfig().security) {
803
+ if (security.connected)
804
+ return undefined;
805
+ return `missing ${security.missing.map((m) => SECURITY_VALUE_LABELS[m]).join(" and ")}`;
806
+ }
393
807
  /**
394
808
  * Resolve config by checking environment variables first, then the config file.
395
809
  * Returns the merged result with the source of each value.
@@ -404,27 +818,17 @@ function resolveConfig() {
404
818
  // ORY_PROJECT_URL is the documented name; ORY_SDK_URL is the name the Ory
405
819
  // Console "Get Started" panel and the Ory SDKs use — accepted as an alias.
406
820
  const envProjectUrl = process.env.ORY_PROJECT_URL?.trim() || process.env.ORY_SDK_URL?.trim() || undefined;
821
+ const envAgentSecurityUrl = process.env.ORY_AGENT_SECURITY_URL?.trim() || undefined;
407
822
  const envProjectId = process.env.ORY_PROJECT_ID?.trim() || undefined;
408
- // ORY_AGENT_API_KEY is the documented name; ORY_API_KEY is the deprecated
409
- // alias kept for back-compat (agent-auth emits a warning when only the
410
- // legacy form is set).
411
- const envApiKey = process.env.ORY_AGENT_API_KEY ?? process.env.ORY_API_KEY;
412
- const envClientId = process.env.ORY_OAUTH2_CLIENT_ID;
413
- const envMode = parsePermissionMode(process.env.ORY_PERMISSION_MODE);
414
- const envUserLogin = parseUserLoginEnv();
823
+ const envClientId = process.env.ORY_OAUTH2_CLIENT_ID?.trim() || undefined;
415
824
  const envUserSubjectNamespace = process.env.ORY_USER_SUBJECT_NAMESPACE?.trim() || undefined;
416
- const permissionMode = envMode ?? file.permissionMode ?? "observe";
417
- const permissionModeSource = envMode
418
- ? "env"
419
- : file.permissionMode
420
- ? "config"
421
- : "default";
422
- const userLogin = envUserLogin ?? file.userLogin ?? false;
423
- const userLoginSource = envUserLogin !== undefined
424
- ? "env"
425
- : file.userLogin !== undefined
426
- ? "config"
427
- : "default";
825
+ // The mode is server-controlled (a Keto permission read per-check by
826
+ // resolvePermissionMode). Here we only surface the last cached value for the
827
+ // synchronous read path; there is no env or local override.
828
+ const permissionMode = file.permissionModeCache?.mode ?? "observe";
829
+ const permissionModeSource = file.permissionModeCache
830
+ ? "cache"
831
+ : "default";
428
832
  const projectId = envProjectId ?? file.projectId;
429
833
  // The SDK URL is only ever an explicit, slug-based URL (from ORY_PROJECT_URL,
430
834
  // ORY_SDK_URL, or the config file). The project id is NOT a URL source — it
@@ -435,16 +839,33 @@ function resolveConfig() {
435
839
  : file.projectUrl
436
840
  ? "config"
437
841
  : "none";
842
+ // Preserve the project URL fallback only for persisted pre-default configs.
843
+ // New env-only and configure flows use the production default unless they
844
+ // explicitly select staging, local, or a custom broker.
845
+ const legacyAgentSecurityUrl = !envProjectUrl && file.projectUrl?.trim() && !file.agentSecurityUrl ? file.projectUrl.trim() : undefined;
846
+ const agentSecurityUrl = envAgentSecurityUrl ?? file.agentSecurityUrl ?? legacyAgentSecurityUrl ?? exports.DEFAULT_AGENT_SECURITY_URL;
847
+ const agentSecurityUrlSource = envAgentSecurityUrl
848
+ ? "env"
849
+ : file.agentSecurityUrl
850
+ ? "config"
851
+ : legacyAgentSecurityUrl
852
+ ? "projectUrl"
853
+ : "default";
854
+ const fileClientId = file.oauth2ClientId?.trim() || undefined;
855
+ const oauth2ClientId = envClientId ?? fileClientId ?? exports.DEFAULT_OAUTH2_CLIENT_ID;
438
856
  return {
439
857
  projectUrl,
858
+ agentSecurityUrl,
440
859
  projectId,
441
- apiKey: envApiKey ?? file.apiKey,
442
- oauth2ClientId: envClientId ?? file.oauth2ClientId,
443
- auditOnly: file.auditOnly === true,
860
+ projectName: file.projectName,
861
+ workspaceName: file.workspaceName,
862
+ oauth2ClientId,
863
+ // Agent Security runs exactly when all resolved values it needs are present.
864
+ // The broker origin can resolve through the legacy project URL fallback.
865
+ security: resolveSecurityConnection(projectUrl, agentSecurityUrl, oauth2ClientId),
444
866
  permissionMode,
445
867
  permissionModeSource,
446
- userLogin,
447
- userLoginSource,
868
+ permissionModeCache: file.permissionModeCache,
448
869
  userSubjectNamespace: envUserSubjectNamespace ?? file.userSubjectNamespace,
449
870
  userSubjectNamespaceSource: envUserSubjectNamespace
450
871
  ? "env"
@@ -452,22 +873,25 @@ function resolveConfig() {
452
873
  ? "config"
453
874
  : "none",
454
875
  projectUrlSource,
876
+ agentSecurityUrlSource,
455
877
  projectIdSource: envProjectId ? "env" : file.projectId ? "config" : "none",
456
- apiKeySource: envApiKey ? "env" : file.apiKey ? "config" : "none",
457
- oauth2ClientIdSource: envClientId ? "env" : file.oauth2ClientId ? "config" : "none",
878
+ oauth2ClientIdSource: envClientId ? "env" : fileClientId ? "config" : "default",
458
879
  };
459
880
  }
460
881
  /**
461
- * Build the "not configured" prompt message for a given harness CLI bin name.
882
+ * Build the "Agent Security is not connected" message for a harness CLI.
883
+ *
884
+ * Deliberately does not frame this as a degraded mode: the plugin is fully
885
+ * installed and its developer-experience half is working. The message states
886
+ * the one thing that is off and the values that turn it on.
462
887
  */
463
888
  function configPromptMessage(binName) {
464
- return ("The Ory agent plugin is installed but not yet configured.\n" +
465
- "\n" +
466
- " Option 1 Connect to Ory (enables authentication and permission checks):\n" +
467
- ` ${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]\n` +
889
+ return ("Ory Agent Security is not connected, so no sign-in or permission checks\n" +
890
+ "run. Everything else the plugin installs — skills, commands, the local Ory\n" +
891
+ "stack, the MCP server, and local activity logging works as it is.\n" +
468
892
  "\n" +
469
- " Option 2 Continue without authentication (audit logging only):\n" +
470
- ` ${(0, cli_invocation_js_1.oryNpx)(binName)} configure --audit-only\n` +
893
+ " Connect it with the values from the Ory Console (Agent Security):\n" +
894
+ ` ${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL>\n` +
471
895
  "\n" +
472
896
  `Configuration is saved to ${configFile()} and shared across all agent plugins.`);
473
897
  }