@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.
@@ -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
- ORY_AUTH_GATE: "1",
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 the auth gate's
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 the auth gate has never run (no PKCE login,
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 `ORY_AUTH_GATE=1` to cache a user token, or set
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
@@ -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 (ORY_AUTH_GATE=1) or set ORY_AGENT_API_KEY / ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET / ORY_AGENT_REGISTRATION_TOKEN.",
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
- ["ORY_AUTH_GATE", process.env.ORY_AUTH_GATE, false],
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 gate — always on in local dev so the launcher demonstrates
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
- ORY_AUTH_GATE: "1",
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 AuthGateDecision, type AuthGateMode, type AuthGateOptions, } from "./auth-gate.js";
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 auth_gate_js_1 = require("./auth-gate.js");
46
- Object.defineProperty(exports, "ensureUserAuthenticated", { enumerable: true, get: function () { return auth_gate_js_1.ensureUserAuthenticated; } });
47
- Object.defineProperty(exports, "ensureAuthenticated", { enumerable: true, get: function () { return auth_gate_js_1.ensureAuthenticated; } });
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; } });
@@ -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 ORY_AUTH_GATE=1`);
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}`);
@@ -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 `ORY_AUTH_GATE` is enabled.
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 the auth gate caches a token) or set");
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 ORY_AUTH_GATE=1 so a user token is cached, or");
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
- ORY_AUTH_GATE Set to "1" to enable the interactive user login gate
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 gate (opt-in):");
395
- console.log(" export ORY_AUTH_GATE=1");
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
  {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Render the "User identity" block. Shows the `ORY_AUTH_GATE` state and,
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;
@@ -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 AUTH_GATE_ENABLED = new Set(["1", "true", "yes", "on"]);
28
- function isAuthGateOn() {
29
- const v = process.env.ORY_AUTH_GATE?.toLowerCase();
30
- return !!v && AUTH_GATE_ENABLED.has(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 `ORY_AUTH_GATE` state and,
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 gateOn = isAuthGateOn();
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(` Gate: ${gateOn ? "on (ORY_AUTH_GATE)" : "off (set ORY_AUTH_GATE=1 to enable)"}`);
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 (gateOn) {
63
+ if (loginOn) {
64
64
  console.log(" Subject: (will resolve at next session start)");
65
65
  }
66
66
  else {
67
- console.log(" Subject: (gate disabled — no PKCE login will run)");
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 ORY_AUTH_GATE=1 once,`);
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 auth-gate's
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 auth-gate's
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 authentication gate. Orchestrates the "first interaction must
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 gate is a no-op (mode `disabled`) unless the
21
- * `ORY_AUTH_GATE` env var is set to `1`/`true` — phased rollout.
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 gate authenticates the *user* (the human at the keyboard). The
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` (forthcoming).
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 AuthGateMode = "disabled" | "audit_only" | "env_token" | "ok" | "refreshed" | "skipped" | "declined" | "error";
33
- export interface AuthGateOptions {
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 AuthGateDecision {
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: AuthGateMode;
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: AuthGateOptions): Promise<AuthGateDecision>;
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 authentication gate. Orchestrates the "first interaction must
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 gate is a no-op (mode `disabled`) unless the
22
- * `ORY_AUTH_GATE` env var is set to `1`/`true` — phased rollout.
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 gate authenticates the *user* (the human at the keyboard). The
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` (forthcoming).
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 isGateEnabled() {
38
- const v = process.env.ORY_AUTH_GATE?.toLowerCase();
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 (!isGateEnabled()) {
66
+ if (!isUserLoginEnabled()) {
67
67
  const decision = {
68
68
  proceed: true,
69
69
  mode: "disabled",
70
- reason: "ORY_AUTH_GATE is not enabled",
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 runGate(client, options);
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 runGate(client, options) {
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; auth gate is a no-op",
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 the
224
- // gate entirely with `ORY_USER_SESSION_TOKEN` or `ORY_AUTH_GATE=0`.
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.6.2",
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",