@ory/argus 0.14.0 → 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 (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 +408 -0
  90. package/dist/runtime.js +748 -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/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,26 +23,80 @@
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");
61
+ }
62
+ /**
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
+ }
38
95
  }
39
96
  /**
40
97
  * Run both auth gates and write the user→agent delegation tuple. The user gate
41
98
  * runs every session to establish/refresh the user identity, records the
42
- * `user.auth` span, and never blocks — enforcement is governed by
99
+ * `user.auth` activity event, and never blocks — enforcement is governed by
43
100
  * `permissionMode` at tool-call time. `SessionStartResult.proceed` is therefore
44
101
  * always `true`; it is retained only for a stable adapter shape.
45
102
  */
@@ -47,15 +104,54 @@ async function sessionStart(client, opts) {
47
104
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
48
105
  const userLogin = opts.userLogin ?? user_login_js_1.ensureUserAuthenticated;
49
106
  const agentGate = opts.agentGate ?? agent_auth_js_1.ensureAgentIdentity;
50
- const decision = await userLogin(client, {
51
- binName: opts.binName ?? `ory-${opts.harness}`,
52
- harness: opts.harness,
53
- });
54
- const creds = await agentGate(client, {
55
- projectUrl,
56
- 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,
57
154
  });
58
- await writeUserDelegatesAgent(client);
59
155
  return {
60
156
  proceed: decision.proceed,
61
157
  userMode: decision.mode,
@@ -63,34 +159,22 @@ async function sessionStart(client, opts) {
63
159
  agentKind: creds.kind,
64
160
  };
65
161
  }
66
- /**
67
- * Write a delegation tuple at most once. The tuple is stable for a given
68
- * (namespace, object, subject), and Keto's create API is not idempotent, so
69
- * this goes through {@link OryAgentClient.ensureRelationship}: it reads the
70
- * exact tuple first and only writes when absent — no duplicate on repeated
71
- * session starts, and self-healing if the tuple was deleted. If the existence
72
- * probe fails we skip the write rather than risk a duplicate (delegation
73
- * tuples are audit-only, so a missed write is harmless and retried next
74
- * session). Best-effort: any failure is logged and swallowed.
75
- */
76
- async function writeDelegationOnce(client, check, options) {
77
- try {
78
- const res = await client.ensureRelationship(check, {
79
- spanAttributes: options.spanAttributes,
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));
80
174
  });
81
- if (res.probeError) {
82
- client.logger.debug("delegation.probe_failed", {
83
- ...options.failedContext,
84
- message: res.probeError.message,
85
- });
86
- }
87
- }
88
- catch (err) {
89
- client.logger.warn(options.failedEvent, {
90
- ...options.failedContext,
91
- message: err instanceof Error ? err.message : String(err),
92
- });
93
- }
175
+ pending.set(key, started);
176
+ return started;
177
+ };
94
178
  }
95
179
  /** Strip a `<ns>:` prefix from a namespaced subject, if present. */
96
180
  function unqualify(subject, ns) {
@@ -100,8 +184,8 @@ function unqualify(subject, ns) {
100
184
  /**
101
185
  * Resolve one end of a delegation edge, preferring the live principal but
102
186
  * falling back to the persisted delegation anchor (`delegation.delegatedBy`
103
- * on the DCR credentials in local config). This is what lets the tuple be
104
- * rebuilt from an authoritative source rather than from nothing when the
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
105
189
  * principal isn't populated for the session. A mismatch between the two is
106
190
  * logged for reconciliation visibility; the live principal wins.
107
191
  */
@@ -112,54 +196,61 @@ function reconcileEdgeEnd(client, principal, anchor, role) {
112
196
  return principal ?? anchor;
113
197
  }
114
198
  /**
115
- * Write the `user → agent` delegation tuple, at most once per install
116
- * (see {@link writeDelegationOnce}). Exported so harness plugins that run
117
- * their own session-start sequence (rather than {@link sessionStart}) share
118
- * the same write-once semantics instead of re-issuing the write every
119
- * session.
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.
120
203
  *
121
- * The tuple is a *derived projection* of the delegation anchor stamped into
122
- * the agent's OAuth2 client metadata at registration: the edge is resolved
123
- * from the live principals when populated, otherwise rebuilt from the persisted
124
- * anchor so a tuple lost out of band can be self-healed from the identity
125
- * layer. No-op only when neither source yields both ends.
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.
126
213
  */
127
- async function writeUserDelegatesAgent(client) {
128
- // The anchor is the delegation *record* stamped into the agent client's
129
- // metadata — not the mere existence of a persisted client. Only when a
130
- // user-to-agent record is present do we know the delegator, so both ends of
131
- // the edge are taken from the same record's client (agent = its clientId,
132
- // user = its delegatedBy). Absent a record, fall back to live principals only.
133
- const anchorCreds = (0, agent_auth_js_1.loadAgentDynamicCredentials)();
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);
134
226
  const anchor = anchorCreds?.delegation;
135
- const hasUserAgentAnchor = anchor?.delegationType === "user-to-agent";
136
- const anchorUser = hasUserAgentAnchor ? unqualify(anchor.delegatedBy, "user") : undefined;
137
- // The agent principal's subject is `ORY_AGENT_SUBJECT_ID ?? clientId` (see
138
- // resolveClientCredentials). The anchor rebuild must apply the *same*
139
- // precedence, or a no-principal session would write `agent:<clientId>` while
140
- // a live session wrote `agent:<override>` — two divergent tuples for one edge.
141
- const anchorAgent = hasUserAgentAnchor
142
- ? (process.env.ORY_AGENT_SUBJECT_ID || anchorCreds.clientId)
143
- : undefined;
144
- const user = reconcileEdgeEnd(client, client.userPrincipal.subject, anchorUser, "user");
145
- const agent = reconcileEdgeEnd(client, client.agentPrincipal.subject, anchorAgent, "agent");
146
- if (!user || !agent) {
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) {
147
230
  client.logger.debug("delegation.skip", {
148
- reason: "missing delegation edge (no principal and no anchor)",
149
- hasUser: !!user,
150
- hasAgent: !!agent,
231
+ reason: "no user delegation edge (no principal and no anchor)",
151
232
  });
152
233
  return;
153
234
  }
154
- await writeDelegationOnce(client, {
155
- namespace: resolveNamespace(),
156
- object: `agent:${agent}`,
157
- relation: "delegate",
158
- subjectId: `user:${user}`,
159
- }, {
160
- spanAttributes: { delegation: "user-to-agent" },
161
- failedEvent: "delegation.user_to_agent.failed",
162
- });
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
+ }
163
254
  }
164
255
  async function registerSubagent(client, opts) {
165
256
  const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
@@ -170,128 +261,306 @@ async function registerSubagent(client, opts) {
170
261
  subAgentType: opts.subAgentType,
171
262
  projectUrl,
172
263
  harness: opts.harness,
264
+ sessionId: opts.sessionId ?? client.sessionId,
265
+ perSpawnId: opts.perSpawnId,
266
+ signal: opts.signal,
173
267
  });
174
268
  }
175
269
  catch (err) {
176
270
  client.logger.warn("subagent.identity.failed", {
177
271
  subAgentType: opts.subAgentType,
178
- message: err instanceof Error ? err.message : String(err),
272
+ ...describeThrown(err),
179
273
  });
180
274
  return;
181
275
  }
182
- if (identity.kind !== "dynamic" || !identity.subject)
276
+ if (identity.kind !== "runtime" && identity.kind !== "dynamic")
183
277
  return;
184
- 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
+ });
185
286
  }
186
287
  /**
187
- * Write the `agent → subagent` delegation tuple, at most once per install
188
- * (see {@link writeDelegationOnce}). Exported so harness plugins that
189
- * resolve the sub-agent identity through their own event plumbing (rather
190
- * than {@link registerSubagent}) still share the write-once semantics.
191
- * 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).
192
300
  */
193
- async function writeAgentDelegatesSubagent(client, subAgentSubject, subAgentType) {
194
- // The delegator (agent) is resolved from the live principal, falling back to
195
- // the sub-agent client's persisted delegation anchor (`delegation.delegatedBy`
196
- // = `agent:<agent>`) so the tuple can be rebuilt when no principal is present.
197
- const anchor = (0, agent_auth_js_1.loadSubAgentDynamicCredentials)(subAgentType)?.delegation;
198
- const anchorAgent = anchor?.delegationType === "agent-to-subagent"
199
- ? unqualify(anchor.delegatedBy, "agent")
200
- : undefined;
201
- const agent = reconcileEdgeEnd(client, client.agentPrincipal.subject, anchorAgent, "agent");
202
- 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) {
203
312
  client.logger.debug("delegation.skip", {
204
- reason: "no agent delegation edge (no principal and no anchor)",
313
+ reason: "no persisted agent delegation node (user→agent edge not recorded yet)",
205
314
  subAgentType,
315
+ sessionKey,
206
316
  });
207
317
  return;
208
318
  }
209
- await writeDelegationOnce(client, {
210
- namespace: resolveNamespace(),
211
- object: `subagent:${subAgentSubject}`,
212
- relation: "delegate",
213
- subjectId: `agent:${agent}`,
214
- }, {
215
- spanAttributes: { delegation: "agent-to-subagent", subAgentType },
216
- failedEvent: "delegation.agent_to_subagent.failed",
217
- failedContext: { subAgentType },
218
- });
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
+ };
219
357
  }
220
358
  /**
221
- * Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
222
- * Records `tool.invoke` on allow/observe and `tool.block` on deny/observe — identical span
223
- * 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.
224
362
  */
225
- async function gate(client, opts) {
363
+ async function decideTool(client, opts) {
226
364
  const namespace = resolveNamespace();
227
365
  const subjectRef = (0, subject_js_1.resolveUserSubject)(client, opts.subjectFallback);
228
366
  const label = (0, subject_js_1.subjectLabel)(subjectRef);
229
367
  const canBlock = opts.canBlock ?? true;
230
- const spanAttrs = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
231
- const check = { namespace, object: opts.toolName, relation: "use", ...subjectRef };
232
- // Audit-only kill switch: Ory is disabled entirely — no permission check.
233
- // Record only the audit `tool.invoke` span (same span semantics as the
234
- // harness plugins' audit-only short-circuit) and pass the tool through.
235
- const resolved = (0, config_js_1.resolveConfig)();
236
- if (resolved.auditOnly) {
237
- client.tracer.record("tool.invoke", "ok", { attributes: spanAttrs });
238
- const decision = {
239
- kind: "allow",
240
- result: { allowed: true, checkedAt: new Date().toISOString(), check },
241
- mode: resolved.permissionMode,
242
- 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,
243
382
  };
244
- return { proceed: true, blocked: false, kind: "allow", decision, subject: label, namespace };
245
383
  }
246
- const outcome = await (0, permissions_js_1.gateToolCall)(client, {
247
- harness: opts.harness,
248
- toolName: opts.toolName,
249
- check,
250
- spanAttributes: spanAttrs,
251
- });
252
- if (outcome.kind === "audit_only") {
253
- // Kill switch: Ory is disabled. Audit the invocation, skip the check.
254
- client.tracer.record("tool.invoke", "ok", {
255
- attributes: { ...spanAttrs, auditOnly: true },
256
- });
257
- 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
+ };
258
450
  }
259
451
  if (outcome.kind === "interactive") {
260
452
  return { proceed: true, blocked: false, kind: "interactive", decision: outcome, subject: label, namespace };
261
453
  }
262
- const decisionAttrs = outcome.spanAttributes;
454
+ const decisionAttrs = outcome.activityAttributes;
263
455
  if (outcome.kind === "fail_open") {
264
456
  client.logger.warn("permission.fail_open", { tool: opts.toolName, code: outcome.error.code });
265
457
  return { proceed: true, blocked: false, kind: "fail_open", decision: outcome, subject: label, namespace };
266
458
  }
267
459
  if (outcome.kind === "allow") {
268
- client.tracer.record("tool.invoke", "ok", {
269
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: true },
270
- });
271
460
  return { proceed: true, blocked: false, kind: "allow", decision: outcome, subject: label, namespace };
272
461
  }
273
462
  if (outcome.kind === "observe") {
274
- client.tracer.record("tool.block", "denied", {
275
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(false) },
276
- });
277
- client.tracer.record("tool.invoke", "ok", {
278
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, observed: true },
279
- });
280
463
  return { proceed: true, blocked: false, kind: "observe", decision: outcome, subject: label, namespace };
281
464
  }
282
- // deny
283
- client.tracer.record("tool.block", "denied", {
284
- attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(canBlock) },
285
- });
286
- 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);
287
473
  client.logger.warn("tool.denied", { tool: opts.toolName, subjectId: label });
288
- 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;
289
537
  }
290
538
  function complete(client, opts) {
291
- 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
+ };
292
545
  if (opts.output !== undefined)
293
546
  attributes.hasOutput = true;
294
- 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
+ });
295
564
  }
296
565
  /**
297
566
  * Wrap a plain `execute(args)` callable with gate + complete. On a hard deny it throws
@@ -307,10 +576,20 @@ function wrapTool(client, opts, execute) {
307
576
  canBlock: opts.canBlock ?? true,
308
577
  });
309
578
  if (result.blocked) {
310
- 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;
311
593
  }
312
- const output = await execute(args);
313
- complete(client, { toolName: opts.toolName, output });
314
- return output;
315
594
  };
316
595
  }