@ory/amp 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractToolName = exports.decideToolPermission = exports.createOryPlugin = void 0;
4
+ const plugin_js_1 = require("./plugin.js");
5
+ Object.defineProperty(exports, "createOryPlugin", { enumerable: true, get: function () { return plugin_js_1.createOryPlugin; } });
6
+ // Re-export the delegate decision logic for programmatic use / testing.
7
+ var permission_js_1 = require("./permission.js");
8
+ Object.defineProperty(exports, "decideToolPermission", { enumerable: true, get: function () { return permission_js_1.decideToolPermission; } });
9
+ Object.defineProperty(exports, "extractToolName", { enumerable: true, get: function () { return permission_js_1.extractToolName; } });
10
+ // Amp loads in-process plugins via import() and expects a default-exported
11
+ // plugin function `(api) => void`. CJS output (module: nodenext → CJS)
12
+ // surfaces `module.exports` as the ESM `default`, so a single default
13
+ // export is what Amp's loader receives. Instantiate the plugin from env.
14
+ // eslint-disable-next-line import/no-default-export
15
+ exports.default = (0, plugin_js_1.createOryPlugin)();
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Amp permission delegate helper (PRIMARY blocking gate).
4
+ *
5
+ * Amp's `amp.permissions` config can route a tool to
6
+ * `{ action: "delegate", to: "ory-amp-permission" }`. Amp then runs this
7
+ * program as a subprocess for each matching tool call, passing the tool
8
+ * parameters as JSON on stdin and reading the decision from the exit code:
9
+ *
10
+ * exit 0 = allow
11
+ * exit 1 = ask the user (RESERVED — not produced by this version)
12
+ * exit ≥2 = reject; stderr is forwarded to the model as the reason
13
+ *
14
+ * NOTE the inversion vs. the other harnesses' subprocess hooks (where
15
+ * exit 2 blocks but the allow path is exit 0 with structured stdout):
16
+ * here a denial is exit 2 *plus a stderr message*, and stdout is unused.
17
+ *
18
+ * TIMEOUT: Amp enforces a 10-second delegate timeout. A delegate that does
19
+ * not exit within 10s is treated as a reject, so every Ory call below
20
+ * (session resolution + permission check) must complete well inside that
21
+ * window; the fail-open arms below keep slow/unreachable Ory from wedging
22
+ * the agent, but a hard 10s hang would still surface as a reject.
23
+ *
24
+ * Fail-open: any error — parse failure, network error, rate limit, crash —
25
+ * resolves to exit 0 (allow). The delegate must never wedge the agent.
26
+ *
27
+ * Verified against the installed `amp` binary (delegate exit-code contract
28
+ * and 10s timeout).
29
+ */
30
+ import { OryAgentClient } from "@ory/argus";
31
+ import { type AmpDelegateInput } from "./types.js";
32
+ export interface DelegateDecision {
33
+ /** Process exit code to return to Amp. */
34
+ exitCode: number;
35
+ /** Reason to write to stderr (only set when rejecting). */
36
+ reason?: string;
37
+ }
38
+ /** Pull the tool name out of the (undocumented) delegate payload. */
39
+ export declare function extractToolName(input: AmpDelegateInput): string;
40
+ /**
41
+ * Core decision logic, factored out so tests can call it directly without
42
+ * spawning a subprocess or hitting `process.exit`. Resolves to the exit
43
+ * code (and stderr reason) Amp expects. Always fails open: any thrown
44
+ * error resolves to ALLOW.
45
+ */
46
+ export declare function decideToolPermission(input: AmpDelegateInput, client: OryAgentClient): Promise<DelegateDecision>;
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Amp permission delegate helper (PRIMARY blocking gate).
5
+ *
6
+ * Amp's `amp.permissions` config can route a tool to
7
+ * `{ action: "delegate", to: "ory-amp-permission" }`. Amp then runs this
8
+ * program as a subprocess for each matching tool call, passing the tool
9
+ * parameters as JSON on stdin and reading the decision from the exit code:
10
+ *
11
+ * exit 0 = allow
12
+ * exit 1 = ask the user (RESERVED — not produced by this version)
13
+ * exit ≥2 = reject; stderr is forwarded to the model as the reason
14
+ *
15
+ * NOTE the inversion vs. the other harnesses' subprocess hooks (where
16
+ * exit 2 blocks but the allow path is exit 0 with structured stdout):
17
+ * here a denial is exit 2 *plus a stderr message*, and stdout is unused.
18
+ *
19
+ * TIMEOUT: Amp enforces a 10-second delegate timeout. A delegate that does
20
+ * not exit within 10s is treated as a reject, so every Ory call below
21
+ * (session resolution + permission check) must complete well inside that
22
+ * window; the fail-open arms below keep slow/unreachable Ory from wedging
23
+ * the agent, but a hard 10s hang would still surface as a reject.
24
+ *
25
+ * Fail-open: any error — parse failure, network error, rate limit, crash —
26
+ * resolves to exit 0 (allow). The delegate must never wedge the agent.
27
+ *
28
+ * Verified against the installed `amp` binary (delegate exit-code contract
29
+ * and 10s timeout).
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.extractToolName = extractToolName;
33
+ exports.decideToolPermission = decideToolPermission;
34
+ const argus_1 = require("@ory/argus");
35
+ const types_js_1 = require("./types.js");
36
+ function resolveNamespace() {
37
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
38
+ }
39
+ /** Pull the tool name out of the (undocumented) delegate payload. */
40
+ function extractToolName(input) {
41
+ return input.tool ?? input.toolName ?? input.name ?? "unknown";
42
+ }
43
+ /** Pull the session/thread id out of the delegate payload, if present. */
44
+ function extractSessionId(input) {
45
+ return input.threadID ?? input.sessionID ?? input.session_id;
46
+ }
47
+ /**
48
+ * Core decision logic, factored out so tests can call it directly without
49
+ * spawning a subprocess or hitting `process.exit`. Resolves to the exit
50
+ * code (and stderr reason) Amp expects. Always fails open: any thrown
51
+ * error resolves to ALLOW.
52
+ */
53
+ async function decideToolPermission(input, client) {
54
+ const toolName = extractToolName(input);
55
+ const sessionId = extractSessionId(input);
56
+ client.tracer.setContext({
57
+ traceId: (0, argus_1.deriveTraceId)(sessionId ?? toolName),
58
+ sessionId,
59
+ });
60
+ client.logger.info("delegate.received", { toolName, sessionId });
61
+ // Audit-only mode: never block, just record the invocation.
62
+ if ((0, argus_1.resolveConfig)().auditOnly) {
63
+ client.tracer.record("tool.invoke", "ok", {
64
+ attributes: { toolName, mode: "audit-only" },
65
+ });
66
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
67
+ }
68
+ const subject = (0, argus_1.resolveUserSubject)(client, sessionId ? `session:${sessionId}` : undefined);
69
+ const subjectId = (0, argus_1.subjectLabel)(subject);
70
+ try {
71
+ const outcome = await (0, argus_1.gateToolCall)(client, {
72
+ harness: "amp",
73
+ toolName,
74
+ check: {
75
+ namespace: resolveNamespace(),
76
+ object: toolName,
77
+ relation: "use",
78
+ ...subject,
79
+ },
80
+ spanAttributes: { toolName },
81
+ });
82
+ // Interactive tools (operator-extensible via ORY_INTERACTIVE_TOOLS):
83
+ // the user.interaction span is already recorded; allow so Amp can
84
+ // surface the prompt to the user.
85
+ if (outcome.kind === "interactive") {
86
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
87
+ }
88
+ const decision = outcome;
89
+ const attrs = { toolName };
90
+ const decisionAttrs = decision.spanAttributes;
91
+ if (decision.kind === "fail_open") {
92
+ // network_error / rate_limited / unknown → allow.
93
+ client.logger.warn("delegate.fail_open", {
94
+ toolName,
95
+ code: decision.error.code,
96
+ message: "Ory unreachable or errored, failing open",
97
+ });
98
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
99
+ }
100
+ if (decision.kind === "allow") {
101
+ client.tracer.record("tool.invoke", "ok", {
102
+ attributes: { ...attrs, ...decisionAttrs, allowed: true },
103
+ });
104
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
105
+ }
106
+ if (decision.kind === "observe") {
107
+ // Observe mode: log the would-be denial but let the tool through.
108
+ client.tracer.record("tool.block", "denied", {
109
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
110
+ });
111
+ client.tracer.record("tool.invoke", "ok", {
112
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, observed: true },
113
+ });
114
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
115
+ }
116
+ // decision.kind === "deny" → enforce mode hard block.
117
+ client.tracer.record("tool.block", "denied", {
118
+ attributes: { ...attrs, ...decisionAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
119
+ });
120
+ const reason = (0, argus_1.formatDenialMessage)({
121
+ tool: toolName,
122
+ subjectId,
123
+ namespace: resolveNamespace(),
124
+ });
125
+ client.logger.warn("tool.denied", { toolName, subjectId, message: reason });
126
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.REJECT, reason };
127
+ }
128
+ catch (err) {
129
+ // Fail open on any unexpected error.
130
+ client.logger.error("delegate.error", {
131
+ toolName,
132
+ message: err instanceof Error ? err.message : String(err),
133
+ });
134
+ return { exitCode: types_js_1.AMP_DELEGATE_EXIT.ALLOW };
135
+ }
136
+ }
137
+ /**
138
+ * Read all of stdin as a string.
139
+ *
140
+ * Uses event listeners instead of `for await` so we don't depend on the
141
+ * parent closing stdin (sending EOF). If no EOF arrives, we resolve after
142
+ * a short idle gap once data has been received. Copied from the gemini-cli
143
+ * subprocess hook for robustness parity.
144
+ */
145
+ function readStdin() {
146
+ return new Promise((resolve, reject) => {
147
+ const chunks = [];
148
+ let resolved = false;
149
+ function done() {
150
+ if (!resolved) {
151
+ resolved = true;
152
+ resolve(Buffer.concat(chunks).toString("utf-8"));
153
+ }
154
+ }
155
+ let idleTimer;
156
+ process.stdin.on("data", (chunk) => {
157
+ chunks.push(chunk);
158
+ clearTimeout(idleTimer);
159
+ idleTimer = setTimeout(done, 100);
160
+ });
161
+ process.stdin.on("end", done);
162
+ process.stdin.on("error", (err) => {
163
+ if (!resolved) {
164
+ resolved = true;
165
+ reject(err);
166
+ }
167
+ });
168
+ });
169
+ }
170
+ async function main() {
171
+ const client = argus_1.OryAgentClient.fromEnv("amp");
172
+ const raw = await readStdin();
173
+ let input;
174
+ try {
175
+ input = raw.trim() ? JSON.parse(raw) : {};
176
+ }
177
+ catch {
178
+ // Parse failure — fail open (allow) rather than wedging the agent.
179
+ client.logger.error("delegate.stdin.parse_failed", { raw: raw.slice(0, 200) });
180
+ await client.tracer.shutdown();
181
+ process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
182
+ }
183
+ const { exitCode, reason } = await decideToolPermission(input, client);
184
+ if (exitCode === types_js_1.AMP_DELEGATE_EXIT.REJECT && reason) {
185
+ // stderr is forwarded to the model as the rejection reason.
186
+ process.stderr.write(reason + "\n");
187
+ }
188
+ await client.tracer.shutdown();
189
+ process.exit(exitCode);
190
+ }
191
+ // Only run the stdin→exit-code pipeline when invoked as the CLI entry
192
+ // point. Tests import this module to call `decideToolPermission` directly,
193
+ // so guarding here keeps `main()` (and its `process.exit`) from firing on
194
+ // import. `module` exists under CJS output (module: nodenext → CJS).
195
+ if (require.main === module) {
196
+ main().catch((err) => {
197
+ // Top-level safety net: never block on an unexpected crash.
198
+ process.stderr.write(`[ory-agent] delegate fatal: ${err}\n`);
199
+ process.exit(types_js_1.AMP_DELEGATE_EXIT.ALLOW);
200
+ });
201
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
3
+ *
4
+ * Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
5
+ * calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
6
+ * Each handler receives the event payload and a `PluginEventContext`. We
7
+ * subscribe to:
8
+ *
9
+ * session.start → run the user gate (advisory, allowBlock:false), the
10
+ * agent gate, and write the user→agent delegation tuple.
11
+ * tool.result → record a `tool.complete` audit span.
12
+ *
13
+ * The BLOCKING permission decision deliberately does NOT live here. Amp's
14
+ * in-process `tool.call` event *can* block (via a `reject-and-continue`
15
+ * result), but we route blocking through the standalone permission delegate
16
+ * helper (`permission.ts`) instead — that gives the same subprocess parity
17
+ * as the other harnesses and keeps the in-process module purely
18
+ * observational. The delegate runs as its own process and decides by exit
19
+ * code; this module only authenticates and traces.
20
+ *
21
+ * Because Amp's `session.start` handler has no return-value channel for
22
+ * blocking, the user login runs in advisory mode (allowBlock:false), like
23
+ * the OpenCode / OpenClaw factory plugins.
24
+ */
25
+ import { OryAgentClient, ensureUserAuthenticated, ensureAgentIdentity } from "@ory/argus";
26
+ import type { AmpPlugin } from "./types.js";
27
+ export interface CreateOryPluginDeps {
28
+ /** Test injection point for the user login flow. */
29
+ userLogin?: typeof ensureUserAuthenticated;
30
+ /** Test injection point for the agent identity gate. */
31
+ agentGate?: typeof ensureAgentIdentity;
32
+ }
33
+ /**
34
+ * Create the Amp in-process plugin factory.
35
+ *
36
+ * Returns a function matching Amp's `AmpPlugin` contract: it receives the
37
+ * PluginAPI and registers event handlers via `api.on(...)`.
38
+ */
39
+ export declare function createOryPlugin(clientOrConfig?: OryAgentClient | {
40
+ projectUrl: string;
41
+ apiKey?: string;
42
+ }, deps?: CreateOryPluginDeps): AmpPlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ /**
3
+ * Amp in-process plugin (SECONDARY: session auth + post-tool tracing).
4
+ *
5
+ * Amp loads a default-exported factory from `.amp/plugins/*.ts` modules and
6
+ * calls it with a `PluginAPI` (the `@ampcode/plugin` contract, Bun runtime).
7
+ * Each handler receives the event payload and a `PluginEventContext`. We
8
+ * subscribe to:
9
+ *
10
+ * session.start → run the user gate (advisory, allowBlock:false), the
11
+ * agent gate, and write the user→agent delegation tuple.
12
+ * tool.result → record a `tool.complete` audit span.
13
+ *
14
+ * The BLOCKING permission decision deliberately does NOT live here. Amp's
15
+ * in-process `tool.call` event *can* block (via a `reject-and-continue`
16
+ * result), but we route blocking through the standalone permission delegate
17
+ * helper (`permission.ts`) instead — that gives the same subprocess parity
18
+ * as the other harnesses and keeps the in-process module purely
19
+ * observational. The delegate runs as its own process and decides by exit
20
+ * code; this module only authenticates and traces.
21
+ *
22
+ * Because Amp's `session.start` handler has no return-value channel for
23
+ * blocking, the user login runs in advisory mode (allowBlock:false), like
24
+ * the OpenCode / OpenClaw factory plugins.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.createOryPlugin = createOryPlugin;
28
+ const argus_1 = require("@ory/argus");
29
+ /**
30
+ * Create the Amp in-process plugin factory.
31
+ *
32
+ * Returns a function matching Amp's `AmpPlugin` contract: it receives the
33
+ * PluginAPI and registers event handlers via `api.on(...)`.
34
+ */
35
+ function createOryPlugin(clientOrConfig, deps = {}) {
36
+ return (api) => {
37
+ const client = clientOrConfig instanceof argus_1.OryAgentClient
38
+ ? clientOrConfig
39
+ : clientOrConfig
40
+ ? new argus_1.OryAgentClient({ ...clientOrConfig, harness: "amp" })
41
+ : argus_1.OryAgentClient.fromEnv("amp");
42
+ api.on("session.start", createSessionStartHandler(client, deps));
43
+ api.on("tool.result", createToolResultHandler(client));
44
+ };
45
+ }
46
+ // ─── session.start ─────────────────────────────────────────────────
47
+ function createSessionStartHandler(client, deps) {
48
+ return async (event, _ctx) => {
49
+ const sessionId = event.thread?.id;
50
+ client.logger.info("lifecycle.session_start", { sessionId });
51
+ client.tracer.setContext({
52
+ traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
53
+ sessionId,
54
+ });
55
+ client.tracer.record("session.start", "ok", {
56
+ attributes: sessionId ? { sessionId } : {},
57
+ });
58
+ // Run the user login. Amp's session.start has no block primitive, so
59
+ // allowBlock is false — the flow emits the user.auth audit span,
60
+ // refreshes tokens, and may prompt when interactive, but never
61
+ // prevents the session from starting. When ORY_USER_LOGIN is unset it
62
+ // is a no-op (mode === "disabled") and we fall through to legacy
63
+ // logging.
64
+ const userGate = deps.userLogin ?? argus_1.ensureUserAuthenticated;
65
+ const decision = await userGate(client, {
66
+ binName: "ory-amp",
67
+ harness: "amp",
68
+ allowBlock: false,
69
+ });
70
+ // Resolve the agent identity (machine credentials). Never blocks;
71
+ // attaches the agent's bearer token to outgoing Ory API calls.
72
+ const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
73
+ await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "amp" });
74
+ // Once both principals are populated, write the user→agent delegation
75
+ // tuple. Idempotent + fail-open: audit-trail data only.
76
+ await recordUserDelegatesAgent(client);
77
+ if (decision.mode !== "disabled") {
78
+ return;
79
+ }
80
+ const resolved = (0, argus_1.resolveConfig)();
81
+ if (resolved.auditOnly) {
82
+ client.logger.info("config.audit_only", {
83
+ message: "Audit-only mode enabled. Auth and permission checks are disabled.",
84
+ });
85
+ return;
86
+ }
87
+ if (!resolved.projectUrl) {
88
+ client.logger.warn("config.not_configured", {
89
+ message: "Ory plugin is not configured. Auth and permission checks are disabled. " +
90
+ "Run 'npx ory-amp configure' to connect to an Ory project.",
91
+ });
92
+ return;
93
+ }
94
+ const sessionToken = process.env.ORY_SESSION_TOKEN;
95
+ const oauth2Token = process.env.ORY_OAUTH2_TOKEN;
96
+ if (sessionToken) {
97
+ await verifySessionToken(client, sessionToken);
98
+ return;
99
+ }
100
+ if (oauth2Token) {
101
+ await verifyOAuth2Token(client, oauth2Token);
102
+ return;
103
+ }
104
+ client.logger.warn("session.no_credentials", {
105
+ message: "Neither ORY_SESSION_TOKEN nor ORY_OAUTH2_TOKEN is set. " +
106
+ "Skipping authentication.",
107
+ });
108
+ };
109
+ }
110
+ async function verifySessionToken(client, token) {
111
+ try {
112
+ const session = await client.verifySession(token);
113
+ if (!session.active) {
114
+ client.logger.warn("session.inactive", {
115
+ message: "Ory session is not active. Re-authenticate to enable auth checks.",
116
+ });
117
+ }
118
+ }
119
+ catch (err) {
120
+ client.logger.warn("session.verify_failed", {
121
+ code: isOryError(err) ? err.code : "unknown",
122
+ message: err instanceof Error ? err.message : String(err),
123
+ });
124
+ }
125
+ }
126
+ async function verifyOAuth2Token(client, token) {
127
+ try {
128
+ const tokenInfo = await client.introspectToken(token);
129
+ if (!tokenInfo.active) {
130
+ client.logger.warn("oauth2.token_inactive", {
131
+ message: "Ory OAuth2 token is not active. Obtain a new token to enable auth checks.",
132
+ });
133
+ return;
134
+ }
135
+ client.logger.info("oauth2.session_authenticated", {
136
+ clientId: tokenInfo.clientId,
137
+ subject: tokenInfo.subject,
138
+ });
139
+ }
140
+ catch (err) {
141
+ client.logger.warn("oauth2.introspect_failed", {
142
+ code: isOryError(err) ? err.code : "unknown",
143
+ message: err instanceof Error ? err.message : String(err),
144
+ });
145
+ }
146
+ }
147
+ // ─── tool.result ───────────────────────────────────────────────────
148
+ function createToolResultHandler(client) {
149
+ return async (event, _ctx) => {
150
+ const toolName = event.tool ?? "unknown";
151
+ const sessionId = event.thread?.id;
152
+ const hasError = event.status === "error";
153
+ client.tracer.setContext({
154
+ traceId: (0, argus_1.deriveTraceId)(sessionId ?? "amp"),
155
+ sessionId,
156
+ });
157
+ client.logger.info("lifecycle.tool_result", {
158
+ toolName,
159
+ sessionId,
160
+ status: event.status,
161
+ hasError,
162
+ });
163
+ client.tracer.record("tool.complete", hasError ? "error" : "ok", {
164
+ attributes: {
165
+ toolName,
166
+ status: event.status,
167
+ ...(0, argus_1.summarizeToolInput)(toolName, event.input),
168
+ ...(0, argus_1.summarizeToolOutput)(toolName, event.output),
169
+ ...(event.error ? { error: event.error } : {}),
170
+ },
171
+ });
172
+ };
173
+ }
174
+ // ─── Helpers ───────────────────────────────────────────────────────
175
+ function resolveNamespace() {
176
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
177
+ }
178
+ function isOryError(err) {
179
+ return (typeof err === "object" &&
180
+ err !== null &&
181
+ "code" in err &&
182
+ "message" in err);
183
+ }
184
+ /**
185
+ * Write the user→agent delegation tuple. Idempotent and fail-open:
186
+ * requires both principal subjects to be populated; any error (including
187
+ * an unconfigured projectUrl, which manifests as a network_error) is
188
+ * logged and swallowed. Purely audit-trail data — does not affect
189
+ * enforcement.
190
+ */
191
+ async function recordUserDelegatesAgent(client) {
192
+ const user = client.userPrincipal.subject;
193
+ const agent = client.agentPrincipal.subject;
194
+ if (!user || !agent) {
195
+ client.logger.debug("delegation.skip", {
196
+ reason: "missing principal",
197
+ hasUser: !!user,
198
+ hasAgent: !!agent,
199
+ });
200
+ return;
201
+ }
202
+ try {
203
+ await client.createRelationship({
204
+ namespace: resolveNamespace(),
205
+ object: `agent:${agent}`,
206
+ relation: "delegate",
207
+ subjectId: `user:${user}`,
208
+ }, { spanAttributes: { delegation: "user-to-agent" } });
209
+ }
210
+ catch (err) {
211
+ const oryErr = err;
212
+ client.logger.warn("delegation.user_to_agent.failed", {
213
+ code: oryErr.code,
214
+ message: oryErr.message,
215
+ });
216
+ }
217
+ }