@ory/argus 0.13.8 → 0.14.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/README.md +2 -1
- package/assets/commands/dashboard.md +34 -0
- package/assets/skills/ory-build-agent/SKILL.md +11 -13
- package/assets/skills/ory-e2b-sandbox/SKILL.md +0 -1
- package/assets/skills/ory-temporal-worker/SKILL.md +8 -10
- package/assets/skills/permissions-onboarding/SKILL.md +3 -3
- package/dist/adapters.d.ts +27 -4
- package/dist/adapters.js +132 -32
- package/dist/agent-auth.d.ts +34 -3
- package/dist/agent-auth.js +58 -8
- package/dist/auth.d.ts +7 -0
- package/dist/auth.js +90 -5
- package/dist/branding.d.ts +67 -0
- package/dist/branding.js +81 -0
- package/dist/build-info.json +4 -4
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +13 -52
- package/dist/client.d.ts +31 -0
- package/dist/client.js +58 -0
- package/dist/config.d.ts +48 -19
- package/dist/config.js +31 -26
- package/dist/contract-suite.d.ts +5 -3
- package/dist/contract-suite.js +13 -22
- package/dist/dashboard-cli.d.ts +8 -0
- package/dist/dashboard-cli.js +70 -0
- package/dist/dev.js +3 -4
- package/dist/index.d.ts +12 -5
- package/dist/index.js +41 -3
- package/dist/interactive-setup.d.ts +144 -23
- package/dist/interactive-setup.js +412 -224
- package/dist/local/health.d.ts +14 -0
- package/dist/local/health.js +46 -0
- package/dist/local/manager.js +1 -3
- package/dist/local/seed.d.ts +9 -20
- package/dist/local/seed.js +26 -18
- package/dist/permissions-cli.js +12 -4
- package/dist/project-api-key.d.ts +69 -0
- package/dist/project-api-key.js +147 -0
- package/dist/registry/manager.js +62 -18
- package/dist/setup-actions.d.ts +232 -0
- package/dist/setup-actions.js +507 -0
- package/dist/setup.d.ts +19 -0
- package/dist/setup.js +44 -14
- package/dist/skills.js +7 -0
- package/dist/status-cli.d.ts +2 -2
- package/dist/status-cli.js +4 -30
- package/dist/status-data.d.ts +96 -0
- package/dist/status-data.js +250 -0
- package/dist/status-system.d.ts +24 -0
- package/dist/status-system.js +56 -0
- package/dist/testing.d.ts +8 -0
- package/dist/testing.js +20 -1
- package/dist/uninstall.d.ts +33 -15
- package/dist/uninstall.js +65 -22
- package/dist/user-login.d.ts +16 -9
- package/dist/user-login.js +18 -28
- package/dist/web/api.d.ts +33 -0
- package/dist/web/api.js +294 -0
- package/dist/web/launch.d.ts +11 -0
- package/dist/web/launch.js +96 -0
- package/dist/web/server.d.ts +20 -0
- package/dist/web/server.js +233 -0
- package/dist/web/types.d.ts +65 -0
- package/dist/web/types.js +2 -0
- package/dist/webapp/assets/index-Wucl4SZs.css +1 -0
- package/dist/webapp/assets/index-m-GtEdq0.js +49 -0
- package/dist/webapp/favicon.ico +0 -0
- package/dist/webapp/index.html +15 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -42,10 +42,11 @@ const client = OryAgentClient.fromEnv("my-harness");
|
|
|
42
42
|
|
|
43
43
|
// 1. Session start: authenticate the human and the agent process.
|
|
44
44
|
async function onSessionStart() {
|
|
45
|
+
// Runs every session and never blocks — it establishes the user identity
|
|
46
|
+
// for attribution; enforcement happens at the tool gate via permissionMode.
|
|
45
47
|
await ensureUserAuthenticated(client, {
|
|
46
48
|
binName: "my-harness",
|
|
47
49
|
harness: "my-harness",
|
|
48
|
-
allowBlock: true,
|
|
49
50
|
});
|
|
50
51
|
await ensureAgentIdentity(client, {
|
|
51
52
|
projectUrl: process.env.ORY_PROJECT_URL,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Open the Ory Plugin Dashboard
|
|
2
|
+
|
|
3
|
+
Open a live, browser-based control panel for the Ory agent plugin. It shows
|
|
4
|
+
the health of the core Ory services the plugin uses (Kratos, Hydra, Keto) and
|
|
5
|
+
the plugin's current configuration (project URL, permission mode, audit mode,
|
|
6
|
+
user-login flag, namespaces, and the resolved user/agent identities),
|
|
7
|
+
refreshing the health status live.
|
|
8
|
+
|
|
9
|
+
Run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
{{NPX}} dashboard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This will:
|
|
16
|
+
|
|
17
|
+
1. Start a small local web server bound to `127.0.0.1`
|
|
18
|
+
2. Open your default browser at the dashboard
|
|
19
|
+
3. Refresh the service health live
|
|
20
|
+
|
|
21
|
+
The dashboard is the read-only status view of the Ory Agent web app — the same
|
|
22
|
+
local React UI, loopback server, and styleguide that power the guided web
|
|
23
|
+
installer. It never changes configuration. Stop it with `Ctrl-C` in the
|
|
24
|
+
terminal (or the "Close" button in the page).
|
|
25
|
+
|
|
26
|
+
When no browser is available (CI, SSH, headless), or with `--json`, it prints a
|
|
27
|
+
machine-readable status report instead:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
{{NPX}} dashboard --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If the core services show as unreachable, start the local stack with
|
|
34
|
+
{{REF_LOCAL_UP}} (or check state with `{{NPX}} local status`).
|
|
@@ -86,16 +86,13 @@ import {
|
|
|
86
86
|
const client = OryAgentClient.fromEnv("my-agent");
|
|
87
87
|
const { projectUrl } = resolveConfig();
|
|
88
88
|
|
|
89
|
-
// 1. User gate — interactive PKCE
|
|
90
|
-
|
|
89
|
+
// 1. User gate — interactive PKCE; runs every session and never blocks.
|
|
90
|
+
// Establishes the user identity for attribution; a declined/skipped login
|
|
91
|
+
// is audited and the flow proceeds (enforcement is at the tool gate).
|
|
92
|
+
await ensureUserAuthenticated(client, {
|
|
91
93
|
binName: "my-agent",
|
|
92
94
|
harness: "my-agent",
|
|
93
|
-
allowBlock: true, // flip to false if your agent can't refuse to start
|
|
94
95
|
});
|
|
95
|
-
if (userDecision.proceed === false) {
|
|
96
|
-
console.error(`Ory user login: ${userDecision.reason}`);
|
|
97
|
-
process.exit(2);
|
|
98
|
-
}
|
|
99
96
|
|
|
100
97
|
// 2. Agent gate — never blocks; resolves machine credentials (DCR by default).
|
|
101
98
|
await ensureAgentIdentity(client, { projectUrl, harness: "my-agent" });
|
|
@@ -113,10 +110,11 @@ if (client.userPrincipal.subject && client.agentPrincipal.subject) {
|
|
|
113
110
|
}
|
|
114
111
|
```
|
|
115
112
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
The user gate runs on every session and is always non-blocking: it refreshes
|
|
114
|
+
tokens, prompts on TTY, and emits the `user.auth` span, but always returns
|
|
115
|
+
`proceed: true`. A missing or declined login never stops the agent — the
|
|
116
|
+
consequence surfaces at the tool gate, where `permissionMode: enforce` denies
|
|
117
|
+
and `observe` audits.
|
|
120
118
|
|
|
121
119
|
## Step 4 — The shared gate body
|
|
122
120
|
|
|
@@ -409,8 +407,8 @@ PKCE flow, permission tuples, and trace spans are all visible:
|
|
|
409
407
|
1. {{REF_LOCAL_UP}} — brings up Kratos / Keto / Hydra on `localhost:4000`
|
|
410
408
|
and seeds a demo user. The banner prints the email + password.
|
|
411
409
|
2. Export the env vars the launcher writes (`ORY_PROJECT_URL`,
|
|
412
|
-
`
|
|
413
|
-
|
|
410
|
+
`ORY_OAUTH2_CLIENT_ID`, optional `ORY_AGENT_TRACE_FILE` for an NDJSON
|
|
411
|
+
span log). The user login runs every session.
|
|
414
412
|
3. Start your agent. Confirm the browser opens for PKCE login.
|
|
415
413
|
4. Invoke a gated tool and `tail -f $ORY_AGENT_TRACE_FILE | jq .` — you
|
|
416
414
|
should see `user.auth` → `agent.auth` → `permission.check` →
|
|
@@ -66,7 +66,6 @@ export const template = Template()
|
|
|
66
66
|
// Sandbox runtime defaults. Per-tenant secrets (project URL, tokens, client
|
|
67
67
|
// IDs) MUST be passed at Sandbox.create() time, never baked into the image.
|
|
68
68
|
.setEnvs({
|
|
69
|
-
ORY_USER_LOGIN: "true",
|
|
70
69
|
ORY_PERMISSION_MODE: "observe",
|
|
71
70
|
ORY_PERMISSION_NAMESPACE: "AgentTools",
|
|
72
71
|
ORY_AGENT_DEBUG: "true",
|
|
@@ -83,12 +83,10 @@ function bootstrap(): Promise<void> {
|
|
|
83
83
|
await ensureUserAuthenticated(ory, {
|
|
84
84
|
binName: "temporal-worker",
|
|
85
85
|
harness: "temporal",
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// enforcement happens at the per-Activity permission check.
|
|
91
|
-
allowBlock: false,
|
|
86
|
+
// The user gate runs every invocation and never blocks: it refreshes
|
|
87
|
+
// tokens and emits the audit span, but the worker proceeds even if the
|
|
88
|
+
// user is unauthenticated. Enforcement happens at the per-Activity
|
|
89
|
+
// permission check (permissionMode).
|
|
92
90
|
});
|
|
93
91
|
await ensureAgentIdentity(ory, {
|
|
94
92
|
projectUrl: process.env.ORY_PROJECT_URL,
|
|
@@ -145,10 +143,10 @@ Key choices:
|
|
|
145
143
|
|
|
146
144
|
- **Activities call Ory, Workflows don't.** Anything that needs a live decision
|
|
147
145
|
goes in an Activity. Workflows only orchestrate.
|
|
148
|
-
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
- **The user gate never blocks.** It runs every invocation to refresh the user
|
|
147
|
+
identity, but always proceeds. Enforcement is at the permission check, which
|
|
148
|
+
throws on deny — Temporal will mark the Activity as failed and surface the
|
|
149
|
+
error via the Workflow result or retry policy.
|
|
152
150
|
- **`harness: "temporal"`.** Distinguishes worker-originated spans in the trace
|
|
153
151
|
file from CLI plugin spans.
|
|
154
152
|
- **Span attributes carry the Workflow + Activity IDs.** This is how operators
|
|
@@ -44,7 +44,7 @@ If any of those are wrong, fix them before continuing:
|
|
|
44
44
|
{{NPX}} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
`--oauth2-client-id` is the public OAuth2 client registered in your Ory project (
|
|
47
|
+
`--oauth2-client-id` is the public OAuth2 client registered in your Ory project (needed for the per-session user login to complete its PKCE browser flow; see the *Register the user OAuth2 client* section of the plugin README).
|
|
48
48
|
|
|
49
49
|
## Step 2: Look at the current permission posture
|
|
50
50
|
|
|
@@ -94,9 +94,9 @@ out-of-band as they come into scope.
|
|
|
94
94
|
Two common failure modes:
|
|
95
95
|
|
|
96
96
|
1. **No user identity cached.** Bootstrap needs to know which subject
|
|
97
|
-
to grant tuples to. If user login has never
|
|
97
|
+
to grant tuples to. If the user login has never completed (no PKCE login,
|
|
98
98
|
no `ORY_USER_SUBJECT_ID`), the command refuses. Run the harness once
|
|
99
|
-
|
|
99
|
+
(the user login runs every session) to cache a user token, or set
|
|
100
100
|
`ORY_USER_SUBJECT_ID=<id>` to target a known subject.
|
|
101
101
|
2. **Credentials lack write scope on the permission namespace.** The
|
|
102
102
|
command prints the full tuple list so you can apply them manually
|
package/dist/adapters.d.ts
CHANGED
|
@@ -30,7 +30,6 @@ export interface SessionStartResult {
|
|
|
30
30
|
}
|
|
31
31
|
export interface SessionStartOptions {
|
|
32
32
|
harness: string;
|
|
33
|
-
allowBlock?: boolean;
|
|
34
33
|
projectUrl?: string;
|
|
35
34
|
binName?: string;
|
|
36
35
|
/** Injectable gates (tests). */
|
|
@@ -38,11 +37,27 @@ export interface SessionStartOptions {
|
|
|
38
37
|
agentGate?: typeof ensureAgentIdentity;
|
|
39
38
|
}
|
|
40
39
|
/**
|
|
41
|
-
* Run both auth gates and write the user→agent delegation tuple.
|
|
42
|
-
*
|
|
43
|
-
* user
|
|
40
|
+
* Run both auth gates and write the user→agent delegation tuple. The user gate
|
|
41
|
+
* runs every session to establish/refresh the user identity, records the
|
|
42
|
+
* `user.auth` span, and never blocks — enforcement is governed by
|
|
43
|
+
* `permissionMode` at tool-call time. `SessionStartResult.proceed` is therefore
|
|
44
|
+
* always `true`; it is retained only for a stable adapter shape.
|
|
44
45
|
*/
|
|
45
46
|
export declare function sessionStart(client: OryAgentClient, opts: SessionStartOptions): Promise<SessionStartResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Write the `user → agent` delegation tuple, at most once per install
|
|
49
|
+
* (see {@link writeDelegationOnce}). Exported so harness plugins that run
|
|
50
|
+
* their own session-start sequence (rather than {@link sessionStart}) share
|
|
51
|
+
* the same write-once semantics instead of re-issuing the write every
|
|
52
|
+
* session.
|
|
53
|
+
*
|
|
54
|
+
* The tuple is a *derived projection* of the delegation anchor stamped into
|
|
55
|
+
* the agent's OAuth2 client metadata at registration: the edge is resolved
|
|
56
|
+
* from the live principals when populated, otherwise rebuilt from the persisted
|
|
57
|
+
* anchor — so a tuple lost out of band can be self-healed from the identity
|
|
58
|
+
* layer. No-op only when neither source yields both ends.
|
|
59
|
+
*/
|
|
60
|
+
export declare function writeUserDelegatesAgent(client: OryAgentClient): Promise<void>;
|
|
46
61
|
export interface RegisterSubagentOptions {
|
|
47
62
|
harness: string;
|
|
48
63
|
subAgentType: string;
|
|
@@ -50,6 +65,14 @@ export interface RegisterSubagentOptions {
|
|
|
50
65
|
subAgentGate?: typeof ensureSubAgentIdentity;
|
|
51
66
|
}
|
|
52
67
|
export declare function registerSubagent(client: OryAgentClient, opts: RegisterSubagentOptions): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Write the `agent → subagent` delegation tuple, at most once per install
|
|
70
|
+
* (see {@link writeDelegationOnce}). Exported so harness plugins that
|
|
71
|
+
* resolve the sub-agent identity through their own event plumbing (rather
|
|
72
|
+
* than {@link registerSubagent}) still share the write-once semantics.
|
|
73
|
+
* No-op until the agent principal is populated.
|
|
74
|
+
*/
|
|
75
|
+
export declare function writeAgentDelegatesSubagent(client: OryAgentClient, subAgentSubject: string, subAgentType: string): Promise<void>;
|
|
53
76
|
export interface GateResult {
|
|
54
77
|
/** False only when the tool was hard-denied in enforce mode. */
|
|
55
78
|
proceed: boolean;
|
package/dist/adapters.js
CHANGED
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
22
|
exports.resolveNamespace = resolveNamespace;
|
|
23
23
|
exports.sessionStart = sessionStart;
|
|
24
|
+
exports.writeUserDelegatesAgent = writeUserDelegatesAgent;
|
|
24
25
|
exports.registerSubagent = registerSubagent;
|
|
26
|
+
exports.writeAgentDelegatesSubagent = writeAgentDelegatesSubagent;
|
|
25
27
|
exports.gate = gate;
|
|
26
28
|
exports.complete = complete;
|
|
27
29
|
exports.wrapTool = wrapTool;
|
|
@@ -35,9 +37,11 @@ function resolveNamespace() {
|
|
|
35
37
|
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
|
-
* Run both auth gates and write the user→agent delegation tuple.
|
|
39
|
-
*
|
|
40
|
-
* user
|
|
40
|
+
* Run both auth gates and write the user→agent delegation tuple. The user gate
|
|
41
|
+
* runs every session to establish/refresh the user identity, records the
|
|
42
|
+
* `user.auth` span, and never blocks — enforcement is governed by
|
|
43
|
+
* `permissionMode` at tool-call time. `SessionStartResult.proceed` is therefore
|
|
44
|
+
* always `true`; it is retained only for a stable adapter shape.
|
|
41
45
|
*/
|
|
42
46
|
async function sessionStart(client, opts) {
|
|
43
47
|
const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
|
|
@@ -46,13 +50,12 @@ async function sessionStart(client, opts) {
|
|
|
46
50
|
const decision = await userLogin(client, {
|
|
47
51
|
binName: opts.binName ?? `ory-${opts.harness}`,
|
|
48
52
|
harness: opts.harness,
|
|
49
|
-
allowBlock: opts.allowBlock ?? false,
|
|
50
53
|
});
|
|
51
54
|
const creds = await agentGate(client, {
|
|
52
55
|
projectUrl,
|
|
53
56
|
harness: opts.harness,
|
|
54
57
|
});
|
|
55
|
-
await
|
|
58
|
+
await writeUserDelegatesAgent(client);
|
|
56
59
|
return {
|
|
57
60
|
proceed: decision.proceed,
|
|
58
61
|
userMode: decision.mode,
|
|
@@ -60,25 +63,104 @@ async function sessionStart(client, opts) {
|
|
|
60
63
|
agentKind: creds.kind,
|
|
61
64
|
};
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Write a delegation tuple at most once. The tuple is stable for a given
|
|
68
|
+
* (namespace, object, subject), and Keto's create API is not idempotent, so
|
|
69
|
+
* this goes through {@link OryAgentClient.ensureRelationship}: it reads the
|
|
70
|
+
* exact tuple first and only writes when absent — no duplicate on repeated
|
|
71
|
+
* session starts, and self-healing if the tuple was deleted. If the existence
|
|
72
|
+
* probe fails we skip the write rather than risk a duplicate (delegation
|
|
73
|
+
* tuples are audit-only, so a missed write is harmless and retried next
|
|
74
|
+
* session). Best-effort: any failure is logged and swallowed.
|
|
75
|
+
*/
|
|
76
|
+
async function writeDelegationOnce(client, check, options) {
|
|
68
77
|
try {
|
|
69
|
-
await client.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
const res = await client.ensureRelationship(check, {
|
|
79
|
+
spanAttributes: options.spanAttributes,
|
|
80
|
+
});
|
|
81
|
+
if (res.probeError) {
|
|
82
|
+
client.logger.debug("delegation.probe_failed", {
|
|
83
|
+
...options.failedContext,
|
|
84
|
+
message: res.probeError.message,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
catch (err) {
|
|
77
|
-
client.logger.warn(
|
|
89
|
+
client.logger.warn(options.failedEvent, {
|
|
90
|
+
...options.failedContext,
|
|
78
91
|
message: err instanceof Error ? err.message : String(err),
|
|
79
92
|
});
|
|
80
93
|
}
|
|
81
94
|
}
|
|
95
|
+
/** Strip a `<ns>:` prefix from a namespaced subject, if present. */
|
|
96
|
+
function unqualify(subject, ns) {
|
|
97
|
+
const prefix = `${ns}:`;
|
|
98
|
+
return subject.startsWith(prefix) ? subject.slice(prefix.length) : undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve one end of a delegation edge, preferring the live principal but
|
|
102
|
+
* falling back to the persisted delegation anchor (`delegation.delegatedBy`
|
|
103
|
+
* on the DCR credentials in local config). This is what lets the tuple be
|
|
104
|
+
* rebuilt from an authoritative source rather than from nothing when the
|
|
105
|
+
* principal isn't populated for the session. A mismatch between the two is
|
|
106
|
+
* logged for reconciliation visibility; the live principal wins.
|
|
107
|
+
*/
|
|
108
|
+
function reconcileEdgeEnd(client, principal, anchor, role) {
|
|
109
|
+
if (principal && anchor && principal !== anchor) {
|
|
110
|
+
client.logger.debug("delegation.anchor_mismatch", { role, principal, anchor });
|
|
111
|
+
}
|
|
112
|
+
return principal ?? anchor;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Write the `user → agent` delegation tuple, at most once per install
|
|
116
|
+
* (see {@link writeDelegationOnce}). Exported so harness plugins that run
|
|
117
|
+
* their own session-start sequence (rather than {@link sessionStart}) share
|
|
118
|
+
* the same write-once semantics instead of re-issuing the write every
|
|
119
|
+
* session.
|
|
120
|
+
*
|
|
121
|
+
* The tuple is a *derived projection* of the delegation anchor stamped into
|
|
122
|
+
* the agent's OAuth2 client metadata at registration: the edge is resolved
|
|
123
|
+
* from the live principals when populated, otherwise rebuilt from the persisted
|
|
124
|
+
* anchor — so a tuple lost out of band can be self-healed from the identity
|
|
125
|
+
* layer. No-op only when neither source yields both ends.
|
|
126
|
+
*/
|
|
127
|
+
async function writeUserDelegatesAgent(client) {
|
|
128
|
+
// The anchor is the delegation *record* stamped into the agent client's
|
|
129
|
+
// metadata — not the mere existence of a persisted client. Only when a
|
|
130
|
+
// user-to-agent record is present do we know the delegator, so both ends of
|
|
131
|
+
// the edge are taken from the same record's client (agent = its clientId,
|
|
132
|
+
// user = its delegatedBy). Absent a record, fall back to live principals only.
|
|
133
|
+
const anchorCreds = (0, agent_auth_js_1.loadAgentDynamicCredentials)();
|
|
134
|
+
const anchor = anchorCreds?.delegation;
|
|
135
|
+
const hasUserAgentAnchor = anchor?.delegationType === "user-to-agent";
|
|
136
|
+
const anchorUser = hasUserAgentAnchor ? unqualify(anchor.delegatedBy, "user") : undefined;
|
|
137
|
+
// The agent principal's subject is `ORY_AGENT_SUBJECT_ID ?? clientId` (see
|
|
138
|
+
// resolveClientCredentials). The anchor rebuild must apply the *same*
|
|
139
|
+
// precedence, or a no-principal session would write `agent:<clientId>` while
|
|
140
|
+
// a live session wrote `agent:<override>` — two divergent tuples for one edge.
|
|
141
|
+
const anchorAgent = hasUserAgentAnchor
|
|
142
|
+
? (process.env.ORY_AGENT_SUBJECT_ID || anchorCreds.clientId)
|
|
143
|
+
: undefined;
|
|
144
|
+
const user = reconcileEdgeEnd(client, client.userPrincipal.subject, anchorUser, "user");
|
|
145
|
+
const agent = reconcileEdgeEnd(client, client.agentPrincipal.subject, anchorAgent, "agent");
|
|
146
|
+
if (!user || !agent) {
|
|
147
|
+
client.logger.debug("delegation.skip", {
|
|
148
|
+
reason: "missing delegation edge (no principal and no anchor)",
|
|
149
|
+
hasUser: !!user,
|
|
150
|
+
hasAgent: !!agent,
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await writeDelegationOnce(client, {
|
|
155
|
+
namespace: resolveNamespace(),
|
|
156
|
+
object: `agent:${agent}`,
|
|
157
|
+
relation: "delegate",
|
|
158
|
+
subjectId: `user:${user}`,
|
|
159
|
+
}, {
|
|
160
|
+
spanAttributes: { delegation: "user-to-agent" },
|
|
161
|
+
failedEvent: "delegation.user_to_agent.failed",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
82
164
|
async function registerSubagent(client, opts) {
|
|
83
165
|
const projectUrl = opts.projectUrl ?? (0, config_js_1.resolveConfig)().projectUrl;
|
|
84
166
|
const subAgentGate = opts.subAgentGate ?? agent_auth_js_1.ensureSubAgentIdentity;
|
|
@@ -99,23 +181,41 @@ async function registerSubagent(client, opts) {
|
|
|
99
181
|
}
|
|
100
182
|
if (identity.kind !== "dynamic" || !identity.subject)
|
|
101
183
|
return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
184
|
+
await writeAgentDelegatesSubagent(client, identity.subject, opts.subAgentType);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Write the `agent → subagent` delegation tuple, at most once per install
|
|
188
|
+
* (see {@link writeDelegationOnce}). Exported so harness plugins that
|
|
189
|
+
* resolve the sub-agent identity through their own event plumbing (rather
|
|
190
|
+
* than {@link registerSubagent}) still share the write-once semantics.
|
|
191
|
+
* No-op until the agent principal is populated.
|
|
192
|
+
*/
|
|
193
|
+
async function writeAgentDelegatesSubagent(client, subAgentSubject, subAgentType) {
|
|
194
|
+
// The delegator (agent) is resolved from the live principal, falling back to
|
|
195
|
+
// the sub-agent client's persisted delegation anchor (`delegation.delegatedBy`
|
|
196
|
+
// = `agent:<agent>`) so the tuple can be rebuilt when no principal is present.
|
|
197
|
+
const anchor = (0, agent_auth_js_1.loadSubAgentDynamicCredentials)(subAgentType)?.delegation;
|
|
198
|
+
const anchorAgent = anchor?.delegationType === "agent-to-subagent"
|
|
199
|
+
? unqualify(anchor.delegatedBy, "agent")
|
|
200
|
+
: undefined;
|
|
201
|
+
const agent = reconcileEdgeEnd(client, client.agentPrincipal.subject, anchorAgent, "agent");
|
|
202
|
+
if (!agent) {
|
|
203
|
+
client.logger.debug("delegation.skip", {
|
|
204
|
+
reason: "no agent delegation edge (no principal and no anchor)",
|
|
205
|
+
subAgentType,
|
|
117
206
|
});
|
|
207
|
+
return;
|
|
118
208
|
}
|
|
209
|
+
await writeDelegationOnce(client, {
|
|
210
|
+
namespace: resolveNamespace(),
|
|
211
|
+
object: `subagent:${subAgentSubject}`,
|
|
212
|
+
relation: "delegate",
|
|
213
|
+
subjectId: `agent:${agent}`,
|
|
214
|
+
}, {
|
|
215
|
+
spanAttributes: { delegation: "agent-to-subagent", subAgentType },
|
|
216
|
+
failedEvent: "delegation.agent_to_subagent.failed",
|
|
217
|
+
failedContext: { subAgentType },
|
|
218
|
+
});
|
|
119
219
|
}
|
|
120
220
|
/**
|
|
121
221
|
* Authorize a tool call, record the spans, and return a normalized {@link GateResult}.
|
package/dist/agent-auth.d.ts
CHANGED
|
@@ -26,8 +26,19 @@
|
|
|
26
26
|
* the user with an `act` claim showing the agent — is intentionally
|
|
27
27
|
* deferred.
|
|
28
28
|
*/
|
|
29
|
-
import { type OryAgentDynamicCredentials } from "./config.js";
|
|
29
|
+
import { type OryAgentDynamicCredentials, type OryDelegationRecord } from "./config.js";
|
|
30
30
|
import { OryAgentClient } from "./client.js";
|
|
31
|
+
/**
|
|
32
|
+
* Delegation context handed to {@link registerAgentClient} so the freshly
|
|
33
|
+
* minted OAuth2 client is stamped with its delegator at the identity layer.
|
|
34
|
+
* `delegatedAt` is stamped by the registrar, not the caller.
|
|
35
|
+
*/
|
|
36
|
+
export interface DelegationInput {
|
|
37
|
+
/** The delegator, namespaced: `user:<sub>` or `agent:<sub>`. */
|
|
38
|
+
delegatedBy: string;
|
|
39
|
+
delegationType: OryDelegationRecord["delegationType"];
|
|
40
|
+
subAgentType?: string;
|
|
41
|
+
}
|
|
31
42
|
export type AgentCredentialKind = "api_key" | "client_credentials" | "dynamic" | "none";
|
|
32
43
|
export interface AgentCredentials {
|
|
33
44
|
kind: AgentCredentialKind;
|
|
@@ -64,6 +75,13 @@ export interface ResolveAgentCredentialsOptions {
|
|
|
64
75
|
env?: NodeJS.ProcessEnv;
|
|
65
76
|
/** User's bearer token; preferred IAT for dynamic registration. */
|
|
66
77
|
userToken?: string;
|
|
78
|
+
/**
|
|
79
|
+
* User's subject. When present, a fresh DCR stamps `metadata.delegatedBy`
|
|
80
|
+
* = `user:<userSubject>` on the agent client so the user→agent delegation
|
|
81
|
+
* has an identity-layer anchor. Absent ⇒ the client registers without a
|
|
82
|
+
* delegation anchor (no delegator known).
|
|
83
|
+
*/
|
|
84
|
+
userSubject?: string;
|
|
67
85
|
/** Harness identifier — baked into the registered client_name. */
|
|
68
86
|
harness?: string;
|
|
69
87
|
/** Inject the client_credentials token-grant function for tests. */
|
|
@@ -125,8 +143,21 @@ export interface RegisterAgentClientArgs {
|
|
|
125
143
|
auth: string;
|
|
126
144
|
/** Harness identifier baked into the client_name for audit. */
|
|
127
145
|
harness?: string;
|
|
146
|
+
/** When set, names the client as a sub-agent of the given type. */
|
|
147
|
+
subAgentType?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Delegation edge recorded for this client. When present, the issued
|
|
150
|
+
* credentials carry a durable record of who delegated to the agent — the
|
|
151
|
+
* integrity anchor from which the Keto delegation tuple can be rebuilt.
|
|
152
|
+
* Persisted in local config only; Ory's public DCR endpoint rejects a
|
|
153
|
+
* `metadata` field, so it is not stored server-side. Omitted when the
|
|
154
|
+
* delegator isn't known at registration time.
|
|
155
|
+
*/
|
|
156
|
+
delegation?: DelegationInput;
|
|
128
157
|
/** Override hostname for testing. */
|
|
129
158
|
hostnameFn?: () => string;
|
|
159
|
+
/** Inject the wall clock for deterministic `delegatedAt` in tests. */
|
|
160
|
+
nowFn?: () => Date;
|
|
130
161
|
}
|
|
131
162
|
/**
|
|
132
163
|
* Register a new OAuth2 client via RFC 7591. Authorization is carried
|
|
@@ -224,8 +255,8 @@ export interface EnsureSubAgentIdentityOptions {
|
|
|
224
255
|
* Resolve a sub-agent identity via OAuth2 Dynamic Client Registration.
|
|
225
256
|
*
|
|
226
257
|
* Reuses persisted credentials when the `(projectUrl, subAgentType)`
|
|
227
|
-
* pair matches; otherwise registers a fresh client
|
|
228
|
-
* `
|
|
258
|
+
* pair matches; otherwise registers a fresh client named
|
|
259
|
+
* `Ory Agent Security · sub-agent · <harness>/<type> @ <host>` and persists the result.
|
|
229
260
|
* Never throws and never blocks — on any failure the identity collapses
|
|
230
261
|
* to `kind: "none"` and the plugin continues without it. Emits exactly
|
|
231
262
|
* one `agent.auth` span so the audit trail records each sub-agent
|
package/dist/agent-auth.js
CHANGED
|
@@ -45,6 +45,7 @@ exports.ensureSubAgentIdentity = ensureSubAgentIdentity;
|
|
|
45
45
|
const node_os_1 = require("node:os");
|
|
46
46
|
const client_1 = require("@ory/client");
|
|
47
47
|
const config_js_1 = require("./config.js");
|
|
48
|
+
const branding_js_1 = require("./branding.js");
|
|
48
49
|
/**
|
|
49
50
|
* Skew window before nominal expiry at which a cached client_credentials
|
|
50
51
|
* token is considered stale. Matches the user-token skew in auth-store.
|
|
@@ -162,9 +163,26 @@ async function revokeAgentDynamicClient(creds) {
|
|
|
162
163
|
*/
|
|
163
164
|
async function registerAgentClient(args) {
|
|
164
165
|
const host = (args.hostnameFn ?? node_os_1.hostname)() || "unknown-host";
|
|
165
|
-
const clientName =
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
const clientName = (0, branding_js_1.agentClientName)({
|
|
167
|
+
harness: args.harness,
|
|
168
|
+
host,
|
|
169
|
+
subAgentType: args.subAgentType,
|
|
170
|
+
});
|
|
171
|
+
// Build the delegation anchor (if a delegator is known) and mirror it into the
|
|
172
|
+
// persisted credentials. This is the integrity anchor the Keto tuple is derived
|
|
173
|
+
// from. It is NOT sent to Ory: the public DCR endpoint rejects a `metadata`
|
|
174
|
+
// field (see the request body below), so the anchor lives only in local config.
|
|
175
|
+
const delegation = args.delegation
|
|
176
|
+
? {
|
|
177
|
+
delegatedBy: args.delegation.delegatedBy,
|
|
178
|
+
delegationType: args.delegation.delegationType,
|
|
179
|
+
...(args.harness ? { harness: args.harness } : {}),
|
|
180
|
+
...(args.delegation.subAgentType
|
|
181
|
+
? { subAgentType: args.delegation.subAgentType }
|
|
182
|
+
: {}),
|
|
183
|
+
delegatedAt: (args.nowFn ?? (() => new Date()))().toISOString(),
|
|
184
|
+
}
|
|
185
|
+
: undefined;
|
|
168
186
|
const oidc = new client_1.OidcApi(new client_1.Configuration({
|
|
169
187
|
basePath: args.projectUrl,
|
|
170
188
|
accessToken: args.auth,
|
|
@@ -178,6 +196,14 @@ async function registerAgentClient(args) {
|
|
|
178
196
|
response_types: ["token"],
|
|
179
197
|
scope: "openid offline_access",
|
|
180
198
|
token_endpoint_auth_method: "client_secret_post",
|
|
199
|
+
// NB: the delegation record is deliberately NOT sent as `metadata`.
|
|
200
|
+
// Ory Network / Hydra reject a `metadata` field on the public DCR
|
|
201
|
+
// endpoints — both RFC 7591 create and RFC 7592 update return
|
|
202
|
+
// `400 invalid_client_metadata: 'metadata' cannot be set for dynamic
|
|
203
|
+
// client registration` (metadata is admin-API-only). Sending it made
|
|
204
|
+
// every fresh registration fail, so the agent never got credentials.
|
|
205
|
+
// The delegation anchor is persisted locally instead (see the return
|
|
206
|
+
// value) and is never read back from the server side.
|
|
181
207
|
},
|
|
182
208
|
},
|
|
183
209
|
// The generated SDK declares no security scheme for the public DCR create
|
|
@@ -206,6 +232,7 @@ async function registerAgentClient(args) {
|
|
|
206
232
|
registeredAt: Math.floor(Date.now() / 1000),
|
|
207
233
|
projectUrl: args.projectUrl,
|
|
208
234
|
harness: args.harness,
|
|
235
|
+
...(delegation ? { delegation } : {}),
|
|
209
236
|
};
|
|
210
237
|
}
|
|
211
238
|
/**
|
|
@@ -300,6 +327,14 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
300
327
|
projectUrl: options.projectUrl,
|
|
301
328
|
auth: iat,
|
|
302
329
|
harness: options.harness,
|
|
330
|
+
...(options.userSubject
|
|
331
|
+
? {
|
|
332
|
+
delegation: {
|
|
333
|
+
delegatedBy: `user:${options.userSubject}`,
|
|
334
|
+
delegationType: "user-to-agent",
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
: {}),
|
|
303
338
|
});
|
|
304
339
|
if (!registered.clientSecret) {
|
|
305
340
|
warnings.push("Dynamic registration returned no client_secret; cannot run client_credentials grant.");
|
|
@@ -341,7 +376,7 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
341
376
|
}
|
|
342
377
|
return {
|
|
343
378
|
kind: "none",
|
|
344
|
-
reason: "No agent credentials configured. Run with a user session (
|
|
379
|
+
reason: "No agent credentials configured. Run with a user session (the interactive user login runs every session) or set ORY_AGENT_API_KEY / ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET / ORY_AGENT_REGISTRATION_TOKEN.",
|
|
345
380
|
warnings,
|
|
346
381
|
};
|
|
347
382
|
}
|
|
@@ -473,9 +508,11 @@ async function ensureAgentIdentity(client, options = {}) {
|
|
|
473
508
|
creds = await resolveFn({
|
|
474
509
|
projectUrl: options.projectUrl,
|
|
475
510
|
env: options.env,
|
|
476
|
-
// The user gate populates userPrincipal
|
|
477
|
-
//
|
|
511
|
+
// The user gate populates userPrincipal on successful login; we hand the
|
|
512
|
+
// token to the resolver so DCR can use it as the IAT, and the subject so
|
|
513
|
+
// the fresh client is stamped with its delegator (metadata.delegatedBy).
|
|
478
514
|
userToken: client.userPrincipal.token,
|
|
515
|
+
userSubject: client.userPrincipal.subject,
|
|
479
516
|
harness: options.harness,
|
|
480
517
|
});
|
|
481
518
|
}
|
|
@@ -512,8 +549,8 @@ async function ensureAgentIdentity(client, options = {}) {
|
|
|
512
549
|
* Resolve a sub-agent identity via OAuth2 Dynamic Client Registration.
|
|
513
550
|
*
|
|
514
551
|
* Reuses persisted credentials when the `(projectUrl, subAgentType)`
|
|
515
|
-
* pair matches; otherwise registers a fresh client
|
|
516
|
-
* `
|
|
552
|
+
* pair matches; otherwise registers a fresh client named
|
|
553
|
+
* `Ory Agent Security · sub-agent · <harness>/<type> @ <host>` and persists the result.
|
|
517
554
|
* Never throws and never blocks — on any failure the identity collapses
|
|
518
555
|
* to `kind: "none"` and the plugin continues without it. Emits exactly
|
|
519
556
|
* one `agent.auth` span so the audit trail records each sub-agent
|
|
@@ -550,6 +587,10 @@ async function ensureSubAgentIdentity(client, options) {
|
|
|
550
587
|
subAgentType: options.subAgentType,
|
|
551
588
|
projectUrl: options.projectUrl,
|
|
552
589
|
harness: options.harness,
|
|
590
|
+
// The delegator for a sub-agent is the parent agent. When its subject is
|
|
591
|
+
// populated, stamp `metadata.delegatedBy` = `agent:<agent>` on the
|
|
592
|
+
// sub-agent client so the agent→subagent edge is anchored to the identity.
|
|
593
|
+
delegatorSubject: client.agentPrincipal.subject,
|
|
553
594
|
iat: options.iat
|
|
554
595
|
?? client.agentPrincipal.token
|
|
555
596
|
?? client.userPrincipal.token
|
|
@@ -633,6 +674,15 @@ async function resolveSubAgentIdentity(args) {
|
|
|
633
674
|
projectUrl: args.projectUrl,
|
|
634
675
|
auth: args.iat,
|
|
635
676
|
harness: subHarness,
|
|
677
|
+
...(args.delegatorSubject
|
|
678
|
+
? {
|
|
679
|
+
delegation: {
|
|
680
|
+
delegatedBy: `agent:${args.delegatorSubject}`,
|
|
681
|
+
delegationType: "agent-to-subagent",
|
|
682
|
+
subAgentType: args.subAgentType,
|
|
683
|
+
},
|
|
684
|
+
}
|
|
685
|
+
: {}),
|
|
636
686
|
});
|
|
637
687
|
args.saveFn(args.subAgentType, registered);
|
|
638
688
|
return {
|
package/dist/auth.d.ts
CHANGED
|
@@ -91,3 +91,10 @@ export declare function refreshAccessToken(args: {
|
|
|
91
91
|
clientId: string;
|
|
92
92
|
refreshToken: string;
|
|
93
93
|
}): Promise<OryOAuth2Tokens>;
|
|
94
|
+
/**
|
|
95
|
+
* Best-effort human-readable name for the signed-in user, pulled from the
|
|
96
|
+
* id_token's identity claims — `email`, then `name`, then `preferred_username`.
|
|
97
|
+
* Returns undefined when the token carries none of them (e.g. an `openid`-only
|
|
98
|
+
* token), so callers fall back to the opaque subject id.
|
|
99
|
+
*/
|
|
100
|
+
export declare function displayNameFromIdToken(idToken: string | undefined): string | undefined;
|