[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-1044":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":15,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":19,"archived":20,"fork":20,"defaultBranch":21,"hasWiki":22,"hasPages":20,"topics":23,"createdAt":10,"pushedAt":10,"updatedAt":24,"readmeContent":25,"aiSummary":26,"trendingCount":16,"starSnapshotCount":16,"syncStatus":15,"lastSyncTime":27,"discoverSource":28},1044,"openclaw-managed-agents","stainlu\u002Fopenclaw-managed-agents","stainlu","OpenClaw Managed Agents — the open alternative to Claude Managed Agents & ChatGPT Workspace Agents. Any model, any cloud, open source.","https:\u002F\u002Fopenclaw-managed-agents.com\u002F",null,"TypeScript",428,38,392,2,0,28,48.57,"MIT License",false,"main",true,[],"2026-06-12 04:00:07","# OpenClaw Managed Agents\n\n[![CI](https:\u002F\u002Fgithub.com\u002Fstainlu\u002Fopenclaw-managed-agents\u002Factions\u002Fworkflows\u002Ftest.yaml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fstainlu\u002Fopenclaw-managed-agents\u002Factions\u002Fworkflows\u002Ftest.yaml)\n[![Images](https:\u002F\u002Fgithub.com\u002Fstainlu\u002Fopenclaw-managed-agents\u002Factions\u002Fworkflows\u002Fpublish-images.yaml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fstainlu\u002Fopenclaw-managed-agents\u002Factions\u002Fworkflows\u002Fpublish-images.yaml)\n[![npm](https:\u002F\u002Fimg.shields.io\u002Fnpm\u002Fv\u002F%40stainlu%2Fopenclaw-managed-agents)](https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002F@stainlu\u002Fopenclaw-managed-agents)\n[![PyPI](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fv\u002Fopenclaw-managed-agents)](https:\u002F\u002Fpypi.org\u002Fproject\u002Fopenclaw-managed-agents\u002F)\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-MIT-black.svg)](.\u002FLICENSE)\n\nThe open alternative to Claude Managed Agents. Run autonomous AI agents via API — any model, any cloud, open source.\n\nBuilt on [OpenClaw](https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fopenclaw), the most popular open-source AI agent framework.\n\n## Demo\n\n\u003Cvideo src=\"https:\u002F\u002Fgithub.com\u002Fuser-attachments\u002Fassets\u002F84a2e1b1-ef6a-4f20-8cbe-4dc36a6c8b77\" controls>\u003C\u002Fvideo>\n\n## What this is\n\n**The name says it:** `openclaw-managed-agents` = **OpenClaw** (the agent runtime — multi-provider, 53 built-in skills, MCP-native, Pi's durable JSONL event log) + **Managed Agents** (the four-primitive API shape — Agent \u002F Environment \u002F Session \u002F Event — that Claude Managed Agents made standard).\n\nYou POST an Agent (model + system prompt + tools + MCP servers), open a Session against it, send Events, stream back Events. Under the hood: one isolated Docker container per active session running OpenClaw, SQLite for orchestrator metadata, SSE for streaming, WebSocket control plane for cancel \u002F model-override \u002F tool-confirmation. Session state is durable even if the live container is later evicted and respawned. Deploy anywhere Docker runs.\n\nIt's the layer that turns OpenClaw from a personal AI assistant into a programmatic agent service your app can call.\n\n## Published artifacts\n\n| Artifact | Package |\n|---|---|\n| Orchestrator image | `ghcr.io\u002Fstainlu\u002Fopenclaw-managed-agents-orchestrator` |\n| Agent runtime image | `ghcr.io\u002Fstainlu\u002Fopenclaw-managed-agents-agent` |\n| Limited-networking egress proxy image | `ghcr.io\u002Fstainlu\u002Fopenclaw-managed-agents-egress-proxy` |\n| Telegram adapter image | `ghcr.io\u002Fstainlu\u002Fopenclaw-managed-agents-telegram-adapter` |\n| TypeScript SDK | `@stainlu\u002Fopenclaw-managed-agents` |\n| Python SDK | `openclaw-managed-agents` |\n| OpenAPI spec | [`openapi\u002Fopenapi.yaml`](.\u002Fopenapi\u002Fopenapi.yaml) |\n\n## vs. Claude Managed Agents\n\n| | Claude Managed Agents | OpenClaw Managed Agents |\n|---|---|---|\n| Models | Claude only | Any — Anthropic, OpenAI, Gemini, Moonshot, DeepSeek, Mistral, xAI, Bedrock, OpenRouter, Groq, and [more](https:\u002F\u002Fopenclaw.ai) |\n| Hosting | Anthropic's cloud only | Any cloud or VPS with Docker — from $0\u002Fmonth (GCE free tier) to $4\u002Fmonth (Hetzner) |\n| Source | Closed | Open source (MIT) |\n| Platform tax | $0.08\u002Fsession-hour on top of tokens | None |\n| Data | Anthropic's infrastructure | Your disk, your VPC, your control |\n| Multi-agent \u002F subagents | Research preview (gated) | GA — children are first-class inspectable sessions |\n| Permission policy | `always_allow` + `always_ask` | `always_allow` + `deny` + `always_ask` |\n| Subagent observability | Opaque (tool result only) | First-class — every child session visible through the same API |\n| Agent versioning | Immutable history + archive | Immutable history + archive + optimistic concurrency on `PATCH` |\n| MCP servers | First-class `mcp_servers` field | First-class `mcpServers` field |\n| Streaming | Real SSE token deltas | Real SSE token deltas |\n| Restart safety | Not documented | Durable event queue, HMAC secret persistence, running-container adoption, observer-side run completion |\n| Per-session quotas | Not exposed | `maxCostUsdPerSession`, `maxTokensPerSession`, `maxWallDurationMs` |\n| Audit log | Telemetry only | Queryable `GET \u002Fv1\u002Faudit` |\n| OpenTelemetry | Built-in | Config-passthrough to OpenClaw's built-in OTEL |\n| SDK | 7 languages + CLI | Python + TypeScript + OpenAI drop-in |\n| Production track record | Notion, Rakuten, Asana, Sentry, Vibecode | New project, no deployed customers yet |\n\nBoth are solid engineering. The choice is about model\u002Fcloud freedom, platform-tax economics, and whether you want the runtime as a black box or as code you can read.\n\n## vs. running OpenClaw directly on a cloud VPS\n\n[AWS's Lightsail OpenClaw blueprint](https:\u002F\u002Faws.amazon.com\u002Fblogs\u002Faws\u002Fintroducing-openclaw-on-amazon-lightsail-to-run-your-autonomous-private-ai-agents\u002F) (or running OpenClaw yourself via its own CLI on a VPS) gives you a personal OpenClaw instance. That's great for *you* — one operator, one browser pairing, chat through WhatsApp \u002F Telegram \u002F Discord, use it as your Jarvis.\n\nOpenClaw Managed Agents is for when you want to **build a product** with OpenClaw instead of just using one.\n\n| | OpenClaw directly on a VPS (e.g. Lightsail blueprint) | OpenClaw Managed Agents |\n|---|---|---|\n| Who it's for | One human operator | Developers building programmatic agent products |\n| Access | Browser pairing + SSH CLI | HTTP REST + SSE + WebSocket |\n| Sessions | 1 shared operator pairing | N durable API sessions, each with an isolated live container when active |\n| Channels | WhatsApp \u002F Telegram \u002F Discord \u002F Slack built-in | None — programmatic only |\n| Agent management | Edit `openclaw.json` + restart the gateway | `POST \u002Fv1\u002Fagents` \u002F `PATCH` \u002F `archive` — versioned with optimistic concurrency, no restart needed |\n| Session isolation | Single workspace | Per-session Docker container with cgroup limits + bind-mounted state |\n| Restart safety | Personal data survives; in-flight work lost | Durable event queue, HMAC token persistence, container reattach on orchestrator restart |\n| Concurrency | One user at a time | Warm pool + active pool; 5-7 concurrent sessions on a $4 Hetzner CAX11 |\n| API shape | Gateway's OpenAI-compat + control plane | Full 4-primitive REST matching the Claude Managed Agents shape |\n| Deploy | Click Lightsail blueprint | `.\u002Fscripts\u002Fdeploy-hetzner.sh` etc. |\n\nNot a competitor to the personal OpenClaw — a different layer. OpenClaw is the framework inside each of our containers. This project is the managed service around it.\n\n## Quick start\n\nRequires Docker and an API key for any [OpenClaw-supported provider](https:\u002F\u002Fopenclaw.ai).\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fstainlu\u002Fopenclaw-managed-agents\ncd openclaw-managed-agents\n\nexport MOONSHOT_API_KEY=sk-...    # or ZENMUX_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, etc.\ndocker compose up --build -d\n```\n\nCreate an agent, open a session, send a message:\n\n```bash\n# Create an agent\nAGENT=$(curl -s -X POST http:\u002F\u002Flocalhost:8080\u002Fv1\u002Fagents \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\"model\":\"moonshot\u002Fkimi-k2.5\",\"instructions\":\"You are a research assistant.\"}' \\\n  | jq -r '.agent_id')\n\n# Open a session (container starts booting in the background)\nSESSION=$(curl -s -X POST http:\u002F\u002Flocalhost:8080\u002Fv1\u002Fsessions \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d \"{\\\"agentId\\\":\\\"$AGENT\\\"}\" | jq -r '.session_id')\n\n# Send a message — first turn spawns the container; subsequent turns reuse it\ncurl -s -X POST \"http:\u002F\u002Flocalhost:8080\u002Fv1\u002Fsessions\u002F$SESSION\u002Fevents\" \\\n  -H 'Content-Type: application\u002Fjson' \\\n  -d '{\"content\":\"What is 2+2? Reply with just the number.\"}'\n\n# Poll until done\nwhile :; do\n  STATUS=$(curl -s http:\u002F\u002Flocalhost:8080\u002Fv1\u002Fsessions\u002F$SESSION | jq -r .status)\n  [ \"$STATUS\" = \"starting\" ] || [ \"$STATUS\" = \"running\" ] || break\n  sleep 2\ndone\n\n# Read the answer\ncurl -s \"http:\u002F\u002Flocalhost:8080\u002Fv1\u002Fsessions\u002F$SESSION\" | jq .output\n```\n\nOr use the Python SDK:\n\n```python\nfrom openclaw_managed_agents import OpenClawClient\n\nclient = OpenClawClient(base_url=\"http:\u002F\u002Flocalhost:8080\")\nagent = client.agents.create(model=\"moonshot\u002Fkimi-k2.5\", instructions=\"You are helpful.\")\nsession = client.sessions.create(agent_id=agent.agent_id)\nclient.sessions.send(session.session_id, content=\"What is 2+2?\")\nfor event in client.sessions.stream(session.session_id):\n    if event.type == \"agent.message\":\n        print(event.content)\n        break\n```\n\nSee [`examples\u002Fresearch-assistant\u002F`](.\u002Fexamples\u002Fresearch-assistant\u002F) for a ~200-line copy-paste starting point that streams events in real time.\n\nOr use the TypeScript SDK:\n\n```ts\nimport { OpenClawClient } from \"@stainlu\u002Fopenclaw-managed-agents\";\n\nconst client = new OpenClawClient({ baseUrl: \"http:\u002F\u002Flocalhost:8080\" });\nconst agent = await client.agents.create({ model: \"moonshot\u002Fkimi-k2.5\", instructions: \"You are helpful.\" });\nconst session = await client.sessions.create({ agentId: agent.agent_id });\nawait client.sessions.send(session.session_id, { content: \"What is 2+2?\" });\nfor await (const event of client.sessions.stream(session.session_id)) {\n  if (event.type === \"agent.message\") { console.log(event.content); break; }\n}\n```\n\nOr point the **OpenAI SDK** at us — just change `base_url` and it Just Works, including real token-level streaming:\n\n```python\nfrom openai import OpenAI\n\nclient = OpenAI(\n    base_url=\"http:\u002F\u002Flocalhost:8080\u002Fv1\",\n    api_key=\"unused\",\n    default_headers={\"x-openclaw-agent-id\": \"\u003Cyour-agent-id>\"},\n)\n\n# Non-streaming\nr = client.chat.completions.create(\n    model=\"placeholder\",\n    messages=[{\"role\": \"user\", \"content\": \"Summarize the agent platform landscape.\"}],\n)\nprint(r.choices[0].message.content)\n\n# Streaming — real per-token SSE frames, not emulated\nfor chunk in client.chat.completions.create(\n    model=\"placeholder\",\n    messages=[{\"role\": \"user\", \"content\": \"Write a haiku about containers.\"}],\n    stream=True,\n):\n    print(chunk.choices[0].delta.content or \"\", end=\"\", flush=True)\n```\n\n## Core concepts\n\nFour primitives, matching Claude Managed Agents' model:\n\n| Concept | What it is | API |\n|---|---|---|\n| **Agent** | Reusable config: model, instructions, tools, MCP servers, permission policy, delegation rules, quotas. Versioned — updates create immutable history. | `POST\u002FGET\u002FPATCH\u002FDELETE \u002Fv1\u002Fagents` |\n| **Environment** | Container config: packages (pip \u002F apt \u002F npm), networking policy. Composed with agents at session time. | `POST\u002FGET\u002FDELETE \u002Fv1\u002Fenvironments` |\n| **Session** | A durable conversation in an environment. Long-lived, multi-turn, with one isolated live container when active. | `POST\u002FGET\u002FDELETE \u002Fv1\u002Fsessions` |\n| **Event** | Messages, tool calls, thinking blocks, status changes in and out of a session. SSE streaming. | `POST\u002FGET \u002Fv1\u002Fsessions\u002F:id\u002Fevents` |\n\n## API reference\n\nThe orchestrator is self-documenting — `curl http:\u002F\u002Flocalhost:8080\u002F` returns the full endpoint map.\n\n**Agents** (versioned, archivable)\n\n```\nPOST   \u002Fv1\u002Fagents                         # create\n       body: { model, instructions, tools?, name?,\n               permissionPolicy?, callableAgents?, maxSubagentDepth?,\n               mcpServers?, quota?, thinkingLevel?, channels? }\nGET    \u002Fv1\u002Fagents                         # list all\nGET    \u002Fv1\u002Fagents\u002F:id                     # get latest version\nPATCH  \u002Fv1\u002Fagents\u002F:id                     # update — { version, ...fields }, 409 on conflict\nGET    \u002Fv1\u002Fagents\u002F:id\u002Fversions            # immutable version history\nPOST   \u002Fv1\u002Fagents\u002F:id\u002Fwarm                # best-effort warm-pool preboot for this template\nPOST   \u002Fv1\u002Fagents\u002F:id\u002Farchive             # soft-delete; blocks new sessions\nDELETE \u002Fv1\u002Fagents\u002F:id                     # hard-delete\nPOST   \u002Fv1\u002Fagents\u002F:id\u002Frun                 # backwards-compatible one-shot adapter\nGET    \u002Fv1\u002Fagents\u002F:id\u002Ffiles?session_id=\u003Cid>&path=\u003Crel>\nGET    \u002Fv1\u002Fagents\u002F:id\u002Ffiles\u002F\u003Crel>?session_id=\u003Cid>\nPUT    \u002Fv1\u002Fagents\u002F:id\u002Ffiles\u002F\u003Crel>?session_id=\u003Cid>\nDELETE \u002Fv1\u002Fagents\u002F:id\u002Ffiles\u002F\u003Crel>?session_id=\u003Cid>\n```\n\nPermission policy options for `permissionPolicy`:\n- `{\"type\":\"always_allow\"}` (default) — all tools execute automatically\n- `{\"type\":\"deny\",\"tools\":[\"bash\",\"write\"]}` — specified tools are blocked entirely\n- `{\"type\":\"always_ask\",\"tools\":[\"bash\"]}` — specified tools pause for client confirmation via `user.tool_confirmation`\n\nPer-session quotas (optional, on `quota`):\n- `maxCostUsdPerSession` — refuse further turns once cumulative cost crosses the cap\n- `maxTokensPerSession` — same, for `tokens_in + tokens_out`\n- `maxWallDurationMs` — session expires after this many ms since creation\n\nRejected runs surface as `HTTP 429 quota_exceeded`.\n\n**Environments** (container configuration)\n\n```\nPOST   \u002Fv1\u002Fenvironments                   # { name, description?,\n                                           #   packages?: { pip?, apt?, npm?, cargo?, gem?, go? },\n                                           #   networking? }\nGET    \u002Fv1\u002Fenvironments                   # list all\nGET    \u002Fv1\u002Fenvironments\u002F:id               # get one\nDELETE \u002Fv1\u002Fenvironments\u002F:id               # 409 if sessions reference it\n```\n\nNetworking modes:\n- `{\"type\":\"unrestricted\"}` (default) — full egress on the shared Docker bridge\n- `{\"type\":\"limited\",\"allowedHosts\":[\"api.openai.com\",\"*.anthropic.com\"],\"allowMcpServers\":false,\"allowPackageManagers\":false}` — per-session confined `--internal` network + egress-proxy sidecar. HTTP\u002FHTTPS filtered at TCP 8118, DNS filtered at UDP 53. Enforced at the Docker bridge, not inside the agent container; raw-socket \u002F DNS-exfil paths both closed. Enable by setting `OPENCLAW_EGRESS_PROXY_IMAGE` + `OPENCLAW_CONTROL_PLANE_NETWORK` on the orchestrator. See [`docs\u002Fdesigns\u002Fnetworking-limited.md`](.\u002Fdocs\u002Fdesigns\u002Fnetworking-limited.md) and [`test\u002Fe2e-networking.sh`](.\u002Ftest\u002Fe2e-networking.sh) for the design + 9-case enforcement proof.\n\n**Vaults** (per-end-user outbound credentials)\n\n```\nPOST   \u002Fv1\u002Fvaults                         # { userId, name }\nGET    \u002Fv1\u002Fvaults?user_id=\u003Cid>            # list, optionally scoped by user id\nGET    \u002Fv1\u002Fvaults\u002F:id                     # get metadata only\nDELETE \u002Fv1\u002Fvaults\u002F:id                     # deletes credentials too\nPOST   \u002Fv1\u002Fvaults\u002F:id\u002Fcredentials         # add static_bearer or mcp_oauth credential\nGET    \u002Fv1\u002Fvaults\u002F:id\u002Fcredentials         # list metadata, never secret material\nDELETE \u002Fv1\u002Fvaults\u002F:id\u002Fcredentials\u002F:credId\n```\n\nSecrets are write-only in the API. Static bearer credentials are injected as `Authorization: Bearer \u003Ctoken>` into matching HTTP MCP server configs. OAuth credentials refresh at session spawn time when they are near expiry.\n\n**Sessions** (the main interaction surface)\n\n```\nPOST   \u002Fv1\u002Fsessions                       # { agentId, environmentId?, vaultId? }\nGET    \u002Fv1\u002Fsessions                       # list all\nGET    \u002Fv1\u002Fsessions\u002F:id                   # status (idle|starting|running|failed), output, rolling tokens, cost_usd\nDELETE \u002Fv1\u002Fsessions\u002F:id                   # tears down container + data\nPOST   \u002Fv1\u002Fsessions\u002F:id\u002Fevents            # send message or tool confirmation (see below)\nGET    \u002Fv1\u002Fsessions\u002F:id\u002Fevents            # full event history\nGET    \u002Fv1\u002Fsessions\u002F:id\u002Fevents?stream=true  # SSE: catch-up + live tail-follow\n                                           # supports Last-Event-ID header for resume\nPOST   \u002Fv1\u002Fsessions\u002F:id\u002Fcancel            # abort in-flight run\nPOST   \u002Fv1\u002Fsessions\u002F:id\u002Fcompact           # ask OpenClaw to compact context history\nGET    \u002Fv1\u002Fsessions\u002F:id\u002Flogs?tail=\u003Cn>     # snapshot live container stdout\u002Fstderr\n```\n\nPOST events accepts two event types:\n- `{\"content\":\"...\",\"model\":\"...\",\"thinkingLevel\":\"medium\"}` — user message (triggers agent loop, optional session-scoped model\u002Fthinking override)\n- `{\"type\":\"user.tool_confirmation\",\"toolUseId\":\"\u003Capproval_id>\",\"result\":\"allow\"}` — resolve a pending tool confirmation (`toolUseId` is a legacy field name; pass the `approval_id` from the SSE event)\n\nModel and thinking overrides are baked into the container boot config for the first turn, then applied through OpenClaw's WebSocket `sessions.patch` on later turns once the Pi session key exists. Busy-session queue entries persist both overrides on the SQLite backend.\n\n**OpenAI compatibility**\n\n```\nPOST   \u002Fv1\u002Fchat\u002Fcompletions              # OpenAI SDK drop-in (x-openclaw-agent-id header required)\n```\n\nReal per-token SSE streaming when `stream: true`. Busy sessions return `HTTP 409 session_busy` so streams don't interleave with the event queue.\n\nConcurrency:\n- Sticky-session calls, streaming or non-streaming, return `HTTP 409 session_busy` when a run is already in flight. Serialize requests per sticky session key or retry after the current run completes.\n\n**Auth helpers**\n\n```\nPOST   \u002Fauth\u002Fanonymous                    # mint a tok_... portal token\nGET    \u002Fauth\u002Fgithub                       # start GitHub OAuth\nGET    \u002Fauth\u002Fgithub\u002Fcallback              # OAuth callback\nGET    \u002Fauth\u002Fme                           # current token metadata\n```\n\nThese routes are auth-bypassed so the bundled portal can bootstrap a user session. They are not a complete tenant boundary; see the Auth note below.\n\n**Audit log**\n\n```\nGET    \u002Fv1\u002Faudit?since=\u003Cts>&until=\u003Cts>&action=\u003Cverb>&target=\u003Cid>&limit=\u003Cn>\n                                          # queryable structured log of mutating API calls;\n                                          # all params optional; `action` supports LIKE\n                                          # wildcards (e.g. \"agent.%\"); newest-first, limit\n                                          # 1..1000 (default 100).\n                                          # retained OPENCLAW_AUDIT_RETENTION_DAYS (default 30)\n```\n\n## Event types\n\nEvents from `GET \u002Fv1\u002Fsessions\u002F:id\u002Fevents` and the SSE stream:\n\n| Type | Source | Description |\n|---|---|---|\n| `user.message` | JSONL | Client message posted via POST \u002Fevents |\n| `agent.message` | JSONL | Agent's text response with tokens, cost, model |\n| `agent.tool_use` | JSONL | Tool invocation: name, arguments, call ID |\n| `agent.tool_result` | JSONL | Tool execution result (content, isError) |\n| `agent.thinking` | JSONL | Thinking blocks (when model supports extended thinking) |\n| `agent.tool_confirmation_request` | Orchestrator | Tool paused for client approval (`always_ask` policy) |\n| `session.model_change` | JSONL | Model switched mid-session |\n| `session.thinking_level_change` | JSONL | Thinking mode toggled |\n| `session.compaction` | JSONL | Context compaction summary |\n| `session.runtime_notice` | JSONL | Runtime\u002Fsystem notice emitted by OpenClaw, not a user turn |\n| `session.status_starting` | SSE only | Session entered container-acquire \u002F boot state |\n| `session.status_idle` | SSE only | Session transitioned to idle |\n| `session.status_running` | SSE only | Session transitioned to running |\n| `session.status_failed` | SSE only | Session transitioned to failed |\n| `session.container_attached` | SSE only | A live container was claimed for the session |\n| `session.container_detached` | SSE only | A live container was evicted or torn down |\n\nThe SSE stream emits an initial status event on connect, checks for status transitions on every yielded event + every 15-second heartbeat, emits container attach\u002Fdetach telemetry, and accepts a resume cursor via the `Last-Event-ID` header or `?after=\u003Cevent_id>` query param so reconnecting clients don't replay history they've already seen.\n\n## Key features\n\n**Agent versioning.** Every update creates an immutable version. Optimistic concurrency via `version` field on PATCH. List the full history. Archive agents without losing data.\n\n**Permission policy.** Three modes: `always_allow` (default), `deny` (block specific tools entirely), and `always_ask` (pause for client confirmation before executing specific tools). The `always_ask` flow uses OpenClaw's `before_tool_call` plugin hook with `requireApproval` — the agent blocks, the orchestrator surfaces a confirmation request via SSE (`approval_id` for resolution, `tool_call_id` for correlation), rehydrates pending approvals from the gateway on warm reuse \u002F adoption, and the client resolves it via `user.tool_confirmation`.\n\n**MCP servers.** Agents declare `mcpServers` (object keyed by server name, value is either a stdio `{command, args, env, cwd}` or HTTP `{url, headers}` config). The orchestrator forwards them into the container's `openclaw.json` at spawn time; OpenClaw's MCP integration handles the rest. Matches Claude Managed Agents' shape so SDKs porting across translate without rewrites.\n\n**Per-session quotas.** `maxCostUsdPerSession` \u002F `maxTokensPerSession` \u002F `maxWallDurationMs` set on the agent (inherited by every session derived from it). Enforced at the runtime edge before each turn; rejected runs return `HTTP 429 quota_exceeded` with a `quota_rejections_total{kind=\"cost\"|\"tokens\"|\"duration\"}` metric increment. Soft-ceiling semantics: a session at $0.99 with a $1.00 cap gets one more turn, the next post rejects — matches operator intent without mid-turn aborts.\n\n**Networking: `limited`.** Per-session confined `--internal` Docker network + egress-proxy sidecar filtering hostname allowlist at HTTP + DNS layers. Enforcement at the Docker bridge, not inside the container — raw-socket \u002F DNS-exfil paths both closed. Proven with a 9-case E2E script in CI on native Linux (`test\u002Fe2e-networking.sh`).\n\n**Pre-warmed container pool.** When `OPENCLAW_MAX_WARM_CONTAINERS > 0` and a non-delegating agent is created, a template-level container boots in the background. The first session whose boot config is also template-level claims the pre-warmed container instead of cold-spawning. Sessions with session-specific boot inputs (vault credentials, `networking: limited`, package preinstalls) bypass warm reuse and cold-spawn their own sandbox. After a warm claim, the pool replenishes automatically. Session-owned containers stay resident between turns, but the active pool is bounded by `OPENCLAW_MAX_ACTIVE_CONTAINERS` with oldest-idle eviction under pressure, so a small host cannot accumulate one immortal container per historical session. Only explicitly-ephemeral sessions use idle-reap via `OPENCLAW_IDLE_TIMEOUT_MS`; the warm bucket is bounded by `OPENCLAW_MAX_WARM_CONTAINERS` with oldest-first eviction. Local compose defaults warm capacity to `0`; deploy scripts typically opt in with `3`. Measured pool reuse: 4 s vs 78 s cold-start on Hetzner CAX11.\n\n**Delegated subagents.** An agent can delegate tasks to other agents via the `openclaw-call-agent` CLI. Children are first-class sessions — fully inspectable through the same API. Allowlists, depth caps, and HMAC-signed tokens enforce who can call whom. Subagent transcripts are not hidden behind an opaque tool result.\n\n**Real token-level streaming on `POST \u002Fv1\u002Fchat\u002Fcompletions`.** `stream: true` pipes the container's real SSE chunks byte-for-byte to the caller — OpenAI-compatible `ChatCompletionChunk` frames with `[DONE]` terminator. A busy session returns `HTTP 409 session_busy` so streams don't interleave with the event queue. If the client disconnects before upstream `[DONE]`, the orchestrator aborts the upstream reader and marks the managed run failed instead of leaving the session running.\n\n**Restart safety.** Four invariants that survive orchestrator crash or deploy:\n1. Parent-token HMAC secret persisted to SQLite — outstanding subagent delegation tokens stay valid across restarts.\n2. Durable event queue (SQLite) — committed `{queued: true}` events are re-dispatched on startup.\n3. Running-container adoption — `DockerContainerRuntime.listManaged()` + `SessionContainerPool.adopt()` reattach labelled containers whose sessions still exist; orphaned containers are selectively stopped. Running sessions that can't be adopted get a recoverable `\"post a new message to resume\"` error.\n4. Observer-side run completion — WS `chat` event subscription on adopted running sessions finalizes the in-flight turn when Pi emits the final message, rolls up cost from JSONL, drains queued events.\n\n**Cancel + queue.** Cancel aborts the in-flight run via the WebSocket control plane. Events posted to a busy session queue automatically and drain in order.\n\n**Per-turn cost.** Each session tracks rolling `tokens_in`, `tokens_out`, and `cost_usd` from provider-side billing data — cache-aware, not a static spreadsheet. In production deployments with `ZENMUX_API_KEY`, all models route through ZenMux and the orchestrator uses ZenMux's live `\u002Fmodels` catalog as the canonical fallback source when a transcript has tokens but no explicit `usage.cost`. The local `provider-prices.json` table is fallback-only for direct-provider Moonshot \u002F DeepSeek bootstrap, not the primary accounting path in a ZenMux deployment.\n\n**OpenAI SDK drop-in.** Point any OpenAI SDK at `http:\u002F\u002F\u003Chost>:8080\u002Fv1` with an `x-openclaw-agent-id` header. Sticky sessions via the `user` field. Real per-token streaming (not emulated) when `stream: true`.\n\n**Per-end-user credential vault.** `POST \u002Fv1\u002Fvaults {userId, name}` creates a bundle. `POST \u002Fv1\u002Fvaults\u002F:id\u002Fcredentials {name, type: \"static_bearer\", matchUrl, token}` adds a credential — the `token` is accepted on write but never returned from any GET\u002FLIST (rotation = delete + re-add). `POST \u002Fv1\u002Fsessions {agentId, vaultId}` binds a vault to a session; at spawn time, the orchestrator walks `agent.mcpServers`, longest-prefix-matches each HTTP server's URL against vault `matchUrl`s, and merges `Authorization: Bearer \u003Ctoken>` into the server's headers before the container boots. Secret material never leaves the orchestrator. Vault-bound sessions bypass the warm pool (container env is immutable post-create, so a warm container can't carry session-specific credentials).\n\n**First-party Telegram adapter.** `docker compose --profile telegram up -d` after setting `TELEGRAM_BOT_TOKEN` + `OPENCLAW_TELEGRAM_AGENT_ID` in `.env` brings up a bridge between Telegram chats and managed-agent sessions. Long-polling (no public HTTPS needed), session-per-chat, typing indicator, auto-split of long replies. Sessions persist across adapter restarts via `\u002Fstate\u002Fchats.json`. See [`docker\u002Ftelegram-adapter\u002FREADME.md`](.\u002Fdocker\u002Ftelegram-adapter\u002FREADME.md).\n\n**Persistent state.** SQLite (WAL mode) for agents, environments, session metadata, queued events, audit log, and the HMAC secret for subagent tokens. Pi's JSONL files for the event log. All of it survives orchestrator restarts. Pre-built multi-arch images (amd64 + arm64) published to GHCR on every push to `main`.\n\n**Observability.** Structured pino logs in JSON (production) or pretty TTY (dev); every log line carries `request_id`, `agent_id`, `session_id` automatically via AsyncLocalStorage. Prometheus metrics at `GET \u002Fmetrics` — HTTP counters + duration histograms, pool active\u002Fwarm gauges, spawn + run duration histograms, per-source pool-acquire counters, quota rejections, JSONL size gauge with configurable warn threshold, startup adoption outcomes, rate-limit rejections. OpenTelemetry config-passthrough: set `OTEL_EXPORTER_OTLP_ENDPOINT` (and optional `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME`, etc.) and OpenClaw's built-in OTEL exporter turns on traces + metrics + logs at boot. See [docs\u002Farchitecture.md#observability](.\u002Fdocs\u002Farchitecture.md#observability).\n\n**Structured audit log.** Every mutating API call writes a row to `audit_events` (SQLite): `ts`, `request_id`, `actor` (token fingerprint or IP), `action`, `target`, `outcome`, optional metadata. Queryable via `GET \u002Fv1\u002Faudit?since=&until=&action=&target=&limit=` (all optional; `action` accepts LIKE wildcards like `agent.%`; newest-first; limit 1-1000, default 100). Retention via `OPENCLAW_AUDIT_RETENTION_DAYS` (default 30); hourly cleanup.\n\n**Auth.** Set `OPENCLAW_API_TOKEN=\u003Crandom-secret>` on the orchestrator host and API requests must attach `Authorization: Bearer \u003Ctoken>` — except `\u002Fhealthz`, `\u002Fmetrics`, `\u002F`, `\u002Fv2`, and the `\u002Fauth\u002F*` helper routes. Unset = auth disabled (localhost dev default). The admin token is the production security boundary today. The `\u002Fauth\u002Fanonymous` and GitHub OAuth helpers mint `tok_...` user tokens for the bundled portal, but the resource model is not yet a complete multi-tenant boundary: agents, environments, vaults, and workspace file APIs remain deployment-global. Do not expose user-token auth as tenant isolation until ownership checks cover every resource. One-command admin-token rotation on any live deploy: `.\u002Fscripts\u002Frotate-api-token.sh hetzner|lightsail|gcp|local \u003Chost-or-instance>` (generates + applies + verifies with a 401-then-200 curl pair).\n\n**Rate limiting.** Per-caller token-bucket in front of every route except `\u002Fhealthz` and `\u002Fmetrics`. Keyed by Bearer token when present, else client IP (`x-forwarded-for` first entry, else peer). Defaults to 120 req\u002Fmin (2 req\u002Fs sustained, 120-burst). Override via `OPENCLAW_RATE_LIMIT_RPM` (0 = disabled). Runs BEFORE auth so unauthenticated floods can't exhaust the orchestrator even while auth middleware rejects them. Rejections surface as HTTP 429 with a `Retry-After` header and increment `rate_limit_rejections_total{kind=\"token\"|\"ip\"}` on `\u002Fmetrics`.\n\n## Deploy\n\nThree one-command deploy scripts, each using the same `DockerContainerRuntime` and the same multi-arch GHCR images.\n\n### Hetzner Cloud (from $4\u002Fmonth)\n\n```bash\nexport HCLOUD_TOKEN=\u003Cyour-token>          # console.hetzner.cloud -> Security -> API Tokens\nexport MOONSHOT_API_KEY=sk-...            # or any provider key\n.\u002Fscripts\u002Fdeploy-hetzner.sh\n```\n\nMeasured on CAX11 (2 vCPU \u002F 4 GB ARM, $4\u002Fmonth): 78 s cold start, 4 s pool reuse, 5-7 concurrent sessions.\n[Full guide](.\u002Fdocs\u002Fdeploying-on-hetzner.md)\n\n### AWS Lightsail (from $12\u002Fmonth)\n\n```bash\nexport AWS_ACCESS_KEY_ID=...\nexport AWS_SECRET_ACCESS_KEY=...\nexport MOONSHOT_API_KEY=sk-...\n.\u002Fscripts\u002Fdeploy-aws-lightsail.sh\n```\n\nMeasured on medium_3_0 (2 vCPU \u002F 4 GB, $24\u002Fmonth): 294 s cold start, 5 s pool reuse, 5-7 concurrent sessions.\n[Full guide](.\u002Fdocs\u002Fdeploying-on-aws-lightsail.md)\n\n### Google Cloud Compute Engine (from $0\u002Fmonth on free tier, $25\u002Fmonth default)\n\n```bash\ngcloud auth login                          # once per machine\ngcloud config set project \u003Cyour-project>   # once per machine\nexport MOONSHOT_API_KEY=sk-...\n.\u002Fscripts\u002Fdeploy-gcp-compute.sh\n```\n\n`e2-medium` (1 vCPU burstable \u002F 4 GB \u002F 20 GB PD, ~$25\u002Fmonth) is the default and matches the Hetzner\u002FLightsail capacity floor. Override `GCE_MACHINE_TYPE=e2-micro` for the Always Free tier ($0\u002Fmo in us-east1\u002Fus-central1\u002Fus-west1, 1 GB RAM — good for smoke testing). GCE's NVMe-backed disk puts first-turn cold spawn in Hetzner territory (~80 s), not Lightsail territory (~5 min).\n[Full guide](.\u002Fdocs\u002Fdeploying-on-gcp-compute.md)\n\n### Routine redeploy on an existing VM\n\n```bash\nssh \u003Cuser>@\u003Chost> 'cd \u002Fopt\u002Fopenclaw && git pull && docker compose pull && docker pull ghcr.io\u002Fstainlu\u002Fopenclaw-managed-agents-egress-proxy:latest && docker compose up -d'\n```\n\nUse `root` on the Hetzner path. On Lightsail and GCE, `\u002Fopt\u002Fopenclaw` is root-owned, so wrap the inner command with `sudo bash -lc '...'`.\n\n### Cost by deployment target (infrastructure only, no token costs)\n\n| | 1 session 24\u002F7 | 10 sessions 24\u002F7 | 100 sessions 24\u002F7 |\n|---|---|---|---|\n| **Claude Managed Agents** (for reference) | $57.60\u002Fmo | $576\u002Fmo | $5,760\u002Fmo |\n| **Hetzner CAX11** | $4\u002Fmo | $8\u002Fmo (2 hosts) | $73\u002Fmo (17 hosts) |\n| **AWS Lightsail medium_3_0** | $24\u002Fmo | $48\u002Fmo (2 hosts) | $408\u002Fmo (17 hosts) |\n| **GCE e2-medium** | $25\u002Fmo | $50\u002Fmo (2 hosts) | $425\u002Fmo (17 hosts) |\n| **GCE e2-micro** (free tier) | $0\u002Fmo* | $0\u002Fmo (1 host, 1 instance free-tier limit) | n\u002Fa |\n\n*Free tier: 1 `e2-micro` in us-east1 \u002F us-central1 \u002F us-west1; 30 GB PD; 1 GB egress\u002Fmonth. Beyond the free tier, `e2-micro` is ~$7\u002Fmo. Token costs are separate and depend on the provider + model you choose.\n\n## Architecture\n\n```\nDeveloper's app\n     |\n     | HTTP REST + SSE + WebSocket\n     v\nOrchestrator (Hono, TypeScript)\n  |-- AgentStore + EnvironmentStore + SessionStore (SQLite, WAL)\n  |-- QueueStore (durable per-session event queue)\n  |-- SecretStore (HMAC secret for subagent tokens)\n  |-- AuditStore (structured audit log with retention)\n  |-- SessionContainerPool (per-session active + template-level warm pool + adopt on restart)\n  |-- GatewayWebSocketClient (cancel, model override, tool confirmation, observer-resume)\n  |-- PiJsonlEventReader (event log, cost, SSE, size sampler)\n  |-- ParentTokenMinter (HMAC-SHA256 subagent auth, persisted)\n  |\n  v\nOpenClaw containers (one per session, isolated)\n  - Full agent loop (tool use, multi-turn, thinking)\n  - Pi SessionManager (append-only JSONL)\n  - Session resume from JSONL across container restarts\n  - confirm-tools plugin (always_ask policy enforcement)\n  - call-agent CLI (delegated subagent spawning)\n  - egress-proxy sidecar (networking: limited enforcement)\n  - openclaw's built-in OTEL exporter (when configured)\n```\n\nThe orchestrator keeps only ephemeral caches in memory; all commitments live in SQLite and Pi's JSONL. Restart reattaches running containers, drains queued events, and subscribes to WS broadcasts to finalize in-flight turns. Pre-built multi-arch images (amd64 + arm64) are published to GHCR on every push to `main`.\n\n## Test status\n\n**229 tests pass**, covering unit + restart-safety + contract + integration shapes:\n\n- Session-centric resume (multi-turn memory across turns, across container restart, across orchestrator restart)\n- Cost accounting from provider billing data\n- SQLite persistence across orchestrator restart (migrations, additive columns, audit retention)\n- Durable event queue (FIFO, per-session isolation, survives close + reopen)\n- HMAC secret persistence (outstanding subagent tokens survive deploys)\n- Container pool adoption (reattach running + stop orphan + selectively fail unrecoverable)\n- Observer-side run completion (WS `chat` event → finalize from JSONL, idempotent)\n- Container pool reuse (4 s warm vs 78 s cold)\n- Real SSE token streaming via OpenAI-compat endpoint\n- SSE event stream with `Last-Event-ID` resume cursor\n- Cancel via WebSocket control plane\n- Event queue with ordered drain\n- OpenAI SDK compatibility (shape, multi-turn memory, streaming, queue race)\n- Delegated subagents (inspectable child sessions)\n- Subagent allowlist rejection + depth-cap rejection\n- Agent versioning (create, update, no-op detection, conflict rejection, archive)\n- Environment abstraction (CRUD, session binding, deletion rejection, backward compat)\n- Networking: `limited` enforcement (9-case E2E: allowed proxy, denied proxy, raw socket blocked, AWS IMDS blocked both layers, DNS NXDOMAIN, DNS resolve, sidecar logs)\n- Per-session quotas (cost \u002F tokens \u002F duration refused pre-turn)\n- Audit log (record, list with filters, retention)\n- Permission policy (deny + always_ask approval flow)\n- ContainerRuntime contract (any backend passing the shared suite is drop-in)\n\n## Relationship to OpenClaw\n\nThis project uses [OpenClaw](https:\u002F\u002Fgithub.com\u002Fopenclaw\u002Fopenclaw) as an npm dependency, not a fork. All agent execution, tool invocation, session management, provider integration, and 53 built-in skills come from OpenClaw core. This repo adds the managed layer: the orchestrator, the container lifecycle, the 4-primitive REST API, the deploy scripts, the restart-safety + audit + quota + observability primitives.\n\nThink of OpenClaw as the runtime framework (the personal AI assistant) and OpenClaw Managed Agents as the cloud service around it (the programmatic agent platform your app calls). OpenClaw-the-framework stays personal and single-user; we bring the multi-session, API-first, restart-safe service layer.\n\n## License\n\nMIT\n","OpenClaw Managed Agents 是一个开源的AI代理管理平台，提供了与Claude Managed Agents和ChatGPT Workspace Agents类似的自主AI代理运行能力。该项目基于流行的开源AI代理框架OpenClaw构建，支持任何模型、云服务，并且是完全开源的。其核心功能包括通过API调用自定义AI代理（包含模型、系统提示、工具等），并利用Docker容器化技术确保每个会话独立运行及持久化存储会话状态。此外，它还提供了TypeScript和Python SDK以及一系列预构建镜像以简化部署流程。适用于需要在自定义环境中灵活部署AI助手或自动化任务处理的各种应用场景，如企业级聊天机器人开发、个人助理服务等。","2026-06-11 02:41:18","CREATED_QUERY"]