@ory/argus 0.10.0 → 0.11.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ory Argus: Agent and Developer Experience
2
2
 
3
- The core API behind every Ory Agent Plugin and Extension. Argus wraps [Ory Identities](https://www.ory.com/ory-ecosystem), [Ory Permissions](https://ory.com/permissions), MCP authorization, and distributed tracing into a single client. Each of the harness packages (`@ory/claude-code`, `@ory/codex`, `@ory/gemini-cli`, `@ory/openclaw`, `@ory/opencode`) is a thin adapter that maps one harness's hook contract onto Argus.
3
+ The core API behind every Ory Agent Plugin and Extension. Argus wraps [Ory Identities](https://www.ory.com/ory-ecosystem), [Ory Permissions](https://ory.com/permissions), MCP authorization, and distributed tracing into a single client. Each harness package (`@ory/claude-code`, `@ory/codex`, `@ory/gemini-cli`, and the rest — see the [root README](../../README.md) for the full list) is a thin adapter that maps one harness's hook contract onto Argus.
4
4
 
5
5
  Argus is also published on its own so you can build new harness plugins or extensions, embed Ory into a custom agent runtime, or instrument any SDK that exposes event lifecycle hooks for session start, tool execution, and tool completion.
6
6
 
package/dist/adapters.js CHANGED
@@ -128,10 +128,25 @@ async function gate(client, opts) {
128
128
  const label = (0, subject_js_1.subjectLabel)(subjectRef);
129
129
  const canBlock = opts.canBlock ?? true;
130
130
  const spanAttrs = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
131
+ const check = { namespace, object: opts.toolName, relation: "use", ...subjectRef };
132
+ // Audit-only kill switch: Ory is disabled entirely — no permission check.
133
+ // Record only the audit `tool.invoke` span (same span semantics as the
134
+ // harness plugins' audit-only short-circuit) and pass the tool through.
135
+ const resolved = (0, config_js_1.resolveConfig)();
136
+ if (resolved.auditOnly) {
137
+ client.tracer.record("tool.invoke", "ok", { attributes: spanAttrs });
138
+ const decision = {
139
+ kind: "allow",
140
+ result: { allowed: true, checkedAt: new Date().toISOString(), check },
141
+ mode: resolved.permissionMode,
142
+ spanAttributes: { permissionMode: resolved.permissionMode },
143
+ };
144
+ return { proceed: true, blocked: false, kind: "allow", decision, subject: label, namespace };
145
+ }
131
146
  const outcome = await (0, permissions_js_1.gateToolCall)(client, {
132
147
  harness: opts.harness,
133
148
  toolName: opts.toolName,
134
- check: { namespace, object: opts.toolName, relation: "use", ...subjectRef },
149
+ check,
135
150
  spanAttributes: spanAttrs,
136
151
  });
137
152
  if (outcome.kind === "audit_only") {
@@ -384,12 +384,13 @@ async function fetchClientCredentialsToken(args) {
384
384
  * the agent identity was available for the session.
385
385
  */
386
386
  async function ensureAgentIdentity(client, options = {}) {
387
- // Kill switch: audit-only mode disables Ory entirely — no agent auth,
388
- // mirroring the user gate's audit_only short-circuit.
387
+ // Audit-only kill switch: Ory is disabled entirely — skip credential
388
+ // resolution (no DCR, no token grant) and record the no-op, shaped like
389
+ // the user gate's audit_only result.
389
390
  if ((0, config_js_1.resolveConfig)().auditOnly) {
390
391
  const creds = {
391
392
  kind: "none",
392
- reason: "Configured for audit-only mode; agent identity is a no-op",
393
+ reason: "Configured for audit-only mode; agent identity resolution is a no-op",
393
394
  warnings: [],
394
395
  };
395
396
  client.tracer.record("agent.auth", "skipped", {
@@ -450,6 +451,25 @@ async function ensureAgentIdentity(client, options = {}) {
450
451
  * resolution.
451
452
  */
452
453
  async function ensureSubAgentIdentity(client, options) {
454
+ // Audit-only kill switch: same no-op as ensureAgentIdentity — no
455
+ // sub-agent DCR while Ory is disabled entirely.
456
+ if ((0, config_js_1.resolveConfig)().auditOnly) {
457
+ const identity = {
458
+ kind: "none",
459
+ subAgentType: options.subAgentType,
460
+ reason: "Configured for audit-only mode; sub-agent identity resolution is a no-op",
461
+ warnings: [],
462
+ };
463
+ client.tracer.record("agent.auth", "skipped", {
464
+ attributes: {
465
+ kind: "subagent_none",
466
+ subAgentType: identity.subAgentType,
467
+ reason: identity.reason,
468
+ auditOnly: true,
469
+ },
470
+ });
471
+ return identity;
472
+ }
453
473
  const env = options.env ?? process.env;
454
474
  const registerFn = options.registerAgentClientFn ?? registerAgentClient;
455
475
  const loadFn = options.loadFn ?? loadSubAgentDynamicCredentials;
package/dist/config.js CHANGED
@@ -290,15 +290,47 @@ function acquireLock() {
290
290
  throw new Error(`Timed out waiting for config lock at ${lockPath}. ` +
291
291
  "Another process may be holding it; remove the file if you are sure no other process is running.");
292
292
  }
293
- sleepSync(LOCK_POLL_MS);
293
+ if (!sleepSync(LOCK_POLL_MS)) {
294
+ // The runtime can't block synchronously (no SharedArrayBuffer /
295
+ // Atomics.wait — e.g. Cloudflare Workers). Rather than hot-spin
296
+ // until the deadline, proceed without the lock: the write itself
297
+ // stays atomic via write-temp + rename.
298
+ return { fd: null };
299
+ }
294
300
  }
295
301
  }
296
302
  }
297
- const SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
303
+ /**
304
+ * Lazily initialized buffer for `Atomics.wait`-based synchronous sleep.
305
+ * `undefined` = not yet probed; `null` = unavailable on this runtime.
306
+ * Never touched at module scope: constructing a SharedArrayBuffer eagerly
307
+ * would make merely importing this module throw on runtimes without it
308
+ * (e.g. Cloudflare Workers).
309
+ */
310
+ let sleepBuf;
311
+ /** Sleep synchronously. Returns false when the runtime can't sleep. */
298
312
  function sleepSync(ms) {
299
- Atomics.wait(SLEEP_BUF, 0, 0, ms);
313
+ if (sleepBuf === undefined) {
314
+ try {
315
+ sleepBuf =
316
+ typeof SharedArrayBuffer === "function" &&
317
+ typeof Atomics !== "undefined" &&
318
+ typeof Atomics.wait === "function"
319
+ ? new Int32Array(new SharedArrayBuffer(4))
320
+ : null;
321
+ }
322
+ catch {
323
+ sleepBuf = null;
324
+ }
325
+ }
326
+ if (sleepBuf === null)
327
+ return false;
328
+ Atomics.wait(sleepBuf, 0, 0, ms);
329
+ return true;
300
330
  }
301
331
  function releaseLock(lock) {
332
+ if (lock.fd === null)
333
+ return; // lock-less fallback: nothing to release
302
334
  try {
303
335
  fs.closeSync(lock.fd);
304
336
  }
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ export { checkAndDecide, applyPermissionMode, gateToolCall, type PermissionDecis
19
19
  export { HARNESS_TOOL_CATALOG, KNOWN_HARNESSES, ALL_TOOLS, getToolCatalog, INTERACTIVE_TOOL_CATALOG, getInteractiveToolCatalog, isInteractiveTool, type KnownHarness, } from "./tool-catalog.js";
20
20
  export { classifyLifecycle, isUserFacingPhase, isToolExecutionPhase, HARNESS_LIFECYCLE_MAP, USER_FACING_PHASES, TOOL_EXECUTION_PHASES, type LifecyclePhase, } from "./lifecycle.js";
21
21
  export { parseClaudeCodeMcpTool, parseGeminiMcpTool, parseMcpToolGeneric, checkMcpPermission, type McpToolIdentifier, type McpPermissionCheckOptions, type McpPermissionResult, } from "./mcp.js";
22
- export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
22
+ export { resolveUserSubject, runWithUserSubject, subjectLabel, type UserSubjectRef, } from "./subject.js";
23
23
  export { formatDenialMessage, formatDenialSummary, formatAlertMessage, formatAlertSummary, alertAttributes, OryDenialError, type DenialContext, type AlertAttributes, } from "./denial.js";
24
24
  export { summarizeToolInput, summarizeToolOutput, type ToolInputSummary, type ToolOutputSummary, } from "./tool-metadata.js";
25
25
  export { OtlpExporter, otlpExporterFromEnv, parseKeyValueList, type SpanExporter, type OtlpExporterOptions, type OtlpProtocol, } from "./otel/index.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.printOryConfig = exports.runAgentCommand = exports.runConfigureCommand = exports.AGENT_TOKEN_EXPIRY_SKEW_SEC = exports.clearSubAgentDynamicCredentials = exports.saveSubAgentDynamicCredentials = exports.loadSubAgentDynamicCredentials = exports.clearAgentDynamicCredentials = exports.saveAgentDynamicCredentials = exports.loadAgentDynamicCredentials = exports.registerAgentClient = exports.fetchClientCredentialsToken = exports.ensureSubAgentIdentity = exports.ensureAgentIdentity = exports.resolveAgentCredentials = exports.ensureAuthenticated = exports.ensureUserAuthenticated = exports.TOKEN_EXPIRY_SKEW_SEC = exports.waitForPeerTokensSync = exports.waitForPeerTokens = exports.clearPkceFlightLock = exports.tryAcquirePkceFlightLock = exports.refreshAndSave = exports.isExpired = exports.clearTokens = exports.saveTokens = exports.loadTokens = exports.DEFAULT_LOGIN_TIMEOUT_MS = exports.LOOPBACK_PORTS = exports.buildAuthorizeUrl = exports.sha256Base64Url = exports.generateCodeVerifier = exports.detectHeadless = exports.refreshAccessToken = exports.pkceLogin = exports.getHarnessDataDir = exports.getDataDir = exports.getConfigPath = exports.mutateConfig = exports.resolveConfig = exports.saveConfig = exports.loadConfig = exports.watchTraceFile = exports.formatSpan = exports.deriveTraceId = exports.ActiveSpan = exports.Tracer = exports.redactLogData = exports.DebugLogger = exports.OryAgentClient = void 0;
4
4
  exports.runLocalCommand = exports.ORY_COMMAND_SLUGS = exports.ORY_COMMAND_SKILL_NAMES = exports.ORY_SKILL_NAMES = exports.removeSkillDirs = exports.writeSkillTree = exports.toSkillMarkdown = exports.commandToPlainMarkdown = exports.commandToFrontmatterMarkdown = exports.commandToToml = exports.commandToSkill = exports.renderOryCommands = exports.renderOrySkills = exports.runDevLauncher = exports.unregisterPlugin = exports.registerPlugin = exports.removeMcpServer = exports.mergeMcpServer = exports.mcpServerEntry = exports.resolveMcpServerCommand = exports.printNextSteps = exports.printSetupHelp = exports.removeFlatHooks = exports.mergeFlatHooks = exports.flatHookEntry = exports.removeMatcherHooks = exports.mergeMatcherHooks = exports.matcherHookEntry = exports.resolveHookCommand = exports.isOryHookCommand = exports.writeJsonFile = exports.readJsonFile = exports.parseSetupArgs = exports.printPermissionsSection = exports.printAgentIdentitySection = exports.printUserIdentitySection = exports.runStatusCommand = exports.printPermissionsOnboardingHelp = exports.maybeAutoBootstrap = exports.isUserIdentityCached = exports.runPermissionsCommand = exports.interactiveConfigPrompt = exports.promptForProjectUrl = exports.promptOnTty = exports.isTtyAvailable = exports.runWatchCommand = exports.printTraceTail = exports.printEnvHelp = exports.printLogTail = exports.printEnvironment = void 0;
5
- exports.wrapTool = exports.registerSubagent = exports.complete = exports.gate = exports.sessionStart = exports.resolveNamespace = exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = exports.isToolExecutionPhase = exports.isUserFacingPhase = exports.classifyLifecycle = exports.isInteractiveTool = exports.getInteractiveToolCatalog = exports.INTERACTIVE_TOOL_CATALOG = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.gateToolCall = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
5
+ exports.wrapTool = exports.registerSubagent = exports.complete = exports.gate = exports.sessionStart = exports.resolveNamespace = exports.parseKeyValueList = exports.otlpExporterFromEnv = exports.OtlpExporter = exports.summarizeToolOutput = exports.summarizeToolInput = exports.OryDenialError = exports.alertAttributes = exports.formatAlertSummary = exports.formatAlertMessage = exports.formatDenialSummary = exports.formatDenialMessage = exports.subjectLabel = exports.runWithUserSubject = exports.resolveUserSubject = exports.checkMcpPermission = exports.parseMcpToolGeneric = exports.parseGeminiMcpTool = exports.parseClaudeCodeMcpTool = exports.TOOL_EXECUTION_PHASES = exports.USER_FACING_PHASES = exports.HARNESS_LIFECYCLE_MAP = exports.isToolExecutionPhase = exports.isUserFacingPhase = exports.classifyLifecycle = exports.isInteractiveTool = exports.getInteractiveToolCatalog = exports.INTERACTIVE_TOOL_CATALOG = exports.getToolCatalog = exports.ALL_TOOLS = exports.KNOWN_HARNESSES = exports.HARNESS_TOOL_CATALOG = exports.gateToolCall = exports.applyPermissionMode = exports.checkAndDecide = exports.runRegistryCommand = exports.DEV_JAEGER_CONTAINER = exports.stopDevJaeger = exports.ensureDevJaeger = void 0;
6
6
  var client_js_1 = require("./client.js");
7
7
  Object.defineProperty(exports, "OryAgentClient", { enumerable: true, get: function () { return client_js_1.OryAgentClient; } });
8
8
  var logger_js_1 = require("./logger.js");
@@ -149,6 +149,7 @@ Object.defineProperty(exports, "parseMcpToolGeneric", { enumerable: true, get: f
149
149
  Object.defineProperty(exports, "checkMcpPermission", { enumerable: true, get: function () { return mcp_js_1.checkMcpPermission; } });
150
150
  var subject_js_1 = require("./subject.js");
151
151
  Object.defineProperty(exports, "resolveUserSubject", { enumerable: true, get: function () { return subject_js_1.resolveUserSubject; } });
152
+ Object.defineProperty(exports, "runWithUserSubject", { enumerable: true, get: function () { return subject_js_1.runWithUserSubject; } });
152
153
  Object.defineProperty(exports, "subjectLabel", { enumerable: true, get: function () { return subject_js_1.subjectLabel; } });
153
154
  var denial_js_1 = require("./denial.js");
154
155
  Object.defineProperty(exports, "formatDenialMessage", { enumerable: true, get: function () { return denial_js_1.formatDenialMessage; } });
package/dist/lifecycle.js CHANGED
@@ -197,6 +197,7 @@ exports.HARNESS_LIFECYCLE_MAP = {
197
197
  "execute.after": "tool.after",
198
198
  },
199
199
  "claude-agent-sdk": {
200
+ SessionStart: "session.start",
200
201
  PreToolUse: "tool.before",
201
202
  PostToolUse: "tool.after",
202
203
  },
@@ -12,8 +12,15 @@
12
12
  * return `{ kind: "observe", … }` so the caller allows the
13
13
  * tool through.
14
14
  * - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
15
- * 3. On a thrown {@link OryError} (network_error, rate_limited, etc.),
16
- * returns `{ kind: "fail_open", … }`. Callers should log and allow.
15
+ * 3. On a thrown {@link OryError}, inspects the classified code:
16
+ * - Infrastructure errors (`network_error`, `rate_limited`,
17
+ * `not_found`, `unknown`) — returns `{ kind: "fail_open", … }`.
18
+ * Callers should log and allow.
19
+ * - Auth-rejected check calls (`forbidden`, `session_inactive`,
20
+ * `session_aal2_required` — the check itself was rejected, so
21
+ * its result cannot be trusted) — in `enforce` mode returns
22
+ * `{ kind: "deny", … }` so the caller blocks; in `observe`
23
+ * mode falls back to `fail_open` (observe never blocks).
17
24
  *
18
25
  * The discriminated `kind` lets plugins map cleanly to their native
19
26
  * decision shape (claude-code exit codes, openclaw `{ block: true }`,
@@ -22,7 +29,7 @@
22
29
  */
23
30
  import type { OryAgentClient } from "./client.js";
24
31
  import { type PermissionMode } from "./config.js";
25
- import type { OryError, PermissionCheck, PermissionResult } from "./types.js";
32
+ import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "./types.js";
26
33
  /**
27
34
  * Attributes describing *what was checked* and *under which posture*.
28
35
  * Plugins spread this onto their `tool.invoke` / `tool.block` spans so
@@ -36,6 +43,13 @@ export interface DecisionSpanAttributes {
36
43
  subjectId?: string;
37
44
  /** SubjectSet rendered as `<namespace>:<object>#<relation>` when used. */
38
45
  subjectSet?: string;
46
+ /**
47
+ * Set when the check call itself was auth-rejected and enforce mode
48
+ * turned that into a deny — the classified {@link OryErrorCode} that
49
+ * caused it. Makes the "denied because the check failed" case
50
+ * distinguishable from a normal Keto deny in the audit trail.
51
+ */
52
+ checkRejected?: OryErrorCode;
39
53
  }
40
54
  export type PermissionDecision = {
41
55
  kind: "allow";
@@ -182,7 +196,9 @@ export interface GateToolCallArgs {
182
196
  export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise<ToolGateOutcome>;
183
197
  /**
184
198
  * Run a permission check and resolve the configured mode against the
185
- * result. Never throws — fail-open scenarios are surfaced as a typed
186
- * `{ kind: "fail_open" }` decision.
199
+ * result. Never throws — infrastructure errors surface as a typed
200
+ * `{ kind: "fail_open" }` decision, and auth-rejected check calls in
201
+ * enforce mode as `{ kind: "deny" }` (see
202
+ * {@link CHECK_AUTH_REJECTED_CODES}).
187
203
  */
188
204
  export declare function checkAndDecide(client: OryAgentClient, check: PermissionCheck, opts?: CheckAndDecideOptions): Promise<PermissionDecision>;
@@ -13,8 +13,15 @@
13
13
  * return `{ kind: "observe", … }` so the caller allows the
14
14
  * tool through.
15
15
  * - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
16
- * 3. On a thrown {@link OryError} (network_error, rate_limited, etc.),
17
- * returns `{ kind: "fail_open", … }`. Callers should log and allow.
16
+ * 3. On a thrown {@link OryError}, inspects the classified code:
17
+ * - Infrastructure errors (`network_error`, `rate_limited`,
18
+ * `not_found`, `unknown`) — returns `{ kind: "fail_open", … }`.
19
+ * Callers should log and allow.
20
+ * - Auth-rejected check calls (`forbidden`, `session_inactive`,
21
+ * `session_aal2_required` — the check itself was rejected, so
22
+ * its result cannot be trusted) — in `enforce` mode returns
23
+ * `{ kind: "deny", … }` so the caller blocks; in `observe`
24
+ * mode falls back to `fail_open` (observe never blocks).
18
25
  *
19
26
  * The discriminated `kind` lets plugins map cleanly to their native
20
27
  * decision shape (claude-code exit codes, openclaw `{ block: true }`,
@@ -27,6 +34,19 @@ exports.gateToolCall = gateToolCall;
27
34
  exports.checkAndDecide = checkAndDecide;
28
35
  const config_js_1 = require("./config.js");
29
36
  const tool_catalog_js_1 = require("./tool-catalog.js");
37
+ /**
38
+ * Error codes meaning the check call itself was rejected for
39
+ * authentication/authorization reasons — an expired or misconfigured
40
+ * agent credential, an inactive session, or a missing MFA step. The
41
+ * check result cannot be trusted, so `enforce` mode denies instead of
42
+ * failing open. Infrastructure errors (`network_error`, `rate_limited`,
43
+ * `not_found`, `unknown`) keep the fail-open posture.
44
+ */
45
+ const CHECK_AUTH_REJECTED_CODES = new Set([
46
+ "forbidden",
47
+ "session_inactive",
48
+ "session_aal2_required",
49
+ ]);
30
50
  function formatSubjectSet(set) {
31
51
  if (!set)
32
52
  return undefined;
@@ -140,8 +160,10 @@ async function gateToolCall(client, args) {
140
160
  }
141
161
  /**
142
162
  * Run a permission check and resolve the configured mode against the
143
- * result. Never throws — fail-open scenarios are surfaced as a typed
144
- * `{ kind: "fail_open" }` decision.
163
+ * result. Never throws — infrastructure errors surface as a typed
164
+ * `{ kind: "fail_open" }` decision, and auth-rejected check calls in
165
+ * enforce mode as `{ kind: "deny" }` (see
166
+ * {@link CHECK_AUTH_REJECTED_CODES}).
145
167
  */
146
168
  async function checkAndDecide(client, check, opts = {}) {
147
169
  const mode = opts.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
@@ -153,7 +175,25 @@ async function checkAndDecide(client, check, opts = {}) {
153
175
  });
154
176
  }
155
177
  catch (err) {
156
- return { kind: "fail_open", error: err, mode, spanAttributes };
178
+ const oryErr = err;
179
+ if (mode === "enforce" && oryErr && CHECK_AUTH_REJECTED_CODES.has(oryErr.code)) {
180
+ // The check call itself was rejected for auth reasons — the
181
+ // result cannot be trusted, so enforce mode must not fail open.
182
+ client.logger.warn("permission.check_rejected", {
183
+ namespace: check.namespace,
184
+ object: check.object,
185
+ relation: check.relation,
186
+ code: oryErr.code,
187
+ note: `permission check rejected (${oryErr.code}); enforce mode denies when checks cannot be completed`,
188
+ });
189
+ return {
190
+ kind: "deny",
191
+ result: { allowed: false, checkedAt: new Date().toISOString(), check },
192
+ mode: "enforce",
193
+ spanAttributes: { ...spanAttributes, checkRejected: oryErr.code },
194
+ };
195
+ }
196
+ return { kind: "fail_open", error: oryErr, mode, spanAttributes };
157
197
  }
158
198
  const inner = applyPermissionMode(client, result.allowed, {
159
199
  namespace: check.namespace,
package/dist/subject.d.ts CHANGED
@@ -27,7 +27,19 @@ export type UserSubjectRef = {
27
27
  };
28
28
  };
29
29
  /**
30
- * Resolve the user subject for permission checks. Prefers the user
30
+ * Run `fn` with a per-call user subject that takes precedence over the
31
+ * client's user principal and the env overrides in
32
+ * {@link resolveUserSubject}. The override only applies within `fn`'s
33
+ * async context, so concurrent calls cannot observe each other's subject.
34
+ * `ORY_USER_SUBJECT_NAMESPACE` SubjectSet shaping still applies.
35
+ *
36
+ * A missing/empty `subject` is a no-op: `fn` runs with the normal
37
+ * resolution chain.
38
+ */
39
+ export declare function runWithUserSubject<T>(subject: string | undefined, fn: () => T): T;
40
+ /**
41
+ * Resolve the user subject for permission checks. Prefers a per-call
42
+ * override installed via {@link runWithUserSubject}, then the user
31
43
  * login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
32
44
  * legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
33
45
  * (typically `session:<id>`).
package/dist/subject.js CHANGED
@@ -18,10 +18,36 @@
18
18
  * falls back to the direct SubjectID chain.
19
19
  */
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.runWithUserSubject = runWithUserSubject;
21
22
  exports.resolveUserSubject = resolveUserSubject;
22
23
  exports.subjectLabel = subjectLabel;
24
+ const node_async_hooks_1 = require("node:async_hooks");
23
25
  /**
24
- * Resolve the user subject for permission checks. Prefers the user
26
+ * Per-async-context user-subject override. Populated by
27
+ * {@link runWithUserSubject} so multi-user server integrations (Vercel AI
28
+ * SDK, Cloudflare Agents) can attribute each request to its own acting
29
+ * user without mutating the shared client or process env — both of which
30
+ * would race under concurrent requests.
31
+ */
32
+ const perCallUserSubject = new node_async_hooks_1.AsyncLocalStorage();
33
+ /**
34
+ * Run `fn` with a per-call user subject that takes precedence over the
35
+ * client's user principal and the env overrides in
36
+ * {@link resolveUserSubject}. The override only applies within `fn`'s
37
+ * async context, so concurrent calls cannot observe each other's subject.
38
+ * `ORY_USER_SUBJECT_NAMESPACE` SubjectSet shaping still applies.
39
+ *
40
+ * A missing/empty `subject` is a no-op: `fn` runs with the normal
41
+ * resolution chain.
42
+ */
43
+ function runWithUserSubject(subject, fn) {
44
+ if (!subject)
45
+ return fn();
46
+ return perCallUserSubject.run(subject, fn);
47
+ }
48
+ /**
49
+ * Resolve the user subject for permission checks. Prefers a per-call
50
+ * override installed via {@link runWithUserSubject}, then the user
25
51
  * login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
26
52
  * legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
27
53
  * (typically `session:<id>`).
@@ -30,7 +56,8 @@ exports.subjectLabel = subjectLabel;
30
56
  * concrete subject is available; otherwise a direct SubjectID.
31
57
  */
32
58
  function resolveUserSubject(client, fallback) {
33
- const subject = client.userPrincipal.subject
59
+ const subject = perCallUserSubject.getStore()
60
+ ?? client.userPrincipal.subject
34
61
  ?? process.env.ORY_USER_SUBJECT_ID
35
62
  ?? process.env.ORY_AGENT_SUBJECT_ID
36
63
  ?? fallback;
@@ -16,6 +16,7 @@
16
16
  */
17
17
  export declare const HARNESS_TOOL_CATALOG: {
18
18
  readonly "claude-code": readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "TodoWrite", "Task"];
19
+ readonly "claude-agent-sdk": readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "TodoWrite", "Task"];
19
20
  readonly codex: readonly ["shell", "apply_patch", "read_file"];
20
21
  readonly "gemini-cli": readonly ["read_file", "write_file", "edit_file", "list_files", "search_files", "shell", "web_search"];
21
22
  readonly openclaw: readonly ["execute_command", "read_file", "write_file", "list_directory", "search_files", "browser"];
@@ -34,6 +34,22 @@ exports.HARNESS_TOOL_CATALOG = {
34
34
  "TodoWrite",
35
35
  "Task",
36
36
  ],
37
+ // The Claude Agent SDK exposes the same built-in tool surface as Claude
38
+ // Code (it is the embeddable form of the same runtime), so the catalog
39
+ // mirrors claude-code's and `permissions bootstrap` can seed it.
40
+ "claude-agent-sdk": [
41
+ "Read",
42
+ "Write",
43
+ "Edit",
44
+ "Bash",
45
+ "Glob",
46
+ "Grep",
47
+ "WebFetch",
48
+ "WebSearch",
49
+ "NotebookEdit",
50
+ "TodoWrite",
51
+ "Task",
52
+ ],
37
53
  codex: ["shell", "apply_patch", "read_file"],
38
54
  "gemini-cli": [
39
55
  "read_file",
@@ -136,6 +152,9 @@ exports.ALL_TOOLS = Array.from(new Set(Object.values(exports.HARNESS_TOOL_CATALO
136
152
  */
137
153
  exports.INTERACTIVE_TOOL_CATALOG = {
138
154
  "claude-code": ["AskUserQuestion", "ExitPlanMode", "TodoWrite"],
155
+ // Same runtime as Claude Code — the SDK delivers the same
156
+ // user-interaction primitives through PreToolUse.
157
+ "claude-agent-sdk": ["AskUserQuestion", "ExitPlanMode", "TodoWrite"],
139
158
  codex: [],
140
159
  "gemini-cli": [],
141
160
  openclaw: [],
@@ -165,8 +165,13 @@ async function runUserLogin(client, options) {
165
165
  };
166
166
  }
167
167
  }
168
- // 2. Pre-supplied env tokens short-circuit the browser flow (CI, scripted runs).
169
- if (process.env.ORY_SESSION_TOKEN || process.env.ORY_OAUTH2_TOKEN) {
168
+ // 2. Pre-supplied env tokens short-circuit the browser flow (CI, scripted
169
+ // runs). ORY_USER_* are the documented names; the legacy single-identity
170
+ // ORY_SESSION_TOKEN / ORY_OAUTH2_TOKEN are still honored.
171
+ if (process.env.ORY_USER_SESSION_TOKEN ||
172
+ process.env.ORY_USER_OAUTH2_TOKEN ||
173
+ process.env.ORY_SESSION_TOKEN ||
174
+ process.env.ORY_OAUTH2_TOKEN) {
170
175
  return {
171
176
  proceed: true,
172
177
  mode: "env_token",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",