@ory/argus 0.2.0 → 0.3.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/client.js CHANGED
@@ -38,6 +38,11 @@ class OryAgentClient {
38
38
  traceFile: config.traceFile,
39
39
  ...(config.exporter ? { exporter: config.exporter } : {}),
40
40
  });
41
+ // Every span — including direct `tracer.record(...)` calls from
42
+ // plugin handlers — gets userSubject / agentSubject merged in, so
43
+ // tool.invoke / tool.block / permission.observe_deny spans carry
44
+ // the same principal attribution as the API-call spans.
45
+ this.tracer.setAttributeEnricher(() => this.principalSpanAttributes());
41
46
  // Seed agent principal from the constructor `apiKey` so existing
42
47
  // call sites keep working until they migrate to ensureAgentIdentity.
43
48
  if (config.apiKey)
package/dist/dev.js CHANGED
@@ -485,7 +485,7 @@ async function bootstrapLocalOry(localDir) {
485
485
  return { active: false };
486
486
  }
487
487
  console.log(`[ory-dev] Local Ory: agent ${seed.agent.identity.email}, user ${seed.user.identity.email}, ` +
488
- `${seed.permissions.tuples} permission tuples in '${namespace}' for ${seed.permissions.subject}.`);
488
+ `${seed.permissions.tuples} permissions in '${namespace}' for ${seed.permissions.subject}.`);
489
489
  return { active: true, gatewayUrl, seed };
490
490
  }
491
491
  /**
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { OryAgentClient, type OryAgentConfig, type PrincipalIdentity, } from "./client.js";
2
2
  export { DebugLogger, redactLogData, type LogEntry, type LogLevel, } from "./logger.js";
3
- export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, } from "./tracer.js";
3
+ export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type TraceEvent, type SpanStatus, type TraceSpan, type SpanOptions, type TracerOptions, type TracerContext, type SpanAttributeEnricher, } from "./tracer.js";
4
4
  export { loadConfig, saveConfig, resolveConfig, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type PermissionMode, } from "./config.js";
5
5
  export { pkceLogin, refreshAccessToken, detectHeadless, generateCodeVerifier, sha256Base64Url, buildAuthorizeUrl, LOOPBACK_PORTS, DEFAULT_LOGIN_TIMEOUT_MS, type PkceLoginOptions, type PkceLoginOutcome, type PkceDeclineReason, } from "./auth.js";
6
6
  export { loadTokens, saveTokens, clearTokens, isExpired, refreshAndSave, tryAcquirePkceFlightLock, clearPkceFlightLock, waitForPeerTokens, waitForPeerTokensSync, TOKEN_EXPIRY_SKEW_SEC, type PkceFlightLock, } from "./auth-store.js";
@@ -14,7 +14,7 @@ export { runDevLauncher, type DevLauncherConfig, type InstallContext, } from "./
14
14
  export { renderOrySkills, renderOryCommands, commandToSkill, commandToToml, commandToFrontmatterMarkdown, commandToPlainMarkdown, toSkillMarkdown, writeSkillTree, removeSkillDirs, ORY_SKILL_NAMES, ORY_COMMAND_SKILL_NAMES, ORY_COMMAND_SLUGS, type RenderedSkill, type RenderedCommand, type RenderProfileOptions, } from "./skills.js";
15
15
  export { runLocalCommand, ensureDevJaeger, stopDevJaeger, DEV_JAEGER_CONTAINER, type EnsureDevJaegerResult, type StopDevJaegerResult, } from "./local/index.js";
16
16
  export { runRegistryCommand } from "./registry/index.js";
17
- export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
17
+ export { checkAndDecide, applyPermissionMode, type PermissionDecision, type ModeDecision, type DecisionSpanAttributes, type CheckAndDecideOptions, type ApplyPermissionModeContext, } from "./permissions.js";
18
18
  export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, type KnownHarness, } from "./tool-catalog.js";
19
19
  export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkMcpPermission, type McpToolIdentifier, type McpPermissionCheckOptions, type McpPermissionResult, } from "./mcp.js";
20
20
  export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
package/dist/local/cli.js CHANGED
@@ -63,7 +63,7 @@ Commands:
63
63
  up [--no-seed] Start local Ory services (Kratos, Keto, Hydra, gateway)
64
64
  down Stop all local services (preserves data)
65
65
  status Show service health and container states
66
- seed Create test identity, session, and permission tuples
66
+ seed Create test identity, session, and permissions
67
67
  logs [service] [-f] View service logs (optionally follow, optionally filter)
68
68
  reset Stop services and remove all data
69
69
  env Print environment variables for connecting to local services
@@ -642,7 +642,7 @@ function printSeedResult(result) {
642
642
  console.log(` User password: ${result.user.password}`);
643
643
  console.log(` User OAuth2: ${result.user.client.clientId} (PKCE)`);
644
644
  console.log("");
645
- console.log(` Permissions: ${result.permissions.tuples} tuples in '${result.permissions.namespace}' for ${result.permissions.subject}`);
645
+ console.log(` Permissions: ${result.permissions.tuples} entries in '${result.permissions.namespace}' for ${result.permissions.subject}`);
646
646
  console.log("");
647
647
  console.log("To use with any Ory agent plugin, set these environment variables:");
648
648
  console.log("");
@@ -110,7 +110,7 @@ async function seedPermissions(userId, namespace) {
110
110
  catch (err) {
111
111
  const msg = err instanceof Error ? err.message : String(err);
112
112
  if (!msg.includes("409")) {
113
- process.stderr.write(` Warning: failed to create tuple for ${tool}: ${msg}\n`);
113
+ process.stderr.write(` Warning: failed to create permission for ${tool}: ${msg}\n`);
114
114
  }
115
115
  else {
116
116
  created++;
@@ -185,9 +185,9 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
185
185
  // Subject is a SubjectSet `User:<id>` so the Console UI can rewrite
186
186
  // the same tuple shape by hand during the runbook demo.
187
187
  const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
188
- process.stderr.write(` Creating permission tuples for ${COMMON_TOOLS.length} tools...\n`);
188
+ process.stderr.write(` Creating permissions for ${COMMON_TOOLS.length} tools...\n`);
189
189
  const tupleCount = await seedPermissions(userIdentity.id, namespace);
190
- process.stderr.write(` Permissions: ${tupleCount} tuples in '${namespace}' for ${subjectLabel}\n`);
190
+ process.stderr.write(` Permissions: ${tupleCount} entries in '${namespace}' for ${subjectLabel}\n`);
191
191
  // 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
192
192
  // PKCE flow has somewhere to authenticate against. The agent's
193
193
  // OAuth2 client is intentionally NOT pre-registered; the harness
@@ -84,7 +84,7 @@ function printPermissionsHelp(binName) {
84
84
  console.log("");
85
85
  console.log("Subcommands:");
86
86
  console.log(" status Show permission mode and tool coverage for the current user");
87
- console.log(" bootstrap Write 'use' tuples for the harness's built-in tools (idempotent)");
87
+ console.log(" bootstrap Grant the current user 'use' on every built-in tool (idempotent)");
88
88
  console.log(" observe Switch to observe mode (denies log but don't block)");
89
89
  console.log(" enforce Switch to enforce mode (denies block — production posture)");
90
90
  console.log("");
@@ -104,7 +104,7 @@ function runPermissionsSetMode(binName, mode) {
104
104
  console.log("Denies will be logged and recorded as `permission.observe_deny` spans,");
105
105
  console.log("but tools will be allowed through. Run");
106
106
  console.log(` ${binName} permissions enforce`);
107
- console.log("once your tuple set is correct.");
107
+ console.log("once your permission set is correct.");
108
108
  }
109
109
  else {
110
110
  console.log("Denies will block tool execution. The plugin is now enforcing.");
@@ -121,9 +121,10 @@ async function runPermissionsStatus(binName, harness) {
121
121
  console.log(`Permission status (${harness})`);
122
122
  console.log("==========================================");
123
123
  console.log("");
124
- console.log(`Mode: ${resolved.permissionMode} [source: ${resolved.permissionModeSource}]`);
124
+ console.log(`Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
125
+ console.log(` (other modes: ${otherModes(resolved.permissionMode).join(", ")})`);
125
126
  console.log(`Namespace: ${namespace}`);
126
- console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"} [source: ${resolved.projectUrlSource}]`);
127
+ console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"}${formatSourceSuffix(resolved.projectUrlSource)}`);
127
128
  console.log(`Audit-only: ${resolved.auditOnly ? "yes (Ory disabled)" : "no"}`);
128
129
  console.log("");
129
130
  if (resolved.auditOnly) {
@@ -132,7 +133,7 @@ async function runPermissionsStatus(binName, harness) {
132
133
  return 0;
133
134
  }
134
135
  if (!resolved.projectUrl) {
135
- console.log("No project URL configured — cannot probe tuple coverage.");
136
+ console.log("No project URL configured — cannot probe permission coverage.");
136
137
  console.log(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
137
138
  return 0;
138
139
  }
@@ -214,7 +215,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
214
215
  return 1;
215
216
  }
216
217
  if (!resolved.projectUrl) {
217
- console.error("No ORY_PROJECT_URL configured — cannot write tuples.");
218
+ console.error("No ORY_PROJECT_URL configured — cannot write permissions.");
218
219
  console.error(`Configure first: ${binName} configure --project-url <URL> --api-key <KEY>`);
219
220
  return 1;
220
221
  }
@@ -231,14 +232,14 @@ async function runPermissionsBootstrap(binName, harness, args) {
231
232
  const subject = (0, subject_js_1.resolveUserSubject)(client);
232
233
  const subjectId = (0, subject_js_1.subjectLabel)(subject);
233
234
  if (subjectId === "agent:unknown") {
234
- console.error("No user identity resolved — refusing to write tuples to an unknown subject.");
235
+ console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
235
236
  console.error("");
236
237
  console.error("Either:");
237
238
  console.error(" - run the harness once with ORY_AUTH_GATE=1 so a user token is cached, or");
238
239
  console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
239
240
  return 1;
240
241
  }
241
- console.log(`Bootstrapping ${catalog.length} permission tuples for ${harness}`);
242
+ console.log(`Bootstrapping ${catalog.length} permissions for ${harness}`);
242
243
  console.log("==========================================");
243
244
  console.log("");
244
245
  console.log(`Namespace: ${namespace}`);
@@ -246,7 +247,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
246
247
  console.log(`Tools: ${catalog.join(", ")}`);
247
248
  if (dryRun) {
248
249
  console.log("");
249
- console.log("(--dry-run) No tuples will be written.");
250
+ console.log("(--dry-run) No permissions will be written.");
250
251
  }
251
252
  console.log("");
252
253
  let created = 0;
@@ -285,7 +286,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
285
286
  }
286
287
  console.log("");
287
288
  if (dryRun) {
288
- console.log(`Dry run: ${catalog.length} tuples would be written.`);
289
+ console.log(`Dry run: ${catalog.length} permissions would be written.`);
289
290
  return 0;
290
291
  }
291
292
  console.log(`Created: ${created} Already existed: ${existed} Failed: ${failures.length}`);
@@ -296,7 +297,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
296
297
  console.log("");
297
298
  console.log("Some writes were rejected — your current credentials may lack");
298
299
  console.log("write scope on the permission namespace. You can apply the");
299
- console.log("tuples manually using the snippet below (one tuple per line):");
300
+ console.log("permissions manually using the snippet below (one per line):");
300
301
  console.log("");
301
302
  for (const tool of catalog) {
302
303
  console.log(` ${namespace}:${tool}#use@${subjectId}`);
@@ -309,7 +310,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
309
310
  return failures.length === catalog.length ? 1 : 0;
310
311
  }
311
312
  console.log("");
312
- console.log(`Permission tuples are in place. Promote to enforce mode when ready:`);
313
+ console.log(`Permissions are in place. Promote to enforce mode when ready:`);
313
314
  console.log(` ${binName} permissions enforce`);
314
315
  return 0;
315
316
  }
@@ -342,21 +343,22 @@ function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
342
343
  const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
343
344
  console.log("");
344
345
  console.log("Permissions:");
345
- console.log(` Mode: ${resolved.permissionMode} [source: ${resolved.permissionModeSource}]`);
346
+ console.log(` Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
347
+ console.log(` Other modes: ${otherModes(resolved.permissionMode).join(", ")}`);
346
348
  if (resolved.auditOnly) {
347
349
  console.log(" Ory is in audit-only mode (kill switch). Permission checks are disabled.");
348
350
  return;
349
351
  }
350
352
  if (resolved.permissionMode === "observe") {
351
353
  console.log(" Observe mode: permission denies are logged + traced, but tools still run.");
352
- console.log(" This is the safe default — promote once your tuple set is correct.");
354
+ console.log(" This is the safe default — promote to 'enforce' once your permission set is correct.");
353
355
  }
354
356
  else {
355
357
  console.log(" Enforce mode: permission denies block tool execution.");
356
358
  }
357
359
  if (opts.bootstrappedAutomatically) {
358
360
  console.log("");
359
- console.log(` Tuples bootstrapped for ${catalog.length} built-in tools.`);
361
+ console.log(` Permissions bootstrapped for ${catalog.length} built-in tools.`);
360
362
  }
361
363
  else if (catalog.length > 0) {
362
364
  console.log("");
@@ -364,12 +366,52 @@ function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
364
366
  console.log(` npx ${binName} permissions bootstrap`);
365
367
  }
366
368
  console.log("");
367
- console.log(" Inspect tuple coverage:");
369
+ console.log(" Inspect permission coverage:");
368
370
  console.log(` npx ${binName} permissions status`);
369
371
  console.log("");
370
372
  console.log(" Promote to enforcing once observe-mode logs look right:");
371
373
  console.log(` npx ${binName} permissions enforce`);
372
374
  }
375
+ /**
376
+ * Render the trailing source annotation on a Mode line. Stays silent
377
+ * when the mode came from the built-in default, since saying "default"
378
+ * just adds noise. When the mode was overridden by env or config, name
379
+ * the override so the reader knows where to look.
380
+ */
381
+ function formatModeSource(source) {
382
+ switch (source) {
383
+ case "env":
384
+ return ` (set via ${ENV_PERMISSION_MODE} env var)`;
385
+ case "config":
386
+ return " (saved in config; change with: permissions observe|enforce)";
387
+ case "default":
388
+ return " (built-in default)";
389
+ }
390
+ }
391
+ /**
392
+ * Generic source-suffix helper for other config fields (project URL,
393
+ * API key). Source 'none' means the value is unset — don't add a suffix.
394
+ */
395
+ function formatSourceSuffix(source) {
396
+ switch (source) {
397
+ case "env":
398
+ return " (from env)";
399
+ case "config":
400
+ return " (from config)";
401
+ case "none":
402
+ return "";
403
+ }
404
+ }
405
+ /**
406
+ * Lists the modes other than the active one, in stable order, so the
407
+ * banner can hint "Other modes: …" without re-listing the active one.
408
+ * Audit-only is a separate kill switch (config.auditOnly), but we still
409
+ * mention it here so first-time readers learn the full set.
410
+ */
411
+ function otherModes(active) {
412
+ const all = ["observe", "enforce", "audit-only"];
413
+ return all.filter((m) => m !== active);
414
+ }
373
415
  /**
374
416
  * If a user identity is already cached and the plugin is configured,
375
417
  * opportunistically run `permissions bootstrap` so the user lands in a
@@ -391,7 +433,7 @@ async function maybeAutoBootstrap(binName, harness) {
391
433
  return false;
392
434
  try {
393
435
  console.log("");
394
- console.log("Bootstrapping permission tuples (cached user identity detected)...");
436
+ console.log("Bootstrapping permissions (cached user identity detected)...");
395
437
  const code = await runPermissionsCommand(binName, harness, ["bootstrap"]);
396
438
  return code === 0;
397
439
  }
@@ -23,22 +23,40 @@
23
23
  import type { OryAgentClient } from "./client.js";
24
24
  import { type PermissionMode } from "./config.js";
25
25
  import type { OryError, PermissionCheck, PermissionResult } from "./types.js";
26
+ /**
27
+ * Attributes describing *what was checked* and *under which posture*.
28
+ * Plugins spread this onto their `tool.invoke` / `tool.block` spans so
29
+ * the audit trail makes the observe-vs-enforce posture and the checked
30
+ * subject visible at a glance — without that, a span with `allowed=false`
31
+ * status=ok is ambiguous (observe pass-through? fail-open? bug?).
32
+ */
33
+ export interface DecisionSpanAttributes {
34
+ permissionMode: PermissionMode;
35
+ /** Direct subject ID when the check used `subjectId`. */
36
+ subjectId?: string;
37
+ /** SubjectSet rendered as `<namespace>:<object>#<relation>` when used. */
38
+ subjectSet?: string;
39
+ }
26
40
  export type PermissionDecision = {
27
41
  kind: "allow";
28
42
  result: PermissionResult;
29
43
  mode: PermissionMode;
44
+ spanAttributes: DecisionSpanAttributes;
30
45
  } | {
31
46
  kind: "deny";
32
47
  result: PermissionResult;
33
48
  mode: "enforce";
49
+ spanAttributes: DecisionSpanAttributes;
34
50
  } | {
35
51
  kind: "observe";
36
52
  result: PermissionResult;
37
53
  mode: "observe";
54
+ spanAttributes: DecisionSpanAttributes;
38
55
  } | {
39
56
  kind: "fail_open";
40
57
  error: OryError;
41
58
  mode: PermissionMode;
59
+ spanAttributes: DecisionSpanAttributes;
42
60
  };
43
61
  /**
44
62
  * The "what should the caller do?" half of a decision, independent of
@@ -49,12 +67,15 @@ export type PermissionDecision = {
49
67
  export type ModeDecision = {
50
68
  kind: "allow";
51
69
  mode: PermissionMode;
70
+ spanAttributes: DecisionSpanAttributes;
52
71
  } | {
53
72
  kind: "deny";
54
73
  mode: "enforce";
74
+ spanAttributes: DecisionSpanAttributes;
55
75
  } | {
56
76
  kind: "observe";
57
77
  mode: "observe";
78
+ spanAttributes: DecisionSpanAttributes;
58
79
  };
59
80
  export interface CheckAndDecideOptions {
60
81
  /**
@@ -81,6 +102,8 @@ export interface ApplyPermissionModeContext {
81
102
  relation?: string;
82
103
  /** Subject ID for the observe-deny log line, if available. */
83
104
  subjectId?: string;
105
+ /** SubjectSet for the observe-deny log line, if available. */
106
+ subjectSet?: PermissionCheck["subjectSet"];
84
107
  /** Attributes merged into the `permission.observe_deny` span. */
85
108
  spanAttributes?: Record<string, unknown>;
86
109
  /** Override the resolved mode (tests). */
@@ -25,6 +25,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.applyPermissionMode = applyPermissionMode;
26
26
  exports.checkAndDecide = checkAndDecide;
27
27
  const config_js_1 = require("./config.js");
28
+ function formatSubjectSet(set) {
29
+ if (!set)
30
+ return undefined;
31
+ return `${set.namespace}:${set.object}#${set.relation}`;
32
+ }
33
+ function buildDecisionAttributes(mode, subjectId, subjectSet) {
34
+ const set = formatSubjectSet(subjectSet);
35
+ // SubjectSet supersedes subjectId in the span (the printable label is
36
+ // derivable from it); avoid emitting both so consumers don't need to
37
+ // dedupe.
38
+ if (set)
39
+ return { permissionMode: mode, subjectSet: set };
40
+ if (subjectId)
41
+ return { permissionMode: mode, subjectId };
42
+ return { permissionMode: mode };
43
+ }
28
44
  /**
29
45
  * Map an `allowed` boolean from any permission check (plain or MCP)
30
46
  * onto a {@link ModeDecision}. When `allowed === false` and the mode
@@ -34,14 +50,16 @@ const config_js_1 = require("./config.js");
34
50
  */
35
51
  function applyPermissionMode(client, allowed, context = {}) {
36
52
  const mode = context.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
53
+ const spanAttributes = buildDecisionAttributes(mode, context.subjectId, context.subjectSet);
37
54
  if (allowed)
38
- return { kind: "allow", mode };
55
+ return { kind: "allow", mode, spanAttributes };
39
56
  if (mode === "observe") {
40
57
  client.logger.warn("permission.observe_deny", {
41
58
  namespace: context.namespace,
42
59
  object: context.object,
43
60
  relation: context.relation,
44
61
  subjectId: context.subjectId,
62
+ subjectSet: spanAttributes.subjectSet,
45
63
  note: "denied by Ory; allowed by plugin in observe mode",
46
64
  });
47
65
  client.tracer.record("permission.observe_deny", "denied", {
@@ -49,13 +67,13 @@ function applyPermissionMode(client, allowed, context = {}) {
49
67
  namespace: context.namespace,
50
68
  object: context.object,
51
69
  relation: context.relation,
52
- mode,
70
+ ...spanAttributes,
53
71
  ...context.spanAttributes,
54
72
  },
55
73
  });
56
- return { kind: "observe", mode: "observe" };
74
+ return { kind: "observe", mode: "observe", spanAttributes };
57
75
  }
58
- return { kind: "deny", mode: "enforce" };
76
+ return { kind: "deny", mode: "enforce", spanAttributes };
59
77
  }
60
78
  /**
61
79
  * Run a permission check and resolve the configured mode against the
@@ -64,6 +82,7 @@ function applyPermissionMode(client, allowed, context = {}) {
64
82
  */
65
83
  async function checkAndDecide(client, check, opts = {}) {
66
84
  const mode = opts.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
85
+ const spanAttributes = buildDecisionAttributes(mode, check.subjectId, check.subjectSet);
67
86
  let result;
68
87
  try {
69
88
  result = await client.checkPermission(check, {
@@ -71,19 +90,20 @@ async function checkAndDecide(client, check, opts = {}) {
71
90
  });
72
91
  }
73
92
  catch (err) {
74
- return { kind: "fail_open", error: err, mode };
93
+ return { kind: "fail_open", error: err, mode, spanAttributes };
75
94
  }
76
95
  const inner = applyPermissionMode(client, result.allowed, {
77
96
  namespace: check.namespace,
78
97
  object: check.object,
79
98
  relation: check.relation,
80
99
  subjectId: check.subjectId,
100
+ subjectSet: check.subjectSet,
81
101
  spanAttributes: opts.spanAttributes,
82
102
  modeOverride: opts.modeOverride,
83
103
  });
84
104
  if (inner.kind === "allow")
85
- return { kind: "allow", result, mode: inner.mode };
105
+ return { kind: "allow", result, mode: inner.mode, spanAttributes: inner.spanAttributes };
86
106
  if (inner.kind === "observe")
87
- return { kind: "observe", result, mode: "observe" };
88
- return { kind: "deny", result, mode: "enforce" };
107
+ return { kind: "observe", result, mode: "observe", spanAttributes: inner.spanAttributes };
108
+ return { kind: "deny", result, mode: "enforce", spanAttributes: inner.spanAttributes };
89
109
  }
package/dist/tracer.d.ts CHANGED
@@ -99,6 +99,14 @@ export interface ProcessMetadata {
99
99
  gitBranch?: string;
100
100
  gitCommit?: string;
101
101
  }
102
+ /**
103
+ * Returns a record of attributes to merge into every span's attribute
104
+ * bag at span-end. Used by `OryAgentClient` to inject the current
105
+ * `userSubject` / `agentSubject` onto every span without each handler
106
+ * having to remember to attach them. Must not throw and should be cheap;
107
+ * it is invoked on every span.
108
+ */
109
+ export type SpanAttributeEnricher = () => Record<string, unknown> | undefined;
102
110
  export declare class Tracer extends EventEmitter {
103
111
  readonly harness: string;
104
112
  readonly processMetadata: ProcessMetadata;
@@ -106,6 +114,7 @@ export declare class Tracer extends EventEmitter {
106
114
  private readonly _spans;
107
115
  private _context;
108
116
  private _exporter;
117
+ private _enricher;
109
118
  constructor(opts: TracerOptions);
110
119
  /** Backwards-compatible accessors for the most common metadata fields. */
111
120
  get hostname(): string;
@@ -135,6 +144,15 @@ export declare class Tracer extends EventEmitter {
135
144
  */
136
145
  setExporter(exporter: SpanExporter | null): void;
137
146
  get exporter(): SpanExporter | null;
147
+ /**
148
+ * Install (or replace) an attribute enricher. The enricher is invoked
149
+ * on every span at end-time and its return value is merged into the
150
+ * span's attributes (with caller-supplied attributes taking precedence).
151
+ * Pass `null` to clear.
152
+ */
153
+ setAttributeEnricher(enricher: SpanAttributeEnricher | null): void;
154
+ /** @internal — invoked by `ActiveSpan.end()`. Never throws. */
155
+ _callEnricher(): Record<string, unknown> | undefined;
138
156
  /**
139
157
  * Flush any pending exports. Call before the host process exits so
140
158
  * the OTLP HTTP request has a chance to complete.
package/dist/tracer.js CHANGED
@@ -98,8 +98,12 @@ class ActiveSpan {
98
98
  }
99
99
  this._ended = true;
100
100
  const durationMs = Math.round(performance.now() - this.startMs);
101
- const merged = this.baseAttributes || attributes
102
- ? { ...this.baseAttributes, ...attributes }
101
+ // Pull enricher attributes (e.g. userSubject / agentSubject from the
102
+ // client's principals) at span-end so every span gets them, including
103
+ // direct `tracer.record(...)` calls from plugin handlers.
104
+ const enriched = this.tracer._callEnricher();
105
+ const merged = enriched || this.baseAttributes || attributes
106
+ ? { ...enriched, ...this.baseAttributes, ...attributes }
103
107
  : undefined;
104
108
  const meta = this.tracer.processMetadata;
105
109
  const span = {
@@ -137,6 +141,7 @@ class Tracer extends node_events_1.EventEmitter {
137
141
  _spans = [];
138
142
  _context = {};
139
143
  _exporter;
144
+ _enricher = null;
140
145
  constructor(opts) {
141
146
  super();
142
147
  this.harness = opts.harness;
@@ -212,6 +217,30 @@ class Tracer extends node_events_1.EventEmitter {
212
217
  get exporter() {
213
218
  return this._exporter;
214
219
  }
220
+ /**
221
+ * Install (or replace) an attribute enricher. The enricher is invoked
222
+ * on every span at end-time and its return value is merged into the
223
+ * span's attributes (with caller-supplied attributes taking precedence).
224
+ * Pass `null` to clear.
225
+ */
226
+ setAttributeEnricher(enricher) {
227
+ this._enricher = enricher;
228
+ }
229
+ /** @internal — invoked by `ActiveSpan.end()`. Never throws. */
230
+ _callEnricher() {
231
+ if (!this._enricher)
232
+ return undefined;
233
+ try {
234
+ const out = this._enricher();
235
+ if (!out || Object.keys(out).length === 0)
236
+ return undefined;
237
+ return out;
238
+ }
239
+ catch {
240
+ // Enricher contract: never throw. Swallow defensively.
241
+ return undefined;
242
+ }
243
+ }
215
244
  /**
216
245
  * Flush any pending exports. Call before the host process exits so
217
246
  * the OTLP HTTP request has a chance to complete.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",