@ory/cloudflare-agents 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,28 +10,74 @@ and propagating user → agent identity — built on [`@ory/argus`](../core).
10
10
  npm install @ory/cloudflare-agents
11
11
  ```
12
12
 
13
+ ## Usage on Workers
14
+
15
+ The Workers runtime has no filesystem, so the shared
16
+ `~/.config/ory-agent-plugins/config.json` that the Node integrations read and write does not
17
+ exist there — and OAuth2 Dynamic Client Registration (which persists issued credentials to
18
+ that file) can't be used either. Construct the `OryAgentClient` explicitly with **static
19
+ agent credentials** from your Worker's bindings and inject it via the `client` option.
20
+
21
+ Instantiate the client and the wrapped tool set **once per agent instance** (or at module
22
+ scope), never inside `onChatMessage` — a fresh wrapper per message would re-run the Ory
23
+ session gates on every message.
24
+
13
25
  ```ts
14
26
  import { AIChatAgent } from "agents/ai-chat-agent";
15
- import { streamText } from "ai";
16
- import { withOry } from "@ory/cloudflare-agents";
27
+ import { openai } from "@ai-sdk/openai";
28
+ import { streamText, tool } from "ai";
29
+ import { z } from "zod";
30
+ import { withOry, OryAgentClient } from "@ory/cloudflare-agents";
31
+
32
+ const getWeather = tool({
33
+ description: "Get the weather for a city",
34
+ inputSchema: z.object({ city: z.string() }),
35
+ execute: async ({ city }) => `Sunny in ${city}`,
36
+ });
17
37
 
18
38
  export class MyAgent extends AIChatAgent<Env> {
39
+ // One client + wrapped tool set per agent instance — not per message.
40
+ ory = new OryAgentClient({
41
+ projectUrl: this.env.ORY_PROJECT_URL,
42
+ apiKey: this.env.ORY_AGENT_API_KEY, // static agent credential — no DCR on Workers
43
+ harness: "cloudflare-agents",
44
+ });
45
+ tools = withOry({ getWeather }, { client: this.ory });
46
+
19
47
  async onChatMessage(onFinish) {
20
- return streamText({
21
- model: this.model,
48
+ const result = streamText({
49
+ model: openai("gpt-4o"),
22
50
  messages: this.messages,
23
- tools: withOry({ ...this.tools, ...this.mcp.getAITools() }), // gate every tool
51
+ tools: this.tools,
24
52
  onFinish,
25
53
  });
54
+ // onChatMessage must return a Response.
55
+ return result.toUIMessageStreamResponse();
26
56
  }
27
57
  }
28
58
  ```
29
59
 
60
+ To gate MCP tools too, include them where you assemble the wrapped tool map:
61
+ `withOry({ getWeather, ...this.mcp.getAITools() }, { client: this.ory })`.
62
+
63
+ In a multi-user Worker, attribute each call to the acting user with `subjectFromOptions` — a
64
+ non-empty return value takes precedence over the client's user principal and env overrides
65
+ for that call:
66
+
67
+ ```ts
68
+ withOry(tools, {
69
+ client: ory,
70
+ subjectFromOptions: (o) => (o as { experimental_context?: { userId?: string } })?.experimental_context?.userId,
71
+ });
72
+ ```
73
+
30
74
  In **enforce** mode (`ORY_PERMISSION_MODE=enforce`) a denied tool throws — the error surfaces
31
75
  to the model and the tool never runs. In **observe** mode (default) the tool runs and a
32
76
  `permission.observe_deny` span is recorded. Spans are tagged with the `cloudflare-agents`
33
77
  integration so the audit trail distinguishes Cloudflare from a plain Vercel app.
34
78
 
35
- Depends only on `@ory/argus` (not `ai`/`agents`); tools are duck-typed. Credentials come from
36
- the shared `~/.config/ory-agent-plugins/config.json`. See
79
+ Depends only on `@ory/argus` (not `ai`/`agents`); tools are duck-typed. The package requires
80
+ the `nodejs_compat` compatibility flag (the Agents SDK already requires it). On Node (tests,
81
+ local dev) the default `OryAgentClient.fromEnv("cloudflare-agents")` and the shared config
82
+ file still work; on Workers always inject an explicitly constructed client. See
37
83
  [docs/sdk-integrations.md](../../docs/sdk-integrations.md).
package/dist/index.d.ts CHANGED
@@ -26,13 +26,24 @@ export interface AiTool {
26
26
  }
27
27
  export type ToolSet = Record<string, AiTool>;
28
28
  export interface WithOryOptions {
29
- /** Client to use. Defaults to `OryAgentClient.fromEnv("cloudflare-agents")`. */
29
+ /**
30
+ * Client to use. Defaults to `OryAgentClient.fromEnv("cloudflare-agents")`. On the Workers
31
+ * runtime there is no filesystem-backed shared config, so construct an `OryAgentClient`
32
+ * explicitly (with static agent credentials from your Worker's env bindings) and pass it
33
+ * here — see the README.
34
+ */
30
35
  client?: OryAgentClient;
31
36
  /** Override the Ory project URL for the session gates. */
32
37
  projectUrl?: string;
33
38
  /** Whether a denied tool is hard-blocked (throws). Default true. */
34
39
  canBlock?: boolean;
35
- /** Derive the permission subject from the AI SDK call options (e.g. a thread/user id). */
40
+ /**
41
+ * Derive the permission subject from the AI SDK call options (e.g. a thread/user id). A
42
+ * non-empty return value takes precedence over the client's user principal and env
43
+ * overrides for that call — this is how multi-user Workers attribute each request to its
44
+ * own acting user. When the function returns undefined/empty, resolution falls back to
45
+ * the principal / env chain.
46
+ */
36
47
  subjectFromOptions?: (options: unknown) => string | undefined;
37
48
  }
38
49
  /**
package/dist/index.js CHANGED
@@ -32,19 +32,22 @@ const HARNESS = "cloudflare-agents";
32
32
  function withOry(tools, options = {}) {
33
33
  const client = options.client ?? argus_1.OryAgentClient.fromEnv(HARNESS);
34
34
  const canBlock = options.canBlock ?? true;
35
- let sessionStarted = false;
36
- const ensureSession = async () => {
37
- if (sessionStarted)
38
- return;
39
- sessionStarted = true;
40
- try {
41
- await (0, argus_1.sessionStart)(client, { harness: HARNESS, projectUrl: options.projectUrl });
42
- }
43
- catch (err) {
35
+ let sessionPromise;
36
+ // Cache the in-flight promise (not a boolean) so concurrent first calls all
37
+ // await the same session start instead of racing past a still-running gate.
38
+ // Fail-open: a rejection is logged, never rethrown, and clears the cache so
39
+ // a later call can retry.
40
+ const ensureSession = () => {
41
+ sessionPromise ??= (0, argus_1.sessionStart)(client, {
42
+ harness: HARNESS,
43
+ projectUrl: options.projectUrl,
44
+ }).then(() => undefined, (err) => {
45
+ sessionPromise = undefined;
44
46
  client.logger.warn("session_start.failed", {
45
47
  message: err instanceof Error ? err.message : String(err),
46
48
  });
47
- }
49
+ });
50
+ return sessionPromise;
48
51
  };
49
52
  const wrapped = {};
50
53
  for (const [name, tool] of Object.entries(tools)) {
@@ -57,13 +60,14 @@ function withOry(tools, options = {}) {
57
60
  ...tool,
58
61
  execute: async (input, callOptions) => {
59
62
  await ensureSession();
60
- const result = await (0, argus_1.gate)(client, {
63
+ // A per-call subject wins over the client principal / env overrides.
64
+ const subjectOverride = options.subjectFromOptions?.(callOptions);
65
+ const result = await (0, argus_1.runWithUserSubject)(subjectOverride, () => (0, argus_1.gate)(client, {
61
66
  harness: HARNESS,
62
67
  toolName: name,
63
68
  toolArgs: input,
64
- subjectFallback: options.subjectFromOptions?.(callOptions),
65
69
  canBlock,
66
- });
70
+ }));
67
71
  if (result.blocked) {
68
72
  throw new argus_1.OryDenialError({
69
73
  tool: name,
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@ory/cloudflare-agents",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Ory Agent Security for the Cloudflare Agents SDK — per-tool authorization, tracing, and identity propagation by wrapping the AI SDK tools passed to streamText. Built on @ory/argus.",
5
5
  "license": "Apache-2.0",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/",
9
+ "provenance": true
10
+ },
6
11
  "main": "dist/index.js",
7
12
  "types": "dist/index.d.ts",
8
13
  "exports": {
@@ -25,7 +30,7 @@
25
30
  "ai"
26
31
  ],
27
32
  "dependencies": {
28
- "@ory/argus": "0.10.0"
33
+ "@ory/argus": "0.11.1"
29
34
  },
30
35
  "devDependencies": {
31
36
  "typescript": "^6.0.2",