@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.
- package/assets/skills/auth-setup/SKILL.md +3 -1
- package/assets/skills/login-flow/SKILL.md +118 -4
- package/dist/adapters.d.ts +104 -0
- package/dist/adapters.js +216 -0
- package/dist/agent-auth.js +33 -0
- package/dist/config.js +35 -3
- package/dist/contract-suite.d.ts +87 -0
- package/dist/contract-suite.js +239 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +9 -1
- package/dist/lifecycle.js +130 -0
- package/dist/local/configs.d.ts +2 -2
- package/dist/local/configs.js +10 -1
- package/dist/local/manager.d.ts +6 -4
- package/dist/local/manager.js +91 -16
- package/dist/permissions.d.ts +36 -8
- package/dist/permissions.js +62 -7
- package/dist/subject.d.ts +13 -1
- package/dist/subject.js +29 -2
- package/dist/testing.d.ts +214 -0
- package/dist/testing.js +372 -0
- package/dist/tool-catalog.d.ts +7 -0
- package/dist/tool-catalog.js +73 -0
- package/dist/user-login.js +7 -2
- package/package.json +1 -1
|
@@ -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
|
-
<
|
|
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
|
|
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
|
|
441
|
-
|
|
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 {};
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
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 check = { namespace, object: opts.toolName, relation: "use", ...subjectRef };
|
|
132
|
+
// Audit-only kill switch: Ory is disabled entirely — no permission check.
|
|
133
|
+
// Record only the audit `tool.invoke` span (same span semantics as the
|
|
134
|
+
// harness plugins' audit-only short-circuit) and pass the tool through.
|
|
135
|
+
const resolved = (0, config_js_1.resolveConfig)();
|
|
136
|
+
if (resolved.auditOnly) {
|
|
137
|
+
client.tracer.record("tool.invoke", "ok", { attributes: spanAttrs });
|
|
138
|
+
const decision = {
|
|
139
|
+
kind: "allow",
|
|
140
|
+
result: { allowed: true, checkedAt: new Date().toISOString(), check },
|
|
141
|
+
mode: resolved.permissionMode,
|
|
142
|
+
spanAttributes: { permissionMode: resolved.permissionMode },
|
|
143
|
+
};
|
|
144
|
+
return { proceed: true, blocked: false, kind: "allow", decision, subject: label, namespace };
|
|
145
|
+
}
|
|
146
|
+
const outcome = await (0, permissions_js_1.gateToolCall)(client, {
|
|
147
|
+
harness: opts.harness,
|
|
148
|
+
toolName: opts.toolName,
|
|
149
|
+
check,
|
|
150
|
+
spanAttributes: spanAttrs,
|
|
151
|
+
});
|
|
152
|
+
if (outcome.kind === "audit_only") {
|
|
153
|
+
// Kill switch: Ory is disabled. Audit the invocation, skip the check.
|
|
154
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
155
|
+
attributes: { ...spanAttrs, auditOnly: true },
|
|
156
|
+
});
|
|
157
|
+
return { proceed: true, blocked: false, kind: "audit_only", decision: outcome, subject: label, namespace };
|
|
158
|
+
}
|
|
159
|
+
if (outcome.kind === "interactive") {
|
|
160
|
+
return { proceed: true, blocked: false, kind: "interactive", decision: outcome, subject: label, namespace };
|
|
161
|
+
}
|
|
162
|
+
const decisionAttrs = outcome.spanAttributes;
|
|
163
|
+
if (outcome.kind === "fail_open") {
|
|
164
|
+
client.logger.warn("permission.fail_open", { tool: opts.toolName, code: outcome.error.code });
|
|
165
|
+
return { proceed: true, blocked: false, kind: "fail_open", decision: outcome, subject: label, namespace };
|
|
166
|
+
}
|
|
167
|
+
if (outcome.kind === "allow") {
|
|
168
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
169
|
+
attributes: { ...spanAttrs, ...decisionAttrs, allowed: true },
|
|
170
|
+
});
|
|
171
|
+
return { proceed: true, blocked: false, kind: "allow", decision: outcome, subject: label, namespace };
|
|
172
|
+
}
|
|
173
|
+
if (outcome.kind === "observe") {
|
|
174
|
+
client.tracer.record("tool.block", "denied", {
|
|
175
|
+
attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(false) },
|
|
176
|
+
});
|
|
177
|
+
client.tracer.record("tool.invoke", "ok", {
|
|
178
|
+
attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, observed: true },
|
|
179
|
+
});
|
|
180
|
+
return { proceed: true, blocked: false, kind: "observe", decision: outcome, subject: label, namespace };
|
|
181
|
+
}
|
|
182
|
+
// deny
|
|
183
|
+
client.tracer.record("tool.block", "denied", {
|
|
184
|
+
attributes: { ...spanAttrs, ...decisionAttrs, allowed: false, ...(0, denial_js_1.alertAttributes)(canBlock) },
|
|
185
|
+
});
|
|
186
|
+
const denialMessage = (0, denial_js_1.formatDenialMessage)({ tool: opts.toolName, subjectId: label, namespace });
|
|
187
|
+
client.logger.warn("tool.denied", { tool: opts.toolName, subjectId: label });
|
|
188
|
+
return { proceed: false, blocked: canBlock, kind: "deny", decision: outcome, subject: label, namespace, denialMessage };
|
|
189
|
+
}
|
|
190
|
+
function complete(client, opts) {
|
|
191
|
+
const attributes = { toolName: opts.toolName, ...(opts.extraSpanAttributes ?? {}) };
|
|
192
|
+
if (opts.output !== undefined)
|
|
193
|
+
attributes.hasOutput = true;
|
|
194
|
+
client.tracer.record("tool.complete", opts.status ?? "ok", { attributes });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Wrap a plain `execute(args)` callable with gate + complete. On a hard deny it throws
|
|
198
|
+
* {@link OryDenialError} (the veto mechanism for tool-boundary SDKs like Vercel AI SDK);
|
|
199
|
+
* otherwise the tool runs and `tool.complete` is recorded.
|
|
200
|
+
*/
|
|
201
|
+
function wrapTool(client, opts, execute) {
|
|
202
|
+
return async (args) => {
|
|
203
|
+
const result = await gate(client, {
|
|
204
|
+
harness: opts.harness,
|
|
205
|
+
toolName: opts.toolName,
|
|
206
|
+
toolArgs: args,
|
|
207
|
+
canBlock: opts.canBlock ?? true,
|
|
208
|
+
});
|
|
209
|
+
if (result.blocked) {
|
|
210
|
+
throw new denial_js_1.OryDenialError({ tool: opts.toolName, subjectId: result.subject, namespace: result.namespace });
|
|
211
|
+
}
|
|
212
|
+
const output = await execute(args);
|
|
213
|
+
complete(client, { toolName: opts.toolName, output });
|
|
214
|
+
return output;
|
|
215
|
+
};
|
|
216
|
+
}
|
package/dist/agent-auth.js
CHANGED
|
@@ -384,6 +384,20 @@ async function fetchClientCredentialsToken(args) {
|
|
|
384
384
|
* the agent identity was available for the session.
|
|
385
385
|
*/
|
|
386
386
|
async function ensureAgentIdentity(client, options = {}) {
|
|
387
|
+
// Audit-only kill switch: Ory is disabled entirely — skip credential
|
|
388
|
+
// resolution (no DCR, no token grant) and record the no-op, shaped like
|
|
389
|
+
// the user gate's audit_only result.
|
|
390
|
+
if ((0, config_js_1.resolveConfig)().auditOnly) {
|
|
391
|
+
const creds = {
|
|
392
|
+
kind: "none",
|
|
393
|
+
reason: "Configured for audit-only mode; agent identity resolution is a no-op",
|
|
394
|
+
warnings: [],
|
|
395
|
+
};
|
|
396
|
+
client.tracer.record("agent.auth", "skipped", {
|
|
397
|
+
attributes: { kind: creds.kind, reason: creds.reason, auditOnly: true },
|
|
398
|
+
});
|
|
399
|
+
return creds;
|
|
400
|
+
}
|
|
387
401
|
const resolveFn = options.resolveFn ?? resolveAgentCredentials;
|
|
388
402
|
let creds;
|
|
389
403
|
try {
|
|
@@ -437,6 +451,25 @@ async function ensureAgentIdentity(client, options = {}) {
|
|
|
437
451
|
* resolution.
|
|
438
452
|
*/
|
|
439
453
|
async function ensureSubAgentIdentity(client, options) {
|
|
454
|
+
// Audit-only kill switch: same no-op as ensureAgentIdentity — no
|
|
455
|
+
// sub-agent DCR while Ory is disabled entirely.
|
|
456
|
+
if ((0, config_js_1.resolveConfig)().auditOnly) {
|
|
457
|
+
const identity = {
|
|
458
|
+
kind: "none",
|
|
459
|
+
subAgentType: options.subAgentType,
|
|
460
|
+
reason: "Configured for audit-only mode; sub-agent identity resolution is a no-op",
|
|
461
|
+
warnings: [],
|
|
462
|
+
};
|
|
463
|
+
client.tracer.record("agent.auth", "skipped", {
|
|
464
|
+
attributes: {
|
|
465
|
+
kind: "subagent_none",
|
|
466
|
+
subAgentType: identity.subAgentType,
|
|
467
|
+
reason: identity.reason,
|
|
468
|
+
auditOnly: true,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
return identity;
|
|
472
|
+
}
|
|
440
473
|
const env = options.env ?? process.env;
|
|
441
474
|
const registerFn = options.registerAgentClientFn ?? registerAgentClient;
|
|
442
475
|
const loadFn = options.loadFn ?? loadSubAgentDynamicCredentials;
|
package/dist/config.js
CHANGED
|
@@ -290,15 +290,47 @@ function acquireLock() {
|
|
|
290
290
|
throw new Error(`Timed out waiting for config lock at ${lockPath}. ` +
|
|
291
291
|
"Another process may be holding it; remove the file if you are sure no other process is running.");
|
|
292
292
|
}
|
|
293
|
-
sleepSync(LOCK_POLL_MS)
|
|
293
|
+
if (!sleepSync(LOCK_POLL_MS)) {
|
|
294
|
+
// The runtime can't block synchronously (no SharedArrayBuffer /
|
|
295
|
+
// Atomics.wait — e.g. Cloudflare Workers). Rather than hot-spin
|
|
296
|
+
// until the deadline, proceed without the lock: the write itself
|
|
297
|
+
// stays atomic via write-temp + rename.
|
|
298
|
+
return { fd: null };
|
|
299
|
+
}
|
|
294
300
|
}
|
|
295
301
|
}
|
|
296
302
|
}
|
|
297
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Lazily initialized buffer for `Atomics.wait`-based synchronous sleep.
|
|
305
|
+
* `undefined` = not yet probed; `null` = unavailable on this runtime.
|
|
306
|
+
* Never touched at module scope: constructing a SharedArrayBuffer eagerly
|
|
307
|
+
* would make merely importing this module throw on runtimes without it
|
|
308
|
+
* (e.g. Cloudflare Workers).
|
|
309
|
+
*/
|
|
310
|
+
let sleepBuf;
|
|
311
|
+
/** Sleep synchronously. Returns false when the runtime can't sleep. */
|
|
298
312
|
function sleepSync(ms) {
|
|
299
|
-
|
|
313
|
+
if (sleepBuf === undefined) {
|
|
314
|
+
try {
|
|
315
|
+
sleepBuf =
|
|
316
|
+
typeof SharedArrayBuffer === "function" &&
|
|
317
|
+
typeof Atomics !== "undefined" &&
|
|
318
|
+
typeof Atomics.wait === "function"
|
|
319
|
+
? new Int32Array(new SharedArrayBuffer(4))
|
|
320
|
+
: null;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
sleepBuf = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (sleepBuf === null)
|
|
327
|
+
return false;
|
|
328
|
+
Atomics.wait(sleepBuf, 0, 0, ms);
|
|
329
|
+
return true;
|
|
300
330
|
}
|
|
301
331
|
function releaseLock(lock) {
|
|
332
|
+
if (lock.fd === null)
|
|
333
|
+
return; // lock-less fallback: nothing to release
|
|
302
334
|
try {
|
|
303
335
|
fs.closeSync(lock.fd);
|
|
304
336
|
}
|