open-managed-agentsDeepSeek Harness plugin
Open-source Claude Managed Agents API implementation and self-hosted Claude Tag-style agent runtime. Drop-in compatible; runs on Cloudflare Workers/Durable Objects or Node.js. Apache 2.0.
- Stars
- 254
- Forks
- 35
- License
- Apache-2.0
- Last commit
- Sep 2, 2026
- Latest release
- @openma/cli@0.5.0
Overview
Open-source Claude Managed Agents API implementation and self-hosted Claude Tag-style agent runtime. Drop-in compatible; runs on Cloudflare Workers/Durable Objects or Node.js. Apache 2.0.
Original README
Cached from the project repository on Sep 3, 2026. This is source content, separate from the Agents.md review above.
Open Managed Agents
Open-source alternative to Claude Managed Agents โ and a foundation for open-source, self-hosted Claude Tag-style agents.
๐ openma.dev ยท ๐ docs.openma.dev ยท ๐ฌ 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 ยท Open-source Claude Tag ยท 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)
bash1git clone https://github.com/openma-ai/open-managed-agents.git 2cd open-managed-agents 3cp .env.example .env 4 5# Two secrets are required before first boot โ both generated locally: 6# BETTER_AUTH_SECRET โ signs Console sessions 7# PLATFORM_ROOT_SECRET โ encrypts credentials, model-card API keys, integration tokens at rest 8# (lose it and every encrypted row is unreadable โ back it up) 9$EDITOR .env 10# BETTER_AUTH_SECRET=$(openssl rand -hex 32) 11# PLATFORM_ROOT_SECRET=$(openssl rand -base64 32) 12# 13# Optional: ANTHROPIC_API_KEY lets the first agent run without a Model Card. 14# In production, add a Model Card per tenant from the Console instead. 15 16# SQLite + LocalSubprocess sandbox (default โ fastest path) 17docker compose up -d 18 19# Or Postgres backend 20# docker compose -f docker-compose.postgres.yml up -d 21 22curl localhost:8787/health 23# โ {"status":"ok","backends":{"db":"sqlite ..."}, ...} 24 25open http://localhost:8787 # Console UI on the same port
Smoke test the harness end-to-end:
bashAID=$(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
Quick start: Cloudflare deploy
Requires Workers Paid plan (for Durable Objects + Containers).
bash1git clone https://github.com/openma-ai/open-managed-agents.git 2cd open-managed-agents 3pnpm install 4 5# Local dev (no CF account needed) โ wrangler dev with simulators 6cp .dev.vars.example .dev.vars && $EDITOR .dev.vars 7# Same two-secret setup as Docker โ PLATFORM_ROOT_SECRET is required to start 8pnpm dev 9# API โ http://localhost:8787 10# Console โ http://localhost:5173 11 12# Deploy 13npx wrangler login 14npx wrangler kv namespace create CONFIG_KV # paste id into wrangler.jsonc 15 16# Required secrets (paste each when prompted) 17npx wrangler secret put BETTER_AUTH_SECRET # openssl rand -hex 32 18npx wrangler secret put PLATFORM_ROOT_SECRET # openssl rand -base64 32 โ back this up 19npx wrangler secret put API_KEY # initial bootstrap key for the REST API 20 21# Optional โ only if you want a tenant-less default LLM (otherwise add a Model Card in the Console) 22# npx wrangler secret put ANTHROPIC_API_KEY 23 24npm run deploy 25# โ https://openma.dev (or https://managed-agents.<your-subdomain>.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. The minimal API equivalent:
bash1BASE=http://localhost:8787 # or your deployed URL 2KEY=dev-test-key # whatever you set as API_KEY 3 4AGENT=$(curl -s $BASE/v1/agents \ 5 -H "x-api-key: $KEY" -H "content-type: application/json" \ 6 -d '{ 7 "name": "Coder", 8 "model": "claude-sonnet-4-6", 9 "system": "You are a helpful coding assistant.", 10 "tools": [{ "type": "agent_toolset_20260401" }] 11 }' | jq -r .id) 12 13SESSION=$(curl -s $BASE/v1/sessions \ 14 -H "x-api-key: $KEY" -H "content-type: application/json" \ 15 -d "{\"agent\":\"$AGENT\"}" | jq -r .id) 16 17# Send a turn AND stream the reply token-by-token in one shot 18curl -N -X POST $BASE/v1/sessions/$SESSION/messages \ 19 -H "x-api-key: $KEY" -H "content-type: application/json" \ 20 -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:
typescript1// my-harness.ts 2import { defineHarness, generateText, stepCountIs } from "@open-managed-agents/sdk"; 3 4export default defineHarness({ 5 name: "research", 6 7 async run(ctx) { 8 let messages = ctx.runtime.history.getMessages(); 9 10 // Your context engineering 11 messages = keepOnly(messages, ["web_search", "web_fetch"]); 12 13 // Your caching strategy 14 markLastN(messages, 3, { cacheControl: "ephemeral" }); 15 16 // Your loop โ tools, sandbox, broadcast are platform-provided 17 const result = await generateText({ 18 model: ctx.model, 19 system: ctx.systemPrompt, 20 messages, 21 tools: ctx.tools, 22 stopWhen: stepCountIs(50), 23 onStepFinish: async ({ text }) => { 24 if (text) ctx.runtime.broadcast({ 25 type: "agent.message", 26 content: [{ type: "text", text }], 27 }); 28 }, 29 }); 30 31 await ctx.runtime.reportUsage?.(result.usage.inputTokens, result.usage.outputTokens); 32 }, 33});
Deploy it:
bashoma 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. Same endpoints, same event types, works with existing SDKs.
Agents โ Create and manage agent configurations
httpPOST /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
httpPOST /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
http1POST /v1/sessions # Create session 2GET /v1/sessions # List sessions 3GET /v1/sessions/:id # Get session 4POST /v1/sessions/:id # Update session 5DELETE /v1/sessions/:id # Delete session 6POST /v1/sessions/:id/archive # Archive session 7 8POST /v1/sessions/:id/events # Send events (user messages) 9GET /v1/sessions/:id/events # Get events (JSON or SSE) 10GET /v1/sessions/:id/events/stream # SSE stream 11 12POST /v1/sessions/:id/resources # Attach resource 13GET /v1/sessions/:id/resources # List resources 14DELETE /v1/sessions/:id/resources/:resId # Remove resource
Vaults โ Secure credential storage
httpPOST /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/<store_name>/. 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 <store_id>/<memory_path>); D1 holds the
index + audit, kept eventually consistent via R2 Event Notifications โ
Cloudflare Queue โ Consumer.
http1POST /v1/memory_stores # Create store 2GET /v1/memory_stores # List stores 3GET /v1/memory_stores/:id # Retrieve store 4POST /v1/memory_stores/:id/archive # Archive (one-way) 5DELETE /v1/memory_stores/:id # Delete store + memories + versions 6 7POST /v1/memory_stores/:id/memories # Create/upsert memory {path, content, precondition?} 8GET /v1/memory_stores/:id/memories?path_prefix=&depth=N # List memories (metadata) 9GET /v1/memory_stores/:id/memories/:mid # Retrieve memory (with content) 10POST /v1/memory_stores/:id/memories/:mid # Update memory {path?, content?, precondition?} 11DELETE /v1/memory_stores/:id/memories/:mid # Delete memory 12 13GET /v1/memory_stores/:id/memory_versions?memory_id= # Audit history (newest first) 14GET /v1/memory_stores/:id/memory_versions/:ver_id # Single version (with snapshot content) 15POST /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:
bashoma memory stores create "User Preferences" oma memory write <store-id> /preferences/formatting.md --content "Always use tabs." oma memory ls <store-id> --prefix /preferences/ oma memory versions <store-id> --memory-id <mem-id>
Files & Skills
httpPOST /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__<server>__<tool> | MCP Servers (double underscore is the actual separator) |
(Memory Stores do not add bespoke tools โ agents access them as filesystem
mounts at /mnt/memory/<store_name>/ via the standard file tools above.)
MCP servers
OMA registers any Model Context Protocol server attached to an agent. Each upstream tool surfaces to the model as mcp__<server>__<tool> (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.
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.
Create a skill (JSON; files inlined):
httpPOST /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=<my-skill.zip>.
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:
<source name="skill:skill_abc123"> <skill name="invoice-parser"> {full SKILL.md body} </skill> </source>
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.
bash1# Create a vault and add a static bearer bound to api.github.com 2VID=$(curl -sX POST $BASE/v1/vaults -H "x-api-key: $KEY" \ 3 -d '{"name":"github-prod"}' | jq -r .id) 4 5curl -sX POST $BASE/v1/vaults/$VID/credentials -H "x-api-key: $KEY" -d '{ 6 "display_name": "gh-pat", 7 "auth": { 8 "type": "static_bearer", 9 "token": "ghp_xxx", 10 "mcp_server_url": "https://api.github.com" 11 } 12}' 13 14# Bind on session create 15curl -sX POST $BASE/v1/sessions -H "x-api-key: $KEY" \ 16 -d "{\"agent\":\"$AGENT\",\"vault_ids\":[\"$VID\"]}" 17 18# 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.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 <slug> --pat <linear-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.
PAT-mode autopilot โ let the bot pick up unassigned issues by label/state/project:
bashoma linear rules create <pub-id> --label triage --state Backlog --project "Inbox" oma linear rules list <pub-id> oma linear rules delete <rule-id>
Inspect / manage:
bashoma linear list # workspaces oma linear pubs <installation-id> # publications (status=live, persona, caps) oma linear get <pub-id> # single publication oma linear update <pub-id> --caps issue.read,comment.write,issue.update,โฆ oma linear unpublish <pub-id>
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 @<slug>[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 <agent-id> --env <env-id> # โ opens one-click GitHub App Manifest flow oma github handoff <form-token> # alt: 7-day URL for an org admin to complete oma github list oma github pubs <installation-id> oma github update <pub-id> --caps pr.read,pr.review.write,issue.comment.write,โฆ oma github unpublish <pub-id>
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 <form-token> --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. @<slug>[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 <repo>#<num>) |
| 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 โ @mentionable 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.
bash1# (1) Console โ humans clicking through a wizard 2Integrations โ Slack โ Publish agent # โ opens api.slack.com with a pre-filled manifest 3 4# (2) CLI โ agents driving openma on a user's behalf 5oma slack publish <agent-id> --env <env-id> # โ returns manifestLaunchUrl + formToken (60 min TTL) 6oma slack submit <form-token> --client-id โฆ --client-secret โฆ --signing-secret โฆ 7oma slack handoff <form-token> # alt: 7-day shareable URL for a workspace admin 8oma slack list 9oma slack pubs <installation-id> 10oma slack update <pub-id> --caps message.write,thread.reply,reaction.add,โฆ 11oma slack unpublish <pub-id>
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.
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 and .env.example / .dev.vars.example.
Model Cards
Per-tenant LLM credentials. An agent references one by setting agent.model = "<model_id>" โ 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 are Pi provider ids. The four legacy tags remain migration aliases:
| provider | Pi protocol / use |
|---|---|---|
| anthropic (ant) | Pi Anthropic Messages; Claude on api.anthropic.com |
| openai (oai) | Pi OpenAI Responses/Completions |
| Any Pi built-in id | deepseek, openrouter, google, minimax, etc.; Pi owns model metadata and wire behavior |
| Custom id | Requires base_url and pi_config.api; compatible aliases still map to Anthropic/OpenAI protocols |
Add one from Console โ Model Cards, or via CLI:
bashoma models create \ --model-id claude-prod \ --provider anthropic \ --model claude-sonnet-4-6 \ --api-key sk-ant-... oma models list
REST: POST /v1/oma/model_cards, GET /v1/oma/model_cards, POST /v1/oma/model_cards/:id (rotate), DELETE /v1/oma/model_cards/:id. Create runs a bounded live probe for the legacy Anthropic/OpenAI protocols; other Pi providers return an explicit skipped probe result and are validated when their runtime request is exercised.
There are two model catalogs. Official GET /v1/models lists this tenant's
executable Model Cards in the Managed Agents shape, with capabilities and token
limits projected from Pi. OMA POST /v1/oma/models/list {provider} lists Pi's
built-in catalog while configuring a card and does not require a provider key;
the old api_key field remains optional for 0.x Anthropic/OpenAI live discovery.
Agent request controls are versioned with the Agent and inherited by Sessions:
model.effort maps to Pi thinking levels (Pi normalizes unsupported levels),
model.speed=fast maps to Anthropic fast mode or OpenAI priority tier, and
model.inference_geo is preserved in the official API but is not currently a
runtime routing input. These controls are not Model Card pi_config fields.
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
bashnpm test # unit + integration suite npm run typecheck # zero errors
Documentation
The user-facing docs site lives at apps/docs (Astro Starlight) and is published to docs.openma.dev.
bashpnpm 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
- Fork the repository
- Create your feature branch (
git checkout -b feat/amazing-feature) - Run tests (
npm test && npm run typecheck) - Commit your changes
- Open a Pull Request