@ory/argus 0.2.1 → 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/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";
@@ -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.1",
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",