@ory/argus 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Shared adapter primitives for Agent SDK integrations (and harness plugins).
3
+ *
4
+ * These extract the session-start sequencing and the `tool.invoke` / `tool.block` /
5
+ * `tool.complete` span boilerplate that each integration would otherwise re-implement, so
6
+ * an SDK binding is a thin translation between the SDK's hook signature and these calls.
7
+ * They are the TypeScript twin of `ory_argus.adapters` in the Python core.
8
+ *
9
+ * - {@link sessionStart} — run the user + agent auth gates and write the user→agent
10
+ * delegation tuple; returns whether the session may proceed.
11
+ * - {@link gate} — resolve the subject, run {@link gateToolCall}, record the spans, and
12
+ * return a normalized {@link GateResult}.
13
+ * - {@link complete} — record the `tool.complete` span.
14
+ * - {@link registerSubagent} — resolve a sub-agent identity + agent→subagent tuple.
15
+ * - {@link wrapTool} — wrap a plain `execute(args)` with gate+complete, for SDKs whose
16
+ * only veto point is the tool boundary (Vercel AI SDK, etc.).
17
+ *
18
+ * Every primitive is best-effort and fail-open.
19
+ */
20
+ import type { OryAgentClient } from "./client.js";
21
+ import { ensureAgentIdentity, ensureSubAgentIdentity } from "./agent-auth.js";
22
+ import { ensureUserAuthenticated } from "./user-login.js";
23
+ import { type PermissionDecision } from "./permissions.js";
24
+ export declare function resolveNamespace(): string;
25
+ export interface SessionStartResult {
26
+ proceed: boolean;
27
+ userMode: string;
28
+ userReason: string;
29
+ agentKind: string;
30
+ }
31
+ export interface SessionStartOptions {
32
+ harness: string;
33
+ allowBlock?: boolean;
34
+ projectUrl?: string;
35
+ binName?: string;
36
+ /** Injectable gates (tests). */
37
+ userLogin?: typeof ensureUserAuthenticated;
38
+ agentGate?: typeof ensureAgentIdentity;
39
+ }
40
+ /**
41
+ * Run both auth gates and write the user→agent delegation tuple. `allowBlock` is advisory
42
+ * for in-process SDK integrations (the agent can't be prevented from constructing), but the
43
+ * user gate still runs, refreshes tokens, and records the `user.auth` span.
44
+ */
45
+ export declare function sessionStart(client: OryAgentClient, opts: SessionStartOptions): Promise<SessionStartResult>;
46
+ export interface RegisterSubagentOptions {
47
+ harness: string;
48
+ subAgentType: string;
49
+ projectUrl?: string;
50
+ subAgentGate?: typeof ensureSubAgentIdentity;
51
+ }
52
+ export declare function registerSubagent(client: OryAgentClient, opts: RegisterSubagentOptions): Promise<void>;
53
+ export interface GateResult {
54
+ /** False only when the tool was hard-denied in enforce mode. */
55
+ proceed: boolean;
56
+ /** True only for that deny (and only when `canBlock`). */
57
+ blocked: boolean;
58
+ /** Discriminator: allow / observe / deny / fail_open / interactive / audit_only. */
59
+ kind: string;
60
+ decision: ToolGateOutcomeLike;
61
+ subject: string;
62
+ namespace: string;
63
+ denialMessage?: string;
64
+ }
65
+ type ToolGateOutcomeLike = {
66
+ kind: "audit_only";
67
+ spanAttributes: Record<string, unknown>;
68
+ } | {
69
+ kind: "interactive";
70
+ spanAttributes: Record<string, unknown>;
71
+ } | PermissionDecision;
72
+ export interface GateOptions {
73
+ harness: string;
74
+ toolName: string;
75
+ toolArgs?: unknown;
76
+ subjectFallback?: string;
77
+ /** Whether the calling integration can actually stop the tool. */
78
+ canBlock?: boolean;
79
+ extraSpanAttributes?: Record<string, unknown>;
80
+ }
81
+ /**
82
+ * Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
83
+ * Records `tool.invoke` on allow/observe and `tool.block` on deny/observe — identical span
84
+ * semantics to the harness plugins.
85
+ */
86
+ export declare function gate(client: OryAgentClient, opts: GateOptions): Promise<GateResult>;
87
+ export declare function complete(client: OryAgentClient, opts: {
88
+ toolName: string;
89
+ output?: unknown;
90
+ extraSpanAttributes?: Record<string, unknown>;
91
+ status?: "ok" | "error";
92
+ }): void;
93
+ export interface WrapToolOptions {
94
+ harness: string;
95
+ toolName: string;
96
+ canBlock?: boolean;
97
+ }
98
+ /**
99
+ * Wrap a plain `execute(args)` callable with gate + complete. On a hard deny it throws
100
+ * {@link OryDenialError} (the veto mechanism for tool-boundary SDKs like Vercel AI SDK);
101
+ * otherwise the tool runs and `tool.complete` is recorded.
102
+ */
103
+ export declare function wrapTool<A, R>(client: OryAgentClient, opts: WrapToolOptions, execute: (args: A) => R | Promise<R>): (args: A) => Promise<R>;
104
+ export {};
@@ -0,0 +1,201 @@
1
+ "use strict";
2
+ /**
3
+ * Shared adapter primitives for Agent SDK integrations (and harness plugins).
4
+ *
5
+ * These extract the session-start sequencing and the `tool.invoke` / `tool.block` /
6
+ * `tool.complete` span boilerplate that each integration would otherwise re-implement, so
7
+ * an SDK binding is a thin translation between the SDK's hook signature and these calls.
8
+ * They are the TypeScript twin of `ory_argus.adapters` in the Python core.
9
+ *
10
+ * - {@link sessionStart} — run the user + agent auth gates and write the user→agent
11
+ * delegation tuple; returns whether the session may proceed.
12
+ * - {@link gate} — resolve the subject, run {@link gateToolCall}, record the spans, and
13
+ * return a normalized {@link GateResult}.
14
+ * - {@link complete} — record the `tool.complete` span.
15
+ * - {@link registerSubagent} — resolve a sub-agent identity + agent→subagent tuple.
16
+ * - {@link wrapTool} — wrap a plain `execute(args)` with gate+complete, for SDKs whose
17
+ * only veto point is the tool boundary (Vercel AI SDK, etc.).
18
+ *
19
+ * Every primitive is best-effort and fail-open.
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.resolveNamespace = resolveNamespace;
23
+ exports.sessionStart = sessionStart;
24
+ exports.registerSubagent = registerSubagent;
25
+ exports.gate = gate;
26
+ exports.complete = complete;
27
+ exports.wrapTool = wrapTool;
28
+ const config_js_1 = require("./config.js");
29
+ const agent_auth_js_1 = require("./agent-auth.js");
30
+ const user_login_js_1 = require("./user-login.js");
31
+ const permissions_js_1 = require("./permissions.js");
32
+ const subject_js_1 = require("./subject.js");
33
+ const denial_js_1 = require("./denial.js");
34
+ function resolveNamespace() {
35
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
36
+ }
37
+ /**
38
+ * Run both auth gates and write the user→agent delegation tuple. `allowBlock` is advisory
39
+ * for in-process SDK integrations (the agent can't be prevented from constructing), but the
40
+ * user gate still runs, refreshes tokens, and records the `user.auth` span.
41
+ */
42
+ async function sessionStart(client, opts) {
43
+ const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
44
+ const userLogin = opts.userLogin ?? user_login_js_1.ensureUserAuthenticated;
45
+ const agentGate = opts.agentGate ?? agent_auth_js_1.ensureAgentIdentity;
46
+ const decision = await userLogin(client, {
47
+ binName: opts.binName ?? `ory-${opts.harness}`,
48
+ harness: opts.harness,
49
+ allowBlock: opts.allowBlock ?? false,
50
+ });
51
+ const creds = await agentGate(client, {
52
+ projectUrl,
53
+ harness: opts.harness,
54
+ });
55
+ await recordUserDelegatesAgent(client);
56
+ return {
57
+ proceed: decision.proceed,
58
+ userMode: decision.mode,
59
+ userReason: decision.reason,
60
+ agentKind: creds.kind,
61
+ };
62
+ }
63
+ async function recordUserDelegatesAgent(client) {
64
+ const user = client.userPrincipal.subject;
65
+ const agent = client.agentPrincipal.subject;
66
+ if (!user || !agent)
67
+ return;
68
+ try {
69
+ await client.createRelationship({
70
+ namespace: resolveNamespace(),
71
+ object: `agent:${agent}`,
72
+ relation: "delegate",
73
+ subjectId: `user:${user}`,
74
+ }, { spanAttributes: { delegation: "user-to-agent" } });
75
+ }
76
+ catch (err) {
77
+ client.logger.warn("delegation.user_to_agent.failed", {
78
+ message: err instanceof Error ? err.message : String(err),
79
+ });
80
+ }
81
+ }
82
+ async function registerSubagent(client, opts) {
83
+ const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
84
+ const subAgentGate = opts.subAgentGate ?? agent_auth_js_1.ensureSubAgentIdentity;
85
+ let identity;
86
+ try {
87
+ identity = await subAgentGate(client, {
88
+ subAgentType: opts.subAgentType,
89
+ projectUrl,
90
+ harness: opts.harness,
91
+ });
92
+ }
93
+ catch (err) {
94
+ client.logger.warn("subagent.identity.failed", {
95
+ subAgentType: opts.subAgentType,
96
+ message: err instanceof Error ? err.message : String(err),
97
+ });
98
+ return;
99
+ }
100
+ if (identity.kind !== "dynamic" || !identity.subject)
101
+ return;
102
+ const agent = client.agentPrincipal.subject;
103
+ if (!agent)
104
+ return;
105
+ try {
106
+ await client.createRelationship({
107
+ namespace: resolveNamespace(),
108
+ object: `subagent:${identity.subject}`,
109
+ relation: "delegate",
110
+ subjectId: `agent:${agent}`,
111
+ }, { spanAttributes: { delegation: "agent-to-subagent", subAgentType: opts.subAgentType } });
112
+ }
113
+ catch (err) {
114
+ client.logger.warn("delegation.agent_to_subagent.failed", {
115
+ subAgentType: opts.subAgentType,
116
+ message: err instanceof Error ? err.message : String(err),
117
+ });
118
+ }
119
+ }
120
+ /**
121
+ * Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
122
+ * Records `tool.invoke` on allow/observe and `tool.block` on deny/observe — identical span
123
+ * semantics to the harness plugins.
124
+ */
125
+ async function gate(client, opts) {
126
+ const namespace = resolveNamespace();
127
+ const subjectRef = (0, subject_js_1.resolveUserSubject)(client, opts.subjectFallback);
128
+ const label = (0, subject_js_1.subjectLabel)(subjectRef);
129
+ const canBlock = opts.canBlock ?? true;
130
+ const spanAttrs = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
131
+ const outcome = await (0, permissions_js_1.gateToolCall)(client, {
132
+ harness: opts.harness,
133
+ toolName: opts.toolName,
134
+ check: { namespace, object: opts.toolName, relation: "use", ...subjectRef },
135
+ spanAttributes: spanAttrs,
136
+ });
137
+ if (outcome.kind === "audit_only") {
138
+ // Kill switch: Ory is disabled. Audit the invocation, skip the check.
139
+ client.tracer.record("tool.invoke", "ok", {
140
+ attributes: { ...spanAttrs, auditOnly: true },
141
+ });
142
+ return { proceed: true, blocked: false, kind: "audit_only", decision: outcome, subject: label, namespace };
143
+ }
144
+ if (outcome.kind === "interactive") {
145
+ return { proceed: true, blocked: false, kind: "interactive", decision: outcome, subject: label, namespace };
146
+ }
147
+ const decisionAttrs = outcome.spanAttributes;
148
+ if (outcome.kind === "fail_open") {
149
+ client.logger.warn("permission.fail_open", { tool: opts.toolName, code: outcome.error.code });
150
+ return { proceed: true, blocked: false, kind: "fail_open", decision: outcome, subject: label, namespace };
151
+ }
152
+ if (outcome.kind === "allow") {
153
+ client.tracer.record("tool.invoke", "ok", {
154
+ attributes: { ...spanAttrs, ...decisionAttrs, allowed: true },
155
+ });
156
+ return { proceed: true, blocked: false, kind: "allow", decision: outcome, subject: label, namespace };
157
+ }
158
+ if (outcome.kind === "observe") {
159
+ client.tracer.record("tool.block", "denied", {
160
+ attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(false) },
161
+ });
162
+ client.tracer.record("tool.invoke", "ok", {
163
+ attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, observed: true },
164
+ });
165
+ return { proceed: true, blocked: false, kind: "observe", decision: outcome, subject: label, namespace };
166
+ }
167
+ // deny
168
+ client.tracer.record("tool.block", "denied", {
169
+ attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(canBlock) },
170
+ });
171
+ const denialMessage = (0, denial_js_1.formatDenialMessage)({ tool: opts.toolName, subjectId: label, namespace });
172
+ client.logger.warn("tool.denied", { tool: opts.toolName, subjectId: label });
173
+ return { proceed: false, blocked: canBlock, kind: "deny", decision: outcome, subject: label, namespace, denialMessage };
174
+ }
175
+ function complete(client, opts) {
176
+ const attributes = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
177
+ if (opts.output !== undefined)
178
+ attributes.hasOutput = true;
179
+ client.tracer.record("tool.complete", opts.status ?? "ok", { attributes });
180
+ }
181
+ /**
182
+ * Wrap a plain `execute(args)` callable with gate + complete. On a hard deny it throws
183
+ * {@link OryDenialError} (the veto mechanism for tool-boundary SDKs like Vercel AI SDK);
184
+ * otherwise the tool runs and `tool.complete` is recorded.
185
+ */
186
+ function wrapTool(client, opts, execute) {
187
+ return async (args) => {
188
+ const result = await gate(client, {
189
+ harness: opts.harness,
190
+ toolName: opts.toolName,
191
+ toolArgs: args,
192
+ canBlock: opts.canBlock ?? true,
193
+ });
194
+ if (result.blocked) {
195
+ throw new denial_js_1.OryDenialError({ tool: opts.toolName, subjectId: result.subject, namespace: result.namespace });
196
+ }
197
+ const output = await execute(args);
198
+ complete(client, { toolName: opts.toolName, output });
199
+ return output;
200
+ };
201
+ }
@@ -384,6 +384,19 @@ 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.
389
+ if ((0, config_js_1.resolveConfig)().auditOnly) {
390
+ const creds = {
391
+ kind: "none",
392
+ reason: "Configured for audit-only mode; agent identity is a no-op",
393
+ warnings: [],
394
+ };
395
+ client.tracer.record("agent.auth", "skipped", {
396
+ attributes: { kind: creds.kind, reason: creds.reason, auditOnly: true },
397
+ });
398
+ return creds;
399
+ }
387
400
  const resolveFn = options.resolveFn ?? resolveAgentCredentials;
388
401
  let creds;
389
402
  try {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared contract suite for harness plugin tests.
3
+ *
4
+ * Every harness plugin translates the same core decisions (session gates,
5
+ * `gateToolCall`, audit-only) onto its own native signals (exit codes,
6
+ * `{ decision: "block" }`, `{ block: true }`, thrown errors, …). The
7
+ * *decision semantics* are core-owned and tested once in `packages/core`;
8
+ * what varies per harness is only the translation. This suite re-runs the
9
+ * canonical scenario tables through a harness's real entry point via a
10
+ * small adapter, so a plugin cannot ship with a divergent understanding of
11
+ * observe / enforce / fail-open / audit-only — without each plugin
12
+ * re-transcribing the tables.
13
+ *
14
+ * A harness test file calls:
15
+ *
16
+ * ```ts
17
+ * runHarnessContractSuite({
18
+ * harness: "claude-code",
19
+ * tool: "Bash",
20
+ * sessionCanBlock: true,
21
+ * sessionStart: async ({ client, gates }) => {
22
+ * const out = await handleHookEvent(sessionInput(), client, gates);
23
+ * return { blocked: out.decision === "block", reason: out.reason };
24
+ * },
25
+ * toolBefore: async ({ client, gates }, tool) => { ... },
26
+ * });
27
+ * ```
28
+ *
29
+ * and then adds only its genuinely harness-specific tests: export shape,
30
+ * event-name dispatch, block-signal details, input parsing (MCP names,
31
+ * `subagent_type`), unique span enrichment, and the install CLI.
32
+ */
33
+ import { OryAgentClient } from "./client.js";
34
+ import type { ensureUserAuthenticated } from "./user-login.js";
35
+ import type { ensureAgentIdentity, ensureSubAgentIdentity } from "./agent-auth.js";
36
+ /** Normalized outcome of driving one lifecycle phase through the plugin. */
37
+ export interface ContractOutcome {
38
+ /** True when the plugin emitted its native block signal. */
39
+ blocked: boolean;
40
+ /** The human-readable reason carried by the block signal, if any. */
41
+ reason?: string;
42
+ }
43
+ /**
44
+ * Injectable auth gates handed to the plugin under test. The suite swaps
45
+ * their behavior per scenario; the adapter forwards them into the plugin's
46
+ * `deps` parameter (every plugin exposes `userLogin` / `agentGate` /
47
+ * `subAgentGate` injection points per AGENTS.md Step 5).
48
+ */
49
+ export interface ContractGates {
50
+ userLogin: typeof ensureUserAuthenticated;
51
+ agentGate: typeof ensureAgentIdentity;
52
+ subAgentGate: typeof ensureSubAgentIdentity;
53
+ }
54
+ export interface ContractContext {
55
+ client: OryAgentClient;
56
+ gates: ContractGates;
57
+ }
58
+ export interface HarnessContractAdapter {
59
+ /** Harness name — used for the describe label and the client. */
60
+ harness: string;
61
+ /** A real, non-interactive tool from this harness's catalog. */
62
+ tool: string;
63
+ /**
64
+ * Whether the harness's session-start primitive can carry a hard block
65
+ * (subprocess exit-code harnesses: true; in-process advisory: false).
66
+ */
67
+ sessionCanBlock: boolean;
68
+ /** Whether the tool gate can hard-block. Defaults to true. */
69
+ toolCanBlock?: boolean;
70
+ /** Set false when the plugin has no legacy OAuth2-token session path. */
71
+ legacyOAuth2?: boolean;
72
+ /** Drive the session-start phase through the plugin's real entry point. */
73
+ sessionStart(ctx: ContractContext): Promise<ContractOutcome>;
74
+ /** Drive the pre-tool gate for `tool` through the plugin's real entry point. */
75
+ toolBefore(ctx: ContractContext, tool: string): Promise<ContractOutcome>;
76
+ /** Drive the post-tool phase (must record `tool.complete`). Optional. */
77
+ toolAfter?(ctx: ContractContext, tool: string): Promise<void>;
78
+ /** Feed an event the plugin does not model; return the raw response. Optional. */
79
+ unknownEvent?(ctx: ContractContext): Promise<unknown>;
80
+ /** Expected raw response for `unknownEvent` (deep-equal). */
81
+ unknownEventOutput?: unknown;
82
+ }
83
+ /**
84
+ * Run the canonical harness contract scenarios through the adapter.
85
+ * Call once per harness test file, then add harness-specific tests.
86
+ */
87
+ export declare function runHarnessContractSuite(adapter: HarnessContractAdapter): void;
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ /**
3
+ * Shared contract suite for harness plugin tests.
4
+ *
5
+ * Every harness plugin translates the same core decisions (session gates,
6
+ * `gateToolCall`, audit-only) onto its own native signals (exit codes,
7
+ * `{ decision: "block" }`, `{ block: true }`, thrown errors, …). The
8
+ * *decision semantics* are core-owned and tested once in `packages/core`;
9
+ * what varies per harness is only the translation. This suite re-runs the
10
+ * canonical scenario tables through a harness's real entry point via a
11
+ * small adapter, so a plugin cannot ship with a divergent understanding of
12
+ * observe / enforce / fail-open / audit-only — without each plugin
13
+ * re-transcribing the tables.
14
+ *
15
+ * A harness test file calls:
16
+ *
17
+ * ```ts
18
+ * runHarnessContractSuite({
19
+ * harness: "claude-code",
20
+ * tool: "Bash",
21
+ * sessionCanBlock: true,
22
+ * sessionStart: async ({ client, gates }) => {
23
+ * const out = await handleHookEvent(sessionInput(), client, gates);
24
+ * return { blocked: out.decision === "block", reason: out.reason };
25
+ * },
26
+ * toolBefore: async ({ client, gates }, tool) => { ... },
27
+ * });
28
+ * ```
29
+ *
30
+ * and then adds only its genuinely harness-specific tests: export shape,
31
+ * event-name dispatch, block-signal details, input parsing (MCP names,
32
+ * `subagent_type`), unique span enrichment, and the install CLI.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.runHarnessContractSuite = runHarnessContractSuite;
36
+ const vitest_1 = require("vitest");
37
+ const config_js_1 = require("./config.js");
38
+ const testing_js_1 = require("./testing.js");
39
+ const DISABLED_DECISION = {
40
+ proceed: true,
41
+ mode: "disabled",
42
+ reason: "ORY_USER_LOGIN not enabled",
43
+ };
44
+ function makeGates(userDecision = DISABLED_DECISION) {
45
+ return {
46
+ userLogin: vitest_1.vi.fn(async () => userDecision),
47
+ agentGate: vitest_1.vi.fn(async () => ({
48
+ kind: "none",
49
+ reason: "not configured in contract suite",
50
+ warnings: [],
51
+ })),
52
+ subAgentGate: vitest_1.vi.fn(async () => ({
53
+ kind: "none",
54
+ subAgentType: "none",
55
+ reason: "not configured in contract suite",
56
+ warnings: [],
57
+ })),
58
+ };
59
+ }
60
+ const ENV_KEYS = [
61
+ "ORY_PROJECT_URL",
62
+ "ORY_API_KEY",
63
+ "ORY_SESSION_TOKEN",
64
+ "ORY_OAUTH2_TOKEN",
65
+ "ORY_USER_SESSION_TOKEN",
66
+ "ORY_USER_OAUTH2_TOKEN",
67
+ "ORY_USER_LOGIN",
68
+ "ORY_USER_SUBJECT_ID",
69
+ "ORY_USER_SUBJECT_NAMESPACE",
70
+ "ORY_AGENT_SUBJECT_ID",
71
+ "ORY_PERMISSION_MODE",
72
+ "ORY_PERMISSION_NAMESPACE",
73
+ "ORY_INTERACTIVE_TOOLS",
74
+ ];
75
+ /**
76
+ * Run the canonical harness contract scenarios through the adapter.
77
+ * Call once per harness test file, then add harness-specific tests.
78
+ */
79
+ function runHarnessContractSuite(adapter) {
80
+ const toolCanBlock = adapter.toolCanBlock ?? true;
81
+ (0, vitest_1.describe)(`${adapter.harness} — Ory harness contract`, () => {
82
+ let client;
83
+ let restoreConfigDir;
84
+ const savedEnv = {};
85
+ (0, vitest_1.beforeEach)(() => {
86
+ for (const key of ENV_KEYS) {
87
+ savedEnv[key] = process.env[key];
88
+ delete process.env[key];
89
+ }
90
+ restoreConfigDir = (0, testing_js_1.useTempConfigDir)();
91
+ process.env.ORY_PROJECT_URL = "https://test.projects.oryapis.com";
92
+ client = (0, testing_js_1.createMockClient)({ harness: adapter.harness });
93
+ });
94
+ (0, vitest_1.afterEach)(() => {
95
+ restoreConfigDir();
96
+ for (const key of ENV_KEYS) {
97
+ if (savedEnv[key] === undefined)
98
+ delete process.env[key];
99
+ else
100
+ process.env[key] = savedEnv[key];
101
+ }
102
+ });
103
+ const ctx = (gates = makeGates()) => ({ client, gates });
104
+ // ── Session start never blocks on auth/infra failures ─────────
105
+ (0, vitest_1.describe)("session start is fail-open", () => {
106
+ vitest_1.it.each([
107
+ { name: "valid session token", env: "session", stub: testing_js_1.stubSessionSuccess },
108
+ { name: "inactive session", env: "session", stub: testing_js_1.stubSessionInactive },
109
+ { name: "session network error", env: "session", stub: testing_js_1.stubSessionNetworkError },
110
+ { name: "MFA required", env: "session", stub: testing_js_1.stubSessionMfaRequired },
111
+ { name: "valid OAuth2 token", env: "oauth2", stub: testing_js_1.stubOAuth2Success },
112
+ { name: "inactive OAuth2 token", env: "oauth2", stub: testing_js_1.stubOAuth2Inactive },
113
+ { name: "unconfigured (no project URL)", env: "none", stub: undefined },
114
+ ])("proceeds with $name", async ({ env, stub, name }) => {
115
+ if (name.includes("OAuth2") && adapter.legacyOAuth2 === false)
116
+ return;
117
+ if (env === "session")
118
+ process.env.ORY_SESSION_TOKEN = "token-under-test";
119
+ if (env === "oauth2")
120
+ process.env.ORY_OAUTH2_TOKEN = "token-under-test";
121
+ if (env === "none")
122
+ delete process.env.ORY_PROJECT_URL;
123
+ stub?.(client);
124
+ const outcome = await adapter.sessionStart(ctx());
125
+ (0, vitest_1.expect)(outcome.blocked).toBe(false);
126
+ });
127
+ });
128
+ // ── User-login gate translation ────────────────────────────────
129
+ (0, vitest_1.describe)("user-login gate", () => {
130
+ const declined = {
131
+ proceed: false,
132
+ mode: "declined",
133
+ reason: "User declined the OAuth2 consent screen",
134
+ };
135
+ (0, vitest_1.it)(adapter.sessionCanBlock
136
+ ? "translates a declined login into the native session block"
137
+ : "treats a declined login as advisory (no session block channel)", async () => {
138
+ const gates = makeGates(declined);
139
+ const outcome = await adapter.sessionStart(ctx(gates));
140
+ (0, vitest_1.expect)(outcome.blocked).toBe(adapter.sessionCanBlock);
141
+ if (adapter.sessionCanBlock) {
142
+ (0, vitest_1.expect)(outcome.reason).toBeTruthy();
143
+ }
144
+ });
145
+ (0, vitest_1.it)("runs the agent gate even when the user gate declines", async () => {
146
+ const gates = makeGates(declined);
147
+ await adapter.sessionStart(ctx(gates));
148
+ (0, vitest_1.expect)(gates.agentGate).toHaveBeenCalledOnce();
149
+ });
150
+ (0, vitest_1.it)("proceeds without legacy verification when the gate handled auth", async () => {
151
+ process.env.ORY_SESSION_TOKEN = "should-not-be-verified";
152
+ const verify = (0, testing_js_1.stubSessionSuccess)(client);
153
+ const gates = makeGates({ proceed: true, mode: "ok", reason: "logged in" });
154
+ const outcome = await adapter.sessionStart(ctx(gates));
155
+ (0, vitest_1.expect)(outcome.blocked).toBe(false);
156
+ (0, vitest_1.expect)(verify).not.toHaveBeenCalled();
157
+ });
158
+ (0, vitest_1.it)("falls through to legacy verification when the gate is disabled", async () => {
159
+ process.env.ORY_SESSION_TOKEN = "legacy-token";
160
+ const verify = (0, testing_js_1.stubSessionSuccess)(client);
161
+ const outcome = await adapter.sessionStart(ctx());
162
+ (0, vitest_1.expect)(outcome.blocked).toBe(false);
163
+ (0, vitest_1.expect)(verify).toHaveBeenCalled();
164
+ });
165
+ });
166
+ // ── Tool gate translation ──────────────────────────────────────
167
+ (0, vitest_1.describe)("tool gate", () => {
168
+ vitest_1.it.each([
169
+ { check: "allowed", mode: "enforce", blocked: false, invoke: true, block: false },
170
+ { check: "denied", mode: "enforce", blocked: toolCanBlock, invoke: false, block: true },
171
+ { check: "denied", mode: "observe", blocked: false, invoke: true, block: true },
172
+ { check: "network_error", mode: "enforce", blocked: false, invoke: false, block: false },
173
+ { check: "rate_limited", mode: "enforce", blocked: false, invoke: false, block: false },
174
+ ])("check=$check × $mode → blocked=$blocked", async ({ check, mode, blocked, invoke, block }) => {
175
+ const stubs = {
176
+ allowed: testing_js_1.stubPermissionAllowed,
177
+ denied: testing_js_1.stubPermissionDenied,
178
+ network_error: testing_js_1.stubPermissionNetworkError,
179
+ rate_limited: testing_js_1.stubPermissionRateLimited,
180
+ };
181
+ stubs[check](client);
182
+ process.env.ORY_PERMISSION_MODE = mode;
183
+ const outcome = await adapter.toolBefore(ctx(), adapter.tool);
184
+ (0, vitest_1.expect)(outcome.blocked).toBe(blocked);
185
+ if (blocked)
186
+ (0, vitest_1.expect)(outcome.reason).toBeTruthy();
187
+ const invokeSpans = (0, testing_js_1.getTraceSpans)(client, "tool.invoke");
188
+ const blockSpans = (0, testing_js_1.getTraceSpans)(client, "tool.block");
189
+ if (invoke) {
190
+ (0, vitest_1.expect)(invokeSpans.length).toBeGreaterThanOrEqual(1);
191
+ (0, vitest_1.expect)(invokeSpans[0].attributes?.toolName).toBe(adapter.tool);
192
+ }
193
+ else {
194
+ (0, vitest_1.expect)(invokeSpans).toHaveLength(0);
195
+ }
196
+ if (block) {
197
+ (0, vitest_1.expect)(blockSpans.length).toBeGreaterThanOrEqual(1);
198
+ (0, vitest_1.expect)(blockSpans[0].attributes?.blocked).toBe(mode === "enforce" && toolCanBlock);
199
+ }
200
+ else {
201
+ (0, vitest_1.expect)(blockSpans).toHaveLength(0);
202
+ }
203
+ // Observe-mode denies also emit the audit span; enforce must not.
204
+ const observeSpans = (0, testing_js_1.getTraceSpans)(client, "permission.observe_deny");
205
+ (0, vitest_1.expect)(observeSpans).toHaveLength(check === "denied" && mode === "observe" ? 1 : 0);
206
+ });
207
+ (0, vitest_1.it)("skips the permission check entirely in audit-only mode", async () => {
208
+ (0, config_js_1.saveConfig)({ auditOnly: true });
209
+ const checkSpy = vitest_1.vi.spyOn(client, "checkPermission");
210
+ process.env.ORY_PERMISSION_MODE = "enforce";
211
+ const outcome = await adapter.toolBefore(ctx(), adapter.tool);
212
+ (0, vitest_1.expect)(outcome.blocked).toBe(false);
213
+ (0, vitest_1.expect)(checkSpy).not.toHaveBeenCalled();
214
+ });
215
+ (0, vitest_1.it)("addresses the check to ORY_USER_SUBJECT_ID when set", async () => {
216
+ process.env.ORY_USER_SUBJECT_ID = "user:custom-override";
217
+ (0, testing_js_1.stubPermissionAllowed)(client);
218
+ await adapter.toolBefore(ctx(), adapter.tool);
219
+ const [span] = (0, testing_js_1.getTraceSpans)(client, "tool.invoke");
220
+ (0, vitest_1.expect)(span.attributes?.subjectId).toBe("user:custom-override");
221
+ });
222
+ });
223
+ // ── Post-tool + passthrough ────────────────────────────────────
224
+ if (adapter.toolAfter) {
225
+ (0, vitest_1.it)("records tool.complete after execution", async () => {
226
+ await adapter.toolAfter(ctx(), adapter.tool);
227
+ const spans = (0, testing_js_1.getTraceSpans)(client, "tool.complete");
228
+ (0, vitest_1.expect)(spans.length).toBeGreaterThanOrEqual(1);
229
+ (0, vitest_1.expect)(spans[0].attributes?.toolName).toBe(adapter.tool);
230
+ });
231
+ }
232
+ if (adapter.unknownEvent) {
233
+ (0, vitest_1.it)("passes unknown events through with the native empty response", async () => {
234
+ const raw = await adapter.unknownEvent(ctx());
235
+ (0, vitest_1.expect)(raw).toEqual(adapter.unknownEventOutput);
236
+ });
237
+ }
238
+ });
239
+ }
package/dist/index.d.ts CHANGED
@@ -23,3 +23,4 @@ export { resolveUserSubject, subjectLabel, type UserSubjectRef, } from "./subjec
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";
26
+ export { resolveNamespace, sessionStart, gate, complete, registerSubagent, wrapTool, type SessionStartResult, type SessionStartOptions, type RegisterSubagentOptions, type GateResult, type GateOptions, type WrapToolOptions, } from "./adapters.js";