Skip to content
Open
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
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ Console UI, operator gotchas): **[docs.openma.dev/self-host/overview](https://do

## Quick start: Cloudflare deploy

Requires [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) (for Durable Objects + Containers).
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/open-ma/open-managed-agents)

> **Note:** The Deploy button above deploys the default (Paid plan) configuration.
> For Free Tier setup, see [Cloudflare Free Tier](#cloudflare-free-tier) below.

Requires [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) (for Durable Objects + Containers) for full functionality.

```bash
git clone https://github.com/open-ma/open-managed-agents.git
Expand Down Expand Up @@ -118,6 +123,13 @@ npm run deploy
# → https://openma.dev (or https://managed-agents.<your-subdomain>.workers.dev for a personal deploy)
```

Or use the interactive setup wizard (recommended for new deployments):

```bash
./scripts/setup-cf.sh # Standard deployment (Paid plan)
./scripts/setup-cf.sh --free-tier # Free Tier deployment
```

What gets deployed:

| Component | What it does |
Expand Down Expand Up @@ -158,6 +170,85 @@ For long-lived sessions use `GET /v1/sessions/$SESSION/events/stream` — replay

---

## Cloudflare Free Tier

OMA can be deployed on the [Cloudflare Free Tier](https://developers.cloudflare.com/workers/platform/pricing/), though some features are unavailable.

### Limitations

| Feature | Free Tier Status | Details |
|---|---|---|
| **Workers Containers** (sandbox) | ❌ Unavailable | Tool execution (`bash`, `read`, `write`, `edit`, etc.) requires Cloudflare Workers Containers (Paid plan). The API, Console UI, and agent/session management still function. |
| **Browser Rendering** | ❌ Unavailable | The `browser` tool is opt-in and gracefully degrades when the binding is absent. |
| **Rate Limiting** | ❌ Unavailable | Rate limit bindings soft-pass when absent. Consider Cloudflare's [WAF dashboard rules](https://developers.cloudflare.com/waf/) as an alternative. |
| **Memory Queue** (R2 events) | ❌ Unavailable | Memory store audit via queues won't function. REST writes still audit inline (D1), but agent FUSE writes won't be audited. |
| **Durable Objects** | ✅ Available | Durable Objects are included in the Free Tier (limited operations). |
| **D1 Databases** | ✅ Available | Included in the Free Tier (limited storage). |
| **R2 Storage** | ✅ Available | Included in the Free Tier (limited storage). |
| **KV Storage** | ✅ Available | Included in the Free Tier (limited operations). |
| **Workers AI** | ✅ Available | Included in the Free Tier (limited requests). |
| **API & Console** | ✅ Available | Full API and Console UI functionality. |
| **Integrations** | ✅ Available | Linear, GitHub, and Slack integrations work. |

### Free Tier Quick Start

```bash
git clone https://github.com/open-ma/open-managed-agents.git
cd open-managed-agents
pnpm install

# Use the interactive setup script with the --free-tier flag
./scripts/setup-cf.sh --free-tier
```

The `--free-tier` flag will:
1. Create all required resources (D1, KV, R2)
2. Patch the wrangler.jsonc configuration files
3. Set required secrets
4. Apply database migrations
5. **Skip** provisioning paid-only resources (queues, containers, browser, rate limits)
6. Deploy the workers

### Manual Free Tier Setup

If you prefer to configure manually:

1. **Edit `apps/main/wrangler.jsonc`**: Comment out the `ratelimits` and `queues` sections at the top level (already commented by default for Free Tier).

2. **Edit `apps/agent/wrangler.jsonc`**: Comment out the `containers` section and the `browser` binding.

3. **Edit `apps/integrations/wrangler.jsonc`**: Comment out the `ratelimits` section at the top level.

4. Deploy normally:
```bash
npx wrangler deploy --config apps/main/wrangler.jsonc
npx wrangler deploy --config apps/agent/wrangler.jsonc
npx wrangler deploy --config apps/integrations/wrangler.jsonc
```

### Upgrading from Free Tier to Paid Plan

When you're ready to upgrade:

1. Upgrade your Cloudflare account to Workers Paid
2. Uncomment the paid feature blocks in the `wrangler.jsonc` files:
- `apps/main/wrangler.jsonc`: uncomment `ratelimits` and `queues`
- `apps/agent/wrangler.jsonc`: uncomment `containers` and `browser`
- `apps/integrations/wrangler.jsonc`: uncomment `ratelimits`
3. Provision a Cloudflare Queue for memory events:
```bash
npx wrangler r2 bucket notification create managed-agents-memory \
--event-type object-create object-delete \
--queue managed-agents-memory-events
```
4. Redeploy:
```bash
npx wrangler deploy --config apps/main/wrangler.jsonc
npx wrangler deploy --config apps/agent/wrangler.jsonc
```

---

## Architecture

A **meta-harness** is not an agent — it's the platform that runs agents. It defines stable interfaces for everything an agent needs, and stays out of the way of the agent loop:
Expand Down
7 changes: 7 additions & 0 deletions apps/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.69",
"@ai-sdk/cloudflare": "^1.2.1",
"@ai-sdk/mcp": "1.0.37",
"@cloudflare/containers": "^0.3.0",
"@cloudflare/playwright": "^1.3.0",
Expand Down Expand Up @@ -48,3 +49,9 @@
"zod": "^4.3.6"
}
}
/home/engine/.bashrc: line 1: syntax error near unexpected token `('
/home/engine/.bashrc: line 1: `. /etc/profile.d/workload-containment.shn# ~/.bashrc: executed by bash(1) for non-login shells.'
/home/engine/.bashrc: line 1: syntax error near unexpected token `('
/home/engine/.bashrc: line 1: `. /etc/profile.d/workload-containment.shn# ~/.bashrc: executed by bash(1) for non-login shells.'
/home/engine/.bashrc: line 1: syntax error near unexpected token `('
/home/engine/.bashrc: line 1: `. /etc/profile.d/workload-containment.shn# ~/.bashrc: executed by bash(1) for non-login shells.'
27 changes: 12 additions & 15 deletions apps/agent/src/harness/provider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAnthropic } from "@ai-sdk/anthropic";
import { createOpenAI } from "@ai-sdk/openai";
import { createWorkersAI } from "@ai-sdk/cloudflare";
import type { LanguageModel } from "ai";

/**
Expand All @@ -8,8 +9,9 @@ import type { LanguageModel } from "ai";
* - "ant-compatible" — Third-party Anthropic-compatible API
* - "oai" — OpenAI official API
* - "oai-compatible" — Third-party OpenAI-compatible API (DeepSeek, Groq, etc.)
* - "cf-workers-ai" — Cloudflare Workers AI
*/
export type ApiCompat = "ant" | "ant-compatible" | "oai" | "oai-compatible";
export type ApiCompat = "ant" | "ant-compatible" | "oai" | "oai-compatible" | "cf-workers-ai";

const KNOWN_CLAUDE_PREFIX = "claude-";

Expand Down Expand Up @@ -117,6 +119,7 @@ export function resolveModel(
baseURL?: string,
compat?: ApiCompat,
customHeaders?: Record<string, string>,
aiBinding?: any,
): LanguageModel {
const modelString = typeof model === "string" ? model : model.id;

Expand All @@ -127,6 +130,14 @@ export function resolveModel(

const effectiveCompat = compat || "ant";

if (effectiveCompat === "cf-workers-ai") {
if (!aiBinding) {
throw new Error("cf-workers-ai requires aiBinding");
}
const cf = createWorkersAI({ binding: aiBinding });
return cf(modelId);
}

if (useOpenAI(effectiveCompat)) {
const openai = createOpenAI({
apiKey,
Expand All @@ -135,13 +146,6 @@ export function resolveModel(
fetch: observingFetch,
});
// Use chat/completions endpoint, not Responses API.
// Reasons:
// - Third-party OpenAI-compat gateways (CF AI Gateway, Groq, DeepSeek,
// xAI Grok, etc.) only support /v1/chat/completions
// - Responses API requires server-side persistence of function call IDs;
// orgs with Zero Data Retention enabled get "Item with id 'fc_...' not
// found" errors mid-loop
// - chat/completions is the de-facto standard contract for OpenAI-compat
return openai.chat(modelId);
}

Expand All @@ -152,10 +156,6 @@ export function resolveModel(
if (baseURL) headers["X-Sub-Module"] = "managed-agents";
if (customHeaders) Object.assign(headers, customHeaders);

// @ai-sdk/anthropic appends `/messages` directly to baseURL — no `/v1`
// segment is added. Real api.anthropic.com endpoints include `/v1` in the
// SDK default, so deployments pointing at proxies must too. Auto-append
// `/v1` if the user supplied a bare host so common env values work.
const normalizedBaseURL = baseURL
? /\/v\d+(\/)?$/.test(baseURL)
? baseURL.replace(/\/$/, "")
Expand All @@ -166,9 +166,6 @@ export function resolveModel(
apiKey,
baseURL: normalizedBaseURL,
headers: Object.keys(headers).length > 0 ? headers : undefined,
// setMaxTokensFetch composes observingFetch internally for non-Claude;
// Claude path uses observingFetch directly so 429/rate-limit logging
// applies regardless of which provider/model we're talking to.
fetch: isKnownClaude ? observingFetch : setMaxTokensFetch,
});

Expand Down
46 changes: 25 additions & 21 deletions apps/agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
* Each environment gets its own agent worker with a custom container image.
* This worker exports SessionDO + Sandbox and routes incoming requests
* from the main worker to the appropriate SessionDO instance.
*
* If SESSION_DO is not bound (Cloudflare Free Tier), it falls back to
* stateless mode.
*/

import { Hono } from "hono";
Expand All @@ -30,30 +33,31 @@ export { outbound, outboundByHost } from "./outbound";
// --- HTTP app: thin router to SessionDO ---
const app = new Hono<{ Bindings: Env }>();

app.get("/health", (c) => c.json({ status: "ok", version: "2" }));

// /__internal/prepare-env, /__internal/prep-tick, /__internal/prep-debug,
// and the buildInstallScript helper were removed when the per-env CI build
// (image_strategy=dockerfile) became the only build path. The lazy-prepare
// branch they fed (base_snapshot) was reverted; see apps/main/src/routes/
// environments.ts pickStrategy for the rationale.
app.get("/health", (c) => c.json({ status: "ok", version: "3", mode: c.env.SESSION_DO ? "stateful" : "stateless" }));

app.all("/sessions/:id/*", async (c) => {
const sessionId = c.req.param("id");
const doId = c.env.SESSION_DO!.idFromName(sessionId);
const doStub = c.env.SESSION_DO!.get(doId);

const url = new URL(c.req.url);
const subPath = url.pathname.replace(`/sessions/${sessionId}`, "") || "/";
const internalUrl = `http://internal${subPath}${url.search}`;

return doStub.fetch(
new Request(internalUrl, {
method: c.req.method,
headers: c.req.raw.headers,
body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : undefined,
})
);

if (c.env.SESSION_DO) {
const doId = c.env.SESSION_DO.idFromName(sessionId);
const doStub = c.env.SESSION_DO.get(doId);

const url = new URL(c.req.url);
const subPath = url.pathname.replace(`/sessions/${sessionId}`, "") || "/";
const internalUrl = `http://internal${subPath}${url.search}`;

return doStub.fetch(
new Request(internalUrl, {
method: c.req.method,
headers: c.req.raw.headers,
body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : undefined,
})
);
} else {
// Stateless mode (Free Tier)
const { statelessApp } = await import("./runtime/stateless");
return statelessApp.fetch(c.req.raw, c.env, c.executionCtx);
}
});

export default app;
Loading