openma

Cloudflare Workers TypeScript Apache 2.0 License Tests Anthropic Compatible

# Open Managed Agents **Open-source alternative to Claude Managed Agents** โ€” and a foundation for open-source, self-hosted Claude Tag-style agents. ๐ŸŒ **[openma.dev](https://openma.dev)** ยท ๐Ÿ“– **[docs.openma.dev](https://docs.openma.dev)** ยท ๐Ÿ’ฌ **[github.com/openma-ai/open-managed-agents](https://github.com/openma-ai/open-managed-agents)** Write a harness. Deploy. The platform runs it โ€” with sessions, sandboxes, tools, memory, vaults, Slack/GitHub/Linear integrations, and crash recovery out of the box. Drop-in compatible with the Claude Managed Agents API; runs on Cloudflare Workers + Durable Objects, or `docker compose up` on your own box. Use Open Managed Agents when you want: - A self-hosted Claude Managed Agents API implementation. - An open-source, self-hosted Claude Tag-style workflow with BYOK model credentials. - MCP, private tools, encrypted vaults, and durable sessions under your own deployment boundary. Compare: [Claude Tag alternative](https://openma.dev/claude-tag-alternative/) ยท [Open-source Claude Tag](https://openma.dev/claude-tag-open-source/) ยท [Self-hosted Claude Tag](https://openma.dev/self-hosted-claude-tag/) --- ## Two ways to run OMA The same harness, business logic, and event-log model run on both. Pick the one that matches your hosting story: | | **Self-host (Node)** | **Cloudflare** | |---|---|---| | Where it lives | Your VPS / Mac / Docker host / fly.io / your k8s | Cloudflare Workers + DO + Containers | | Storage | SQLite or Postgres + local FS | D1 + KV + R2 | | Sandbox | LocalSubprocess / LiteBox / Daytona / E2B / BoxRun | Cloudflare Sandbox (Containers) | | Time to running | `docker compose up` (~2 min) | wrangler deploy (~10 min once configured) | | Best for | OSS users, on-prem, no CF account, data-resident deploys | Edge scale, no host management, already on CF | **Same SDK.** Same `/v1/agents` / `/v1/sessions` API. Same Console UI. Same crash-recovery semantics. Switch between them by changing env vars, not code. --- ## Quick start: self-host (Docker) ```bash git clone https://github.com/openma-ai/open-managed-agents.git cd open-managed-agents cp .env.example .env # Two secrets are required before first boot โ€” both generated locally: # BETTER_AUTH_SECRET โ€” signs Console sessions # PLATFORM_ROOT_SECRET โ€” encrypts credentials, model-card API keys, integration tokens at rest # (lose it and every encrypted row is unreadable โ€” back it up) $EDITOR .env # BETTER_AUTH_SECRET=$(openssl rand -hex 32) # PLATFORM_ROOT_SECRET=$(openssl rand -base64 32) # # Optional: ANTHROPIC_API_KEY lets the first agent run without a Model Card. # In production, add a Model Card per tenant from the Console instead. # SQLite + LocalSubprocess sandbox (default โ€” fastest path) docker compose up -d # Or Postgres backend # docker compose -f docker-compose.postgres.yml up -d curl localhost:8787/health # โ†’ {"status":"ok","backends":{"db":"sqlite ..."}, ...} open http://localhost:8787 # Console UI on the same port ``` Smoke test the harness end-to-end: ```bash AID=$(curl -s -X POST localhost:8787/v1/agents -H 'content-type: application/json' \ -d '{"name":"hello","model":"claude-sonnet-4-6","tools":[{"type":"agent_toolset_20260401"}]}' | jq -r .id) SID=$(curl -s -X POST localhost:8787/v1/sessions -H 'content-type: application/json' \ -d "{\"agent\":\"$AID\"}" | jq -r .id) curl -s -X POST localhost:8787/v1/sessions/$SID/events -H 'content-type: application/json' \ -d '{"events":[{"type":"user.message","content":[{"type":"text","text":"Run: uname -a"}]}]}' ``` Full self-host guide (sandbox modes, Postgres, BoxRun, vault sidecar, Console UI, operator gotchas): **[docs.openma.dev/self-host/overview](https://docs.openma.dev/self-host/overview/)** --- ## Quick start: Cloudflare deploy Requires [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/) (for Durable Objects + Containers). ```bash git clone https://github.com/openma-ai/open-managed-agents.git cd open-managed-agents pnpm install # Local dev (no CF account needed) โ€” wrangler dev with simulators cp .dev.vars.example .dev.vars && $EDITOR .dev.vars # Same two-secret setup as Docker โ€” PLATFORM_ROOT_SECRET is required to start pnpm dev # API โ†’ http://localhost:8787 # Console โ†’ http://localhost:5173 # Deploy npx wrangler login npx wrangler kv namespace create CONFIG_KV # paste id into wrangler.jsonc # Required secrets (paste each when prompted) npx wrangler secret put BETTER_AUTH_SECRET # openssl rand -hex 32 npx wrangler secret put PLATFORM_ROOT_SECRET # openssl rand -base64 32 โ€” back this up npx wrangler secret put API_KEY # initial bootstrap key for the REST API # Optional โ€” only if you want a tenant-less default LLM (otherwise add a Model Card in the Console) # npx wrangler secret put ANTHROPIC_API_KEY npm run deploy # โ†’ https://openma.dev (or https://managed-agents..workers.dev for a personal deploy) ``` What gets deployed: | Component | What it does | |---|---| | **Main Worker** | API routes โ€” agents, sessions, environments, vaults, memory, files | | **Agent Worker** | SessionDO + harness + sandbox per environment | | **KV Namespace** | Config storage for agents, environments, credentials | | **R2 Bucket** | Workspace file persistence across container restarts | ### Create your first agent The smoke test above works against any deployment. For the Console-driven flow (Model Cards, vaults, integrations) see **[docs.openma.dev/quickstart](https://docs.openma.dev/quickstart)**. The minimal API equivalent: ```bash BASE=http://localhost:8787 # or your deployed URL KEY=dev-test-key # whatever you set as API_KEY AGENT=$(curl -s $BASE/v1/agents \ -H "x-api-key: $KEY" -H "content-type: application/json" \ -d '{ "name": "Coder", "model": "claude-sonnet-4-6", "system": "You are a helpful coding assistant.", "tools": [{ "type": "agent_toolset_20260401" }] }' | jq -r .id) SESSION=$(curl -s $BASE/v1/sessions \ -H "x-api-key: $KEY" -H "content-type: application/json" \ -d "{\"agent\":\"$AGENT\"}" | jq -r .id) # Send a turn AND stream the reply token-by-token in one shot curl -N -X POST $BASE/v1/sessions/$SESSION/messages \ -H "x-api-key: $KEY" -H "content-type: application/json" \ -d '{"content":"Write a Python script that fetches HN top stories"}' ``` For long-lived sessions use `GET /v1/sessions/$SESSION/events/stream` โ€” replays history on connect, never closes. --- ## 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: ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Harness (the brain โ€” your code) โ”‚ โ”‚ - Reads events, builds context, calls the model โ”‚ โ”‚ - Decides HOW: caching, compaction, tool delivery โ”‚ โ”‚ - Stateless: crash โ†’ rebuild from event log โ†’ resume โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Meta-Harness (the platform โ€” SessionDO) โ”‚ โ”‚ - Prepares WHAT is available: tools, skills, history โ”‚ โ”‚ - Manages lifecycle: sandbox, events, WebSocket โ”‚ โ”‚ - Crash recovery, credential isolation, usage tracking โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”‚ Infrastructure (Cloudflare or Node self-host) โ”‚ โ”‚ - Event log: Durable-Object SQLite (CF) or SQLite/Pg โ”‚ โ”‚ - Sandbox: CF Containers / subprocess / LiteBox / E2B โ”‚ โ”‚ - Storage: KV + R2 (CF) or local FS (self-host) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` **The platform prepares _what_ is available. The harness decides _how_ to deliver it to the model.** | Platform manages | Harness decides | |---|---| | Event log persistence (SQLite) | Context engineering (filtering, ordering) | | Sandbox lifecycle (containers) | Caching strategy (cache breakpoints) | | Tool registration (built-in + MCP) | Compaction strategy (when to compress) | | WebSocket broadcast | Retry strategy (backoff, transient detection) | | Crash recovery | Stop conditions (max steps, completion signals) | | Credential isolation (vaults) | System prompt construction | | Memory (vector search) | Tool delivery (all at once vs. progressive) | --- ## Write a Harness The default harness works out of the box. When you need custom behavior โ€” different caching, compaction, context engineering โ€” write your own: ```typescript // my-harness.ts import { defineHarness, generateText, stepCountIs } from "@open-managed-agents/sdk"; export default defineHarness({ name: "research", async run(ctx) { let messages = ctx.runtime.history.getMessages(); // Your context engineering messages = keepOnly(messages, ["web_search", "web_fetch"]); // Your caching strategy markLastN(messages, 3, { cacheControl: "ephemeral" }); // Your loop โ€” tools, sandbox, broadcast are platform-provided const result = await generateText({ model: ctx.model, system: ctx.systemPrompt, messages, tools: ctx.tools, stopWhen: stepCountIs(50), onStepFinish: async ({ text }) => { if (text) ctx.runtime.broadcast({ type: "agent.message", content: [{ type: "text", text }], }); }, }); await ctx.runtime.reportUsage?.(result.usage.inputTokens, result.usage.outputTokens); }, }); ``` Deploy it: ```bash oma deploy --harness my-harness.ts --agent agent_abc123 ``` The harness is bundled into the agent worker at build time. Your code runs in the same isolate as SessionDO โ€” direct access to the event log, sandbox, and WebSocket broadcast. No RPC, no serialization boundary. --- ## API Compatible with the [Claude Managed Agents API](https://docs.anthropic.com/en/docs/agents/managed-agents). Same endpoints, same event types, works with existing SDKs.
Agents โ€” Create and manage agent configurations ```http POST /v1/agents # Create agent GET /v1/agents # List agents GET /v1/agents/:id # Get agent PUT /v1/agents/:id # Update agent DELETE /v1/agents/:id # Delete agent POST /v1/agents/:id/archive # Archive agent GET /v1/agents/:id/versions # Version history GET /v1/agents/:id/versions/:version # Get specific version ```
Environments โ€” Sandbox execution environments ```http POST /v1/environments # Create environment GET /v1/environments # List environments GET /v1/environments/:id # Get environment PUT /v1/environments/:id # Update environment DELETE /v1/environments/:id # Delete environment ```
Sessions โ€” Run agent conversations ```http POST /v1/sessions # Create session GET /v1/sessions # List sessions GET /v1/sessions/:id # Get session POST /v1/sessions/:id # Update session DELETE /v1/sessions/:id # Delete session POST /v1/sessions/:id/archive # Archive session POST /v1/sessions/:id/events # Send events (user messages) GET /v1/sessions/:id/events # Get events (JSON or SSE) GET /v1/sessions/:id/events/stream # SSE stream POST /v1/sessions/:id/resources # Attach resource GET /v1/sessions/:id/resources # List resources DELETE /v1/sessions/:id/resources/:resId # Remove resource ```
Vaults โ€” Secure credential storage ```http POST /v1/vaults # Create vault POST /v1/vaults/:id/credentials # Add credential GET /v1/vaults/:id/credentials # List (secrets stripped) ```
Memory Stores โ€” persistent storage; Claude Managed Agents Memory contract When attached to a session, each store is mounted into the sandbox at `/mnt/memory//`. The agent reads and writes it with the **standard file tools** (bash/read/write/edit/glob/grep) โ€” there are no bespoke `memory_*` tools. R2 holds the bytes-of-truth (key `/`); D1 holds the index + audit, kept eventually consistent via R2 Event Notifications โ†’ Cloudflare Queue โ†’ Consumer. ```http POST /v1/memory_stores # Create store GET /v1/memory_stores # List stores GET /v1/memory_stores/:id # Retrieve store POST /v1/memory_stores/:id/archive # Archive (one-way) DELETE /v1/memory_stores/:id # Delete store + memories + versions POST /v1/memory_stores/:id/memories # Create/upsert memory {path, content, precondition?} GET /v1/memory_stores/:id/memories?path_prefix=&depth=N # List memories (metadata) GET /v1/memory_stores/:id/memories/:mid # Retrieve memory (with content) POST /v1/memory_stores/:id/memories/:mid # Update memory {path?, content?, precondition?} DELETE /v1/memory_stores/:id/memories/:mid # Delete memory GET /v1/memory_stores/:id/memory_versions?memory_id= # Audit history (newest first) GET /v1/memory_stores/:id/memory_versions/:ver_id # Single version (with snapshot content) POST /v1/memory_stores/:id/memory_versions/:ver_id/redact # Redact prior version (refuses live head) ``` CAS via `precondition: { type: "content_sha256", content_sha256 }`. 100KB cap per memory. 30-day version retention with the most-recent version per memory always preserved. Rollback = retrieve a version and write its content as a new memory revision (no special endpoint). CLI: ```bash oma memory stores create "User Preferences" oma memory write /preferences/formatting.md --content "Always use tabs." oma memory ls --prefix /preferences/ oma memory versions --memory-id ```
Files & Skills ```http POST /v1/files # Upload file GET /v1/files/:id/content # Download file POST /v1/skills # Create skill GET /v1/skills # List skills ```
--- ## Built-in Tools The `agent_toolset_20260401` provides: | Tool | Description | |---|---| | `bash` | Execute commands in the sandbox | | `read` | Read files from sandbox filesystem | | `write` | Write/create files (auto-creates directories) | | `edit` | Surgical string replacement in files | | `glob` | Find files matching a pattern | | `grep` | Search file contents with regex | | `web_fetch` | URL โ†’ markdown via Workers AI; auto-summarized when `agent.aux_model` is set, raw saved to `/workspace/.web/` | | `web_search` | Web search via Tavily API (requires `TAVILY_API_KEY`) | | `schedule` / `cancel_schedule` / `list_schedules` | Cron-style self-wakeup for long-running agents | | `browser` (opt-in) | Headless browser session โ€” navigate, click, screenshot. Opt-in via `tools: [{ name: "browser", enabled: true }]` so the default-tool list nudges agents toward cheaper `web_fetch` | Derived tools are auto-generated based on session config: | Tool | Source | |---|---| | `call_agent_*` | Callable Agents (multi-agent delegation) | | `mcp____` | MCP Servers (double underscore is the actual separator) | (Memory Stores do **not** add bespoke tools โ€” agents access them as filesystem mounts at `/mnt/memory//` via the standard file tools above.) --- ## MCP servers OMA registers any [Model Context Protocol](https://modelcontextprotocol.io) server attached to an agent. Each upstream tool surfaces to the model as `mcp____` (double underscore โ€” copy the name exactly). Up to 20 servers per agent. | Transport | When to use | How | |---|---|---| | HTTP / SSE | Hosted MCP servers (Linear, GitHub Copilot, Notion, โ€ฆ) | `{"type":"url","url":"https://mcp.linear.app/mcp"}` | | stdio | npm / PyPI MCP packages with no hosted endpoint | `{"type":"stdio","command":"uvx","args":[...],"port":8765}` โ€” OMA spawns inside the sandbox container, talks to `127.0.0.1:port/sse` | Credentials never enter the sandbox; the outbound resolver matches by host and injects at forward time. | Auth mode | Configured as | Refresh | |---|---|---| | none | no `authorization_token`, no matching vault credential | n/a | | inline bearer | `"authorization_token": "..."` on the server entry | no | | vault static bearer | session vault has a `static_bearer` credential whose `mcp_server_url` matches | no | | vault OAuth | session vault has an `mcp_oauth` credential (with `refresh_token` + `token_endpoint`) | yes โ€” on 401 **and 403** (Airtable/Asana/Sentry use 403 for expired tokens), CAS-writes new token to D1, retries once | ```bash # Servers attach to the agent (not the session) curl -X PUT $BASE/v1/agents/$AGENT -H "x-api-key: $KEY" -H "content-type: application/json" \ -d '{"mcp_servers":[{"name":"linear","type":"url","url":"https://mcp.linear.app/mcp"}]}' # Bind an OAuth credential via Vault oma connect linear --vault $VAULT_ID ``` Tool discovery is bounded at 15 s per server; one bad server logs and skips, the rest stay live. Full design: [docs.openma.dev/build/vault-and-mcp](https://docs.openma.dev/build/vault-and-mcp/). --- ## Skills A skill is a `SKILL.md` plus reference files (templates, schemas, examples). At session start the platform mounts everything under `/home/user/.skills/{name}/` in the sandbox **and inlines the SKILL.md body directly into the system prompt** โ€” no lazy read, no follow-up `read` tool call. Format is compatible with Anthropic's [Claude Code skills](https://github.com/anthropics/skills). Create a skill (JSON; files inlined): ```http POST /v1/skills { "files": [ { "filename": "SKILL.md", "content": "---\nname: invoice-parser\ndescription: Parse supplier invoices.\n---\n\n# Steps\n1. ..." }, { "filename": "schema.json", "content": "{...}" } ] } ``` For large skills with binaries: `POST /v1/skills/upload` multipart with `file=`. Attach to an agent with the **object form** โ€” a bare string array silently does not bind: ```json { "skills": [{ "skill_id": "skill_abc123", "type": "custom" }] } ``` The agent's system prompt then receives, at session start: ```text {full SKILL.md body} ``` and the files appear at `/home/user/.skills/invoice-parser/SKILL.md` etc. Four built-in skills ship ready to attach (no upload): `xlsx`, `pdf`, `docx`, `pptx`. Reference them with `{"skill_id":"builtin_pdf","type":"anthropic"}`. --- ## Vaults & outbound credentials **Tools never see your tokens.** When a sandbox makes an HTTP request, an outbound resolver โ€” `oma-vault` sidecar on self-host (mockttp HTTPS proxy with a trusted self-signed CA), the agent worker's `outboundByHost` interceptor on Cloudflare โ€” matches the request hostname against the session's vaults, **strips any inbound `Authorization`/`x-api-key`/`x-goog-api-key`**, injects the real credential, and forwards. A prompt-injected agent has nothing to leak; `env | grep TOKEN` returns nothing inside the sandbox. ```bash # Create a vault and add a static bearer bound to api.github.com VID=$(curl -sX POST $BASE/v1/vaults -H "x-api-key: $KEY" \ -d '{"name":"github-prod"}' | jq -r .id) curl -sX POST $BASE/v1/vaults/$VID/credentials -H "x-api-key: $KEY" -d '{ "display_name": "gh-pat", "auth": { "type": "static_bearer", "token": "ghp_xxx", "mcp_server_url": "https://api.github.com" } }' # Bind on session create curl -sX POST $BASE/v1/sessions -H "x-api-key: $KEY" \ -d "{\"agent\":\"$AGENT\",\"vault_ids\":[\"$VID\"]}" # Inside the sandbox: curl https://api.github.com/user โ†’ 200, Authorization injected at the network layer ``` Three credential types share one resolver: | Type | Match by | Refresh | |---|---|---| | `static_bearer` | request host matches `mcp_server_url` | never | | `mcp_oauth` | request host matches `mcp_server_url` | on 401 / 403 via `token_endpoint`, CAS-writes new token to D1 | | `cap_cli` | sandbox CLI invocations match `cli_id` in the cap registry (`gh`, `glab`, `aws`, โ€ฆ) | per-CLI | Max 20 credentials per vault. Each forward emits a structured `op:"mcp_proxy.forward"` log. Full design: [`docs/mcp-credential-architecture.md`](docs/mcp-credential-architecture.md), [docs.openma.dev/build/vault-and-mcp](https://docs.openma.dev/build/vault-and-mcp/). --- ## Integrations Publish an agent into a third-party tool and have it act as a real teammate there โ€” assigned, mentioned, replied to like any other user. ### Linear Make an agent a member of your Linear workspace with its own identity, avatar, and `@autocomplete` slot. The agent appears in the assignee dropdown, gets pinged on `@mentions`, replies in the Agent panel, and pushes status back to issues it's working on. Two install kinds: | Kind | When to pick | Setup | |---|---|---| | **`personal_token`** (PAT) | Single workspace, fastest path, no OAuth App | `oma linear install-pat --workspace --pat ` | | **`dedicated`** (OAuth App) | Multi-workspace, proper bot identity, OAuth refresh | Console **Integrations โ†’ Linear โ†’ Publish agent** (wizard issues per-publication callback + webhook URLs to paste into your own Linear OAuth App at `linear.app/settings/api`) | The full agent-side playbook (when to ask the human, how to offer browser automation, exactly what to paste into Linear's form) lives at [`skills/openma/integrations-linear.md`](skills/openma/integrations-linear.md). PAT-mode autopilot โ€” let the bot pick up unassigned issues by label/state/project: ```bash oma linear rules create --label triage --state Backlog --project "Inbox" oma linear rules list oma linear rules delete ``` Inspect / manage: ```bash oma linear list # workspaces oma linear pubs # publications (status=live, persona, caps) oma linear get # single publication oma linear update --caps issue.read,comment.write,issue.update,โ€ฆ oma linear unpublish ``` How it works: | Piece | What it does | |---|---| | **Per-publication identity** | `dedicated` registers a per-agent Linear OAuth App; `personal_token` shares the human's PAT (no App registered) | | **Inbound webhook** | Linear events become user messages on a session โ€” assigned, `@mention`, comment-mention, new comment in an active thread, **Agent panel** (`agentSessionCreated` / `agentSessionPrompted`, `commentReply` for threaded continuation) | | **Outbound MCP** | The agent talks back through `mcp.linear.app/mcp` with its own bearer (PAT or OAuth-refreshed), so writes are attributed to the persona | | **Capability gate** | Per-publication allowlist (issues / comments / labels / assignment / triage) limits what the agent can do | The Linear integration ships in `packages/linear/` (provider logic, webhook signing, MCP wiring) with thin CF wrappers in `apps/integrations/src/routes/linear/publications.ts`. ### GitHub Give an agent its own GitHub App with a real bot identity โ€” assignable on issues, requestable as a reviewer on PRs, posts comments under its own `@[bot]` handle. Each agent is a separate App on github.com (per-publication, not a shared marketplace bot), so credentials and audit trails stay isolated. ```bash # (1) Console โ€” humans clicking through a wizard Integrations โ†’ GitHub โ†’ Publish agent # (2) CLI โ€” agents driving openma on a user's behalf oma github bind --env # โ†’ opens one-click GitHub App Manifest flow oma github handoff # alt: 7-day URL for an org admin to complete oma github list oma github pubs oma github update --caps pr.read,pr.review.write,issue.comment.write,โ€ฆ oma github unpublish ``` `bind` returns a `manifestStartUrl`; opening it auto-POSTs an App manifest to `github.com/settings/apps/new` with redirect URL + webhook URL + recommended permissions baked in. After confirming, GitHub redirects through to "Install on org" and the publication flips to `live`. Manual fallback: `oma github submit --app-id โ€ฆ --private-key-file โ€ฆ --webhook-secret โ€ฆ` if you registered the App by hand. **Engagement is label-based.** On install OMA auto-creates a label (default: lowercased persona name) in every selected repo. Add the label to any issue/PR to engage the bot for every subsequent activity on that thread; remove the label to mute. `@[bot]` mention in body or comment is the fallback path (GitHub's `@` autocomplete excludes Bot accounts, so it's plain-text). How it works: | Piece | What it does | |---|---| | **Per-publication App** | Each agent registers its own GitHub App via Manifest flow; credentials stored encrypted per-publication | | **Inbound webhook** | `issues`, `issue_comment`, `pull_request`, `pull_request_review`, `pull_request_review_comment` become user messages on a session (one per `#`) | | **Outbound MCP** | Agent talks to GitHub's hosted MCP at `api.githubcopilot.com/mcp/` with the installation token; same token also injected as `GITHUB_TOKEN` for sandbox `gh` / `git` | | **Token rotation** | 1-hour installation token auto-refreshed via App JWT on every webhook dispatch | | **Capability gate** | Per-publication allowlist; destructive ops (`pr.merge`, `repo.branch.delete`, `workflow.dispatch`, `release.create`, `*.delete`) require explicit opt-in | The GitHub integration ships in `packages/github/` with thin CF wrappers in `apps/integrations/src/routes/github/`. ### Slack Publish an agent into a Slack workspace as a dedicated bot โ€” `@mention`able in channels, replies in threads, joins DMs, hosts the AI assistant pane. Per-channel sessions: one running session per `(publication, channel)`, with all events in that channel converging on the same session id. ```bash # (1) Console โ€” humans clicking through a wizard Integrations โ†’ Slack โ†’ Publish agent # โ†‘ opens api.slack.com with a pre-filled manifest # (2) CLI โ€” agents driving openma on a user's behalf oma slack publish --env # โ†’ returns manifestLaunchUrl + formToken (60 min TTL) oma slack submit --client-id โ€ฆ --client-secret โ€ฆ --signing-secret โ€ฆ oma slack handoff # alt: 7-day shareable URL for a workspace admin oma slack list oma slack pubs oma slack update --caps message.write,thread.reply,reaction.add,โ€ฆ oma slack unpublish ``` The full agent-side playbook (manifest-flow caveats, `GATEWAY_ORIGIN` HTTPS requirement, what to paste where, MCP toggle probe) lives at [`skills/openma/integrations-slack.md`](skills/openma/integrations-slack.md). How it works: | Piece | What it does | |---|---| | **Per-publication App** | Each agent registers as its own dedicated Slack App via the "Create from manifest" URL flow โ€” own client id, signing secret, bot user; no shared marketplace App | | **Inbound webhook** | `app_mention` / DM / thread reply โ†’ `direct_invocation` signal; top-level channel post โ†’ debounced `channel_scan_armed` (90 s window); reactions on bot-authored messages โ†’ `reaction_on_bot_message`; `member_joined`/`member_left_channel` for the bot โ†’ `joined_channel` / `session_closed`; `channel_archive` / `channel_unarchive` โ†’ close / reopen | | **Dual-token outbound** | OAuth v2 yields both bot (`xoxb-`) and user (`xoxp-`) tokens. The `xoxp-` vault binds to `mcp.slack.com/mcp` for typed `mcp__slack__*` tools (search, history, canvases); the `xoxb-` vault binds to `slack.com/api` for `chat.postMessage`, reactions, etc. Bot replies default to in-thread | | **Capability gate** | Per-publication allowlist (`message.read/write/update/delete`, `thread.reply`, `reaction.add/remove`, `user.read`, `search.read`, `canvas.write`) | | **Resumable install** | Publication-first โ€” the row exists from minute one with callback + webhook URLs baked into the manifest. Mid-flow failures stay resumable from Console (`pending_setup` โ†’ `credentials_filled` โ†’ `awaiting_install` โ†’ `live`) | The Slack integration ships in `packages/slack/` with thin CF wrappers in `apps/integrations/src/routes/slack/`. **Operator setup:** the integrations gateway needs `GATEWAY_ORIGIN` pointing at a publicly-reachable HTTPS host โ€” Slack verifies both the OAuth redirect URL and the Events Request URL before letting an install complete. --- ## Project Structure ``` open-managed-agents/ โ”œโ”€โ”€ apps/ โ”‚ โ”œโ”€โ”€ main/ # API worker (Cloudflare) โ€” Hono routes, auth, rate limiting โ”‚ โ”œโ”€โ”€ main-node/ # API worker (Node self-host) โ€” same routes on Hono/Node โ”‚ โ”œโ”€โ”€ agent/ # Agent worker โ€” SessionDO + harness + sandbox โ”‚ โ”œโ”€โ”€ integrations/ # Integrations gateway โ€” Linear / GitHub / Slack OAuth + webhooks โ”‚ โ”œโ”€โ”€ oma-vault/ # Vault sidecar โ€” outbound auth-header injection (per-host secrets) โ”‚ โ”œโ”€โ”€ console/ # Web dashboard โ€” React + Vite + Tailwind v4 โ”‚ โ”œโ”€โ”€ docs/ # Docs site (Astro Starlight) โ€” published to docs.openma.dev โ”‚ โ””โ”€โ”€ web/ # Marketing site (Astro) โ€” published to openma.dev โ”œโ”€โ”€ packages/ โ”‚ โ”œโ”€โ”€ cli/ # `oma` CLI โ€” agent / session / integration commands โ”‚ โ”œโ”€โ”€ sdk/ # Harness SDK โ€” defineHarness, generateText helpers โ”‚ โ”œโ”€โ”€ api-types/ # Shared TypeScript types (config schemas, events) โ”‚ โ”œโ”€โ”€ http-routes/ # Public REST route definitions (shared by main + main-node) โ”‚ โ”œโ”€โ”€ session-runtime/ # Harness runtime โ€” event log, broadcast, recovery โ”‚ โ”œโ”€โ”€ sandbox/ # Sandbox adapters (subprocess / litebox / daytona / e2b / boxrun) โ”‚ โ”œโ”€โ”€ credentials-store/ # Encrypted credentials (AES-GCM under PLATFORM_ROOT_SECRET) โ”‚ โ”œโ”€โ”€ model-cards-store/ # Encrypted model-card API keys โ”‚ โ”œโ”€โ”€ vaults-store/ # Vault definitions + outbound auth wiring โ”‚ โ”œโ”€โ”€ linear/ github/ slack/ # Provider logic (OAuth, webhook signing, MCP wiring) โ”‚ โ”œโ”€โ”€ integrations-core/ # Provider-neutral persistence interfaces โ”‚ โ””โ”€โ”€ integrations-adapters-{cf,node}/ # D1 / KV / Workers + Postgres / FS implementations โ”œโ”€โ”€ docs/ # Internal design RFCs (not the user-facing site) โ”œโ”€โ”€ test/ # Unit + integration tests โ””โ”€โ”€ scripts/ # Deployment + maintenance scripts ``` --- ## Configuration The variables that gate boot and at-rest safety: | Variable | Required | Description | |---|---|---| | `PLATFORM_ROOT_SECRET` | **Yes** | AES-GCM key for `credentials.auth`, `model_cards.api_key_cipher`, and integration tokens. Workers refuse to start without it. **Back this up** โ€” losing it makes every encrypted row unreadable. Generate with `openssl rand -base64 32`. | | `BETTER_AUTH_SECRET` | **Yes** (prod) | better-auth session signing key. Sessions don't survive restart if missing. Generate with `openssl rand -hex 32`. | | `API_KEY` | Yes | Bootstrap key for the REST API in dev / first-run. Once the Console is up, prefer per-tenant API keys minted from there. | | `INTEGRATIONS_INTERNAL_SECRET` | Yes (if `apps/integrations` runs) | Shared secret between `apps/main` and `apps/integrations`. | | `ANTHROPIC_API_KEY` | No | Fallback LLM credential used when a tenant has not added a Model Card. **In production, add a Model Card per tenant from the Console** โ€” the key is encrypted at rest under `PLATFORM_ROOT_SECRET`, scoped to the tenant, and rotatable without redeploy. | | `ANTHROPIC_BASE_URL` | No | Override for Anthropic-compatible proxies. | | `PUBLIC_BASE_URL` | No (dev) / Yes (prod) | Cookie domain + OAuth redirect base. Defaults to `*` trusted-origins โ€” only safe for local dev. | | `SANDBOX_PROVIDER` | No | `subprocess` (default, no isolation), `litebox` (Firecracker), `daytona`, `e2b`, or `boxrun`. Use an isolated backend for untrusted agents. | | `TAVILY_API_KEY` | No | Backend for the `web_search` built-in tool. | Full list (integrations OAuth credentials, Postgres URL, sandbox tunables, memory-bucket config, Google sign-in, etc.) โ€” see **[docs.openma.dev/reference/configuration](https://docs.openma.dev/reference/configuration/)** and `.env.example` / `.dev.vars.example`. --- ## Model Cards Per-tenant LLM credentials. An agent references one by setting `agent.model = ""` โ€” the worker looks up the card and signs the outbound request with its api_key, base_url, and headers. This is the canonical replacement for the global `ANTHROPIC_API_KEY` env var. Providers (wire tag โ†’ request shape): | tag | shape | typical use | |---|---|---| | `ant` | Anthropic `/v1/messages` | Claude on `api.anthropic.com` | | `ant-compatible` | Anthropic shape, custom `base_url` | Bedrock proxy, self-hosted Anthropic-compatible | | `oai` | OpenAI `/v1/chat/completions` | OpenAI, Azure OpenAI | | `oai-compatible` | OpenAI shape, custom `base_url` | vLLM, OpenRouter, Groq, etc. | Add one from **Console โ†’ Model Cards**, or via CLI: ```bash oma models create \ --model-id claude-prod \ --provider ant \ --model claude-sonnet-4-6 \ --api-key sk-ant-... oma models list ``` REST: `POST /v1/model_cards`, `GET /v1/model_cards`, `POST /v1/model_cards/:id` (rotate), `DELETE /v1/model_cards/:id`. Create runs a 6-second probe so a bad key fails loudly, not at first turn. Keys are AES-256-GCM-encrypted at rest under `PLATFORM_ROOT_SECRET` (label `model.cards.keys`); list responses surface only the last-4 preview. Rotate by POSTing a new `api_key` โ€” no redeploy, no key versioning (re-run the backfill script if you rotate `PLATFORM_ROOT_SECRET` itself). --- ## Testing ```bash npm test # unit + integration suite npm run typecheck # zero errors ``` --- ## Documentation The user-facing docs site lives at [`apps/docs`](apps/docs/) (Astro Starlight) and is published to **[docs.openma.dev](https://docs.openma.dev)**. ```bash pnpm dev:docs # local preview at http://localhost:4321 pnpm build:docs # static build into apps/docs/dist/ pnpm deploy:docs # build + wrangler deploy (Cloudflare Worker static assets) ``` The `docs/` folder at the repo root contains **internal design RFCs** โ€” not the user-facing site. --- ## Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feat/amazing-feature`) 3. Run tests (`npm test && npm run typecheck`) 4. Commit your changes 5. Open a Pull Request --- ## License [Apache 2.0](LICENSE)