@ory/argus 0.9.1 → 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.
@@ -233,7 +233,9 @@ Create the following page structure:
233
233
  - `app/auth/registration/page.tsx` — `<Registration flow={flow} />`
234
234
  - `app/auth/recovery/page.tsx` — `<Recovery flow={flow} />`
235
235
  - `app/auth/verification/page.tsx` — `<Verification flow={flow} />`
236
- - `app/auth/settings/page.tsx` — `<Settings flow={flow} />`
236
+ - `app/auth/settings/page.tsx` — `<Settings flow={flow} />`, wrapped in
237
+ `<SessionProvider>` from `@ory/elements-react/client` (Settings is the only
238
+ flow that needs it — see {{REF_LOGIN_FLOW}})
237
239
 
238
240
  Each page initializes its flow with `getLoginFlow`, `getRegistrationFlow`,
239
241
  etc. from `@ory/nextjs/app`, then hands the flow to the matching
@@ -264,6 +264,7 @@ Create `app/auth/settings/page.tsx`:
264
264
 
265
265
  ```typescript
266
266
  import { Settings } from "@ory/elements-react/theme"
267
+ import { SessionProvider } from "@ory/elements-react/client"
267
268
  import { getSettingsFlow, OryPageLayout } from "@ory/nextjs/app"
268
269
 
269
270
  export default async function SettingsPage(props: {
@@ -278,16 +279,26 @@ export default async function SettingsPage(props: {
278
279
 
279
280
  return (
280
281
  <OryPageLayout>
281
- <Settings flow={flow} />
282
+ <SessionProvider>
283
+ <Settings flow={flow} />
284
+ </SessionProvider>
282
285
  </OryPageLayout>
283
286
  )
284
287
  }
285
288
  ```
286
289
 
290
+ Unlike the other flows, **`<Settings>` must be wrapped in `<SessionProvider>`**
291
+ from `@ory/elements-react/client`. Settings renders session-dependent controls
292
+ (connected social accounts, unlinking, logout of other sessions) that read from
293
+ the session context; the pre-auth pages (login, registration, recovery,
294
+ verification) have no session yet and do not need the provider.
295
+
287
296
  ### Settings — React SPA
288
297
 
289
298
  Initialize with `createBrowserSettingsFlow` and render
290
- `<Settings flow={flow} />` from `@ory/elements-react/theme`. The
299
+ `<Settings flow={flow} />` from `@ory/elements-react/theme`, wrapped in
300
+ `<SessionProvider>` from `@ory/elements-react/client` (the same requirement
301
+ as the App Router page above — Settings reads session-dependent state). The
291
302
  component handles password changes, profile traits, MFA enrollment,
292
303
  and connected social providers.
293
304
 
@@ -385,6 +396,71 @@ For larger design changes, the Ory Elements primitives package exposes
385
396
  the underlying card, button, and input components — use those before
386
397
  falling back to custom node rendering.
387
398
 
399
+ ## Enable MFA and passkeys
400
+
401
+ These are **project-side** capabilities: once enabled on the Ory project, the
402
+ `<Login>`, `<Registration>`, and `<Settings>` components render the second-factor
403
+ and passkey UI automatically — **no page code changes**. Configure them with the
404
+ Ory CLI against your project (`ory list projects` for the id), the same idiom as
405
+ {{REF_SOCIAL_LOGIN}}. Against the local stack, apply the equivalent keys to the
406
+ local Kratos config instead ({{REF_LOCAL_DEV}}).
407
+
408
+ ### Passkeys
409
+
410
+ ```bash
411
+ ory patch identity-config <project-id> \
412
+ --add '/selfservice/methods/passkey/enabled=true' \
413
+ --add '/selfservice/methods/passkey/config/rp/display_name="My App"' \
414
+ --add '/selfservice/methods/passkey/config/rp/id="your-domain.com"' \
415
+ --add '/selfservice/methods/passkey/config/rp/origins=["https://your-domain.com"]'
416
+ ```
417
+
418
+ - `rp.id` is the **domain only** — no scheme, no port (`example.com`, not
419
+ `https://example.com:3000`).
420
+ - `rp.origins` must list the **exact scheme+host+port** the browser uses.
421
+ - For local dev, WebAuthn treats `localhost` as a secure origin: set
422
+ `rp.id="localhost"` and `rp.origins=["http://localhost:3000"]` (match your app
423
+ URL). Passkeys registered against `localhost` won't work on the deployed domain
424
+ and vice-versa.
425
+
426
+ ### MFA methods
427
+
428
+ Enable the second factors you want. Backup codes (`lookup_secret`) should always
429
+ be enabled alongside any other method so a user who loses their device can
430
+ recover.
431
+
432
+ ```bash
433
+ # TOTP (authenticator app)
434
+ ory patch identity-config <project-id> \
435
+ --add '/selfservice/methods/totp/enabled=true' \
436
+ --add '/selfservice/methods/totp/config/issuer="My App"'
437
+
438
+ # Backup codes
439
+ ory patch identity-config <project-id> \
440
+ --add '/selfservice/methods/lookup_secret/enabled=true'
441
+
442
+ # Email code as a second factor
443
+ ory patch identity-config <project-id> \
444
+ --add '/selfservice/methods/code/mfa_enabled=true'
445
+ ```
446
+
447
+ ### Require the second factor (do not skip)
448
+
449
+ Enabling a method only lets users *enroll* — it does not *require* the second
450
+ factor. Without this step MFA is opt-in and unenforced. Set the required
451
+ assurance level to the highest the identity has available:
452
+
453
+ ```bash
454
+ ory patch identity-config <project-id> \
455
+ --add '/selfservice/flows/settings/required_aal="highest_available"' \
456
+ --add '/session/whoami/required_aal="highest_available"'
457
+ ```
458
+
459
+ With `highest_available`, `toSession()` / `getServerSession()` return an
460
+ incomplete session (AAL1) until the user clears the second factor, and Elements
461
+ prompts for it on the next `<Login>`. Your route protection should treat an
462
+ AAL1 session on an MFA-enrolled user as unauthenticated.
463
+
388
464
  ## Test the flow
389
465
 
390
466
  Don't stop at "the page renders." Run these in order — each one isolates a
@@ -415,6 +491,43 @@ If a submit fails with a CSRF error, the SDK URL is cross-site (trap #1). If a
415
491
  redirect 404s, a route doesn't match the project config (trap #2). Neither is a
416
492
  plugin bug — see the **App bug vs. plugin bug** triage in {{REF_AUTH_SETUP}}.
417
493
 
494
+ ### Writing E2E tests (Playwright)
495
+
496
+ Automating these flows has three gotchas that cause flaky or wrong-for-the-wrong-
497
+ reason failures. Get them right up front:
498
+
499
+ 1. **Match URLs by pattern, never literally.** Ory appends `?flow=<uuid>` on
500
+ every flow redirect, so an exact-string wait never matches.
501
+
502
+ ```typescript
503
+ await page.waitForURL(/\/auth\/login\?flow=/) // regex
504
+ await page.waitForURL("**/dashboard**") // glob — also matches query params
505
+ // wrong: await page.waitForURL("http://localhost:3000/auth/login")
506
+ ```
507
+
508
+ 2. **Use strong, dissimilar credentials.** Kratos rejects passwords that are too
509
+ similar to the identifier or that appear in a breach database — a weak fixture
510
+ password fails validation, not the flow you meant to test.
511
+
512
+ ```typescript
513
+ const email = `test-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`
514
+ const password = "Str0ngP@ssword!123" // fixed, strong, not in any breach list
515
+ ```
516
+
517
+ 3. **Assert on Ory Elements' own testids, not form-level selectors.** Validation
518
+ messages render inside the auth card, keyed by Kratos UI message id (the
519
+ `4xxxxxx` range is validation errors), not on the flow wrapper.
520
+
521
+ ```typescript
522
+ page.locator('[data-testid^="ui/message/4"]') // any validation error
523
+ page.locator('[data-testid="login-auth-card"]').getByText(/credentials|invalid/i)
524
+ // wrong: page.locator('[data-testid="login-flow"]').getByText(...) — errors aren't here
525
+ ```
526
+
527
+ These selectors track the installed `@ory/elements-react` version; if a testid
528
+ assertion breaks after an Elements upgrade, inspect the rendered DOM before
529
+ assuming a flow bug.
530
+
418
531
  ## Fallback: rendering UI nodes by hand
419
532
 
420
533
  Use this path **only** when Ory Elements cannot run in the target
@@ -437,8 +550,9 @@ Ory flow API changes manually.
437
550
  - Add social login providers: use {{REF_SOCIAL_LOGIN}}.
438
551
  Elements renders the buttons automatically once providers are
439
552
  configured server-side.
440
- - Add multi-factor authentication via Ory project settings the
441
- `<Login>` and `<Settings>` components handle the second-factor UI.
553
+ - Add multi-factor authentication and passkeys see **Enable MFA and
554
+ passkeys** above. Once the project is configured, the `<Login>` and
555
+ `<Settings>` components render the second-factor and passkey UI automatically.
442
556
  - Customize the identity schema for additional profile fields. Elements
443
557
  reads the schema from the flow and renders new fields automatically.
444
558
  - Set up webhooks for registration events.
@@ -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;