@ory/argus 0.6.2 → 0.7.1
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/ory-build-agent/SKILL.md +441 -0
- package/assets/skills/ory-e2b-sandbox/SKILL.md +2 -2
- package/assets/skills/permissions-onboarding/SKILL.md +2 -2
- package/dist/agent-auth.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/dev.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/local/manager.js +1 -1
- package/dist/permissions-cli.js +3 -3
- package/dist/setup.js +3 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +6 -1
- package/dist/status-cli.d.ts +1 -1
- package/dist/status-cli.js +10 -10
- package/dist/subject.d.ts +2 -2
- package/dist/subject.js +2 -2
- package/dist/{auth-gate.d.ts → user-login.d.ts} +10 -10
- package/dist/{auth-gate.js → user-login.js} +14 -14
- package/package.json +1 -1
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ory-build-agent
|
|
3
|
+
description: Build your own AI agent that authenticates the user, authorizes every tool call against Ory Permissions, and emits trace spans — by dropping `@ory/argus` directly into the Claude Agent SDK, OpenAI Agents SDK, Mastra, Vercel AI SDK, PydanticAI / LangGraph, or as an external service called by Salesforce Agentforce. Use when the user wants to wire Ory into a custom agent they own — phrases like "add Ory to my own agent", "build a custom agent with Ory auth", "wrap my Claude Agent SDK tools with Ory permissions", "OpenAI Agents SDK with Ory", "Mastra agent with Ory permissions", "Agentforce action with Ory", "use `@ory/argus` directly". For wiring Ory into an existing agent harness (Claude Code, Codex, Gemini CLI, OpenClaw, OpenCode) use the corresponding `@ory/<harness>` plugin instead.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build your own agent with `@ory/argus`
|
|
7
|
+
|
|
8
|
+
You are helping the user wire Ory Identities, Permissions, and tracing into
|
|
9
|
+
**an agent they are building themselves**. They are not extending Claude Code,
|
|
10
|
+
Codex, or one of the other harness plugins — they own the agent loop and
|
|
11
|
+
choose where to intercept tool calls.
|
|
12
|
+
|
|
13
|
+
The integration is the same three moves regardless of SDK:
|
|
14
|
+
|
|
15
|
+
1. **User gate at start.** `ensureUserAuthenticated(client, …)` — the human at
|
|
16
|
+
the keyboard becomes the subject of every permission check.
|
|
17
|
+
2. **Agent gate at start.** `ensureAgentIdentity(client, …)` — the process
|
|
18
|
+
making outbound Ory API calls gets its own credential (OAuth2 Dynamic
|
|
19
|
+
Client Registration by default, persisted across sessions).
|
|
20
|
+
3. **Permission check on every tool call.** Wrap the SDK's tool dispatch with
|
|
21
|
+
`checkAndDecide(client, …)` and branch on `decision.kind`. Record a
|
|
22
|
+
`tool.complete` span after the tool returns.
|
|
23
|
+
|
|
24
|
+
`@ory/argus` ships every helper and handles fail-open semantics (network
|
|
25
|
+
errors, rate limits, unconfigured project → allow). The SDKs differ only in
|
|
26
|
+
**where** that wrapper goes.
|
|
27
|
+
|
|
28
|
+
> **Precondition:** the user has an Ory project (or will spin up the local
|
|
29
|
+
> stack — see {{REF_LOCAL_DEV}}) and has the env vars from
|
|
30
|
+
> {{REF_AUTH_SETUP}} figured out. Do not fabricate credentials or scaffold a
|
|
31
|
+
> project on their behalf.
|
|
32
|
+
|
|
33
|
+
## Step 1 — Pick the SDK and confirm the agent shape
|
|
34
|
+
|
|
35
|
+
Ask the user which SDK they're using and what the agent looks like. Below
|
|
36
|
+
are the SDKs this skill carries explicit recipes for. Others (LangChain,
|
|
37
|
+
LlamaIndex, generic OpenAI tool-calling loops) follow the same pattern —
|
|
38
|
+
wrap each tool dispatch with the gate from Step 4.
|
|
39
|
+
|
|
40
|
+
| SDK | Language | Where Ory hooks in |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) | TypeScript / Python | `canUseTool` callback on `query({...})` |
|
|
43
|
+
| OpenAI Agents SDK (`@openai/agents`) | TypeScript | Per-tool `execute` wrapper or `RunHooks.onToolStart` |
|
|
44
|
+
| Salesforce Agentforce (Agent Builder) | declarative + Apex | External Service / side-car — see "Salesforce" below |
|
|
45
|
+
| Mastra (`@mastra/core`) | TypeScript | Higher-order wrapper around each tool's `execute` |
|
|
46
|
+
| Mistral AI (`@mistralai/mistralai` / `mistralai`) | TypeScript / Python | Per-tool wrapper inside the chat-completion loop or the Agents API tool registry |
|
|
47
|
+
| PydanticAI (`pydantic-ai`) | Python | `@agent.tool` decorator stack |
|
|
48
|
+
| Vercel AI SDK (`ai`) | TypeScript | Higher-order wrapper at `streamText({ tools })` |
|
|
49
|
+
| LangGraph (`langgraph`) | Python / TypeScript | `ToolNode` wrapper or `RunnableLambda` per tool |
|
|
50
|
+
|
|
51
|
+
Also establish:
|
|
52
|
+
|
|
53
|
+
- **Interactive vs headless.** Desktop / terminal agents can run PKCE login.
|
|
54
|
+
Headless services (CI, daemons, Salesforce side-cars) must pre-supply
|
|
55
|
+
`ORY_USER_SESSION_TOKEN` or `ORY_USER_OAUTH2_TOKEN`.
|
|
56
|
+
- **Which tools to gate.** Usually all of them. Some SDKs have built-in
|
|
57
|
+
"safe" steps (an LLM-only reasoning step, a model-provided memory tool)
|
|
58
|
+
that don't need a permission check.
|
|
59
|
+
- **Language.** `@ory/argus` is JavaScript-first. Python frameworks call out
|
|
60
|
+
to a tiny Node side-car or use the official `ory-client` Python SDK
|
|
61
|
+
directly; the snippet below shows the side-car shape.
|
|
62
|
+
|
|
63
|
+
## Step 2 — Install `@ory/argus`
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install @ory/argus
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
That's the only Ory dependency you need. The Ory SDK clients
|
|
70
|
+
(`@ory/client`) and the OAuth2/PKCE plumbing are re-exported and ready to
|
|
71
|
+
use.
|
|
72
|
+
|
|
73
|
+
## Step 3 — Construct the client and run both gates
|
|
74
|
+
|
|
75
|
+
Put this at the top of the agent's bootstrap, before the agent loop starts
|
|
76
|
+
processing the first message:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import {
|
|
80
|
+
OryAgentClient,
|
|
81
|
+
ensureUserAuthenticated,
|
|
82
|
+
ensureAgentIdentity,
|
|
83
|
+
resolveConfig,
|
|
84
|
+
} from "@ory/argus";
|
|
85
|
+
|
|
86
|
+
const client = OryAgentClient.fromEnv("my-agent");
|
|
87
|
+
const { projectUrl } = resolveConfig();
|
|
88
|
+
|
|
89
|
+
// 1. User gate — interactive PKCE when ORY_USER_LOGIN=1, no-op otherwise.
|
|
90
|
+
const userDecision = await ensureUserAuthenticated(client, {
|
|
91
|
+
binName: "my-agent",
|
|
92
|
+
harness: "my-agent",
|
|
93
|
+
allowBlock: true, // flip to false if your agent can't refuse to start
|
|
94
|
+
});
|
|
95
|
+
if (userDecision.proceed === false) {
|
|
96
|
+
console.error(`Ory user login: ${userDecision.reason}`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Agent gate — never blocks; resolves machine credentials (DCR by default).
|
|
101
|
+
await ensureAgentIdentity(client, { projectUrl, harness: "my-agent" });
|
|
102
|
+
|
|
103
|
+
// 3. (optional) write the user→agent delegation tuple for audit.
|
|
104
|
+
if (client.userPrincipal.subject && client.agentPrincipal.subject) {
|
|
105
|
+
await client
|
|
106
|
+
.createRelationship({
|
|
107
|
+
namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
|
|
108
|
+
object: `agent:${client.agentPrincipal.subject}`,
|
|
109
|
+
relation: "delegate",
|
|
110
|
+
subjectId: `user:${client.userPrincipal.subject}`,
|
|
111
|
+
})
|
|
112
|
+
.catch(() => undefined); // audit-only — swallow failures
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Set `allowBlock: false` when the agent runs in-process inside a parent
|
|
117
|
+
application and can't refuse to start. The gate still runs in advisory mode
|
|
118
|
+
— it refreshes tokens, prompts on TTY, emits the `user.auth` span — but
|
|
119
|
+
always returns `proceed: true`.
|
|
120
|
+
|
|
121
|
+
## Step 4 — The shared gate body
|
|
122
|
+
|
|
123
|
+
This snippet is reused verbatim from every SDK-specific section in Step 5.
|
|
124
|
+
Put it next to where you construct the client.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import {
|
|
128
|
+
checkAndDecide,
|
|
129
|
+
resolveUserSubject,
|
|
130
|
+
subjectLabel,
|
|
131
|
+
} from "@ory/argus";
|
|
132
|
+
|
|
133
|
+
async function gateTool(toolName: string, sessionId: string) {
|
|
134
|
+
const subject = resolveUserSubject(client, `session:${sessionId}`);
|
|
135
|
+
const decision = await checkAndDecide(
|
|
136
|
+
client,
|
|
137
|
+
{
|
|
138
|
+
namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
|
|
139
|
+
object: toolName,
|
|
140
|
+
relation: "use",
|
|
141
|
+
...subject,
|
|
142
|
+
},
|
|
143
|
+
{ spanAttributes: { toolName } }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
switch (decision.kind) {
|
|
147
|
+
case "allow":
|
|
148
|
+
case "observe":
|
|
149
|
+
case "fail_open":
|
|
150
|
+
return { allow: true as const };
|
|
151
|
+
case "deny":
|
|
152
|
+
return {
|
|
153
|
+
allow: false as const,
|
|
154
|
+
message: `Ory denied ${toolName} for ${subjectLabel(subject)}.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
After the tool finishes (whichever SDK), record a completion span:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
client.tracer.record("tool.complete", "ok", {
|
|
164
|
+
attributes: { toolName, durationMs },
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`checkAndDecide` already records `permission.check`, `permission.observe_deny`
|
|
169
|
+
(on observe), and `tool.block` (on deny). You only own `tool.complete`.
|
|
170
|
+
|
|
171
|
+
## Step 5 — SDK-specific wiring
|
|
172
|
+
|
|
173
|
+
### Claude Agent SDK
|
|
174
|
+
|
|
175
|
+
The Claude Agent SDK exposes a `canUseTool` callback that fires before every
|
|
176
|
+
tool invocation. Drop the gate there:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
180
|
+
|
|
181
|
+
const stream = query({
|
|
182
|
+
prompt,
|
|
183
|
+
options: {
|
|
184
|
+
canUseTool: async (toolName, input, { signal }) => {
|
|
185
|
+
const sessionId = currentSessionId(); // your own correlation id
|
|
186
|
+
const gate = await gateTool(toolName, sessionId);
|
|
187
|
+
if (!gate.allow) {
|
|
188
|
+
return { behavior: "deny", message: gate.message };
|
|
189
|
+
}
|
|
190
|
+
return { behavior: "allow", updatedInput: input };
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
for await (const msg of stream) { /* standard handling */ }
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`canUseTool` only fires for tools the SDK controls. If the agent also
|
|
199
|
+
registers MCP servers, wrap each MCP tool handler the same way — see the
|
|
200
|
+
`@ory/argus` `parseClaudeCodeMcpTool` / `checkMcpPermission` helpers for an
|
|
201
|
+
MCP-flavored version of the gate.
|
|
202
|
+
|
|
203
|
+
### OpenAI Agents SDK
|
|
204
|
+
|
|
205
|
+
The OpenAI Agents SDK supports per-run lifecycle hooks via `RunHooks` plus
|
|
206
|
+
per-tool `execute` overrides. Pick whichever you prefer:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { Agent, Runner, tool } from "@openai/agents";
|
|
210
|
+
|
|
211
|
+
const search = tool({
|
|
212
|
+
name: "search",
|
|
213
|
+
description: "...",
|
|
214
|
+
parameters: SearchParams,
|
|
215
|
+
execute: async (input, ctx) => {
|
|
216
|
+
const gate = await gateTool("search", ctx.runId);
|
|
217
|
+
if (!gate.allow) return { error: gate.message };
|
|
218
|
+
return realSearch(input);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const agent = new Agent({ name: "my-agent", tools: [search], model: "gpt-4.1" });
|
|
223
|
+
await new Runner().run(agent, prompt);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
For agent-wide enforcement without per-tool wrapping, register a
|
|
227
|
+
`on_tool_start` hook on the runner and throw on deny — the SDK surfaces the
|
|
228
|
+
throw to the model as a tool error.
|
|
229
|
+
|
|
230
|
+
### Salesforce Agentforce (Agent Builder)
|
|
231
|
+
|
|
232
|
+
Agentforce is a declarative agent inside the Salesforce platform — you
|
|
233
|
+
**cannot** embed `@ory/argus` in the agent process. Instead:
|
|
234
|
+
|
|
235
|
+
1. Stand up a small Node.js service that hosts the gated tools and exposes
|
|
236
|
+
each as an HTTP endpoint. Inside that service, run Steps 3 + 4 exactly as
|
|
237
|
+
above, then call `gateTool(...)` at the top of every handler.
|
|
238
|
+
2. Register the service in Salesforce as a **Named Credential** plus an
|
|
239
|
+
**External Service** (OpenAPI 3 spec). Each operation becomes an
|
|
240
|
+
Agentforce **Action**.
|
|
241
|
+
3. Define an Agentforce **Topic** whose actions call the External Service
|
|
242
|
+
operations. The gate runs inside your Node service on every call; denies
|
|
243
|
+
come back as tool errors the agent surfaces to the user.
|
|
244
|
+
|
|
245
|
+
Pre-supply `ORY_USER_SESSION_TOKEN` (or `ORY_USER_OAUTH2_TOKEN`) to the
|
|
246
|
+
side-car from a session the user established out-of-band — for example, a
|
|
247
|
+
PKCE flow at sign-on into the Experience Cloud site that fronts the agent.
|
|
248
|
+
A headless side-car cannot run PKCE on its own.
|
|
249
|
+
|
|
250
|
+
### Mastra Agent Framework
|
|
251
|
+
|
|
252
|
+
Mastra runs tools via `tool.execute({ context, runtimeContext })`. Wrap the
|
|
253
|
+
agent's tool registry at construction:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { Agent } from "@mastra/core";
|
|
257
|
+
|
|
258
|
+
function gated<T extends { id: string; execute: (a: any) => Promise<any> }>(t: T): T {
|
|
259
|
+
const original = t.execute.bind(t);
|
|
260
|
+
return {
|
|
261
|
+
...t,
|
|
262
|
+
execute: async (args: any) => {
|
|
263
|
+
const sessionId = args.runtimeContext?.sessionId ?? "unknown";
|
|
264
|
+
const gate = await gateTool(t.id, sessionId);
|
|
265
|
+
if (!gate.allow) return { error: gate.message };
|
|
266
|
+
return original(args);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const agent = new Agent({
|
|
272
|
+
name: "my-agent",
|
|
273
|
+
model,
|
|
274
|
+
tools: Object.fromEntries(
|
|
275
|
+
Object.entries(tools).map(([id, t]) => [id, gated(t)])
|
|
276
|
+
),
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Mistral AI
|
|
281
|
+
|
|
282
|
+
Mistral's SDK (`@mistralai/mistralai` for TypeScript, `mistralai` for
|
|
283
|
+
Python) exposes two surfaces. Both gate the same way.
|
|
284
|
+
|
|
285
|
+
**Chat-completion loop with tools.** You own the loop: call `chat.complete`,
|
|
286
|
+
inspect `tool_calls` on the response, run each tool, send the results back.
|
|
287
|
+
Gate inside the tool dispatcher:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { Mistral } from "@mistralai/mistralai";
|
|
291
|
+
|
|
292
|
+
const client_ai = new Mistral({ apiKey: process.env.MISTRAL_API_KEY });
|
|
293
|
+
|
|
294
|
+
async function step(messages: any[], sessionId: string) {
|
|
295
|
+
const res = await client_ai.chat.complete({
|
|
296
|
+
model: "mistral-large-latest",
|
|
297
|
+
messages,
|
|
298
|
+
tools, // [{ type: "function", function: { name, parameters, description } }]
|
|
299
|
+
toolChoice: "auto",
|
|
300
|
+
});
|
|
301
|
+
const choice = res.choices[0];
|
|
302
|
+
if (!choice.message.toolCalls?.length) return choice.message;
|
|
303
|
+
|
|
304
|
+
const toolResults = await Promise.all(
|
|
305
|
+
choice.message.toolCalls.map(async (call) => {
|
|
306
|
+
const gate = await gateTool(call.function.name, sessionId);
|
|
307
|
+
if (!gate.allow) {
|
|
308
|
+
return { toolCallId: call.id, name: call.function.name, content: gate.message };
|
|
309
|
+
}
|
|
310
|
+
const output = await runTool(call.function.name, JSON.parse(call.function.arguments));
|
|
311
|
+
return { toolCallId: call.id, name: call.function.name, content: JSON.stringify(output) };
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
return step(
|
|
315
|
+
[...messages, choice.message, ...toolResults.map((r) => ({ role: "tool", ...r }))],
|
|
316
|
+
sessionId
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Mistral Agents API (la Plateforme).** When you use the managed Agents API
|
|
322
|
+
(`agents.create({ tools })`, `conversations.start`), Mistral runs the tool
|
|
323
|
+
loop server-side and only calls back to your code for tools it can't
|
|
324
|
+
execute itself — i.e. your "function" tools delivered via webhook. Wrap
|
|
325
|
+
each webhook handler with `gateTool(...)` and return either the result or
|
|
326
|
+
the gate's denial message. The server-side connectors (`web_search`,
|
|
327
|
+
`code_interpreter`, MCP connectors) execute inside Mistral and bypass your
|
|
328
|
+
gate — model them explicitly in your Ory namespace if you want to control
|
|
329
|
+
them, e.g. by writing per-connector tuples and skipping the agent
|
|
330
|
+
definition for users without the relation.
|
|
331
|
+
|
|
332
|
+
The same Python recipe applies via the `mistralai` package — replace the
|
|
333
|
+
`client_ai.chat.complete(...)` call with `client_ai.chat.complete(...)` from
|
|
334
|
+
the Python SDK and use the side-car pattern for `gateTool` (see
|
|
335
|
+
PydanticAI).
|
|
336
|
+
|
|
337
|
+
### PydanticAI (Python — covers the "Pi"-style framework slot)
|
|
338
|
+
|
|
339
|
+
Pure-Python agents don't link `@ory/argus` directly. The two supported
|
|
340
|
+
patterns:
|
|
341
|
+
|
|
342
|
+
- **Side-car HTTP service.** Run a small Node process that exposes
|
|
343
|
+
`POST /gate` (calls `gateTool`) and `POST /trace` (calls
|
|
344
|
+
`client.tracer.record(...)`). Your Python agent calls these from inside
|
|
345
|
+
each `@agent.tool`.
|
|
346
|
+
- **Direct Ory APIs.** Use the official `ory-client` Python SDK to call
|
|
347
|
+
`PermissionApi.check_permission()` and post audit spans to your own
|
|
348
|
+
collector. You lose the fail-open / observe-mode helpers; re-implement
|
|
349
|
+
them in Python.
|
|
350
|
+
|
|
351
|
+
Side-car pattern:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from pydantic_ai import Agent, RunContext
|
|
355
|
+
import httpx
|
|
356
|
+
|
|
357
|
+
agent = Agent("openai:gpt-4.1", deps_type=AgentDeps)
|
|
358
|
+
|
|
359
|
+
@agent.tool
|
|
360
|
+
async def search(ctx: RunContext[AgentDeps], q: str) -> str:
|
|
361
|
+
r = await httpx.post("http://localhost:5310/gate",
|
|
362
|
+
json={"tool": "search", "session": ctx.deps.session_id})
|
|
363
|
+
if not r.json()["allow"]:
|
|
364
|
+
return r.json()["message"]
|
|
365
|
+
return real_search(q)
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The same Python pattern applies verbatim to **LangGraph** (wrap each
|
|
369
|
+
`ToolNode` in a `RunnableLambda` that calls `/gate` first) and to
|
|
370
|
+
**LlamaIndex** agents (override `FunctionTool.acall`).
|
|
371
|
+
|
|
372
|
+
### Vercel AI SDK
|
|
373
|
+
|
|
374
|
+
The `ai` package's `tool()` helper produces descriptors consumed by
|
|
375
|
+
`streamText` / `generateText`. Wrap them at construction:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
import { streamText, tool } from "ai";
|
|
379
|
+
|
|
380
|
+
function gated(name: string, def: ReturnType<typeof tool>) {
|
|
381
|
+
return tool({
|
|
382
|
+
...def,
|
|
383
|
+
execute: async (input, ctx) => {
|
|
384
|
+
const gate = await gateTool(name, ctx.toolCallId);
|
|
385
|
+
if (!gate.allow) return { error: gate.message };
|
|
386
|
+
return def.execute(input, ctx);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
await streamText({
|
|
392
|
+
model,
|
|
393
|
+
tools: { search: gated("search", searchTool), write: gated("write", writeTool) },
|
|
394
|
+
prompt,
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### LangGraph (TypeScript)
|
|
399
|
+
|
|
400
|
+
LangGraph's `ToolNode` runs a registered tool array. Wrap each tool the same
|
|
401
|
+
way Vercel AI SDK does, then pass the wrapped array to `new ToolNode(...)`.
|
|
402
|
+
The `gateTool` body does not change.
|
|
403
|
+
|
|
404
|
+
## Step 6 — Test against the local Ory stack
|
|
405
|
+
|
|
406
|
+
Before pointing at production, run the gate against the local stack so the
|
|
407
|
+
PKCE flow, permission tuples, and trace spans are all visible:
|
|
408
|
+
|
|
409
|
+
1. {{REF_LOCAL_UP}} — brings up Kratos / Keto / Hydra on `localhost:4000`
|
|
410
|
+
and seeds a demo user. The banner prints the email + password.
|
|
411
|
+
2. Export the env vars the launcher writes (`ORY_PROJECT_URL`,
|
|
412
|
+
`ORY_USER_LOGIN=1`, `ORY_OAUTH2_CLIENT_ID`, optional
|
|
413
|
+
`ORY_AGENT_TRACE_FILE` for an NDJSON span log).
|
|
414
|
+
3. Start your agent. Confirm the browser opens for PKCE login.
|
|
415
|
+
4. Invoke a gated tool and `tail -f $ORY_AGENT_TRACE_FILE | jq .` — you
|
|
416
|
+
should see `user.auth` → `agent.auth` → `permission.check` →
|
|
417
|
+
`tool.complete` for every call.
|
|
418
|
+
5. Promote to enforce once the `use` tuples are seeded: either
|
|
419
|
+
`ORY_PERMISSION_MODE=enforce` for one launch, or use one of the harness
|
|
420
|
+
CLIs to flip it persistently (e.g. `npx -y -p @ory/claude-code ory-claude
|
|
421
|
+
permissions enforce` — same shared config file).
|
|
422
|
+
6. {{REF_LOCAL_DOWN}} when done. Volumes persist, so the seeded user
|
|
423
|
+
survives across runs.
|
|
424
|
+
|
|
425
|
+
For full env-var coverage (including the user/agent split,
|
|
426
|
+
`ORY_USER_SUBJECT_NAMESPACE`, agent DCR knobs), see {{REF_AUTH_SETUP}}.
|
|
427
|
+
|
|
428
|
+
## What this skill does NOT do
|
|
429
|
+
|
|
430
|
+
- It does not generate the agent. The user owns the agent loop, tool
|
|
431
|
+
catalog, and deployment shape. This skill only drops `@ory/argus` into
|
|
432
|
+
whatever they already have.
|
|
433
|
+
- It does not write the permission tuples. Seed them with
|
|
434
|
+
`... permissions bootstrap` (run via any of the harness CLIs — same shared
|
|
435
|
+
config file) or by calling `client.createRelationship` directly.
|
|
436
|
+
- It does not adapt one of the existing harness plugins (`@ory/claude-code`,
|
|
437
|
+
`@ory/codex`, `@ory/gemini-cli`, `@ory/openclaw`, `@ory/opencode`). Those
|
|
438
|
+
are for users running those harnesses — not building a custom agent.
|
|
439
|
+
- It does not invent SDK-internal types. The snippets are the canonical
|
|
440
|
+
shape, but SDK hook signatures drift release-to-release — verify against
|
|
441
|
+
the user's pinned version before pasting.
|
|
@@ -66,7 +66,7 @@ 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
|
-
|
|
69
|
+
ORY_USER_LOGIN: "1",
|
|
70
70
|
ORY_PERMISSION_MODE: "observe",
|
|
71
71
|
ORY_PERMISSION_NAMESPACE: "AgentTools",
|
|
72
72
|
ORY_AGENT_DEBUG: "true",
|
|
@@ -198,7 +198,7 @@ await sbx.commands.run("{{BIN}} --version"); // sanity check
|
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
Sandboxes are headless, so the user **must** pre-supply
|
|
201
|
-
`ORY_USER_SESSION_TOKEN` or `ORY_USER_OAUTH2_TOKEN` — otherwise
|
|
201
|
+
`ORY_USER_SESSION_TOKEN` or `ORY_USER_OAUTH2_TOKEN` — otherwise user login's
|
|
202
202
|
PKCE browser flow has no target and hangs. See {{REF_AUTH_SETUP}} for the full
|
|
203
203
|
env-var matrix.
|
|
204
204
|
|
|
@@ -92,9 +92,9 @@ out-of-band as they come into scope.
|
|
|
92
92
|
Two common failure modes:
|
|
93
93
|
|
|
94
94
|
1. **No user identity cached.** Bootstrap needs to know which subject
|
|
95
|
-
to grant tuples to. If
|
|
95
|
+
to grant tuples to. If user login has never run (no PKCE login,
|
|
96
96
|
no `ORY_USER_SUBJECT_ID`), the command refuses. Run the harness once
|
|
97
|
-
with `
|
|
97
|
+
with `ORY_USER_LOGIN=1` to cache a user token, or set
|
|
98
98
|
`ORY_USER_SUBJECT_ID=<id>` to target a known subject.
|
|
99
99
|
2. **Credentials lack write scope on the permission namespace.** The
|
|
100
100
|
command prints the full tuple list so you can apply them manually
|
package/dist/agent-auth.js
CHANGED
|
@@ -290,7 +290,7 @@ async function resolveAgentCredentials(options = {}) {
|
|
|
290
290
|
}
|
|
291
291
|
return {
|
|
292
292
|
kind: "none",
|
|
293
|
-
reason: "No agent credentials configured. Run with a user session (
|
|
293
|
+
reason: "No agent credentials configured. Run with a user session (ORY_USER_LOGIN=1) or set ORY_AGENT_API_KEY / ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET / ORY_AGENT_REGISTRATION_TOKEN.",
|
|
294
294
|
warnings,
|
|
295
295
|
};
|
|
296
296
|
}
|
package/dist/cli.js
CHANGED
|
@@ -146,7 +146,7 @@ function printEnvironment() {
|
|
|
146
146
|
console.log("");
|
|
147
147
|
console.log("Environment:");
|
|
148
148
|
const vars = [
|
|
149
|
-
["
|
|
149
|
+
["ORY_USER_LOGIN", process.env.ORY_USER_LOGIN, false],
|
|
150
150
|
[
|
|
151
151
|
"ORY_AGENT_API_KEY",
|
|
152
152
|
process.env.ORY_AGENT_API_KEY ? "(set)" : undefined,
|
package/dist/dev.js
CHANGED
|
@@ -403,9 +403,9 @@ function buildLocalOryEnv(gatewayUrl, seed) {
|
|
|
403
403
|
return {
|
|
404
404
|
ORY_PROJECT_URL: gatewayUrl,
|
|
405
405
|
ORY_PERMISSION_NAMESPACE: seed.permissions.namespace,
|
|
406
|
-
// User
|
|
406
|
+
// User login — always on in local dev so the launcher demonstrates
|
|
407
407
|
// the interactive PKCE login UX end-to-end every session.
|
|
408
|
-
|
|
408
|
+
ORY_USER_LOGIN: "1",
|
|
409
409
|
// Agent identity — the harness self-registers via DCR on first run
|
|
410
410
|
// using the user's bearer as the initial access token. No static
|
|
411
411
|
// client_credentials are seeded; explicitly clear them so a leaking
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { Tracer, ActiveSpan, deriveTraceId, formatSpan, watchTraceFile, type Tra
|
|
|
4
4
|
export { loadConfig, saveConfig, resolveConfig, mutateConfig, getConfigPath, getDataDir, getHarnessDataDir, type OryPluginConfig, type OryOAuth2Tokens, type OryUserCredentials, type OryAgentCredentialsBlock, type OryAgentDynamicCredentials, type PermissionMode, } from "./config.js";
|
|
5
5
|
export { pkceLogin, refreshAccessToken, detectHeadless, generateCodeVerifier, sha256Base64Url, buildAuthorizeUrl, LOOPBACK_PORTS, DEFAULT_LOGIN_TIMEOUT_MS, type PkceLoginOptions, type PkceLoginOutcome, type PkceDeclineReason, } from "./auth.js";
|
|
6
6
|
export { loadTokens, saveTokens, clearTokens, isExpired, refreshAndSave, tryAcquirePkceFlightLock, clearPkceFlightLock, waitForPeerTokens, waitForPeerTokensSync, TOKEN_EXPIRY_SKEW_SEC, type PkceFlightLock, } from "./auth-store.js";
|
|
7
|
-
export { ensureUserAuthenticated, ensureAuthenticated, type
|
|
7
|
+
export { ensureUserAuthenticated, ensureAuthenticated, type UserLoginDecision, type UserLoginMode, type UserLoginOptions, } from "./user-login.js";
|
|
8
8
|
export { resolveAgentCredentials, ensureAgentIdentity, ensureSubAgentIdentity, fetchClientCredentialsToken, registerAgentClient, loadAgentDynamicCredentials, saveAgentDynamicCredentials, clearAgentDynamicCredentials, loadSubAgentDynamicCredentials, saveSubAgentDynamicCredentials, clearSubAgentDynamicCredentials, AGENT_TOKEN_EXPIRY_SKEW_SEC, type AgentCredentials, type AgentCredentialKind, type ResolveAgentCredentialsOptions, type EnsureAgentIdentityOptions, type RegisterAgentClientArgs, type SubAgentIdentity, type EnsureSubAgentIdentityOptions, } from "./agent-auth.js";
|
|
9
9
|
export { type SessionInfo, type OAuth2TokenInfo, type PermissionCheck, type PermissionResult, type BatchPermissionResult, type OryError, type OryErrorCode, } from "./types.js";
|
|
10
10
|
export { runConfigureCommand, runAgentCommand, printOryConfig, printEnvironment, printLogTail, printEnvHelp, printTraceTail, runWatchCommand, isTtyAvailable, promptOnTty, promptForProjectUrl, interactiveConfigPrompt, } from "./cli.js";
|
package/dist/index.js
CHANGED
|
@@ -42,9 +42,9 @@ Object.defineProperty(exports, "clearPkceFlightLock", { enumerable: true, get: f
|
|
|
42
42
|
Object.defineProperty(exports, "waitForPeerTokens", { enumerable: true, get: function () { return auth_store_js_1.waitForPeerTokens; } });
|
|
43
43
|
Object.defineProperty(exports, "waitForPeerTokensSync", { enumerable: true, get: function () { return auth_store_js_1.waitForPeerTokensSync; } });
|
|
44
44
|
Object.defineProperty(exports, "TOKEN_EXPIRY_SKEW_SEC", { enumerable: true, get: function () { return auth_store_js_1.TOKEN_EXPIRY_SKEW_SEC; } });
|
|
45
|
-
var
|
|
46
|
-
Object.defineProperty(exports, "ensureUserAuthenticated", { enumerable: true, get: function () { return
|
|
47
|
-
Object.defineProperty(exports, "ensureAuthenticated", { enumerable: true, get: function () { return
|
|
45
|
+
var user_login_js_1 = require("./user-login.js");
|
|
46
|
+
Object.defineProperty(exports, "ensureUserAuthenticated", { enumerable: true, get: function () { return user_login_js_1.ensureUserAuthenticated; } });
|
|
47
|
+
Object.defineProperty(exports, "ensureAuthenticated", { enumerable: true, get: function () { return user_login_js_1.ensureAuthenticated; } });
|
|
48
48
|
var agent_auth_js_1 = require("./agent-auth.js");
|
|
49
49
|
Object.defineProperty(exports, "resolveAgentCredentials", { enumerable: true, get: function () { return agent_auth_js_1.resolveAgentCredentials; } });
|
|
50
50
|
Object.defineProperty(exports, "ensureAgentIdentity", { enumerable: true, get: function () { return agent_auth_js_1.ensureAgentIdentity; } });
|
package/dist/local/manager.js
CHANGED
|
@@ -647,7 +647,7 @@ function printSeedResult(result) {
|
|
|
647
647
|
console.log("To use with any Ory agent plugin, set these environment variables:");
|
|
648
648
|
console.log("");
|
|
649
649
|
console.log(` export ORY_PROJECT_URL=${configs_js_1.GATEWAY_URL}`);
|
|
650
|
-
console.log(` export
|
|
650
|
+
console.log(` export ORY_USER_LOGIN=1`);
|
|
651
651
|
console.log(` export ORY_OAUTH2_CLIENT_ID=${result.user.client.clientId}`);
|
|
652
652
|
console.log(` export ORY_USER_SUBJECT_NAMESPACE=User`);
|
|
653
653
|
console.log(` export ORY_USER_SUBJECT_ID=${result.user.identity.id}`);
|
package/dist/permissions-cli.js
CHANGED
|
@@ -40,7 +40,7 @@ function resolveNamespace() {
|
|
|
40
40
|
* Apply persisted user OAuth2 tokens (if any) to the client's user
|
|
41
41
|
* principal so downstream subject resolution and tuple writes see a
|
|
42
42
|
* concrete identity. Mirrors what `ensureUserAuthenticated` does on a
|
|
43
|
-
* cache hit — but works whether or not `
|
|
43
|
+
* cache hit — but works whether or not `ORY_USER_LOGIN` is enabled.
|
|
44
44
|
*/
|
|
45
45
|
function attachCachedUserPrincipal(client) {
|
|
46
46
|
const tokens = (0, auth_store_js_1.loadTokens)();
|
|
@@ -151,7 +151,7 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
151
151
|
const subjectId = (0, subject_js_1.subjectLabel)(subject);
|
|
152
152
|
if (subjectId === "agent:unknown") {
|
|
153
153
|
console.log("No user identity resolved.");
|
|
154
|
-
console.log("Run the harness once (so
|
|
154
|
+
console.log("Run the harness once (so user login caches a token) or set");
|
|
155
155
|
console.log("ORY_USER_SUBJECT_ID to probe against a known subject.");
|
|
156
156
|
return 0;
|
|
157
157
|
}
|
|
@@ -235,7 +235,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
|
|
|
235
235
|
console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
|
|
236
236
|
console.error("");
|
|
237
237
|
console.error("Either:");
|
|
238
|
-
console.error(" - run the harness once with
|
|
238
|
+
console.error(" - run the harness once with ORY_USER_LOGIN=1 so a user token is cached, or");
|
|
239
239
|
console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
|
|
240
240
|
return 1;
|
|
241
241
|
}
|
package/dist/setup.js
CHANGED
|
@@ -374,7 +374,7 @@ Environment variables:
|
|
|
374
374
|
ORY_PROJECT_URL Your Ory project URL (required at runtime)
|
|
375
375
|
ORY_AGENT_API_KEY Agent API key / OAuth2 bearer (preferred name;
|
|
376
376
|
ORY_API_KEY is honored as a deprecated alias)
|
|
377
|
-
|
|
377
|
+
ORY_USER_LOGIN Set to "1" to enable the interactive user login
|
|
378
378
|
ORY_PERMISSION_MODE "observe" (default) or "enforce" — what to do on deny
|
|
379
379
|
ORY_AGENT_DEBUG Set to "true" for debug logging
|
|
380
380
|
ORY_AGENT_LOG_FILE Path to write debug logs
|
|
@@ -391,8 +391,8 @@ function printNextSteps(harnessName, uninstallCmd) {
|
|
|
391
391
|
console.log(" export ORY_PROJECT_URL=https://your-project.projects.oryapis.com");
|
|
392
392
|
console.log(" export ORY_AGENT_API_KEY=ory_pat_...");
|
|
393
393
|
console.log("");
|
|
394
|
-
console.log(" 2. Turn on the interactive user login
|
|
395
|
-
console.log(" export
|
|
394
|
+
console.log(" 2. Turn on the interactive user login (opt-in):");
|
|
395
|
+
console.log(" export ORY_USER_LOGIN=1");
|
|
396
396
|
console.log(` Without this, ${harnessName} runs without a human Ory identity attached`);
|
|
397
397
|
console.log(" to the session and permission checks fall back to a session:<id> subject.");
|
|
398
398
|
console.log("");
|
package/dist/skills.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The skill playbooks (auth-setup, login-flow, social-login, local-dev,
|
|
5
5
|
* permissions-onboarding, contribute-integration, build-integration,
|
|
6
|
-
* e2b-sandbox) and the local-stack commands (local-up, local-down) live once,
|
|
6
|
+
* e2b-sandbox, build-agent) and the local-stack commands (local-up, local-down) live once,
|
|
7
7
|
* as token-bearing templates under `packages/core/assets/`. Every harness plugin renders them
|
|
8
8
|
* through {@link renderOrySkills} / {@link renderOryCommands}, substituting the
|
|
9
9
|
* harness's CLI binary, package name, and the way it references sibling skills
|
package/dist/skills.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The skill playbooks (auth-setup, login-flow, social-login, local-dev,
|
|
6
6
|
* permissions-onboarding, contribute-integration, build-integration,
|
|
7
|
-
* e2b-sandbox) and the local-stack commands (local-up, local-down) live once,
|
|
7
|
+
* e2b-sandbox, build-agent) and the local-stack commands (local-up, local-down) live once,
|
|
8
8
|
* as token-bearing templates under `packages/core/assets/`. Every harness plugin renders them
|
|
9
9
|
* through {@link renderOrySkills} / {@link renderOryCommands}, substituting the
|
|
10
10
|
* harness's CLI binary, package name, and the way it references sibling skills
|
|
@@ -86,6 +86,11 @@ const SKILL_SOURCES = [
|
|
|
86
86
|
name: "ory-e2b-sandbox",
|
|
87
87
|
file: "skills/ory-e2b-sandbox/SKILL.md",
|
|
88
88
|
},
|
|
89
|
+
{
|
|
90
|
+
id: "build-agent",
|
|
91
|
+
name: "ory-build-agent",
|
|
92
|
+
file: "skills/ory-build-agent/SKILL.md",
|
|
93
|
+
},
|
|
89
94
|
];
|
|
90
95
|
const COMMAND_SOURCES = [
|
|
91
96
|
{
|
package/dist/status-cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render the "User identity" block. Shows the `
|
|
2
|
+
* Render the "User identity" block. Shows the `ORY_USER_LOGIN` state and,
|
|
3
3
|
* when a PKCE token is cached, the resolved subject and expiry.
|
|
4
4
|
*/
|
|
5
5
|
export declare function printUserIdentitySection(): void;
|
package/dist/status-cli.js
CHANGED
|
@@ -24,10 +24,10 @@ const cli_js_1 = require("./cli.js");
|
|
|
24
24
|
function resolveNamespace() {
|
|
25
25
|
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
26
26
|
}
|
|
27
|
-
const
|
|
28
|
-
function
|
|
29
|
-
const v = process.env.
|
|
30
|
-
return !!v &&
|
|
27
|
+
const USER_LOGIN_ENABLED = new Set(["1", "true", "yes", "on"]);
|
|
28
|
+
function isUserLoginEnabled() {
|
|
29
|
+
const v = process.env.ORY_USER_LOGIN?.toLowerCase();
|
|
30
|
+
return !!v && USER_LOGIN_ENABLED.has(v);
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Format a relative duration like "4h 32m" or "12s" for a future deadline.
|
|
@@ -49,22 +49,22 @@ function formatExpiresIn(expiresAtSec, nowMs = Date.now()) {
|
|
|
49
49
|
return `${remaining}s`;
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Render the "User identity" block. Shows the `
|
|
52
|
+
* Render the "User identity" block. Shows the `ORY_USER_LOGIN` state and,
|
|
53
53
|
* when a PKCE token is cached, the resolved subject and expiry.
|
|
54
54
|
*/
|
|
55
55
|
function printUserIdentitySection() {
|
|
56
|
-
const
|
|
56
|
+
const loginOn = isUserLoginEnabled();
|
|
57
57
|
const tokens = (0, auth_store_js_1.loadTokens)();
|
|
58
58
|
console.log("");
|
|
59
59
|
console.log("User identity (interactive PKCE login):");
|
|
60
|
-
console.log(`
|
|
60
|
+
console.log(` Login: ${loginOn ? "on (ORY_USER_LOGIN)" : "off (set ORY_USER_LOGIN=1 to enable)"}`);
|
|
61
61
|
if (!tokens) {
|
|
62
62
|
console.log(" Token cache: empty");
|
|
63
|
-
if (
|
|
63
|
+
if (loginOn) {
|
|
64
64
|
console.log(" Subject: (will resolve at next session start)");
|
|
65
65
|
}
|
|
66
66
|
else {
|
|
67
|
-
console.log(" Subject: (
|
|
67
|
+
console.log(" Subject: (login disabled — no PKCE flow will run)");
|
|
68
68
|
}
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -209,7 +209,7 @@ async function printPermissionsSection(binName, harness) {
|
|
|
209
209
|
}
|
|
210
210
|
const hasUser = !!process.env.ORY_USER_SUBJECT_ID || isUserTokenUsable();
|
|
211
211
|
if (!hasUser) {
|
|
212
|
-
console.log(` Coverage: n/a (no cached user identity — run with
|
|
212
|
+
console.log(` Coverage: n/a (no cached user identity — run with ORY_USER_LOGIN=1 once,`);
|
|
213
213
|
console.log(` or set ORY_USER_SUBJECT_ID to probe a known subject)`);
|
|
214
214
|
return;
|
|
215
215
|
}
|
package/dist/subject.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type UserSubjectRef = {
|
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
29
|
/**
|
|
30
|
-
* Resolve the user subject for permission checks. Prefers the
|
|
31
|
-
* `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
30
|
+
* Resolve the user subject for permission checks. Prefers the user
|
|
31
|
+
* login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
32
32
|
* legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
|
|
33
33
|
* (typically `session:<id>`).
|
|
34
34
|
*
|
package/dist/subject.js
CHANGED
|
@@ -21,8 +21,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
21
21
|
exports.resolveUserSubject = resolveUserSubject;
|
|
22
22
|
exports.subjectLabel = subjectLabel;
|
|
23
23
|
/**
|
|
24
|
-
* Resolve the user subject for permission checks. Prefers the
|
|
25
|
-
* `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
24
|
+
* Resolve the user subject for permission checks. Prefers the user
|
|
25
|
+
* login's `userPrincipal.subject`, falls back to `ORY_USER_SUBJECT_ID`, then the
|
|
26
26
|
* legacy `ORY_AGENT_SUBJECT_ID`, then the caller-supplied `fallback`
|
|
27
27
|
* (typically `session:<id>`).
|
|
28
28
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* User
|
|
2
|
+
* User login. Orchestrates the "first interaction must
|
|
3
3
|
* authenticate the human user" requirement across all plugins:
|
|
4
4
|
*
|
|
5
5
|
* 1. Resolve config (env + ~/.config/ory-agent-plugins/config.json).
|
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
* Every terminal path emits exactly one `user.auth` trace span so that
|
|
18
18
|
* the audit trail is complete regardless of outcome.
|
|
19
19
|
*
|
|
20
|
-
* The whole
|
|
21
|
-
* `
|
|
20
|
+
* The whole flow is a no-op (mode `disabled`) unless the
|
|
21
|
+
* `ORY_USER_LOGIN` env var is set to `1`/`true`.
|
|
22
22
|
*
|
|
23
|
-
* This
|
|
23
|
+
* This authenticates the *user* (the human at the keyboard). The
|
|
24
24
|
* separate agent identity (the AI process making the calls) is resolved
|
|
25
25
|
* non-interactively via env-configured machine credentials and is not
|
|
26
|
-
* handled here — see `ensureAgentIdentity
|
|
26
|
+
* handled here — see `ensureAgentIdentity`.
|
|
27
27
|
*/
|
|
28
28
|
import { OryAgentClient } from "./client.js";
|
|
29
29
|
import { type OryOAuth2Tokens } from "./config.js";
|
|
30
30
|
import { promptForProjectUrl } from "./cli.js";
|
|
31
31
|
import { pkceLogin } from "./auth.js";
|
|
32
|
-
export type
|
|
33
|
-
export interface
|
|
32
|
+
export type UserLoginMode = "disabled" | "audit_only" | "env_token" | "ok" | "refreshed" | "skipped" | "declined" | "error";
|
|
33
|
+
export interface UserLoginOptions {
|
|
34
34
|
/** Bin name used in user-facing prompts (e.g. "ory-claude"). */
|
|
35
35
|
binName: string;
|
|
36
36
|
/** Logical harness name (used by the tracer). */
|
|
@@ -47,11 +47,11 @@ export interface AuthGateOptions {
|
|
|
47
47
|
/** Override the prompt-for-URL step (for tests). */
|
|
48
48
|
promptForProjectUrlFn?: typeof promptForProjectUrl;
|
|
49
49
|
}
|
|
50
|
-
export interface
|
|
50
|
+
export interface UserLoginDecision {
|
|
51
51
|
/** Whether the caller should let the session proceed. */
|
|
52
52
|
proceed: boolean;
|
|
53
53
|
/** Stable mode identifier for telemetry and tests. */
|
|
54
|
-
mode:
|
|
54
|
+
mode: UserLoginMode;
|
|
55
55
|
/** Human-readable reason; safe to surface to the operator. */
|
|
56
56
|
reason: string;
|
|
57
57
|
/** Subject (sub claim) when authenticated. */
|
|
@@ -61,7 +61,7 @@ export interface AuthGateDecision {
|
|
|
61
61
|
* The entry point that every plugin's session-start handler should call
|
|
62
62
|
* to authenticate the human user. Always returns a decision; never throws.
|
|
63
63
|
*/
|
|
64
|
-
export declare function ensureUserAuthenticated(client: OryAgentClient, options:
|
|
64
|
+
export declare function ensureUserAuthenticated(client: OryAgentClient, options: UserLoginOptions): Promise<UserLoginDecision>;
|
|
65
65
|
/** Re-export so callers don't need to import auth.ts directly. */
|
|
66
66
|
export type { OryOAuth2Tokens };
|
|
67
67
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* User
|
|
3
|
+
* User login. Orchestrates the "first interaction must
|
|
4
4
|
* authenticate the human user" requirement across all plugins:
|
|
5
5
|
*
|
|
6
6
|
* 1. Resolve config (env + ~/.config/ory-agent-plugins/config.json).
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
* Every terminal path emits exactly one `user.auth` trace span so that
|
|
19
19
|
* the audit trail is complete regardless of outcome.
|
|
20
20
|
*
|
|
21
|
-
* The whole
|
|
22
|
-
* `
|
|
21
|
+
* The whole flow is a no-op (mode `disabled`) unless the
|
|
22
|
+
* `ORY_USER_LOGIN` env var is set to `1`/`true`.
|
|
23
23
|
*
|
|
24
|
-
* This
|
|
24
|
+
* This authenticates the *user* (the human at the keyboard). The
|
|
25
25
|
* separate agent identity (the AI process making the calls) is resolved
|
|
26
26
|
* non-interactively via env-configured machine credentials and is not
|
|
27
|
-
* handled here — see `ensureAgentIdentity
|
|
27
|
+
* handled here — see `ensureAgentIdentity`.
|
|
28
28
|
*/
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
30
|
exports.ensureAuthenticated = void 0;
|
|
@@ -34,8 +34,8 @@ const cli_js_1 = require("./cli.js");
|
|
|
34
34
|
const auth_store_js_1 = require("./auth-store.js");
|
|
35
35
|
const auth_js_1 = require("./auth.js");
|
|
36
36
|
const ENABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
37
|
-
function
|
|
38
|
-
const v = process.env.
|
|
37
|
+
function isUserLoginEnabled() {
|
|
38
|
+
const v = process.env.ORY_USER_LOGIN?.toLowerCase();
|
|
39
39
|
return !!v && ENABLED_VALUES.has(v);
|
|
40
40
|
}
|
|
41
41
|
function clientId() {
|
|
@@ -63,17 +63,17 @@ function recordAuthSpan(client, decision) {
|
|
|
63
63
|
* to authenticate the human user. Always returns a decision; never throws.
|
|
64
64
|
*/
|
|
65
65
|
async function ensureUserAuthenticated(client, options) {
|
|
66
|
-
if (!
|
|
66
|
+
if (!isUserLoginEnabled()) {
|
|
67
67
|
const decision = {
|
|
68
68
|
proceed: true,
|
|
69
69
|
mode: "disabled",
|
|
70
|
-
reason: "
|
|
70
|
+
reason: "ORY_USER_LOGIN is not enabled",
|
|
71
71
|
};
|
|
72
72
|
recordAuthSpan(client, decision);
|
|
73
73
|
return decision;
|
|
74
74
|
}
|
|
75
75
|
try {
|
|
76
|
-
const decision = await
|
|
76
|
+
const decision = await runUserLogin(client, options);
|
|
77
77
|
attachUserPrincipal(client, decision);
|
|
78
78
|
recordAuthSpan(client, decision);
|
|
79
79
|
return decision;
|
|
@@ -123,7 +123,7 @@ function attachUserPrincipal(client, decision) {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
|
-
async function
|
|
126
|
+
async function runUserLogin(client, options) {
|
|
127
127
|
const ttyCheck = options.isTtyAvailableFn ?? cli_js_1.isTtyAvailable;
|
|
128
128
|
const tty = ttyCheck();
|
|
129
129
|
let resolved = (0, config_js_1.resolveConfig)();
|
|
@@ -131,7 +131,7 @@ async function runGate(client, options) {
|
|
|
131
131
|
return {
|
|
132
132
|
proceed: true,
|
|
133
133
|
mode: "audit_only",
|
|
134
|
-
reason: "Configured for audit-only mode;
|
|
134
|
+
reason: "Configured for audit-only mode; user login is a no-op",
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
// 1. Project URL.
|
|
@@ -220,8 +220,8 @@ async function runGate(client, options) {
|
|
|
220
220
|
// `-t`, `docker exec` without `-it`, IDE-integrated terminals) can still
|
|
221
221
|
// complete sign-in by pasting the URL into any browser that can reach
|
|
222
222
|
// 127.0.0.1. Truly unattended runs (CI=true) are short-circuited by
|
|
223
|
-
// `pkceLogin` itself via `detectHeadless`, and operators can bypass
|
|
224
|
-
//
|
|
223
|
+
// `pkceLogin` itself via `detectHeadless`, and operators can bypass
|
|
224
|
+
// login entirely with `ORY_USER_SESSION_TOKEN` or `ORY_USER_LOGIN=0`.
|
|
225
225
|
// PKCE-flight lock so concurrent processes share a single browser flow.
|
|
226
226
|
const lock = (0, auth_store_js_1.tryAcquirePkceFlightLock)();
|
|
227
227
|
if (!lock) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/argus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|