@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.
Files changed (150) hide show
  1. package/README.md +31 -46
  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 +39 -91
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
  7. package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +88 -32
  10. package/dist/adapters.js +443 -164
  11. package/dist/agent-auth.d.ts +226 -67
  12. package/dist/agent-auth.js +951 -205
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +33 -4
  16. package/dist/auth.js +163 -20
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +77 -16
  20. package/dist/branding.js +93 -23
  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 -239
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +217 -54
  29. package/dist/config.js +461 -42
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +3 -5
  33. package/dist/contract-suite.js +75 -47
  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 +28 -23
  47. package/dist/index.js +173 -58
  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.js +7 -7
  54. package/dist/local/index.d.ts +2 -2
  55. package/dist/local/index.js +24 -10
  56. package/dist/local/manager.d.ts +20 -1
  57. package/dist/local/manager.js +159 -36
  58. package/dist/local/ports.d.ts +158 -0
  59. package/dist/local/ports.js +443 -0
  60. package/dist/local/seed.d.ts +13 -5
  61. package/dist/local/seed.js +62 -38
  62. package/dist/logger.d.ts +54 -25
  63. package/dist/logger.js +329 -63
  64. package/dist/mcp.d.ts +2 -2
  65. package/dist/mcp.js +10 -5
  66. package/dist/mirror-bootstrap.d.ts +48 -0
  67. package/dist/mirror-bootstrap.js +254 -0
  68. package/dist/opl.d.ts +289 -0
  69. package/dist/opl.js +446 -0
  70. package/dist/permission-mode.d.ts +87 -0
  71. package/dist/permission-mode.js +307 -0
  72. package/dist/permissions-cli.d.ts +13 -49
  73. package/dist/permissions-cli.js +154 -348
  74. package/dist/permissions.d.ts +148 -38
  75. package/dist/permissions.js +591 -45
  76. package/dist/post-install.d.ts +33 -0
  77. package/dist/post-install.js +127 -0
  78. package/dist/read-credential.d.ts +65 -0
  79. package/dist/read-credential.js +86 -0
  80. package/dist/registry/cli.js +5 -2
  81. package/dist/registry/config.d.ts +0 -17
  82. package/dist/registry/config.js +0 -23
  83. package/dist/registry/index.d.ts +1 -1
  84. package/dist/registry/index.js +2 -2
  85. package/dist/registry/manager.d.ts +4 -21
  86. package/dist/registry/manager.js +21 -37
  87. package/dist/runtime-credential.d.ts +140 -0
  88. package/dist/runtime-credential.js +572 -0
  89. package/dist/runtime.d.ts +413 -0
  90. package/dist/runtime.js +825 -0
  91. package/dist/setup.d.ts +23 -47
  92. package/dist/setup.js +59 -116
  93. package/dist/skills.js +0 -7
  94. package/dist/status-cli.d.ts +27 -11
  95. package/dist/status-cli.js +121 -115
  96. package/dist/status-data.d.ts +116 -17
  97. package/dist/status-data.js +121 -38
  98. package/dist/subject.d.ts +126 -20
  99. package/dist/subject.js +215 -30
  100. package/dist/testing.d.ts +74 -38
  101. package/dist/testing.js +185 -68
  102. package/dist/tool-catalog.d.ts +53 -11
  103. package/dist/tool-catalog.js +164 -13
  104. package/dist/tool-metadata.d.ts +7 -6
  105. package/dist/tool-metadata.js +6 -5
  106. package/dist/types.d.ts +11 -1
  107. package/dist/uninstall.d.ts +71 -34
  108. package/dist/uninstall.js +217 -85
  109. package/dist/user-login.d.ts +9 -10
  110. package/dist/user-login.js +56 -75
  111. package/dist/watch-cli.d.ts +6 -0
  112. package/dist/watch-cli.js +217 -0
  113. package/package.json +4 -27
  114. package/assets/commands/dashboard.md +0 -34
  115. package/dist/dashboard-cli.d.ts +0 -8
  116. package/dist/dashboard-cli.js +0 -70
  117. package/dist/dev.d.ts +0 -103
  118. package/dist/dev.js +0 -583
  119. package/dist/interactive-setup.d.ts +0 -286
  120. package/dist/interactive-setup.js +0 -1734
  121. package/dist/local/jaeger-main.d.ts +0 -13
  122. package/dist/local/jaeger-main.js +0 -85
  123. package/dist/local/jaeger.d.ts +0 -50
  124. package/dist/local/jaeger.js +0 -162
  125. package/dist/otel/exporter.d.ts +0 -17
  126. package/dist/otel/exporter.js +0 -12
  127. package/dist/otel/index.d.ts +0 -2
  128. package/dist/otel/index.js +0 -8
  129. package/dist/otel/otlp.d.ts +0 -103
  130. package/dist/otel/otlp.js +0 -385
  131. package/dist/project-api-key.d.ts +0 -69
  132. package/dist/project-api-key.js +0 -147
  133. package/dist/setup-actions.d.ts +0 -232
  134. package/dist/setup-actions.js +0 -507
  135. package/dist/tracer.d.ts +0 -190
  136. package/dist/tracer.js +0 -481
  137. package/dist/watch-sandbox.d.ts +0 -9
  138. package/dist/watch-sandbox.js +0 -81
  139. package/dist/web/api.d.ts +0 -33
  140. package/dist/web/api.js +0 -294
  141. package/dist/web/launch.d.ts +0 -11
  142. package/dist/web/launch.js +0 -96
  143. package/dist/web/server.d.ts +0 -20
  144. package/dist/web/server.js +0 -233
  145. package/dist/web/types.d.ts +0 -65
  146. package/dist/web/types.js +0 -2
  147. package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
  148. package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
  149. package/dist/webapp/favicon.ico +0 -0
  150. package/dist/webapp/index.html +0 -15
package/dist/config.js CHANGED
@@ -33,19 +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;
41
+ exports.listInstalledHarnesses = listInstalledHarnesses;
42
+ exports.registerInstalledHarness = registerInstalledHarness;
43
+ exports.unregisterInstalledHarness = unregisterInstalledHarness;
40
44
  exports.saveConfig = saveConfig;
41
45
  exports.mutateConfig = mutateConfig;
42
46
  exports.looksLikeProjectId = looksLikeProjectId;
47
+ exports.isSecurityConnected = isSecurityConnected;
48
+ exports.describeSecurityGap = describeSecurityGap;
43
49
  exports.resolveConfig = resolveConfig;
44
50
  exports.configPromptMessage = configPromptMessage;
45
51
  const fs = __importStar(require("node:fs"));
46
52
  const os = __importStar(require("node:os"));
47
53
  const path = __importStar(require("node:path"));
48
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";
49
79
  /**
50
80
  * Single OS-agnostic data directory for *all* Ory agent plugin state:
51
81
  * the shared config file, persisted DCR credentials, and any harness
@@ -116,6 +146,7 @@ function loadConfig() {
116
146
  const parsed = JSON.parse(raw);
117
147
  return {
118
148
  projectUrl: typeof parsed.projectUrl === "string" ? parsed.projectUrl : undefined,
149
+ agentSecurityUrl: typeof parsed.agentSecurityUrl === "string" ? parsed.agentSecurityUrl : undefined,
119
150
  projectId: typeof parsed.projectId === "string" && parsed.projectId.trim()
120
151
  ? parsed.projectId.trim()
121
152
  : undefined,
@@ -125,16 +156,17 @@ function loadConfig() {
125
156
  workspaceName: typeof parsed.workspaceName === "string" && parsed.workspaceName.trim()
126
157
  ? parsed.workspaceName.trim()
127
158
  : undefined,
128
- apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
129
- apiKeyId: typeof parsed.apiKeyId === "string" && parsed.apiKeyId.trim()
130
- ? parsed.apiKeyId.trim()
131
- : undefined,
132
159
  oauth2ClientId: typeof parsed.oauth2ClientId === "string" ? parsed.oauth2ClientId : undefined,
133
- auditOnly: parsed.auditOnly === true ? true : undefined,
134
- permissionMode: parsePermissionMode(parsed.permissionMode),
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),
135
165
  userSubjectNamespace: typeof parsed.userSubjectNamespace === "string" && parsed.userSubjectNamespace.trim()
136
166
  ? parsed.userSubjectNamespace.trim()
137
167
  : undefined,
168
+ installedHarnesses: parseInstalledHarnesses(parsed.installedHarnesses),
169
+ delegationAnchors: parseDelegationAnchors(parsed.delegationAnchors),
138
170
  user: parseUserCredentials(parsed),
139
171
  agent: parseAgentCredentials(parsed),
140
172
  };
@@ -143,32 +175,195 @@ function loadConfig() {
143
175
  return {};
144
176
  }
145
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
+ }
146
195
  function parsePermissionMode(value) {
147
196
  return value === "observe" || value === "enforce" ? value : undefined;
148
197
  }
198
+ /**
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).
203
+ */
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;
238
+ }
149
239
  function parseAgentCredentials(parsed) {
150
240
  const raw = parsed.agent;
151
241
  if (!raw || typeof raw !== "object")
152
242
  return undefined;
153
243
  const block = raw;
154
- const dynamic = parseAgentDynamicCredentials(block.dynamic);
244
+ const retired = parseRetiredCredentials(block.retired);
245
+ const dynamic = parseAgentDynamicByHarness(block.dynamic);
155
246
  const subAgents = parseSubAgentCredentials(block.subAgents);
156
- if (!dynamic && !subAgents)
247
+ if (!dynamic && !subAgents && retired.length === 0)
157
248
  return undefined;
158
249
  return {
159
250
  ...(dynamic ? { dynamic } : {}),
160
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;
161
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;
162
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
+ */
163
333
  function parseSubAgentCredentials(value) {
164
334
  if (!value || typeof value !== "object")
165
335
  return undefined;
166
- const entries = value;
167
336
  const result = {};
168
- for (const [key, raw] of Object.entries(entries)) {
169
- const creds = parseAgentDynamicCredentials(raw);
170
- if (creds)
171
- 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
+ }
172
367
  }
173
368
  return Object.keys(result).length > 0 ? result : undefined;
174
369
  }
@@ -192,8 +387,33 @@ function parseAgentDynamicCredentials(value) {
192
387
  ...(parseDelegationRecord(v.delegation)
193
388
  ? { delegation: parseDelegationRecord(v.delegation) }
194
389
  : {}),
390
+ ...(parseDelegationNodeIds(v.delegationNodeIds, v.delegationNodeId)
391
+ ? { delegationNodeIds: parseDelegationNodeIds(v.delegationNodeIds, v.delegationNodeId) }
392
+ : {}),
195
393
  };
196
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
+ }
197
417
  function parseDelegationRecord(value) {
198
418
  if (!value || typeof value !== "object")
199
419
  return undefined;
@@ -211,6 +431,57 @@ function parseDelegationRecord(value) {
211
431
  delegatedAt: typeof v.delegatedAt === "string" ? v.delegatedAt : "",
212
432
  };
213
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
+ }
214
485
  function parseUserCredentials(parsed) {
215
486
  // Prefer the new nested shape; fall back to the legacy root-level
216
487
  // `oauth2` block so configs written before the user/agent split still
@@ -236,9 +507,6 @@ function parseOAuth2(value) {
236
507
  refreshToken: typeof v.refreshToken === "string" ? v.refreshToken : undefined,
237
508
  expiresAt: v.expiresAt,
238
509
  subject: typeof v.subject === "string" ? v.subject : undefined,
239
- clientId: typeof v.clientId === "string" ? v.clientId : undefined,
240
- idToken: typeof v.idToken === "string" ? v.idToken : undefined,
241
- scope: typeof v.scope === "string" ? v.scope : undefined,
242
510
  };
243
511
  }
244
512
  /**
@@ -265,7 +533,8 @@ function mutateConfig(mutator) {
265
533
  try {
266
534
  const existing = loadConfig();
267
535
  const next = mutator(existing);
268
- writeConfigAtomic(next);
536
+ if (next)
537
+ writeConfigAtomic(next);
269
538
  }
270
539
  finally {
271
540
  releaseLock(lock);
@@ -384,9 +653,101 @@ function releaseLock(lock) {
384
653
  function writeConfigAtomic(config) {
385
654
  const target = configFile();
386
655
  const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
387
- 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
+ });
388
659
  fs.renameSync(tmp, target);
389
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
+ }
390
751
  /** Matches an Ory project UUID (the value `ORY_PROJECT_ID` carries). */
391
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;
392
753
  /**
@@ -402,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]
402
763
  function looksLikeProjectId(value) {
403
764
  return PROJECT_ID_RE.test(value.trim());
404
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
+ }
405
807
  /**
406
808
  * Resolve config by checking environment variables first, then the config file.
407
809
  * Returns the merged result with the source of each value.
@@ -416,20 +818,17 @@ function resolveConfig() {
416
818
  // ORY_PROJECT_URL is the documented name; ORY_SDK_URL is the name the Ory
417
819
  // Console "Get Started" panel and the Ory SDKs use — accepted as an alias.
418
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;
419
822
  const envProjectId = process.env.ORY_PROJECT_ID?.trim() || undefined;
420
- // ORY_AGENT_API_KEY is the documented name; ORY_API_KEY is the deprecated
421
- // alias kept for back-compat (agent-auth emits a warning when only the
422
- // legacy form is set).
423
- const envApiKey = process.env.ORY_AGENT_API_KEY ?? process.env.ORY_API_KEY;
424
- const envClientId = process.env.ORY_OAUTH2_CLIENT_ID;
425
- const envMode = parsePermissionMode(process.env.ORY_PERMISSION_MODE);
823
+ const envClientId = process.env.ORY_OAUTH2_CLIENT_ID?.trim() || undefined;
426
824
  const envUserSubjectNamespace = process.env.ORY_USER_SUBJECT_NAMESPACE?.trim() || undefined;
427
- const permissionMode = envMode ?? file.permissionMode ?? "observe";
428
- const permissionModeSource = envMode
429
- ? "env"
430
- : file.permissionMode
431
- ? "config"
432
- : "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";
433
832
  const projectId = envProjectId ?? file.projectId;
434
833
  // The SDK URL is only ever an explicit, slug-based URL (from ORY_PROJECT_URL,
435
834
  // ORY_SDK_URL, or the config file). The project id is NOT a URL source — it
@@ -440,16 +839,33 @@ function resolveConfig() {
440
839
  : file.projectUrl
441
840
  ? "config"
442
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;
443
856
  return {
444
857
  projectUrl,
858
+ agentSecurityUrl,
445
859
  projectId,
446
860
  projectName: file.projectName,
447
861
  workspaceName: file.workspaceName,
448
- apiKey: envApiKey ?? file.apiKey,
449
- oauth2ClientId: envClientId ?? file.oauth2ClientId,
450
- auditOnly: file.auditOnly === true,
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),
451
866
  permissionMode,
452
867
  permissionModeSource,
868
+ permissionModeCache: file.permissionModeCache,
453
869
  userSubjectNamespace: envUserSubjectNamespace ?? file.userSubjectNamespace,
454
870
  userSubjectNamespaceSource: envUserSubjectNamespace
455
871
  ? "env"
@@ -457,22 +873,25 @@ function resolveConfig() {
457
873
  ? "config"
458
874
  : "none",
459
875
  projectUrlSource,
876
+ agentSecurityUrlSource,
460
877
  projectIdSource: envProjectId ? "env" : file.projectId ? "config" : "none",
461
- apiKeySource: envApiKey ? "env" : file.apiKey ? "config" : "none",
462
- oauth2ClientIdSource: envClientId ? "env" : file.oauth2ClientId ? "config" : "none",
878
+ oauth2ClientIdSource: envClientId ? "env" : fileClientId ? "config" : "default",
463
879
  };
464
880
  }
465
881
  /**
466
- * 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.
467
887
  */
468
888
  function configPromptMessage(binName) {
469
- return ("The Ory agent plugin is installed but not yet configured.\n" +
470
- "\n" +
471
- " Option 1 Connect to Ory (enables authentication and permission checks):\n" +
472
- ` ${(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" +
473
892
  "\n" +
474
- " Option 2 Continue without authentication (audit logging only):\n" +
475
- ` ${(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` +
476
895
  "\n" +
477
896
  `Configuration is saved to ${configFile()} and shared across all agent plugins.`);
478
897
  }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeCredential } from "./runtime-credential.js";
2
+ export interface SessionContext {
3
+ sessionId?: string;
4
+ runtimeCredential?: RuntimeCredential;
5
+ }
6
+ export declare function currentSessionContext(): Readonly<SessionContext>;
7
+ export declare function withSessionContext<T>(context: SessionContext, work: () => T): T;
8
+ /** Test and low-level integration helper. Prefer `withHookContext` in adapters. */
9
+ export declare function setSessionContext(context: SessionContext): void;
10
+ export declare function clearSessionContext(): void;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.currentSessionContext = currentSessionContext;
4
+ exports.withSessionContext = withSessionContext;
5
+ exports.setSessionContext = setSessionContext;
6
+ exports.clearSessionContext = clearSessionContext;
7
+ const node_async_hooks_1 = require("node:async_hooks");
8
+ const sessionStorage = new node_async_hooks_1.AsyncLocalStorage();
9
+ function currentSessionContext() {
10
+ return sessionStorage.getStore() ?? {};
11
+ }
12
+ function withSessionContext(context, work) {
13
+ return sessionStorage.run({ ...currentSessionContext(), ...context }, work);
14
+ }
15
+ /** Test and low-level integration helper. Prefer `withHookContext` in adapters. */
16
+ function setSessionContext(context) {
17
+ sessionStorage.enterWith({ ...context });
18
+ }
19
+ function clearSessionContext() {
20
+ sessionStorage.enterWith({});
21
+ }