@ory/argus 0.7.0 → 0.7.2
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,43 @@
|
|
|
1
|
+
# Start Local Temporal Dev Server
|
|
2
|
+
|
|
3
|
+
Start a local [Temporal](https://temporal.io) development server alongside the
|
|
4
|
+
local Ory stack so the worker scaffolded by `ory-temporal-worker` can connect.
|
|
5
|
+
This runs Temporal's bundled dev server (Temporal Server + Postgres + Web UI in
|
|
6
|
+
one process) — not a production cluster.
|
|
7
|
+
|
|
8
|
+
Prerequisite: the [Temporal CLI](https://docs.temporal.io/cli) is installed and
|
|
9
|
+
`temporal` is on `PATH`. On macOS: `brew install temporal`. On Linux/Windows:
|
|
10
|
+
download from <https://temporal.download>.
|
|
11
|
+
|
|
12
|
+
Run:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
temporal server start-dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This will:
|
|
19
|
+
|
|
20
|
+
1. Start the Temporal Server on `localhost:7233` (the gRPC endpoint workers and
|
|
21
|
+
clients connect to)
|
|
22
|
+
2. Expose the Web UI on <http://localhost:8233>
|
|
23
|
+
3. Persist state under `~/.config/temporalio/` so workflows survive a restart
|
|
24
|
+
|
|
25
|
+
The process runs in the foreground. Stop it with `Ctrl+C`; data is preserved.
|
|
26
|
+
For a clean reset, delete `~/.config/temporalio/` and start again.
|
|
27
|
+
|
|
28
|
+
For the matching Ory side of the stack (Identities, Permissions, OAuth2, login
|
|
29
|
+
UI, Jaeger), use {{REF_LOCAL_UP}}. The two stacks are independent — Temporal
|
|
30
|
+
runs on `:7233/:8233`, Ory runs on `:4000` and friends — so they can run side
|
|
31
|
+
by side without port conflicts.
|
|
32
|
+
|
|
33
|
+
Once both are up, point the worker at them:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
export ORY_PROJECT_URL=http://localhost:4000
|
|
37
|
+
export ORY_AUTH_GATE=1
|
|
38
|
+
cd temporal-worker
|
|
39
|
+
npm run start
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
See the `ory-temporal-worker` skill for the full worker scaffold and the
|
|
43
|
+
permission-gate wiring.
|
|
@@ -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.
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ory-temporal-worker
|
|
3
|
+
description: Scaffold a [Temporal](https://temporal.io) TypeScript worker where every Activity execution is gated by Ory — the user is authenticated, the worker's agent identity is resolved via DCR, each Activity invocation runs an Ory Permission check, and the full lifecycle emits trace spans. Use when the user asks to "add Ory to my Temporal worker", "wire Ory permissions into Temporal activities", "create a Temporal worker with Ory auth", "build a Temporal TypeScript project with the Ory agent client", or any close variant. The skill scaffolds the project in the user's repo following <https://docs.temporal.io/develop/typescript/set-up-your-local-typescript> — it does not run the worker.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ory-authed Temporal TypeScript worker
|
|
7
|
+
|
|
8
|
+
You are helping the user scaffold a [Temporal](https://temporal.io) TypeScript
|
|
9
|
+
worker where every Activity execution is gated by Ory: the user is
|
|
10
|
+
authenticated, the worker's agent identity is resolved via OAuth2 Dynamic Client
|
|
11
|
+
Registration, each Activity invocation runs an Ory Permission check, and the
|
|
12
|
+
full lifecycle emits trace spans. **Workflows stay deterministic** — only
|
|
13
|
+
Activities call out to Ory.
|
|
14
|
+
|
|
15
|
+
This skill carries the workflow. You generate the files in the user's repo; the
|
|
16
|
+
user runs the Temporal CLI and the worker.
|
|
17
|
+
|
|
18
|
+
> **Precondition:** Node.js 20+ is installed, and the user has installed (or
|
|
19
|
+
> will install) the [Temporal CLI](https://docs.temporal.io/cli). On macOS:
|
|
20
|
+
> `brew install temporal`. On Linux/Windows: download from
|
|
21
|
+
> <https://temporal.download> and put `temporal` on `PATH`. If the binary is
|
|
22
|
+
> missing, point the user at the docs and stop — do not fabricate it.
|
|
23
|
+
|
|
24
|
+
## Step 1 — Confirm the target
|
|
25
|
+
|
|
26
|
+
Before writing files, confirm with the user:
|
|
27
|
+
|
|
28
|
+
1. **Where the worker should live.** Default to `temporal-worker/` at the repo
|
|
29
|
+
root unless the user says otherwise.
|
|
30
|
+
2. **Task queue name.** Default `agent-tools`. The worker and the workflow
|
|
31
|
+
starter must agree on this string.
|
|
32
|
+
3. **Ory project URL.** For local development point at the local Ory stack
|
|
33
|
+
({{REF_LOCAL_UP}}) on `http://localhost:4000`. For Ory Network, the user
|
|
34
|
+
plumbs their project URL through the worker's env. Either way the worker
|
|
35
|
+
needs network reachability to the Ory APIs at runtime.
|
|
36
|
+
|
|
37
|
+
## Step 2 — Scaffold the Temporal project
|
|
38
|
+
|
|
39
|
+
Run the official Temporal scaffold and select the `hello-world` sample when
|
|
40
|
+
prompted. Then add the Ory agent client:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx @temporalio/create@latest temporal-worker
|
|
44
|
+
cd temporal-worker
|
|
45
|
+
npm install @ory/argus
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The scaffold lays down `src/{activities.ts, workflows.ts, worker.ts, client.ts}`
|
|
49
|
+
and pre-configures the Worker to connect to the Temporal dev server at
|
|
50
|
+
`localhost:7233`. The Web UI lives at <http://localhost:8233> once the dev
|
|
51
|
+
server is running.
|
|
52
|
+
|
|
53
|
+
## Step 3 — Wrap Activities with the Ory gate
|
|
54
|
+
|
|
55
|
+
Activities are where side effects happen, so they are the right place to put
|
|
56
|
+
the permission check. **Never call Ory from inside a Workflow** — Workflows are
|
|
57
|
+
deterministic and replayed; a live Ory call would break replay safety.
|
|
58
|
+
|
|
59
|
+
Replace the scaffold's `src/activities.ts` with:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { Context } from "@temporalio/activity";
|
|
63
|
+
import {
|
|
64
|
+
OryAgentClient,
|
|
65
|
+
ensureUserAuthenticated,
|
|
66
|
+
ensureAgentIdentity,
|
|
67
|
+
checkAndDecide,
|
|
68
|
+
resolveUserSubject,
|
|
69
|
+
subjectLabel,
|
|
70
|
+
} from "@ory/argus";
|
|
71
|
+
|
|
72
|
+
// One client per worker process. `harness` is a label that shows up on
|
|
73
|
+
// every trace span so worker-originated audit lives in its own namespace
|
|
74
|
+
// alongside the CLI plugins.
|
|
75
|
+
const ory = OryAgentClient.fromEnv("temporal");
|
|
76
|
+
|
|
77
|
+
// Run the user + agent gates exactly once per process. Activities call
|
|
78
|
+
// `bootstrap()` lazily and await the same promise on subsequent calls.
|
|
79
|
+
let bootstrapped: Promise<void> | undefined;
|
|
80
|
+
function bootstrap(): Promise<void> {
|
|
81
|
+
if (!bootstrapped) {
|
|
82
|
+
bootstrapped = (async () => {
|
|
83
|
+
await ensureUserAuthenticated(ory, {
|
|
84
|
+
binName: "temporal-worker",
|
|
85
|
+
harness: "temporal",
|
|
86
|
+
// Temporal's Activity entry point has no channel to carry a
|
|
87
|
+
// session-start block, so the user gate runs in advisory mode:
|
|
88
|
+
// it still refreshes tokens and emits the audit span, but the
|
|
89
|
+
// worker proceeds even if the user is unauthenticated. Hard
|
|
90
|
+
// enforcement happens at the per-Activity permission check.
|
|
91
|
+
allowBlock: false,
|
|
92
|
+
});
|
|
93
|
+
await ensureAgentIdentity(ory, {
|
|
94
|
+
projectUrl: process.env.ORY_PROJECT_URL,
|
|
95
|
+
});
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
return bootstrapped;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function gate(toolName: string, userSubject: string): Promise<void> {
|
|
102
|
+
await bootstrap();
|
|
103
|
+
const subject = resolveUserSubject(ory, userSubject);
|
|
104
|
+
const decision = await checkAndDecide(
|
|
105
|
+
ory,
|
|
106
|
+
{
|
|
107
|
+
namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
|
|
108
|
+
object: toolName,
|
|
109
|
+
relation: "use",
|
|
110
|
+
...subject,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
spanAttributes: {
|
|
114
|
+
toolName,
|
|
115
|
+
workflowId: Context.current().info.workflowExecution.workflowId,
|
|
116
|
+
activityId: Context.current().info.activityId,
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
switch (decision.kind) {
|
|
122
|
+
case "allow":
|
|
123
|
+
case "observe":
|
|
124
|
+
case "fail_open":
|
|
125
|
+
return;
|
|
126
|
+
case "deny":
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Ory denied use of ${toolName} for ${subjectLabel(subject)}`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function sendEmail(input: {
|
|
134
|
+
user: string;
|
|
135
|
+
to: string;
|
|
136
|
+
subject: string;
|
|
137
|
+
}): Promise<string> {
|
|
138
|
+
await gate("send_email", input.user);
|
|
139
|
+
// …real side effect here…
|
|
140
|
+
return `sent to ${input.to}`;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Key choices:
|
|
145
|
+
|
|
146
|
+
- **Activities call Ory, Workflows don't.** Anything that needs a live decision
|
|
147
|
+
goes in an Activity. Workflows only orchestrate.
|
|
148
|
+
- **`allowBlock: false`.** The Activity boundary can't carry a session-start
|
|
149
|
+
block, so the user gate runs in advisory mode. Enforcement is at the
|
|
150
|
+
permission check, which throws on deny — Temporal will mark the Activity as
|
|
151
|
+
failed and surface the error via the Workflow result or retry policy.
|
|
152
|
+
- **`harness: "temporal"`.** Distinguishes worker-originated spans in the trace
|
|
153
|
+
file from CLI plugin spans.
|
|
154
|
+
- **Span attributes carry the Workflow + Activity IDs.** This is how operators
|
|
155
|
+
correlate Ory denials back to Temporal executions in the Web UI.
|
|
156
|
+
|
|
157
|
+
## Step 4 — Pass the user subject through the Workflow
|
|
158
|
+
|
|
159
|
+
Workflows must be deterministic, so they cannot read `process.env` or call out
|
|
160
|
+
to Ory. The user subject travels as Workflow input. Update `src/workflows.ts`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { proxyActivities } from "@temporalio/workflow";
|
|
164
|
+
import type * as activities from "./activities";
|
|
165
|
+
|
|
166
|
+
const { sendEmail } = proxyActivities<typeof activities>({
|
|
167
|
+
startToCloseTimeout: "1 minute",
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export async function notifyUser(input: {
|
|
171
|
+
user: string;
|
|
172
|
+
to: string;
|
|
173
|
+
}): Promise<string> {
|
|
174
|
+
return sendEmail({
|
|
175
|
+
user: input.user,
|
|
176
|
+
to: input.to,
|
|
177
|
+
subject: "hello",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
And `src/client.ts`:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { Connection, Client } from "@temporalio/client";
|
|
186
|
+
import { nanoid } from "nanoid";
|
|
187
|
+
import { notifyUser } from "./workflows";
|
|
188
|
+
|
|
189
|
+
const client = new Client({ connection: await Connection.connect() });
|
|
190
|
+
const handle = await client.workflow.start(notifyUser, {
|
|
191
|
+
taskQueue: "agent-tools",
|
|
192
|
+
workflowId: `notify-${nanoid()}`,
|
|
193
|
+
args: [{ user: "user@example.com", to: "alice@example.com" }],
|
|
194
|
+
});
|
|
195
|
+
console.log("workflow started:", handle.workflowId);
|
|
196
|
+
console.log("result:", await handle.result());
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`src/worker.ts` stays as the scaffold writes it — `Worker.create` already
|
|
200
|
+
registers Workflows and Activities together and listens on the task queue.
|
|
201
|
+
|
|
202
|
+
## Step 5 — Run it
|
|
203
|
+
|
|
204
|
+
Three terminals. See {{REF_LOCAL_UP}} for the Ory side; the Temporal side uses
|
|
205
|
+
the Temporal CLI's bundled dev server (Postgres + Temporal Server + Web UI in
|
|
206
|
+
one process):
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# Terminal 1 — local Ory stack (Kratos, Keto, Hydra, gateway)
|
|
210
|
+
{{REF_LOCAL_UP}}
|
|
211
|
+
|
|
212
|
+
# Terminal 2 — local Temporal dev server (Web UI at http://localhost:8233)
|
|
213
|
+
temporal server start-dev
|
|
214
|
+
|
|
215
|
+
# Terminal 3 — the worker, pointed at both
|
|
216
|
+
cd temporal-worker
|
|
217
|
+
export ORY_PROJECT_URL=http://localhost:4000
|
|
218
|
+
export ORY_AUTH_GATE=1
|
|
219
|
+
export ORY_AGENT_DEBUG=true
|
|
220
|
+
export ORY_AGENT_TRACE_FILE=$PWD/ory-trace.ndjson
|
|
221
|
+
npm run start # boots the worker, polls task queue
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
In a fourth terminal, kick the workflow once:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
cd temporal-worker
|
|
228
|
+
npm run workflow
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Tail the trace file to confirm the gates fired:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
tail -f ory-trace.ndjson | jq .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
You should see:
|
|
238
|
+
|
|
239
|
+
- exactly one `user.auth` span (the worker's first Activity triggered
|
|
240
|
+
`bootstrap()`),
|
|
241
|
+
- exactly one `agent.auth` span,
|
|
242
|
+
- one `tool.invoke` (allow) or `tool.block` (deny) span **per Activity
|
|
243
|
+
execution**.
|
|
244
|
+
|
|
245
|
+
The Workflow itself produces no Ory spans — only its Activities do.
|
|
246
|
+
|
|
247
|
+
## Step 6 — Promotion from observe to enforce
|
|
248
|
+
|
|
249
|
+
The worker starts in `observe` mode by default: denies pass through but each is
|
|
250
|
+
recorded as a `permission.observe_deny` audit span. Once the user has confirmed
|
|
251
|
+
the deny set is what they expect, promote to enforcement:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
export ORY_PERMISSION_MODE=enforce
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
On a fresh Ory project, run the permissions bootstrap once before flipping the
|
|
258
|
+
switch so the `use` tuples for each Activity name (`send_email`, …) exist —
|
|
259
|
+
see {{REF_PERMISSIONS_ONBOARDING}}.
|
|
260
|
+
|
|
261
|
+
To exercise the deny path locally, write a tuple that explicitly removes `use`
|
|
262
|
+
for the test user against one Activity object, kick the Workflow, and watch the
|
|
263
|
+
Activity fail with the `Ory denied use of …` error in the Temporal Web UI.
|
|
264
|
+
|
|
265
|
+
## Step 7 — Beyond the dev server
|
|
266
|
+
|
|
267
|
+
This skill stops at the local dev server. For production:
|
|
268
|
+
|
|
269
|
+
- Pin a static agent identity with `ORY_AGENT_API_KEY` (single key) or
|
|
270
|
+
`ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET` (client_credentials) so the
|
|
271
|
+
worker doesn't re-register on every cold start.
|
|
272
|
+
- Persist the worker's `ory-trace.ndjson` somewhere durable, or replace the
|
|
273
|
+
file tracer with an OpenTelemetry exporter wired up around `ory.tracer`.
|
|
274
|
+
- Use Temporal Cloud or a self-hosted Temporal cluster instead of
|
|
275
|
+
`temporal server start-dev`; the worker code does not change.
|
|
276
|
+
|
|
277
|
+
## What this skill does NOT do
|
|
278
|
+
|
|
279
|
+
- It does not modify the user's Ory project — use {{REF_AUTH_SETUP}} for that.
|
|
280
|
+
- It does not call Ory from inside a Workflow. Workflows are deterministic and
|
|
281
|
+
must never make non-deterministic calls; all gating lives in Activities.
|
|
282
|
+
- It does not deploy the worker. The user runs `temporal server start-dev` and
|
|
283
|
+
`npm run start` locally; production deployment is out of scope.
|
|
284
|
+
- It does not pin Temporal or `@ory/argus` versions. For reproducibility, pin
|
|
285
|
+
both in the generated `package.json` before committing.
|
package/dist/skills.d.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
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
|
+
* e2b-sandbox, temporal-worker) and the local-stack commands (local-up,
|
|
9
|
+
* local-down, temporal-up) live once, as token-bearing templates under
|
|
10
|
+
* `packages/core/assets/`. Every harness plugin renders them
|
|
8
11
|
* through {@link renderOrySkills} / {@link renderOryCommands}, substituting the
|
|
9
12
|
* harness's CLI binary, package name, and the way it references sibling skills
|
|
10
13
|
* and commands. Plugins then write the rendered docs into whatever location
|
package/dist/skills.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
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
|
+
* e2b-sandbox, temporal-worker) and the local-stack commands (local-up,
|
|
10
|
+
* local-down, temporal-up) live once, as token-bearing templates under
|
|
11
|
+
* `packages/core/assets/`. Every harness plugin renders them
|
|
9
12
|
* through {@link renderOrySkills} / {@link renderOryCommands}, substituting the
|
|
10
13
|
* harness's CLI binary, package name, and the way it references sibling skills
|
|
11
14
|
* and commands. Plugins then write the rendered docs into whatever location
|
|
@@ -86,6 +89,16 @@ const SKILL_SOURCES = [
|
|
|
86
89
|
name: "ory-e2b-sandbox",
|
|
87
90
|
file: "skills/ory-e2b-sandbox/SKILL.md",
|
|
88
91
|
},
|
|
92
|
+
{
|
|
93
|
+
id: "build-agent",
|
|
94
|
+
name: "ory-build-agent",
|
|
95
|
+
file: "skills/ory-build-agent/SKILL.md",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: "temporal-worker",
|
|
99
|
+
name: "ory-temporal-worker",
|
|
100
|
+
file: "skills/ory-temporal-worker/SKILL.md",
|
|
101
|
+
},
|
|
89
102
|
];
|
|
90
103
|
const COMMAND_SOURCES = [
|
|
91
104
|
{
|
|
@@ -102,6 +115,13 @@ const COMMAND_SOURCES = [
|
|
|
102
115
|
description: "Stop the local Ory dev stack, preserving data volumes.",
|
|
103
116
|
file: "commands/local-down.md",
|
|
104
117
|
},
|
|
118
|
+
{
|
|
119
|
+
id: "temporal-up",
|
|
120
|
+
name: "ory-temporal-up",
|
|
121
|
+
slug: "temporal-up",
|
|
122
|
+
description: "Start the local Temporal TypeScript dev server (Temporal Server + Web UI) for the Ory-authed worker scaffold.",
|
|
123
|
+
file: "commands/temporal-up.md",
|
|
124
|
+
},
|
|
105
125
|
];
|
|
106
126
|
/** Names of the skills materialized by the plugins (for uninstall cleanup). */
|
|
107
127
|
exports.ORY_SKILL_NAMES = SKILL_SOURCES.map((s) => s.name);
|
|
@@ -128,21 +148,25 @@ function buildProfile(harness, opts) {
|
|
|
128
148
|
const skillRef = (name) => harness === "claude-code" ? code(`/project:${name}`) : code(name);
|
|
129
149
|
let localUp;
|
|
130
150
|
let localDown;
|
|
151
|
+
let temporalUp;
|
|
131
152
|
switch (harness) {
|
|
132
153
|
case "claude-code":
|
|
133
154
|
localUp = code("/ory-agent-plugin:local-up");
|
|
134
155
|
localDown = code("/ory-agent-plugin:local-down");
|
|
156
|
+
temporalUp = code("/ory-agent-plugin:temporal-up");
|
|
135
157
|
break;
|
|
136
158
|
case "gemini-cli":
|
|
137
159
|
case "opencode":
|
|
138
160
|
localUp = code("/ory:local-up");
|
|
139
161
|
localDown = code("/ory:local-down");
|
|
162
|
+
temporalUp = code("/ory:temporal-up");
|
|
140
163
|
break;
|
|
141
164
|
case "codex":
|
|
142
165
|
case "openclaw":
|
|
143
166
|
default:
|
|
144
167
|
localUp = code("ory-local-up");
|
|
145
168
|
localDown = code("ory-local-down");
|
|
169
|
+
temporalUp = code("ory-temporal-up");
|
|
146
170
|
break;
|
|
147
171
|
}
|
|
148
172
|
return {
|
|
@@ -156,8 +180,11 @@ function buildProfile(harness, opts) {
|
|
|
156
180
|
"{{REF_LOGIN_FLOW}}": skillRef("ory-login-flow"),
|
|
157
181
|
"{{REF_SOCIAL_LOGIN}}": skillRef("ory-social-login"),
|
|
158
182
|
"{{REF_LOCAL_DEV}}": skillRef("ory-local-dev"),
|
|
183
|
+
"{{REF_PERMISSIONS_ONBOARDING}}": skillRef("ory-permissions-onboarding"),
|
|
184
|
+
"{{REF_TEMPORAL_WORKER}}": skillRef("ory-temporal-worker"),
|
|
159
185
|
"{{REF_LOCAL_UP}}": localUp,
|
|
160
186
|
"{{REF_LOCAL_DOWN}}": localDown,
|
|
187
|
+
"{{REF_TEMPORAL_UP}}": temporalUp,
|
|
161
188
|
},
|
|
162
189
|
};
|
|
163
190
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/argus",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|