Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `http_request` is no longer open to DNS rebinding (N13a). It resolved a host name once to check it against the private-address list and then let `fetch` resolve it again to connect, so a name that answered a public address to the check and `127.0.0.1` (or a cloud metadata or intranet address) to the connection reached the private address; the docs and a comment in the Worker runtime described the check as rebinding-safe. Now the one resolution is the check: the in-process path connects through an undici `Agent` whose `connect.lookup` checks every address and hands the socket the address it checked (each redirect hop included), and the sandboxed path (`sandboxExecute`, which agents use) resolves and checks the host in the agent's process and has the sandboxed process connect to that address (node:http/https with a fixed lookup; the host name still goes in `Host` and TLS SNI). The private-address list is now the shared one (below), which adds `0.0.0.0/8`, `100.64.0.0/10`, `192.0.0.0/24`, `198.18.0.0/15`, multicast and reserved `224.0.0.0/3`, `::`, `ff00::/8`, NAT64 `64:ff9b::/96` and 6to4 `2002::/16` to what `http_request` refused. Behavior changes: a host name that cannot be resolved now fails with the resolver's error instead of being passed to `fetch`; `undici` is loaded on the first `http_request` request (not only with `validateSSL: false`). New option `createHttpTool({ allowPrivate })` for hosts that may resolve to private addresses. Cloudflare Workers have no DNS hook, so neither tool exists in the Worker build; docs/deployment.md now says what a Worker can and cannot check.

### Added
- Cloudflare Worker target: the `openrouter` provider and the `http` tool (with a required `LOUSHO_HTTP_ALLOW` host allowlist) (M3a). `openrouter` reads its key from the `OPENROUTER_API_KEY` binding and is `@ai-sdk/openai` pointed at OpenRouter, so its bundle passes the `node:` leak check like `openai`. The Worker's `http` is `http_request` with the Node tool's input, over the platform `fetch()`: since a Worker cannot see or pin where a host name resolves, it reaches only the host names (`github.com/ghapi`) and `*.` wildcards (subdomains only) listed in the comma-separated `LOUSHO_HTTP_ALLOW` binding, refuses IP-address hosts even when listed and any scheme but `http:`/`https:`, checks every redirect hop the same way, and refuses every request when the binding is unset or empty. An invalid entry fails every request with an error naming it. `wrangler.toml` gets a commented `[vars]` `LOUSHO_HTTP_ALLOW` line when the spec lists `http`. `ollama` and `web-fetch` stay unsupported on Workers. Internally, the request, redirect and response logic of `http_request` moved to the Node-free `src/tools/built-in/httpCore.ts`, shared by both tools; the Node tool's behaviour is unchanged. See docs/cloudflare-workers.md#providers-and-tools.
- `fromAiSdk(model)`: any AI SDK `LanguageModel` (Google, Bedrock, Azure, Mistral, Gateway, ...) as `createAgent({ provider })`. Options `name` (default: the model's `provider` field), `fileMediaTypes`, `replaysReasoning` and `maxRetries` (default 0); exported with `FromAiSdkOptions` from the package root. Throws `LOUSHO_CONFIG_INVALID` for a model id string, for a model built for another `ai` major than the installed one, and for a call with another model id than the wrapped one. See docs/providers.md#any-ai-sdk-model-fromaisdk.
- Two ready-made durable stores (R2). `@lousho/build-ai-agent/kv` is a new subpath exporting `KVStore`, `KVCheckpointStore`, `CHECKPOINT_KV_BINDING` and the types `KVStoreOptions`, `KVBinding`, `KVPutOptions`, so a hand-written Cloudflare Worker can use `createAgent({ provider, store: new KVStore(env.AGENT_KV) })`; nothing in its import graph touches `node:*`, so it bundles without shims (`assertSessionId` moved to the Node-free `src/session/sessionId.ts` and is still exported where it was). `fileStore(dir, { historyLimit? })` (root export) is an `AgentStore` of plain JSON files: `sessions/<id>.json`, `checkpoints/<id>.json`, `checkpoint-history/<id>.json` and `approvals/<id>.json`, each written to a temp file and renamed into place, with no lock files and no `StorageService`; resolving an approval claims it with an exclusive create, so of two processes resolving one approval only one gets it. It replaces combining `FileSessionStore`, `LocalStorageCheckpointStore` and `StorageServiceApprovalStore` by hand. See docs/sessions.md#choosing-a-store and docs/deployment.md.
- `web_fetch` built-in tool (N13a): `webFetchTool` / `createWebFetchTool(options)`, and `web-fetch` in spec files. It `GET`s one public web page and returns `{ url, finalUrl, status, contentType, content, truncated }`: HTML converted to text by a small built-in converter (scripts and styles dropped, link URLs kept, entities decoded), JSON and `text/*` as sent, a note for other types; a 4xx/5xx is returned with its body. Loopback, private and link-local destinations are refused on every hop with the connection pinned to the checked address; redirects (10), bytes read (2 MiB), characters returned (50 000) and time (30 s) are capped; `allowedHosts` / `blockedHosts` are checked before DNS and `allowPrivate` exempts named hosts. Node only. See docs/tools.md#built-in-tools.
Expand Down
75 changes: 50 additions & 25 deletions docs/cloudflare-workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
| ------- | ----------------- | ------------------------ |
| Agent spec files | yes | yes |
| [Agent directories](./agent-directories.md) | no | yes |
| Providers | `mock`, `openai`, `anthropic` | `mock`, `openai`, `anthropic`, `ollama`, `openrouter` |
| Built-in tools | `current-date`, `day-name` | all, including `http` and `web-fetch` |
| Providers | `mock`, `openai`, `anthropic`, `openrouter` | `mock`, `openai`, `anthropic`, `ollama`, `openrouter` |
| Built-in tools | `current-date`, `day-name`, `http` (listed host names only, from the `LOUSHO_HTTP_ALLOW` binding) | all, including `http` and `web-fetch` |
| Tools written in TypeScript | no (a spec file cannot hold code) | yes, in an agent directory |
| Sandboxed tool execution | no (the build swaps the sandbox for a shim that fails when used) | yes |
| MCP servers over stdio | no (the child process cannot start; the shim fails when used) | yes |
Expand All @@ -26,26 +26,42 @@

The reasons behind the provider and tool rows:

- providers: `mock`, `openai` and `anthropic`. The `openai`/`anthropic`
- providers: `mock`, `openai`, `anthropic` and `openrouter`. The real
providers are built on the Vercel `ai` SDK's `generateText`/`streamText`
plus `@ai-sdk/openai`/`@ai-sdk/anthropic`, which are pure
`fetch()`/Web-standard implementations with no `node:*` imports anywhere
in their dependency graph, so they bundle and run on Workers cleanly.
`ollama` and `openrouter` are **not** supported here - `ollama` defaults
to a local `http://localhost:11434` endpoint that a Worker can't reach,
and `openrouter` hasn't had a Workers-compatibility audit; use
`node-server` or `docker` for those;
- tools: `current-date` and `day-name`. `http` and `web-fetch` are **not**
supported. On Node both refuse private destinations with a DNS lookup of
their own (`node:dns` inside an `undici` `Agent`) that checks every
address a host resolves to and connects to the address it checked, so DNS
rebinding cannot get past the check. A Worker has neither: its `fetch()`
resolves names inside Cloudflare's network and gives no hook to see or pin
the address. What a Worker can check is the URL (scheme, host name, an
IP-literal host); what it cannot guarantee is where a host name connects,
so a name that resolves to an internal address would not be caught. Rather
than ship a weaker tool under the same name, the Worker build has neither.
A tool of your own that calls `fetch()` in a Worker gets no SSRF
plus `@ai-sdk/openai`/`@ai-sdk/anthropic` (`openrouter` is
`@ai-sdk/openai` pointed at `https://openrouter.ai/api/v1`), which are
pure `fetch()`/Web-standard implementations with no `node:*` imports, so
they bundle and run on Workers cleanly; the build's leak check verifies
every bundle. `ollama` is **not** supported here: it defaults to a local
`http://localhost:11434` endpoint that a Worker can't reach; use
`node-server` or `docker` for it;
- tools: `current-date`, `day-name` and `http`. `web-fetch` is **not**
supported. On Node, `http` and `web-fetch` refuse private destinations
with a DNS lookup of their own (`node:dns` inside an `undici` `Agent`)
that checks every address a host resolves to and connects to the address
it checked, so DNS rebinding cannot get past the check. A Worker has
neither: its `fetch()` resolves names inside Cloudflare's network and
gives no hook to see or pin the address. What a Worker can check is the
URL (scheme, host name, an IP-literal host); what it cannot guarantee is
where a host name connects, so a name that resolves to an internal
address would not be caught. So the Worker's `http` (the same
`http_request` tool name and input as on Node) is a different tool: it
reaches only the host names you list. On the first URL and on every
redirect it refuses:
- any scheme but `http:` and `https:`;
- an IP-address host (`http://10.0.0.1/`, `http://[::1]/`), even a listed
one: there is no address check, so only names are allowed;
- a host that does not match the `LOUSHO_HTTP_ALLOW` binding, a
comma-separated list of host names (`github.com/ghapi`) and `*.` wildcards
(`*.example.com`, which matches subdomains only, not `example.com`).
Unset or empty, every request is refused (fail closed); an entry that is
not a host name fails every request with an error naming it.

A model therefore cannot pick an arbitrary host, or a DNS-rebinding one,
because only the names you listed pass; a listed name is trusted wherever
it resolves, so list only hosts you control or trust. TLS is always
validated (there is no `validateSSL` option) and the tool runs without a
sandbox. A tool of your own that calls `fetch()` in a Worker gets no SSRF
protection from the SDK.

## Build and deploy
Expand All @@ -65,25 +81,34 @@ npx wrangler deploy # requires a Cloudflare account (`wrangler login`)
```

Provider API keys are read from Worker bindings named `<TYPE>_API_KEY` (for example
`wrangler secret put OPENAI_API_KEY` or `wrangler secret put ANTHROPIC_API_KEY`). The
`openai` and `anthropic` peer packages (`@ai-sdk/openai` or `@ai-sdk/anthropic`, and `ai`)
must be installed alongside `@lousho/build-ai-agent` for `lousho build` to bundle them.
`wrangler secret put OPENAI_API_KEY`, `wrangler secret put ANTHROPIC_API_KEY` or
`wrangler secret put OPENROUTER_API_KEY`). The peer packages (`@ai-sdk/openai` for
`openai` and `openrouter`, `@ai-sdk/anthropic` for `anthropic`, and `ai`) must be
installed alongside `@lousho/build-ai-agent` for `lousho build` to bundle them.

## Bindings

The Worker serves the [HTTP API](./deployment.md#http-api): `GET /health`, `POST /chat`
streamed as SSE (`ReadableStream`), `GET /chat/:sessionId`, the approvals
endpoint and the deprecated `{ "message" }` body. Its two bindings:
endpoint and the deprecated `{ "message" }` body. Its bindings:

| Binding | Kind | What it does |
| ------- | ---- | ------------ |
| `LOUSHO_API_TOKEN` | secret (`npx wrangler secret put LOUSHO_API_TOKEN`) | Makes every route except `/health` require `Authorization: Bearer <token>` (constant-time compare, `401` JSON otherwise). Without it the Worker is open to anyone who has its URL, so **set it before you deploy**. |
| `AGENT_CHECKPOINTS` | KV namespace | Holds sessions, checkpoints and paused approvals, in one namespace, as `KVStore` (below). Without it they live in the memory of one isolate, which Cloudflare recycles at will: fine for trying a deploy out, not for production. |
| `LOUSHO_HTTP_ALLOW` | variable (`[vars]` in `wrangler.toml`) | The host names the `http` tool may reach, comma-separated: `github.com/ghapi`, or `*.example.com` for subdomains only. Unset or empty, every `http` request is refused. See [Providers and tools](#providers-and-tools). |

`wrangler.toml` is scaffolded with the `[[kv_namespaces]]` block for
`AGENT_CHECKPOINTS` commented out, with the commands to create the namespace
(`npx wrangler kv namespace create AGENT_CHECKPOINTS`, plus a `--preview`
variant) and where to paste the resulting ids. Uncomment it and fill in the ids.
When the spec lists `http`, it also has a commented `[vars]` block for
`LOUSHO_HTTP_ALLOW`; uncomment it and list your hosts:

```toml
[vars]
LOUSHO_HTTP_ALLOW = "github.com/ghapi,*.example.com"
```

```bash
cd .lousho/build/cloudflare-worker
Expand Down
3 changes: 2 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ browser-platform ES module; the build fails if any `node:` import ends up in the
`wrangler`, and it serves the [HTTP API](#http-api) above. It has the tightest limits of the three targets:

- spec files only (no agent directories, no tools written in TypeScript, no sandboxed tools);
- providers `mock`, `openai` and `anthropic`; built-in tools `current-date` and `day-name`;
- providers `mock`, `openai`, `anthropic` and `openrouter`; built-in tools `current-date`, `day-name` and `http`
(`http` reaches only the host names listed in the `LOUSHO_HTTP_ALLOW` binding);
- sessions, checkpoints and approvals in one KV namespace; cron triggers in UTC.

[Cloudflare Workers](./cloudflare-workers.md) has the table of limits, the build and deploy commands, the bindings, the KV stores, scheduled runs and the bundle checks.
Expand Down
5 changes: 3 additions & 2 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,5 +310,6 @@ takes for images, so there is no deprecation warning). `ai` v5 is not supported.
## Where each provider runs

All providers run on Node. The `cloudflare-worker` deploy target supports
`mock`, `openai` and `anthropic`; `ollama` and `openrouter` need the
`node-server` or `docker` target (see [Cloudflare Workers](./cloudflare-workers.md)).
`mock`, `openai`, `anthropic` and `openrouter` (key from the
`OPENROUTER_API_KEY` binding); `ollama` needs the `node-server` or `docker`
target (see [Cloudflare Workers](./cloudflare-workers.md)).
6 changes: 4 additions & 2 deletions docs/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,10 @@ const agent = createAgent({ model: 'openai/gpt-4o-mini', tools: [webFetch] });

Each `createWebFetchTool()` has its own connection pool, kept for as long as
the tool exists and released with it by garbage collection (there is no close
method). Neither `http_request` nor `web_fetch` exists in the Cloudflare
Worker build; see [Deployment](./deployment.md#cloudflare-worker).
method). `web_fetch` does not exist in the Cloudflare Worker build, and the
Worker's `http_request` is a different tool that reaches only the host names
listed in its `LOUSHO_HTTP_ALLOW` binding; see
[Cloudflare Workers](./cloudflare-workers.md#providers-and-tools).

## Todo tools

Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ environment (`LOUSHO_CONFIG_MISSING_PROVIDER`).

### My Worker build rejects the provider or tool

- **Cause:** the `cloudflare-worker` target supports the `mock`, `openai` and `anthropic` providers, and the `current-date` and `day-name` tools. `ollama`, `openrouter` and the `http` tool are not supported there, and `lousho build` fails with an error naming the one it rejected.
- **Cause:** the `cloudflare-worker` target supports the `mock`, `openai`, `anthropic` and `openrouter` providers, and the `current-date`, `day-name` and `http` tools (`http` reaches only the hosts listed in the `LOUSHO_HTTP_ALLOW` binding). `ollama` and the `web-fetch` tool are not supported there, and `lousho build` fails with an error naming the one it rejected.
- **Fix:** use another provider or tool, or build for the `node-server` or `docker` target.
- **More:** [Deployment](./deployment.md#cloudflare-worker).
- **More:** [Cloudflare Workers](./cloudflare-workers.md#providers-and-tools).

## Getting help

Expand Down
Loading
Loading