@ory/argus 0.9.1 → 0.11.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.
@@ -57,6 +57,7 @@ exports.localEnv = localEnv;
57
57
  exports.localConfigure = localConfigure;
58
58
  const fs = __importStar(require("node:fs"));
59
59
  const path = __importStar(require("node:path"));
60
+ const crypto = __importStar(require("node:crypto"));
60
61
  const node_child_process_1 = require("node:child_process");
61
62
  const configs_js_1 = require("./configs.js");
62
63
  const health_js_1 = require("./health.js");
@@ -196,28 +197,86 @@ function dumpCapturedOutput(label, result) {
196
197
  process.stderr.write(`── end ${label} output ──\n\n`);
197
198
  }
198
199
  // ─── Write config files ────────────────────────────────────────────
200
+ /**
201
+ * Render the full set of local-stack config files as `{ relPath, content }`
202
+ * pairs. Pure function of the code, `projectRoot`, and `consoleCfg` — no I/O.
203
+ * `writeConfigs` writes these and `configFingerprint` hashes them, so the
204
+ * on-disk stack and its change-detection signal never drift.
205
+ */
206
+ function renderConfigs(projectRoot, consoleCfg) {
207
+ return [
208
+ {
209
+ relPath: "docker-compose.yml",
210
+ content: (0, configs_js_1.dockerComposeYaml)(projectRoot, consoleCfg),
211
+ },
212
+ { relPath: "kratos/kratos.yml", content: (0, configs_js_1.kratosConfigYaml)() },
213
+ {
214
+ relPath: "kratos/identity-schema.json",
215
+ content: (0, configs_js_1.kratosIdentitySchema)(),
216
+ },
217
+ { relPath: "keto/keto.yml", content: (0, configs_js_1.ketoConfigYaml)() },
218
+ { relPath: "hydra/hydra.yml", content: (0, configs_js_1.hydraConfigYaml)() },
219
+ // nginx config is always emitted — the gateway is no longer optional.
220
+ { relPath: "nginx/nginx.conf", content: (0, configs_js_1.nginxConf)() },
221
+ ];
222
+ }
223
+ /**
224
+ * Stable content hash of the rendered config set. Used to decide whether an
225
+ * already-running stack still matches the config the current code would
226
+ * generate; a change (e.g. the login-UI port, a service env var) invalidates
227
+ * the fast-path short-circuit so the stack is reconciled instead of reused.
228
+ */
229
+ function configFingerprint(projectRoot, consoleCfg) {
230
+ const hash = crypto.createHash("sha256");
231
+ for (const { relPath, content } of renderConfigs(projectRoot, consoleCfg)) {
232
+ hash.update(relPath);
233
+ hash.update("\0");
234
+ hash.update(content);
235
+ hash.update("\0");
236
+ }
237
+ return hash.digest("hex");
238
+ }
239
+ const CONFIG_FINGERPRINT_FILE = ".config-fingerprint";
240
+ /** Read the fingerprint recorded after the last successful bring-up. */
241
+ function readConfigFingerprint(localDir) {
242
+ try {
243
+ return fs
244
+ .readFileSync(path.join(localDir, CONFIG_FINGERPRINT_FILE), "utf8")
245
+ .trim();
246
+ }
247
+ catch {
248
+ return undefined;
249
+ }
250
+ }
251
+ /** Record the fingerprint of the config the running stack was brought up with. */
252
+ function writeConfigFingerprint(localDir, fingerprint) {
253
+ try {
254
+ fs.writeFileSync(path.join(localDir, CONFIG_FINGERPRINT_FILE), fingerprint + "\n");
255
+ }
256
+ catch {
257
+ // Best-effort: a missing fingerprint only costs a reconcile next launch.
258
+ }
259
+ }
199
260
  function writeConfigs(localDir, projectRoot, consoleCfg) {
200
261
  ensureDir(localDir);
201
262
  ensureDir(path.join(localDir, "kratos"));
202
263
  ensureDir(path.join(localDir, "keto"));
203
264
  ensureDir(path.join(localDir, "hydra"));
204
265
  ensureDir(path.join(localDir, "nginx"));
205
- fs.writeFileSync(path.join(localDir, "docker-compose.yml"), (0, configs_js_1.dockerComposeYaml)(projectRoot, consoleCfg));
206
- fs.writeFileSync(path.join(localDir, "kratos", "kratos.yml"), (0, configs_js_1.kratosConfigYaml)());
207
- fs.writeFileSync(path.join(localDir, "kratos", "identity-schema.json"), (0, configs_js_1.kratosIdentitySchema)());
208
- fs.writeFileSync(path.join(localDir, "keto", "keto.yml"), (0, configs_js_1.ketoConfigYaml)());
209
- fs.writeFileSync(path.join(localDir, "hydra", "hydra.yml"), (0, configs_js_1.hydraConfigYaml)());
210
- // nginx config is always emitted — the gateway is no longer optional.
211
- fs.writeFileSync(path.join(localDir, "nginx", "nginx.conf"), (0, configs_js_1.nginxConf)());
266
+ for (const { relPath, content } of renderConfigs(projectRoot, consoleCfg)) {
267
+ fs.writeFileSync(path.join(localDir, relPath), content);
268
+ }
212
269
  }
213
270
  /**
214
271
  * Bring up the local Ory stack and wait for the gateway to be healthy.
215
272
  *
216
273
  * Idempotent and fail-graceful: returns a typed result instead of exiting.
217
- * If the gateway is already healthy the function short-circuits without
218
- * touching Docker. When `quiet` is set, progress is logged to stderr only
219
- * (suitable for the dev launcher); otherwise it prints to stdout in the
220
- * format `localUp` historically used.
274
+ * If the gateway is already healthy AND the running stack's config matches
275
+ * what the current code generates (verified by a config fingerprint), the
276
+ * function short-circuits without touching Docker; otherwise it regenerates
277
+ * the config and runs `docker compose up -d --wait` to reconcile. When
278
+ * `quiet` is set, progress is logged to stderr only (suitable for the dev
279
+ * launcher); otherwise it prints to stdout in the format `localUp` historically used.
221
280
  */
222
281
  async function ensureLocalOryStack(opts = {}) {
223
282
  const localDir = opts.localDir ?? getLocalDir();
@@ -232,13 +291,25 @@ async function ensureLocalOryStack(opts = {}) {
232
291
  }
233
292
  const gatewayCfg = (0, configs_js_1.getGatewayConfig)();
234
293
  const healthUrl = `${configs_js_1.GATEWAY_URL}${gatewayCfg.healthPath}`;
235
- // Short-circuit if the gateway is already serving keeps repeated dev
236
- // launches fast and avoids re-pulling/restarting healthy containers.
237
- // Skip the short-circuit when ORY_CONSOLE_LITE_PATH is explicitly set so
238
- // a path change on a subsequent run actually triggers a rebuild (compose
239
- // diffs the regenerated YAML and recreates the console when needed).
294
+ // Short-circuit if the gateway is already serving AND the running stack was
295
+ // brought up with the same config the current code would generate — keeps
296
+ // repeated dev launches fast without re-pulling/restarting healthy
297
+ // containers. Gateway health alone is not enough: a config change to any
298
+ // service (e.g. the login-UI port, a service env var) leaves the gateway
299
+ // healthy while the rest of the stack runs stale config, so we also require
300
+ // the config fingerprint to match. On a mismatch we fall through and let
301
+ // `docker compose up -d --wait` diff the regenerated YAML and recreate only
302
+ // the changed containers.
303
+ //
304
+ // Skip the short-circuit entirely when ORY_CONSOLE_LITE_PATH is explicitly
305
+ // set so a rebuild is forced even when the path is unchanged (the console
306
+ // source tree behind the path may have changed, which the fingerprint of the
307
+ // generated config can't see).
240
308
  const explicitConsoleLiteOverride = (process.env.ORY_CONSOLE_LITE_PATH ?? "").trim() !== "";
309
+ const fingerprint = configFingerprint(projectRoot, consoleCfg);
310
+ const configUnchanged = readConfigFingerprint(localDir) === fingerprint;
241
311
  if (!explicitConsoleLiteOverride &&
312
+ configUnchanged &&
242
313
  (await isGatewayHealthy(1_500, healthUrl))) {
243
314
  return { status: "already-running", gatewayUrl: configs_js_1.GATEWAY_URL, localDir };
244
315
  }
@@ -320,6 +391,10 @@ async function ensureLocalOryStack(opts = {}) {
320
391
  detail: "Gateway did not become healthy within 2 minutes.",
321
392
  };
322
393
  }
394
+ // Record the fingerprint of the config the stack was actually brought up
395
+ // with (consoleCfg may have fallen back to disabled above), so the next
396
+ // launch can short-circuit only while the config is unchanged.
397
+ writeConfigFingerprint(localDir, configFingerprint(projectRoot, consoleCfg));
323
398
  return { status: "started", gatewayUrl: configs_js_1.GATEWAY_URL, localDir };
324
399
  }
325
400
  async function localUp(opts = {}) {
@@ -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";
@@ -118,12 +132,20 @@ export interface ApplyPermissionModeContext {
118
132
  */
119
133
  export declare function applyPermissionMode(client: OryAgentClient, allowed: boolean, context?: ApplyPermissionModeContext): ModeDecision;
120
134
  /**
121
- * Outcome of {@link gateToolCall}. Either the tool is a user-interaction
135
+ * Outcome of {@link gateToolCall}. Either Ory is disabled entirely
136
+ * (`config.auditOnly` kill switch), the tool is a user-interaction
122
137
  * primitive (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, …) and we
123
138
  * pass through with a single audit span, or it's a real tool execution
124
139
  * and the caller gets the standard {@link PermissionDecision}.
125
140
  */
126
141
  export type ToolGateOutcome = {
142
+ kind: "audit_only";
143
+ /** Attributes to attach to the caller's pass-through trace, if any. */
144
+ spanAttributes: {
145
+ auditOnly: true;
146
+ toolName: string;
147
+ };
148
+ } | {
127
149
  kind: "interactive";
128
150
  /** Attributes to attach to the caller's pass-through trace, if any. */
129
151
  spanAttributes: {
@@ -147,8 +169,12 @@ export interface GateToolCallArgs {
147
169
  modeOverride?: PermissionMode;
148
170
  }
149
171
  /**
150
- * Single entry point for the pre-tool-use gate. Splits the harness's
151
- * incoming "tool" into two semantic categories:
172
+ * Single entry point for the pre-tool-use gate. Applies the
173
+ * `config.auditOnly` kill switch first when set, Ory is disabled
174
+ * entirely and the caller gets `{ kind: "audit_only" }` back without any
175
+ * permission check (record the invocation as an audit span and proceed).
176
+ * Otherwise splits the harness's incoming "tool" into two semantic
177
+ * categories:
152
178
  *
153
179
  * - **Interactive** — the tool surfaces UI to the user
154
180
  * (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, plus anything
@@ -170,7 +196,9 @@ export interface GateToolCallArgs {
170
196
  export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise<ToolGateOutcome>;
171
197
  /**
172
198
  * Run a permission check and resolve the configured mode against the
173
- * result. Never throws — fail-open scenarios are surfaced as a typed
174
- * `{ 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}).
175
203
  */
176
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;
@@ -78,8 +98,12 @@ function applyPermissionMode(client, allowed, context = {}) {
78
98
  return { kind: "deny", mode: "enforce", spanAttributes };
79
99
  }
80
100
  /**
81
- * Single entry point for the pre-tool-use gate. Splits the harness's
82
- * incoming "tool" into two semantic categories:
101
+ * Single entry point for the pre-tool-use gate. Applies the
102
+ * `config.auditOnly` kill switch first when set, Ory is disabled
103
+ * entirely and the caller gets `{ kind: "audit_only" }` back without any
104
+ * permission check (record the invocation as an audit span and proceed).
105
+ * Otherwise splits the harness's incoming "tool" into two semantic
106
+ * categories:
83
107
  *
84
108
  * - **Interactive** — the tool surfaces UI to the user
85
109
  * (`AskUserQuestion`, `ExitPlanMode`, `TodoWrite`, plus anything
@@ -99,6 +123,17 @@ function applyPermissionMode(client, allowed, context = {}) {
99
123
  * semantics; the new `interactive` kind means "do nothing else."
100
124
  */
101
125
  async function gateToolCall(client, args) {
126
+ if ((0, config_js_1.resolveConfig)().auditOnly) {
127
+ client.logger.debug("tool.audit_only", {
128
+ harness: args.harness,
129
+ toolName: args.toolName,
130
+ note: "audit-only mode — skipping permission check",
131
+ });
132
+ return {
133
+ kind: "audit_only",
134
+ spanAttributes: { auditOnly: true, toolName: args.toolName },
135
+ };
136
+ }
102
137
  if ((0, tool_catalog_js_1.isInteractiveTool)(args.harness, args.toolName)) {
103
138
  client.logger.debug("tool.interactive", {
104
139
  harness: args.harness,
@@ -125,8 +160,10 @@ async function gateToolCall(client, args) {
125
160
  }
126
161
  /**
127
162
  * Run a permission check and resolve the configured mode against the
128
- * result. Never throws — fail-open scenarios are surfaced as a typed
129
- * `{ 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}).
130
167
  */
131
168
  async function checkAndDecide(client, check, opts = {}) {
132
169
  const mode = opts.modeOverride ?? (0, config_js_1.resolveConfig)().permissionMode;
@@ -138,7 +175,25 @@ async function checkAndDecide(client, check, opts = {}) {
138
175
  });
139
176
  }
140
177
  catch (err) {
141
- 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 };
142
197
  }
143
198
  const inner = applyPermissionMode(client, result.allowed, {
144
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;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Shared test utilities for Ory agent plugin integration tests.
3
+ *
4
+ * These helpers provide mock Ory API responses, error factories, and
5
+ * common assertion patterns so that harness plugin tests focus on
6
+ * harness-specific behavior rather than duplicating boilerplate.
7
+ */
8
+ import { vi } from "vitest";
9
+ import { OryAgentClient } from "./client.js";
10
+ import type { TraceSpan, TraceEvent } from "./tracer.js";
11
+ export { runHarnessContractSuite, type HarnessContractAdapter, type ContractContext, type ContractGates, type ContractOutcome, } from "./contract-suite.js";
12
+ /**
13
+ * Create an OryAgentClient with session caching disabled.
14
+ * Pass overrides to customize (e.g. a different harness name).
15
+ */
16
+ export declare function createMockClient(overrides?: Partial<ConstructorParameters<typeof OryAgentClient>[0]>): OryAgentClient;
17
+ /**
18
+ * Stub an internal API instance method on the client.
19
+ * Returns a vi.fn mock so callers can assert on calls.
20
+ */
21
+ export declare function stubApi<K extends "frontend" | "oauth2" | "permission" | "relationship">(client: OryAgentClient, api: K, method: string, impl: (...args: unknown[]) => unknown): ReturnType<typeof vi.fn>;
22
+ /** Successful session verification response (wraps Ory API shape). */
23
+ export declare const MOCK_SESSION_RESPONSE: {
24
+ data: {
25
+ id: string;
26
+ active: boolean;
27
+ authenticated_at: string;
28
+ expires_at: string;
29
+ authenticator_assurance_level: string;
30
+ authentication_methods: {
31
+ method: string;
32
+ completed_at: string;
33
+ }[];
34
+ identity: {
35
+ id: string;
36
+ traits: {
37
+ email: string;
38
+ };
39
+ };
40
+ };
41
+ };
42
+ /** Inactive session (same shape, active: false). */
43
+ export declare const MOCK_INACTIVE_SESSION_RESPONSE: {
44
+ data: {
45
+ active: boolean;
46
+ id: string;
47
+ authenticated_at: string;
48
+ expires_at: string;
49
+ authenticator_assurance_level: string;
50
+ authentication_methods: {
51
+ method: string;
52
+ completed_at: string;
53
+ }[];
54
+ identity: {
55
+ id: string;
56
+ traits: {
57
+ email: string;
58
+ };
59
+ };
60
+ };
61
+ };
62
+ /** Successful OAuth2 token introspection response. */
63
+ export declare const MOCK_OAUTH2_RESPONSE: {
64
+ data: {
65
+ active: boolean;
66
+ client_id: string;
67
+ sub: string;
68
+ scope: string;
69
+ aud: string[];
70
+ exp: number;
71
+ iat: number;
72
+ };
73
+ };
74
+ /** Inactive OAuth2 token response. */
75
+ export declare const MOCK_INACTIVE_OAUTH2_RESPONSE: {
76
+ data: {
77
+ active: boolean;
78
+ };
79
+ };
80
+ /** Permission check allowed response. */
81
+ export declare const PERMISSION_ALLOWED: {
82
+ data: {
83
+ allowed: boolean;
84
+ };
85
+ };
86
+ /** Permission check denied response. */
87
+ export declare const PERMISSION_DENIED: {
88
+ data: {
89
+ allowed: boolean;
90
+ };
91
+ };
92
+ /** Batch permission check: both results allowed. */
93
+ export declare const BATCH_BOTH_ALLOWED: {
94
+ data: {
95
+ results: {
96
+ allowed: boolean;
97
+ }[];
98
+ };
99
+ };
100
+ /** Batch permission check: server allowed, tool denied. */
101
+ export declare const BATCH_SERVER_ALLOWED_TOOL_DENIED: {
102
+ data: {
103
+ results: {
104
+ allowed: boolean;
105
+ }[];
106
+ };
107
+ };
108
+ /** Batch permission check: server denied. */
109
+ export declare const BATCH_SERVER_DENIED: {
110
+ data: {
111
+ results: {
112
+ allowed: boolean;
113
+ }[];
114
+ };
115
+ };
116
+ /** Create an Axios-shaped error with a response. */
117
+ export declare function makeAxiosError(status: number, body?: unknown, code?: string): Record<string, unknown>;
118
+ /** Create a Node.js network error (ECONNREFUSED, ETIMEDOUT, etc.). */
119
+ export declare function makeNetworkError(code?: string): NodeJS.ErrnoException;
120
+ /** Axios 429 rate-limit error. */
121
+ export declare function makeRateLimitError(): Record<string, unknown>;
122
+ /** Axios 403 with session_aal2_required error id. */
123
+ export declare function makeMfaRequiredError(): Record<string, unknown>;
124
+ /** Axios 401 with session_inactive error id. */
125
+ export declare function makeSessionInactiveError(): Record<string, unknown>;
126
+ /**
127
+ * Get all recorded trace spans from a client, optionally filtered by event.
128
+ */
129
+ export declare function getTraceSpans(client: OryAgentClient, event?: TraceEvent): TraceSpan[];
130
+ /**
131
+ * Assert the tracer recorded the expected tool names for a given event type.
132
+ * Checks the `toolName` attribute on each span.
133
+ */
134
+ export declare function expectTracedTools(client: OryAgentClient, event: TraceEvent, expectedTools: string[]): void;
135
+ /**
136
+ * Set standard Ory env vars for a configured + authenticated test.
137
+ * Returns a cleanup function that restores the previous state.
138
+ */
139
+ export declare function setOryEnv(overrides?: Partial<{
140
+ projectUrl: string;
141
+ sessionToken: string;
142
+ oauth2Token: string;
143
+ subjectId: string;
144
+ namespace: string;
145
+ /**
146
+ * Permission mode for the test. Defaults to `"enforce"` so existing
147
+ * test assertions that exercise the deny path still see a block.
148
+ * Pass `"observe"` explicitly to exercise the new observe branch.
149
+ */
150
+ permissionMode: "observe" | "enforce";
151
+ }>): () => void;
152
+ /**
153
+ * Point `XDG_CONFIG_HOME` at a fresh temp directory so `resolveConfig()`
154
+ * reads from a known-empty state instead of the developer's real
155
+ * `~/.config/ory-agent-plugins/config.json`. Returns a cleanup function
156
+ * that restores the env var and removes the temp directory.
157
+ *
158
+ * Use `saveConfig(...)` from `./config.js` inside the test to shape the
159
+ * isolated config (e.g. `saveConfig({ auditOnly: true })`).
160
+ */
161
+ export declare function useTempConfigDir(): () => void;
162
+ /**
163
+ * Clear all Ory env vars (simulate unconfigured state).
164
+ */
165
+ export declare function clearOryEnv(): void;
166
+ /** Stub verifySession to succeed. */
167
+ export declare function stubSessionSuccess(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
168
+ /** Stub verifySession to return inactive. */
169
+ export declare function stubSessionInactive(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
170
+ /** Stub verifySession to throw a network error. */
171
+ export declare function stubSessionNetworkError(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
172
+ /** Stub verifySession to throw MFA required. */
173
+ export declare function stubSessionMfaRequired(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
174
+ /** Stub verifySession to throw session_inactive. */
175
+ export declare function stubSessionExpired(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
176
+ /** Stub introspectToken to succeed. */
177
+ export declare function stubOAuth2Success(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
178
+ /** Stub introspectToken to return inactive. */
179
+ export declare function stubOAuth2Inactive(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
180
+ /** Stub checkPermission to allow. */
181
+ export declare function stubPermissionAllowed(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
182
+ /** Stub checkPermission to deny. */
183
+ export declare function stubPermissionDenied(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
184
+ /** Stub checkPermission to throw a network error. */
185
+ export declare function stubPermissionNetworkError(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
186
+ /** Stub checkPermission to throw a rate-limit error. */
187
+ export declare function stubPermissionRateLimited(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
188
+ /**
189
+ * Spy on the public `createRelationship` method and resolve successfully.
190
+ * Prefer this over stubbing the internal relationship API: `setAgentPrincipal`
191
+ * rebuilds the API instances when the agent token changes (e.g. inside
192
+ * `sessionStart`), which silently discards API-level stubs.
193
+ */
194
+ export declare function spyRelationshipCreated(client: OryAgentClient): import("vitest").Mock<(check: import("./types.js").PermissionCheck, options?: {
195
+ spanAttributes?: Record<string, unknown>;
196
+ }) => Promise<{
197
+ created: boolean;
198
+ alreadyExisted: boolean;
199
+ }>>;
200
+ /** Spy on the public `createRelationship` method and reject. */
201
+ export declare function spyRelationshipCreateError(client: OryAgentClient): import("vitest").Mock<(check: import("./types.js").PermissionCheck, options?: {
202
+ spanAttributes?: Record<string, unknown>;
203
+ }) => Promise<{
204
+ created: boolean;
205
+ alreadyExisted: boolean;
206
+ }>>;
207
+ /** Stub both checkPermission (server allow) and batchCheckPermission (both allow). */
208
+ export declare function stubMcpAllowed(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
209
+ /** Stub checkPermission to deny (server-only MCP check). */
210
+ export declare function stubMcpServerDenied(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
211
+ /** Stub batchCheckPermission: server allowed, tool denied. */
212
+ export declare function stubMcpToolDenied(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
213
+ /** Stub checkPermission to throw network error (MCP fail-open). */
214
+ export declare function stubMcpNetworkError(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;