@ory/argus 0.7.1 → 0.7.3

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,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/config.js CHANGED
@@ -42,6 +42,7 @@ exports.mutateConfig = mutateConfig;
42
42
  exports.resolveConfig = resolveConfig;
43
43
  exports.configPromptMessage = configPromptMessage;
44
44
  const fs = __importStar(require("node:fs"));
45
+ const os = __importStar(require("node:os"));
45
46
  const path = __importStar(require("node:path"));
46
47
  /**
47
48
  * Single OS-agnostic data directory for *all* Ory agent plugin state:
@@ -92,7 +93,7 @@ const LOCK_POLL_MS = 25;
92
93
  /** Maximum total time to wait for the lock before giving up. */
93
94
  const LOCK_TIMEOUT_MS = 5_000;
94
95
  function getHomeDir() {
95
- return process.env.HOME ?? process.env.USERPROFILE ?? "~";
96
+ return os.homedir();
96
97
  }
97
98
  /**
98
99
  * Return the path to the config file.
package/dist/setup.js CHANGED
@@ -53,6 +53,7 @@ exports.unregisterPlugin = unregisterPlugin;
53
53
  exports.printSetupHelp = printSetupHelp;
54
54
  exports.printNextSteps = printNextSteps;
55
55
  const fs = __importStar(require("node:fs"));
56
+ const os = __importStar(require("node:os"));
56
57
  const path = __importStar(require("node:path"));
57
58
  // ─── Arg Parsing ───────────────────────────────────────────────────
58
59
  /**
@@ -126,7 +127,7 @@ function resolveHookCommand(packageName, binName) {
126
127
  // The package root resolves to the main entry (dist/index.js).
127
128
  // The hook script lives alongside it as dist/hook.js.
128
129
  const hookPath = path.resolve(path.dirname(binPath), "hook.js");
129
- return `node ${hookPath}`;
130
+ return `node "${hookPath}"`;
130
131
  }
131
132
  catch {
132
133
  return binName;
@@ -273,8 +274,7 @@ function removeMcpServer(existing) {
273
274
  * Get the path to the Claude Code plugins registry file.
274
275
  */
275
276
  function getPluginsRegistryPath() {
276
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "~";
277
- return path.join(home, ".claude", "plugins", "installed_plugins.json");
277
+ return path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json");
278
278
  }
279
279
  /**
280
280
  * Read the Claude Code plugins registry.
package/dist/skills.d.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  * permissions-onboarding, contribute-integration, build-integration,
6
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
@@ -6,6 +6,9 @@
6
6
  * permissions-onboarding, contribute-integration, build-integration,
7
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
@@ -91,6 +94,11 @@ const SKILL_SOURCES = [
91
94
  name: "ory-build-agent",
92
95
  file: "skills/ory-build-agent/SKILL.md",
93
96
  },
97
+ {
98
+ id: "temporal-worker",
99
+ name: "ory-temporal-worker",
100
+ file: "skills/ory-temporal-worker/SKILL.md",
101
+ },
94
102
  ];
95
103
  const COMMAND_SOURCES = [
96
104
  {
@@ -107,6 +115,13 @@ const COMMAND_SOURCES = [
107
115
  description: "Stop the local Ory dev stack, preserving data volumes.",
108
116
  file: "commands/local-down.md",
109
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
+ },
110
125
  ];
111
126
  /** Names of the skills materialized by the plugins (for uninstall cleanup). */
112
127
  exports.ORY_SKILL_NAMES = SKILL_SOURCES.map((s) => s.name);
@@ -133,21 +148,25 @@ function buildProfile(harness, opts) {
133
148
  const skillRef = (name) => harness === "claude-code" ? code(`/project:${name}`) : code(name);
134
149
  let localUp;
135
150
  let localDown;
151
+ let temporalUp;
136
152
  switch (harness) {
137
153
  case "claude-code":
138
154
  localUp = code("/ory-agent-plugin:local-up");
139
155
  localDown = code("/ory-agent-plugin:local-down");
156
+ temporalUp = code("/ory-agent-plugin:temporal-up");
140
157
  break;
141
158
  case "gemini-cli":
142
159
  case "opencode":
143
160
  localUp = code("/ory:local-up");
144
161
  localDown = code("/ory:local-down");
162
+ temporalUp = code("/ory:temporal-up");
145
163
  break;
146
164
  case "codex":
147
165
  case "openclaw":
148
166
  default:
149
167
  localUp = code("ory-local-up");
150
168
  localDown = code("ory-local-down");
169
+ temporalUp = code("ory-temporal-up");
151
170
  break;
152
171
  }
153
172
  return {
@@ -161,8 +180,11 @@ function buildProfile(harness, opts) {
161
180
  "{{REF_LOGIN_FLOW}}": skillRef("ory-login-flow"),
162
181
  "{{REF_SOCIAL_LOGIN}}": skillRef("ory-social-login"),
163
182
  "{{REF_LOCAL_DEV}}": skillRef("ory-local-dev"),
183
+ "{{REF_PERMISSIONS_ONBOARDING}}": skillRef("ory-permissions-onboarding"),
184
+ "{{REF_TEMPORAL_WORKER}}": skillRef("ory-temporal-worker"),
164
185
  "{{REF_LOCAL_UP}}": localUp,
165
186
  "{{REF_LOCAL_DOWN}}": localDown,
187
+ "{{REF_TEMPORAL_UP}}": temporalUp,
166
188
  },
167
189
  };
168
190
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",