@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/adapters.js CHANGED
@@ -3,15 +3,18 @@
3
3
  * Shared adapter primitives for Agent SDK integrations (and harness plugins).
4
4
  *
5
5
  * These extract the session-start sequencing and the `tool.invoke` / `tool.block` /
6
- * `tool.complete` span boilerplate that each integration would otherwise re-implement, so
6
+ * `tool.complete` activity boilerplate that each integration would otherwise re-implement, so
7
7
  * an SDK binding is a thin translation between the SDK's hook signature and these calls.
8
8
  * They are the TypeScript twin of `ory_argus.adapters` in the Python core.
9
9
  *
10
+ * - {@link withHookContext} — scope session state to one lifecycle invocation.
10
11
  * - {@link sessionStart} — run the user + agent auth gates and write the user→agent
11
12
  * delegation tuple; returns whether the session may proceed.
12
- * - {@link gate} — resolve the subject, run {@link gateToolCall}, record the spans, and
13
+ * - {@link createSessionStarter} — coalesce concurrent lazy SDK session starts.
14
+ * - {@link decideTool} — normalize native/MCP policy without execution activity.
15
+ * - {@link gate} — resolve the subject, run {@link gateToolCall}, record activity, and
13
16
  * return a normalized {@link GateResult}.
14
- * - {@link complete} — record the `tool.complete` span.
17
+ * - {@link complete} / {@link fail} — record safe terminal tool activity.
15
18
  * - {@link registerSubagent} — resolve a sub-agent identity + agent→subagent tuple.
16
19
  * - {@link wrapTool} — wrap a plain `execute(args)` with gate+complete, for SDKs whose
17
20
  * only veto point is the tool boundary (Vercel AI SDK, etc.).
@@ -20,41 +23,135 @@
20
23
  */
21
24
  Object.defineProperty(exports, "__esModule", { value: true });
22
25
  exports.resolveNamespace = resolveNamespace;
26
+ exports.withHookContext = withHookContext;
23
27
  exports.sessionStart = sessionStart;
28
+ exports.createSessionStarter = createSessionStarter;
24
29
  exports.writeUserDelegatesAgent = writeUserDelegatesAgent;
25
30
  exports.registerSubagent = registerSubagent;
26
31
  exports.writeAgentDelegatesSubagent = writeAgentDelegatesSubagent;
32
+ exports.decideTool = decideTool;
27
33
  exports.gate = gate;
28
34
  exports.complete = complete;
35
+ exports.fail = fail;
29
36
  exports.wrapTool = wrapTool;
37
+ const node_os_1 = require("node:os");
30
38
  const config_js_1 = require("./config.js");
31
39
  const agent_auth_js_1 = require("./agent-auth.js");
32
40
  const user_login_js_1 = require("./user-login.js");
33
41
  const permissions_js_1 = require("./permissions.js");
42
+ const permission_mode_js_1 = require("./permission-mode.js");
43
+ const tool_catalog_js_1 = require("./tool-catalog.js");
34
44
  const subject_js_1 = require("./subject.js");
35
45
  const denial_js_1 = require("./denial.js");
46
+ const mcp_js_1 = require("./mcp.js");
47
+ const tool_metadata_js_1 = require("./tool-metadata.js");
36
48
  function resolveNamespace() {
37
- return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
49
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTool";
50
+ }
51
+ /** Scope ambient session state to one lifecycle invocation. */
52
+ function withHookContext(client, opts, work) {
53
+ return client.withRuntimeContext(opts, work);
54
+ }
55
+ /** True when a thrown value is a classified {@link OryError} rather than an `Error`. */
56
+ function isOryError(err) {
57
+ return (typeof err === "object" &&
58
+ err !== null &&
59
+ typeof err.code === "string" &&
60
+ typeof err.message === "string");
38
61
  }
39
62
  /**
40
- * Run both auth gates and write the user→agent delegation tuple. `allowBlock` is advisory
41
- * for in-process SDK integrations (the agent can't be prevented from constructing), but the
42
- * user gate still runs, refreshes tokens, and records the `user.auth` span.
63
+ * Describe a thrown value as structured log fields.
64
+ *
65
+ * The best-effort paths below swallow their failures, so the debug log is the
66
+ * only place the cause survives — it has to actually say something. Two shapes
67
+ * reach them: a real `Error`, and the **plain object** `classifyError` produces
68
+ * (`{ code, status, message, requestId }` — not an `Error` instance). A bare
69
+ * `String(err)` renders that second shape as `"[object Object]"`, which is how a
70
+ * broker returning HTTP 500 showed up as a log line naming no cause at all.
71
+ *
72
+ * Fields are returned separately rather than concatenated because
73
+ * {@link DebugLogger} emits JSON — `code` and `status` are worth grepping for.
74
+ */
75
+ function describeThrown(err) {
76
+ if (isOryError(err)) {
77
+ return {
78
+ message: err.message,
79
+ code: err.code,
80
+ ...(err.status !== undefined ? { status: err.status } : {}),
81
+ ...(err.requestId ? { requestId: err.requestId } : {}),
82
+ };
83
+ }
84
+ if (err instanceof Error)
85
+ return { message: err.message };
86
+ if (typeof err === "string")
87
+ return { message: err };
88
+ // Anything else: serialize rather than let it stringify to "[object Object]".
89
+ try {
90
+ return { message: JSON.stringify(err) ?? String(err) };
91
+ }
92
+ catch {
93
+ return { message: String(err) };
94
+ }
95
+ }
96
+ /**
97
+ * Run both auth gates and write the user→agent delegation tuple. The user gate
98
+ * runs every session to establish/refresh the user identity, records the
99
+ * `user.auth` activity event, and never blocks — enforcement is governed by
100
+ * `permissionMode` at tool-call time. `SessionStartResult.proceed` is therefore
101
+ * always `true`; it is retained only for a stable adapter shape.
43
102
  */
44
103
  async function sessionStart(client, opts) {
45
104
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
46
105
  const userLogin = opts.userLogin ?? user_login_js_1.ensureUserAuthenticated;
47
106
  const agentGate = opts.agentGate ?? agent_auth_js_1.ensureAgentIdentity;
48
- const decision = await userLogin(client, {
49
- binName: opts.binName ?? `ory-${opts.harness}`,
50
- harness: opts.harness,
51
- allowBlock: opts.allowBlock ?? false,
52
- });
53
- const creds = await agentGate(client, {
54
- projectUrl,
55
- harness: opts.harness,
107
+ let decision = {
108
+ proceed: true,
109
+ mode: "skipped",
110
+ reason: "User authentication failed open",
111
+ };
112
+ try {
113
+ decision = await userLogin(client, {
114
+ binName: opts.binName ?? `ory-${opts.harness}`,
115
+ harness: opts.harness,
116
+ });
117
+ }
118
+ catch (err) {
119
+ client.logger.warn("session_start.user_auth.failed", describeThrown(err));
120
+ }
121
+ let creds = {
122
+ kind: "none",
123
+ reason: "Agent authentication failed open",
124
+ warnings: [],
125
+ };
126
+ try {
127
+ creds = await agentGate(client, {
128
+ projectUrl,
129
+ harness: opts.harness,
130
+ sessionId: opts.sessionId ?? client.sessionId,
131
+ });
132
+ }
133
+ catch (err) {
134
+ client.logger.warn("session_start.agent_auth.failed", describeThrown(err));
135
+ }
136
+ await writeUserDelegatesAgent(client, opts.harness, opts.sessionId);
137
+ // Warm the permission-mode cache from the server once per session, so the
138
+ // synchronous `resolveConfig().permissionMode` read (the MCP
139
+ // `applyPermissionMode` path, display surfaces) reflects the current project
140
+ // posture even before the first `gateToolCall`. Best-effort; never blocks —
141
+ // a no-op when Agent Security isn't connected (no checks) and on any Keto error.
142
+ if ((0, config_js_1.isSecurityConnected)()) {
143
+ const subject = (0, subject_js_1.resolveUserSubject)(client);
144
+ const agentSubject = (0, subject_js_1.resolveAgentSubject)(client);
145
+ await (0, permission_mode_js_1.warmPermissionModeCache)(client, subject, {
146
+ additionalSubjects: agentSubject ? [{ scope: "agent", subject: agentSubject }] : [],
147
+ }).catch(() => {
148
+ /* best-effort cache warm; gate-time resolution retries */
149
+ });
150
+ }
151
+ client.logger.activity("session.start", "ok", {
152
+ attributes: { harness: opts.harness, ...(opts.activityAttributes ?? {}) },
153
+ sessionId: opts.sessionId,
56
154
  });
57
- await writeUserDelegatesAgent(client);
58
155
  return {
59
156
  proceed: decision.proceed,
60
157
  userMode: decision.mode,
@@ -62,62 +159,98 @@ async function sessionStart(client, opts) {
62
159
  agentKind: creds.kind,
63
160
  };
64
161
  }
162
+ /** Build a concurrency-safe, retryable lazy session-start trigger. */
163
+ function createSessionStarter(client, opts) {
164
+ const pending = new Map();
165
+ return () => {
166
+ const sessionId = opts.sessionId ?? client.sessionId;
167
+ const key = sessionId?.trim() || "sessionless";
168
+ const existing = pending.get(key);
169
+ if (existing)
170
+ return existing;
171
+ const started = sessionStart(client, { ...opts, sessionId }).then(() => undefined, (err) => {
172
+ pending.delete(key);
173
+ client.logger.warn("session_start.failed", describeThrown(err));
174
+ });
175
+ pending.set(key, started);
176
+ return started;
177
+ };
178
+ }
179
+ /** Strip a `<ns>:` prefix from a namespaced subject, if present. */
180
+ function unqualify(subject, ns) {
181
+ const prefix = `${ns}:`;
182
+ return subject.startsWith(prefix) ? subject.slice(prefix.length) : undefined;
183
+ }
65
184
  /**
66
- * Write a delegation tuple at most once. The tuple is stable for a given
67
- * (namespace, object, subject), and Keto's create API is not idempotent, so
68
- * this goes through {@link OryAgentClient.ensureRelationship}: it reads the
69
- * exact tuple first and only writes when absent no duplicate on repeated
70
- * session starts, and self-healing if the tuple was deleted. If the existence
71
- * probe fails we skip the write rather than risk a duplicate (delegation
72
- * tuples are audit-only, so a missed write is harmless and retried next
73
- * session). Best-effort: any failure is logged and swallowed.
185
+ * Resolve one end of a delegation edge, preferring the live principal but
186
+ * falling back to the persisted delegation anchor (`delegation.delegatedBy`
187
+ * on the DCR credentials in local config). This is what lets the edge be
188
+ * recorded from an authoritative source rather than from nothing when the
189
+ * principal isn't populated for the session. A mismatch between the two is
190
+ * logged for reconciliation visibility; the live principal wins.
74
191
  */
75
- async function writeDelegationOnce(client, check, options) {
76
- try {
77
- const res = await client.ensureRelationship(check, {
78
- spanAttributes: options.spanAttributes,
79
- });
80
- if (res.probeError) {
81
- client.logger.debug("delegation.probe_failed", {
82
- ...options.failedContext,
83
- message: res.probeError.message,
84
- });
85
- }
86
- }
87
- catch (err) {
88
- client.logger.warn(options.failedEvent, {
89
- ...options.failedContext,
90
- message: err instanceof Error ? err.message : String(err),
91
- });
192
+ function reconcileEdgeEnd(client, principal, anchor, role) {
193
+ if (principal && anchor && principal !== anchor) {
194
+ client.logger.debug("delegation.anchor_mismatch", { role, principal, anchor });
92
195
  }
196
+ return principal ?? anchor;
93
197
  }
94
198
  /**
95
- * Write the `user → agent` delegation tuple, at most once per install
96
- * (see {@link writeDelegationOnce}). Exported so harness plugins that run
97
- * their own session-start sequence (rather than {@link sessionStart}) share
98
- * the same write-once semantics instead of re-issuing the write every
99
- * session. No-op until both principals are populated.
199
+ * Record the `user → agent` delegation edge through the agent-security broker
200
+ * (see {@link OryAgentClient.recordDelegation}). Exported so harness plugins
201
+ * that run their own session-start sequence (rather than {@link sessionStart})
202
+ * record the edge the same way.
203
+ *
204
+ * The plugin supplies only the delegating user identity (resolved from the live
205
+ * principal, else the persisted delegation anchor) plus the harness and host;
206
+ * the broker owns the Keto tuple and its join-key encoding. The node id the
207
+ * broker returns (`agent:<join>`) is persisted so a later sub-agent
208
+ * registration — which may run in a separate subprocess with no live principal
209
+ * — can reference it verbatim as `delegated_by`. Best-effort and fail-open:
210
+ * any failure (including a 404 against the local stack, which has no broker) is
211
+ * logged and swallowed. No-op when Agent Security isn't connected (there is no
212
+ * broker to record against) and when no user identity resolves.
100
213
  */
101
- async function writeUserDelegatesAgent(client) {
102
- const user = client.userPrincipal.subject;
103
- const agent = client.agentPrincipal.subject;
104
- if (!user || !agent) {
214
+ async function writeUserDelegatesAgent(client, harness, sessionId) {
215
+ if (!(0, config_js_1.isSecurityConnected)())
216
+ return;
217
+ // Resolve the delegating user (the anchor of the edge). Prefer the live
218
+ // principal; fall back to the delegator persisted in the agent client's
219
+ // delegation anchor so the edge can still be recorded in a session where the
220
+ // user principal wasn't populated. The anchor is per-harness — reading a
221
+ // different harness's anchor would attribute this harness's delegation to
222
+ // another agent's client.
223
+ const harnessKey = harness ?? client.harness;
224
+ const sessionKey = (sessionId ?? client.sessionId)?.trim() || config_js_1.SESSIONLESS_KEY;
225
+ const anchorCreds = (0, agent_auth_js_1.loadAgentDynamicCredentials)(harnessKey, sessionKey);
226
+ const anchor = anchorCreds?.delegation;
227
+ const anchorUser = anchor?.delegationType === "user-to-agent" ? unqualify(anchor.delegatedBy, "user") : undefined;
228
+ const identityId = reconcileEdgeEnd(client, client.userPrincipalForSession(sessionKey).subject, anchorUser, "user");
229
+ if (!identityId) {
105
230
  client.logger.debug("delegation.skip", {
106
- reason: "missing principal",
107
- hasUser: !!user,
108
- hasAgent: !!agent,
231
+ reason: "no user delegation edge (no principal and no anchor)",
109
232
  });
110
233
  return;
111
234
  }
112
- await writeDelegationOnce(client, {
113
- namespace: resolveNamespace(),
114
- object: `agent:${agent}`,
115
- relation: "delegate",
116
- subjectId: `user:${user}`,
117
- }, {
118
- spanAttributes: { delegation: "user-to-agent" },
119
- failedEvent: "delegation.user_to_agent.failed",
120
- });
235
+ // The broker derives the agent node from (identity, harness, host, session)
236
+ // and writes `AgentTool:agent:<join>#delegate@(User:<id>)`. Persist the
237
+ // returned node id under *this session* so the sub-agent edge can reference it
238
+ // without re-deriving the join key.
239
+ try {
240
+ const res = await client.recordDelegation({
241
+ kind: "agent",
242
+ identityId,
243
+ host: (0, node_os_1.hostname)(),
244
+ ...(sessionKey !== config_js_1.SESSIONLESS_KEY ? { sessionId: sessionKey } : {}),
245
+ }, {
246
+ activityAttributes: { delegation: "user-to-agent" },
247
+ });
248
+ if (res.nodeId)
249
+ (0, agent_auth_js_1.saveDelegationNodeId)(harnessKey, sessionKey, res.nodeId);
250
+ }
251
+ catch (err) {
252
+ client.logger.warn("delegation.user_to_agent.failed", describeThrown(err));
253
+ }
121
254
  }
122
255
  async function registerSubagent(client, opts) {
123
256
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
@@ -128,121 +261,306 @@ async function registerSubagent(client, opts) {
128
261
  subAgentType: opts.subAgentType,
129
262
  projectUrl,
130
263
  harness: opts.harness,
264
+ sessionId: opts.sessionId ?? client.sessionId,
265
+ perSpawnId: opts.perSpawnId,
266
+ signal: opts.signal,
131
267
  });
132
268
  }
133
269
  catch (err) {
134
270
  client.logger.warn("subagent.identity.failed", {
135
271
  subAgentType: opts.subAgentType,
136
- message: err instanceof Error ? err.message : String(err),
272
+ ...describeThrown(err),
137
273
  });
138
274
  return;
139
275
  }
140
- if (identity.kind !== "dynamic" || !identity.subject)
276
+ if (identity.kind !== "runtime" && identity.kind !== "dynamic")
141
277
  return;
142
- await writeAgentDelegatesSubagent(client, identity.subject, opts.subAgentType);
278
+ await writeAgentDelegatesSubagent(client, identity.subject ?? "", opts.subAgentType, {
279
+ harness: opts.harness,
280
+ sessionId: opts.sessionId,
281
+ perSpawnId: opts.perSpawnId,
282
+ signal: opts.signal,
283
+ runtimeCredential: identity.runtimeCredential,
284
+ agentToken: identity.token,
285
+ });
143
286
  }
144
287
  /**
145
- * Write the `agent → subagent` delegation tuple, at most once per install
146
- * (see {@link writeDelegationOnce}). Exported so harness plugins that
147
- * resolve the sub-agent identity through their own event plumbing (rather
148
- * than {@link registerSubagent}) still share the write-once semantics.
149
- * No-op until the agent principal is populated.
288
+ * Record the `agent → subagent` delegation edge through the agent-security
289
+ * broker. Exported so harness plugins that resolve the sub-agent identity
290
+ * through their own event plumbing (rather than {@link registerSubagent})
291
+ * record the edge the same way.
292
+ *
293
+ * The broker builds the sub-agent node from the parent agent's delegation node
294
+ * (`delegated_by`) and the sub-agent type — so the plugin passes the node id
295
+ * the broker assigned to the `user → agent` edge (persisted at session start),
296
+ * echoed verbatim, and never reconstructs the join-key encoding itself.
297
+ * Best-effort and fail-open. No-op when Agent Security isn't connected and until the
298
+ * `user → agent` edge has been
299
+ * recorded (so the parent node id is available).
150
300
  */
151
- async function writeAgentDelegatesSubagent(client, subAgentSubject, subAgentType) {
152
- const agent = client.agentPrincipal.subject;
153
- if (!agent) {
301
+ async function writeAgentDelegatesSubagent(client, subAgentSubject, subAgentType, opts = {}) {
302
+ if (!(0, config_js_1.isSecurityConnected)())
303
+ return;
304
+ // The parent agent's delegation node (`agent:<join>`) was assigned by the
305
+ // broker when the user→agent edge was recorded and persisted at session start.
306
+ // Echo it verbatim as `delegated_by`. It is read for *this harness and this
307
+ // session*: the parent of this sub-agent is this run's session credential, so
308
+ // the harness alone would not identify it.
309
+ const sessionKey = (opts.sessionId ?? client.sessionId)?.trim() || config_js_1.SESSIONLESS_KEY;
310
+ const delegatedBy = (0, agent_auth_js_1.loadDelegationNodeId)(opts.harness ?? client.harness, sessionKey);
311
+ if (!delegatedBy) {
154
312
  client.logger.debug("delegation.skip", {
155
- reason: "agent principal not populated",
313
+ reason: "no persisted agent delegation node (user→agent edge not recorded yet)",
156
314
  subAgentType,
315
+ sessionKey,
157
316
  });
158
317
  return;
159
318
  }
160
- await writeDelegationOnce(client, {
161
- namespace: resolveNamespace(),
162
- object: `subagent:${subAgentSubject}`,
163
- relation: "delegate",
164
- subjectId: `agent:${agent}`,
165
- }, {
166
- spanAttributes: { delegation: "agent-to-subagent", subAgentType },
167
- failedEvent: "delegation.agent_to_subagent.failed",
168
- failedContext: { subAgentType },
169
- });
319
+ try {
320
+ await client.recordDelegation({
321
+ kind: "subagent",
322
+ subAgentType,
323
+ delegatedBy,
324
+ ...(sessionKey !== config_js_1.SESSIONLESS_KEY ? { sessionId: sessionKey } : {}),
325
+ ...(opts.perSpawnId ? { perSpawnId: opts.perSpawnId } : {}),
326
+ }, {
327
+ signal: opts.signal,
328
+ agentToken: opts.agentToken,
329
+ runtimeCredential: opts.runtimeCredential,
330
+ activityAttributes: {
331
+ delegation: "agent-to-subagent",
332
+ subAgentType,
333
+ ...(subAgentSubject ? { subAgentSubject } : {}),
334
+ ...(opts.perSpawnId ? { perSpawnId: opts.perSpawnId } : {}),
335
+ },
336
+ });
337
+ }
338
+ catch (err) {
339
+ client.logger.warn("delegation.agent_to_subagent.failed", {
340
+ subAgentType,
341
+ ...describeThrown(err),
342
+ });
343
+ }
344
+ }
345
+ function gateActivityAttributes(opts) {
346
+ return {
347
+ toolName: opts.toolName,
348
+ ...(0, tool_metadata_js_1.summarizeToolInput)(opts.toolName, opts.toolArgs),
349
+ ...(opts.mcpTool
350
+ ? {
351
+ mcpServer: opts.mcpTool.serverName,
352
+ ...(opts.mcpTool.toolName ? { mcpTool: opts.mcpTool.toolName } : {}),
353
+ }
354
+ : {}),
355
+ ...(opts.extraActivityAttributes ?? {}),
356
+ };
170
357
  }
171
358
  /**
172
- * Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
173
- * Records `tool.invoke` on allow/observe and `tool.block` on deny/observe — identical span
174
- * semantics to the harness plugins.
359
+ * Authorize a tool call and return a normalized {@link GateResult} without
360
+ * recording execution activity. Permission-ask hooks use this when the harness
361
+ * separately reports actual execution; {@link gate} adds the standard events.
175
362
  */
176
- async function gate(client, opts) {
363
+ async function decideTool(client, opts) {
177
364
  const namespace = resolveNamespace();
178
365
  const subjectRef = (0, subject_js_1.resolveUserSubject)(client, opts.subjectFallback);
179
366
  const label = (0, subject_js_1.subjectLabel)(subjectRef);
180
367
  const canBlock = opts.canBlock ?? true;
181
- const spanAttrs = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
182
- const check = { namespace, object: opts.toolName, relation: "use", ...subjectRef };
183
- // Audit-only kill switch: Ory is disabled entirely — no permission check.
184
- // Record only the audit `tool.invoke` span (same span semantics as the
185
- // harness plugins' audit-only short-circuit) and pass the tool through.
186
- const resolved = (0, config_js_1.resolveConfig)();
187
- if (resolved.auditOnly) {
188
- client.tracer.record("tool.invoke", "ok", { attributes: spanAttrs });
189
- const decision = {
190
- kind: "allow",
191
- result: { allowed: true, checkedAt: new Date().toISOString(), check },
192
- mode: resolved.permissionMode,
193
- spanAttributes: { permissionMode: resolved.permissionMode },
368
+ const activityAttrs = gateActivityAttributes(opts);
369
+ const check = { namespace, object: opts.toolName, relation: "users", ...subjectRef };
370
+ if (!(0, config_js_1.isSecurityConnected)()) {
371
+ const outcome = {
372
+ kind: "not_connected",
373
+ activityAttributes: { securityConnected: false, toolName: opts.toolName },
374
+ };
375
+ return {
376
+ proceed: true,
377
+ blocked: false,
378
+ kind: outcome.kind,
379
+ decision: outcome,
380
+ subject: label,
381
+ namespace,
194
382
  };
195
- return { proceed: true, blocked: false, kind: "allow", decision, subject: label, namespace };
196
383
  }
197
- const outcome = await (0, permissions_js_1.gateToolCall)(client, {
198
- harness: opts.harness,
199
- toolName: opts.toolName,
200
- check,
201
- spanAttributes: spanAttrs,
202
- });
203
- if (outcome.kind === "audit_only") {
204
- // Kill switch: Ory is disabled. Audit the invocation, skip the check.
205
- client.tracer.record("tool.invoke", "ok", {
206
- attributes: { ...spanAttrs, auditOnly: true },
207
- });
208
- return { proceed: true, blocked: false, kind: "audit_only", decision: outcome, subject: label, namespace };
384
+ let outcome;
385
+ try {
386
+ if (opts.mcpTool) {
387
+ const mcpResult = await (0, mcp_js_1.checkMcpPermission)(client, opts.mcpTool, {
388
+ subject: subjectRef,
389
+ activityAttributes: activityAttrs,
390
+ });
391
+ const agentSubject = (0, subject_js_1.resolveAgentSubject)(client);
392
+ const subagentSubject = (0, subject_js_1.resolveSubAgentSubject)(opts.principals?.subAgentClientId);
393
+ const mode = await (0, permission_mode_js_1.warmPermissionModeCache)(client, subjectRef, {
394
+ additionalSubjects: [
395
+ ...(agentSubject ? [{ scope: "agent", subject: agentSubject }] : []),
396
+ ...(subagentSubject ? [{ scope: "subagent", subject: subagentSubject }] : []),
397
+ ],
398
+ });
399
+ outcome = (0, permissions_js_1.applyPermissionMode)(client, mcpResult.allowed, {
400
+ namespace: process.env.ORY_MCP_SERVER_NAMESPACE ?? "mcp_servers",
401
+ object: opts.mcpTool.serverName,
402
+ relation: "users",
403
+ ...subjectRef,
404
+ activityAttributes: {
405
+ ...activityAttrs,
406
+ mcpServer: opts.mcpTool.serverName,
407
+ ...(opts.mcpTool.toolName ? { mcpTool: opts.mcpTool.toolName } : {}),
408
+ },
409
+ modeOverride: mode.mode,
410
+ });
411
+ }
412
+ else {
413
+ outcome = await (0, permissions_js_1.gateToolCall)(client, {
414
+ harness: opts.harness,
415
+ toolName: opts.toolName,
416
+ check,
417
+ activityAttributes: activityAttrs,
418
+ shellCommand: (0, tool_catalog_js_1.extractShellCommand)(opts.harness, opts.toolName, opts.toolArgs),
419
+ principals: opts.principals,
420
+ });
421
+ }
422
+ }
423
+ catch (err) {
424
+ const error = client.classifyError(err);
425
+ client.logger.warn("permission.fail_open", { tool: opts.toolName, ...describeThrown(error) });
426
+ const outcome = {
427
+ kind: "fail_open",
428
+ error,
429
+ mode: (0, config_js_1.resolveConfig)().permissionMode,
430
+ activityAttributes: { permissionMode: (0, config_js_1.resolveConfig)().permissionMode },
431
+ };
432
+ return {
433
+ proceed: true,
434
+ blocked: false,
435
+ kind: outcome.kind,
436
+ decision: outcome,
437
+ subject: label,
438
+ namespace,
439
+ };
440
+ }
441
+ if (outcome.kind === "not_connected") {
442
+ return {
443
+ proceed: true,
444
+ blocked: false,
445
+ kind: "not_connected",
446
+ decision: outcome,
447
+ subject: label,
448
+ namespace,
449
+ };
209
450
  }
210
451
  if (outcome.kind === "interactive") {
211
452
  return { proceed: true, blocked: false, kind: "interactive", decision: outcome, subject: label, namespace };
212
453
  }
213
- const decisionAttrs = outcome.spanAttributes;
454
+ const decisionAttrs = outcome.activityAttributes;
214
455
  if (outcome.kind === "fail_open") {
215
456
  client.logger.warn("permission.fail_open", { tool: opts.toolName, code: outcome.error.code });
216
457
  return { proceed: true, blocked: false, kind: "fail_open", decision: outcome, subject: label, namespace };
217
458
  }
218
459
  if (outcome.kind === "allow") {
219
- client.tracer.record("tool.invoke", "ok", {
220
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: true },
221
- });
222
460
  return { proceed: true, blocked: false, kind: "allow", decision: outcome, subject: label, namespace };
223
461
  }
224
462
  if (outcome.kind === "observe") {
225
- client.tracer.record("tool.block", "denied", {
226
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(false) },
227
- });
228
- client.tracer.record("tool.invoke", "ok", {
229
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, observed: true },
230
- });
231
463
  return { proceed: true, blocked: false, kind: "observe", decision: outcome, subject: label, namespace };
232
464
  }
233
- // deny
234
- client.tracer.record("tool.block", "denied", {
235
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(canBlock) },
236
- });
237
- const denialMessage = (0, denial_js_1.formatDenialMessage)({ tool: opts.toolName, subjectId: label, namespace });
465
+ const denialContext = {
466
+ tool: opts.toolName,
467
+ subjectId: label,
468
+ namespace,
469
+ ...(opts.mcpTool ? { mcp: opts.mcpTool } : {}),
470
+ ...(0, denial_js_1.shellDenialFields)(outcome),
471
+ };
472
+ const denialMessage = (0, denial_js_1.formatDenialMessage)(denialContext);
238
473
  client.logger.warn("tool.denied", { tool: opts.toolName, subjectId: label });
239
- return { proceed: false, blocked: canBlock, kind: "deny", decision: outcome, subject: label, namespace, denialMessage };
474
+ return {
475
+ proceed: false,
476
+ blocked: canBlock,
477
+ kind: "deny",
478
+ decision: outcome,
479
+ subject: label,
480
+ namespace,
481
+ denialMessage,
482
+ denialContext,
483
+ };
484
+ }
485
+ async function gate(client, opts) {
486
+ const result = await decideTool(client, opts);
487
+ const activityAttrs = gateActivityAttributes(opts);
488
+ const decisionAttrs = result.decision.activityAttributes;
489
+ if (result.kind === "not_connected") {
490
+ client.logger.activity("tool.invoke", "ok", {
491
+ attributes: { ...activityAttrs, securityConnected: false, decision: result.kind },
492
+ });
493
+ }
494
+ else if (result.kind === "allow") {
495
+ client.logger.activity("tool.invoke", "ok", {
496
+ attributes: { ...activityAttrs, ...decisionAttrs, allowed: true, decision: result.kind },
497
+ });
498
+ }
499
+ else if (result.kind === "observe") {
500
+ client.logger.activity("tool.block", "denied", {
501
+ attributes: {
502
+ ...activityAttrs,
503
+ ...decisionAttrs,
504
+ allowed: false,
505
+ observed: true,
506
+ decision: result.kind,
507
+ ...(0, denial_js_1.alertAttributes)(false),
508
+ },
509
+ });
510
+ client.logger.activity("tool.invoke", "ok", {
511
+ attributes: {
512
+ ...activityAttrs,
513
+ ...decisionAttrs,
514
+ allowed: false,
515
+ observed: true,
516
+ decision: result.kind,
517
+ },
518
+ });
519
+ }
520
+ else if (result.kind === "deny") {
521
+ client.logger.activity("tool.block", "denied", {
522
+ attributes: {
523
+ ...activityAttrs,
524
+ ...decisionAttrs,
525
+ allowed: false,
526
+ decision: result.kind,
527
+ ...(0, denial_js_1.alertAttributes)(opts.canBlock ?? true),
528
+ },
529
+ });
530
+ }
531
+ else if (result.kind === "interactive" || result.kind === "fail_open") {
532
+ client.logger.activity("tool.invoke", "ok", {
533
+ attributes: { ...activityAttrs, ...decisionAttrs, decision: result.kind },
534
+ });
535
+ }
536
+ return result;
240
537
  }
241
538
  function complete(client, opts) {
242
- const attributes = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
539
+ const attributes = {
540
+ toolName: opts.toolName,
541
+ ...(0, tool_metadata_js_1.summarizeToolInput)(opts.toolName, opts.input),
542
+ ...(0, tool_metadata_js_1.summarizeToolOutput)(opts.toolName, opts.output),
543
+ ...(opts.extraActivityAttributes ?? {}),
544
+ };
243
545
  if (opts.output !== undefined)
244
546
  attributes.hasOutput = true;
245
- client.tracer.record("tool.complete", opts.status ?? "ok", { attributes });
547
+ client.logger.activity("tool.complete", opts.status ?? "ok", { attributes });
548
+ }
549
+ function fail(client, opts) {
550
+ if (opts.error !== undefined) {
551
+ client.logger.debug("tool.failure.detail", {
552
+ toolName: opts.toolName,
553
+ ...describeThrown(opts.error),
554
+ });
555
+ }
556
+ client.logger.activity("tool.fail", "error", {
557
+ attributes: {
558
+ toolName: opts.toolName,
559
+ ...(0, tool_metadata_js_1.summarizeToolInput)(opts.toolName, opts.input),
560
+ ...(opts.error !== undefined ? { hasError: true } : {}),
561
+ ...(opts.extraActivityAttributes ?? {}),
562
+ },
563
+ });
246
564
  }
247
565
  /**
248
566
  * Wrap a plain `execute(args)` callable with gate + complete. On a hard deny it throws
@@ -258,10 +576,20 @@ function wrapTool(client, opts, execute) {
258
576
  canBlock: opts.canBlock ?? true,
259
577
  });
260
578
  if (result.blocked) {
261
- throw new denial_js_1.OryDenialError({ tool: opts.toolName, subjectId: result.subject, namespace: result.namespace });
579
+ throw new denial_js_1.OryDenialError(result.denialContext ?? {
580
+ tool: opts.toolName,
581
+ subjectId: result.subject,
582
+ namespace: result.namespace,
583
+ });
584
+ }
585
+ try {
586
+ const output = await execute(args);
587
+ complete(client, { toolName: opts.toolName, input: args, output });
588
+ return output;
589
+ }
590
+ catch (error) {
591
+ fail(client, { toolName: opts.toolName, input: args, error });
592
+ throw error;
262
593
  }
263
- const output = await execute(args);
264
- complete(client, { toolName: opts.toolName, output });
265
- return output;
266
594
  };
267
595
  }