🔁 OpenLoop
The open-source control plane & runtime for team AI agents
Persistent, asynchronous teammates that work across your channels — Slack, Discord, Zoom, GitHub, Linear — each with their own memory, tools, model policy, budget, approval rules, and audit trail.
Why · vs. Claude Tag · Features · How it works · Quickstart · Capabilities · Contributing
Status: early / WIP. Not production-ready. APIs, config formats, and the commands below will change. Commands marked (preliminary) are placeholders.
Why
Most AI assistants are personal and single-player. Real work happens across shared channels, threads, calls, repos, and issues — and it spans more than one tool. The useful primitive isn't "a Slack bot" — it's a persistent team teammate that remembers your team's decisions, uses a scoped set of tools, routes tasks to the right model, stays in budget, asks before doing anything risky, and leaves an audit trail.
OpenLoop gives your team such an agent, reachable across the surfaces you already use, with an open-source runtime you can self-host and inspect. Slack/Discord/Zoom/GitHub/Linear are surfaces — the product is the agent runtime and control plane behind them. Memory, tools, and policy can be scoped per channel so context doesn't leak across teams.
For: engineering, data, and platform teams; AI-heavy startups; OSS maintainers; anyone who wants Claude Tag–style workflows without single-vendor lock-in.
How this differs from Claude Tag
Comparison based on Anthropic's Claude Tag announcement (June 2026). Claude Tag is evolving quickly — check the source for current details.
OpenLoop shares Claude Tag's core model — persistent, asynchronous, multiplayer team agents with tools and memory — and extends it in a model-agnostic, configurable, multi-surface, open-source direction. The first rows below are common ground; the rest are where OpenLoop goes further.
| Dimension | Claude Tag | OpenLoop |
|---|---|---|
| Persistent | Long-lived agent that stays active and retains team context | Long-lived agent that stays active and retains team context |
| Asynchronous | Event- and mention-driven; works in the background, replies when ready | Event- and mention-driven; works in the background, replies when ready |
| Multiplayer | Shared in team channels, not a personal assistant | Shared across team surfaces, not a personal assistant |
| Team context | Channel-level Claude identities with scoped memories and shared conversations | Configurable memory scope across channel, agent, or workspace |
| Tools | Admin-approved tool access inside Claude's managed product | Explicit MCP/native tool allowlists, approval gates, and auditable actions |
| Models | Claude-native / Anthropic-first | Model-agnostic routing across Anthropic, OpenAI, Gemini, Ollama, OpenRouter, or any LiteLLM-compatible provider |
| Configuration | Admin-managed product settings | Config-as-code agents for surfaces, memory, model policy, tools, approvals, and budgets |
| Surfaces | Slack first, with other workplace integrations planned | Designed as a surface-agnostic runtime for Slack, Discord, GitHub, Linear, Zoom, and more |
| Deployment | Managed Anthropic experience | Self-hostable, inspectable, open-source infrastructure |
| Best for | Teams that want a managed Claude-centered teammate | Teams that want multiplayer agents with provider choice, deeper policy control, and open infrastructure |
What it does (MVP)
- Summarize Slack/Discord threads and remember team decisions
- Create GitHub issues from discussions
- Investigate failing CI/builds
- Draft PRs via a coding worker, gated on human approval
- Track token/cost usage per channel, agent, task, and model
- Route tasks across providers (OpenAI, Anthropic, Gemini, Ollama, OpenRouter, any LiteLLM-compatible) — not only Claude
- Require human approval before risky write actions
Concepts
| Concept | Meaning |
|---|---|
| Workspace | Top-level tenant; owns agents, integrations, policy defaults. |
| Agent | A team agent reachable across surfaces — the unit of identity, memory, and policy. |
| Surface | Where you reach the agent: Slack, Discord, Zoom, GitHub, Linear. |
| Memory Scope | What an agent recalls; can be scoped per channel so context doesn't leak across teams. |
| Tool Policy | Explicit allowlist of tools (MCP / native) and their permissions. |
| Model Policy | Which model handles which kind of task. |
| Human Approval | Gates requiring a human before write/risky actions. |
| Budget | Per-agent (optionally per-channel) spend and token limits, tracked and enforced. |
How it works
flowchart TB
subgraph SURFACES["🗣️ Surfaces <sub>(shared team channels)</sub>"]
direction LR
S1[Slack] ~~~ S2[Discord] ~~~ S3[Zoom] ~~~ S4[GitHub] ~~~ S5[Linear]
end
subgraph SELF["🏠 Self-hosted — your infra, your keys, inspectable"]
RT["⚙️ Agent Runtime<br/><sub>shared team agent · loads policy · budget</sub>"]
MG["🧠 Model Gateway<br/><sub>LiteLLM · model-agnostic</sub>"]
TG["🔧 Tool Gateway<br/><sub>MCP + native · allowlist</sub>"]
DB[("🗄️ Postgres + pgvector<br/><sub>channel-scoped memory · budget · audit</sub>")]
HA{{"✋ Human approval"}}
end
subgraph MODELS["☁️ Model providers <sub>(external)</sub>"]
direction TB
M1[OpenAI] ~~~ M2[Anthropic] ~~~ M3[Gemini] ~~~ M4[Ollama] ~~~ M5[OpenRouter]
end
subgraph TOOLS["🛠️ Tool targets <sub>(external)</sub>"]
direction LR
T1[GitHub] ~~~ T2[CI] ~~~ T3[Coding worker]
end
SURFACES -- "mention / event" --> RT
RT -- "model policy" --> MG
RT -- "tool policy" --> TG
RT <-. "read / write" .-> DB
MG -- "your provider keys" --> MODELS
TG -- "reads" --> TOOLS
TG -- "write actions" --> HA
HA -- "approved" --> TOOLS
classDef runtime fill:#7c3aed,stroke:#5b21b6,color:#fff,font-weight:bold
classDef gateway fill:#ede9fe,stroke:#7c3aed,color:#4c1d95
classDef store fill:#0ea5e9,stroke:#0369a1,color:#fff
classDef gate fill:#f59e0b,stroke:#b45309,color:#1f2937,font-weight:bold
class RT runtime
class MG,TG gateway
class DB store
class HA gate
style SELF fill:#faf5ff,stroke:#7c3aed,stroke-width:2px,stroke-dasharray:7 4
A mention or event on any surface triggers the team's agent. The runtime loads its memory, tool policy, model policy, and budget; routes the task to a model; uses only allowed tools; pauses for approval on write actions; then records usage, cost, and outcome to the audit trail and memory.
Example: Slack
#dev-platform
@maciag.artur @openloop summarize this thread and capture the decision
@openloop 📝 Summary (last 24 messages):
• Moving ingestion off the legacy queue.
• Redis Streams chosen over Kafka for v1 (lower ops overhead).
• Owner: @maciag.artur, target: end of next sprint.
🧠 Saved to channel memory: "Use Redis Streams for ingestion v1. Owner: @maciag.artur."
Open a GitHub issue to track it?
@maciag.artur yes
@openloop ⏳ Write action (create issue in openloop-team/openloop) — approval required.
@maciag.artur, approve? 💸 est. $0.02 · claude-sonnet-4-6
Example: agent config
Agents are config-as-code (apiVersion v1alpha1, preliminary). Full example:
agents/dev-platform.yaml.
apiVersion: openloop.team/v1alpha1
kind: Agent
metadata: { name: dev-platform, workspace: acme }
spec:
surfaces:
- { type: slack, channel: "#dev-platform" }
- { type: discord, channel: "eng" }
memory: { scope: channel, backend: postgres, retention_days: 90 }
model_policy:
default: anthropic/claude-sonnet-4-6
routes:
- { match: { task: summarize }, model: openai/gpt-4o-mini }
tools:
- { name: github, type: native, permissions: ["issues:write", "pulls:read"] }
approvals:
require_for: ["github.issues:write", "github.pulls:write"]
approvers: ["@maciag.artur", "@maintainers"]
budget: { monthly_usd: 50, per_task_usd: 0.50, on_exceeded: block }
limits: { max_concurrent_tasks: 4, tasks_per_minute: 30 }
Architecture
| Layer | Now | Later |
|---|---|---|
| API / backend | FastAPI | — |
| Agent runtime | durable workflow engine (worker + chat pipeline as workflows) | model-call replay/caching semantics; retry policies |
| Model gateway | LiteLLM | routing analytics |
| Tools | MCP gateway + native GitHub/Slack | more native connectors |
| Storage | Postgres + pgvector | — |
| Queue | Redis | — |
| Surfaces | Slack | Discord / Zoom / GitHub / Linear |
| Delivery | persisted Slack sessions, background progress/final postbacks, approval + thread-reply continuation, startup reconciler | provider idempotency keys, cross-process delivery locks, more surface adapters |
| Coding worker | draft PRs (credential-free worker edits a prepared workspace; the orchestrating boundary owns clone/commit/push) | OpenHands-style |
| Dashboard | — | Next.js |
| Observability | — | OpenTelemetry / Langfuse traces |
The open-source runtime covers: agent runtime, Slack integration with async delivery, model adapters (LiteLLM), MCP tool gateway, local Postgres channel/thread memory, approval flow, basic token/cost tracking, Docker Compose deploy, and config-as-code agents.
Quickstart (preliminary — commands are placeholders)
Requires Docker + Compose and at least one model provider key.
git clone https://github.com/p1c2u/openloop.git
cd openloop
cp .env.example .env # non-secret Compose values
# Configure the openloop-deploy, openloop-runtime, and openloop-broker
# Doppler projects, then inject their secrets into Compose:
mise run secrets-invoke -- mise run compose-up-d
openloop agents apply -f agents/dev-platform.yaml
# invite the bot to a channel, then: @agent summarize this thread
Nothing leaves your machine except calls to the providers/tools you configure.
External OpenHands broker
The external-broker composition keeps Docker authority out of the application
runtime. A digest-pinned, networkless HAProxy container is the only service
that mounts the raw Docker socket. It forwards the unfiltered Docker API to a
private named-volume UDS owned by
root:${OPENLOOP_DATA_GID:-10777} with mode 0660; the non-root broker keeps
that data group and mounts the volume read-only. The adapter is a permission
shim, not an API policy boundary—access to its forwarded socket remains
root-equivalent Docker access.
cp .env.example .env
# Set an absolute OPENLOOP_BROKER_ROOT in .env.
# Edit tracked non-secret settings under configs/prd/.
mise run compose-build
mise run secrets-invoke -- mise run compose-up
Compose loads configs/prd/runtime.env and configs/prd/broker.env through
service-level env_file. These tracked files contain only ordinary settings
and public verification keys. Doppler injects true secrets into the Compose
process, and Compose creates the declared /run/secrets/* files without
writing a plaintext secret inventory into the repository.
DOCKER_SOCKET may select a non-default upstream Unix socket. Do not configure
DOCKER_GID or a TCP Docker endpoint. The adapter healthcheck uses a separate
root-only UDS and blocks broker startup unless HAProxy's checked /_ping
backend is available. Under its read-only root filesystem, only the forwarded
socket volume, private health tmpfs, and mounted upstream socket are writable.
Mounted secrets
Each top-level Compose secret names an environment value supplied by the
appropriate Doppler project. Compose materializes it as
/run/secrets/<lowercase-settings-field> and grants it only to the services
that declare it. OpenLoop type-validates file contents like environment values,
including JSON root maps. The GitHub App PEM is available at
/run/secrets/github_app_private_key.
The grants are:
- Postgres receives only
postgres_password. - Runtime receives the database password, application credentials, and app-side broker secrets.
- Broker receives the database password and broker-owned root maps.
See
ops/compose-secrets.md for the complete inventory.
Local development (preliminary)
The runtime is a Python/FastAPI app. Local dev uses mise to pin the toolchain and manage a project virtualenv — no global installs.
mise install # Python 3.12 + uv + an auto-created .venv
mise run install # install the runtime + dev deps (uv) into .venv
mise run test # run the test suite
mise run dev # FastAPI runtime with autoreload on :8000
mise run apply -- -f agents/dev-platform.yaml # validate an agent config
Try it against real Slack (Socket Mode)
Socket Mode opens an outbound WebSocket, so you can test a real mention → reply → approval round-trip locally — no public URL or tunnel. Set up a Slack app once, then run the socket:
- Enable Socket Mode and generate an app-level token with the
connections:writescope → this is yourSLACK_APP_TOKEN(xapp-…). - Add bot OAuth scopes (OAuth & Permissions → Bot Token Scopes):
chat:write,app_mentions:read,channels:history, andfiles:write(long answers, diffs, and logs use hosted snippets). For private channels and DMs, also addgroups:history,im:history,mpim:history. - Subscribe to bot events (Event Subscriptions → Subscribe to bot events) —
Slack only delivers events you subscribe to:
app_mention— direct@botmentions.message.channels— lets the bot follow up on replies in a thread it already owns without being re-mentioned.message.groups/message.im/message.mpim— the same for private channels and DMs.
- Install the app to your workspace, copy the bot token →
SLACK_BOT_TOKEN(xoxb-…), and invite the bot to a channel. - Set the runtime secrets: add a model key,
SLACK_BOT_TOKEN, andSLACK_APP_TOKENto theopenloop-runtimeDoppler project. Then run:
doppler run --project openloop-runtime --config dev_personal -- \
mise exec -- openloop slack socket # connects; mention the bot in a channel
Mention the bot to start a thread; you can
keep replying in that thread without re-tagging it. Read-only observability while
it runs: GET /usage (spend vs. budget) and GET /audit (recent token/cost
records).
End-to-end tests
The E2E suite is layered so each slower or less deterministic dependency is isolated.
A. Unit and integration tests: isolated logic tests plus in-process runtime, memory, usage, tool gateway, approval, Slack, MCP, and HTTP API tests. External providers, GitHub, and Postgres are faked, so this runs without credentials, Docker, or network:
mise run test
mise run test-unit # optional: only tests/unit
mise run test-integration # optional: only tests/integration
B. Postgres E2E: real Postgres/pgvector stores, fake model and fake GitHub. This validates SQL, asyncpg type handling, vector recall, approval persistence, and usage persistence:
mise run test-e2e
C. Runtime/GitHub live E2E: real model, approval gate, and real GitHub issue creation. It creates and then closes one issue, so it is explicitly opt-in:
export E2E_CONFIRM=1
export E2E_MODEL=groq/llama-3.3-70b-versatile
export GROQ_API_KEY=gsk_… # or set another LiteLLM provider key
export GITHUB_TOKEN=github_pat_… # issues:write on the repo
export E2E_GITHUB_REPO=you/sandbox
export DATABASE_URL=postgresql://openloop:change-me@localhost:5432/openloop # optional
mise run test-e2e-runtime-github-live
You can also invoke the live pytest target directly:
E2E_LIVE=1 GITHUB_TOKEN=… E2E_GITHUB_REPO=you/sandbox \
E2E_MODEL=groq/llama-3.3-70b-versatile GROQ_API_KEY=gsk_… \
mise exec -- python -m pytest tests/e2e/test_runtime_github_live.py -v
GitHub Actions runs unit and integration tests on pull requests and pushes to main.
The runtime/GitHub live E2E workflow is separate, runs nightly or manually, and
uses a protected live-e2e environment. Configure repository secrets
GROQ_API_KEY and LIVE_E2E_GITHUB_TOKEN; the latter should be a fine-grained
token with issue write access to openloop/openloop-e2e-sandbox.
D. Interactive Slack smoke (a human in the loop).
With SLACK_BOT_TOKEN + SLACK_APP_TOKEN set, run openloop slack socket,
mention the bot in a channel, and click Approve on a held write action.
E. Automated Slack live E2E: real Socket Mode round-trip with a stubbed
model, so it proves the Slack wire (auth, event delivery, in-thread reply)
without LLM nondeterminism. The triggering mention is posted with a user
token, not the bot token — Slack suppresses app_mention for messages an app
posts as itself, so a bot mentioning itself never fires the event. Add the
user-token scope chat:write (OAuth & Permissions → User Token Scopes) and
reinstall to get an xoxp-… token:
cp .env.e2e.example .env.e2e
# Set E2E_SLACK_USER_TOKEN and E2E_SLACK_CHANNEL in .env.e2e.
export E2E_CONFIRM=1
mise run secrets-invoke -- mise run test-e2e-slack-live
The Slack live E2E GitHub Actions workflow is manual-only and uses a protected
slack-live-e2e environment. Configure environment secrets SLACK_BOT_TOKEN,
SLACK_APP_TOKEN, and E2E_SLACK_USER_TOKEN; set E2E_SLACK_CHANNEL as an
environment/repository variable, or pass a channel ID when manually dispatching
the workflow.
Current capabilities
- Core async runtime + task pipeline
- Slack surface (mentions, thread replies, approvals)
- LiteLLM gateway + model-policy routing
- MCP tool gateway + native GitHub connector
- Channel/thread memory (Postgres + pgvector)
- Human approval flow + token/cost tracking
- Docker Compose + config-as-code
- Coding worker (draft PRs) — connector + approval gate + crash-resumable; hardened (Phase 2): the worker is credential-free (edits a prepared workspace), all credential-bearing git ops live in one orchestrating boundary shared by both durable paths, and git auth rides a per-command header (never a token-in-URL clone, nothing in the workspace)
- Worker sandbox (Phase 3) — model-generated edits can run in a throwaway
docker container: default-deny egress (network none), no env forwarded (LLM
key stays in the controller), capabilities dropped, auto-reaped; fail-closed
wiring (an unusable sandbox disables the worker, never silently runs on the
host).
CODING_WORKER_SANDBOX=docker - OpenHands worker backend + spend ledger (Phase 4) — pluggable worker
backends behind
CODING_WORKER_BACKEND=builtin|openhands: the default stays the light diff worker;openhandsdrives an agentic OpenHands run over the same prepared, credential-free workspace (openhandsdependency group; withCODING_WORKER_SANDBOX=dockerthe agent server runs containerized with the workspace bind-mounted — the container never holds a git credential, the host pushes). Every attempt's model spend is recorded to the usage store and capped fail-closed by the owning agent'sper_task_usdbefore anything is pushed — on both durable paths — and the agentic backend refuses to register without that cap - OpenHands cold resume — the Docker
agent-server is digest-pinned, loopback-only, and authenticated; conversation
state lives in a private per-job mount outside the checkout; independent
per-job keys protect conversation state and AES-GCM workspace artifacts.
Confirmation boundaries park without staging or pushing, settle cumulative
spend once per segment, discard the container, and expose explicit Slack
Accept/Reject actions. Resume reconstructs a fresh checkout from the recorded
base SHA and authenticated delta, attaches without resending the prompt, and
retains a final artifact until the draft PR is durable. Enabled by default for
Docker OpenHands; set
CODING_WORKER_OPENHANDS_COLD_RESUME_ENABLED=falseas an operational rollback. - Claude Code worker backend (
CODING_WORKER_BACKEND=claude) — experimental, personal use only: drives theclaudeCLI in headless mode (claude -p) over the same prepared, credential-free workspace, authenticating with whateverclaudeis logged into — including a Pro/Max subscription. Because the subscription dollar signal is unreliable (total_cost_usdis an API-equivalent estimate, often0), the load-bearing fail-closed bound is resource-based —--max-turnsand a hard wall-clock deadline (the subprocess is killed on expiry) — while the spend ledger still records the estimate and enforcesper_task_usdwhen it is non-zero. Host sandbox only (docker isolation for this backend is not implemented; requesting it fails closed). ⚠️ Anthropic designs the subscription for individual use through its first-party apps, not for powering a shared team runtime — pooling one subscription across a team surface is outside that intent and may violate the Consumer Terms. Availability rides on a private CLI/auth contract that can change betweenclaudereleases. Keep it off by default; for anything shared or production use metered API keys (builtin/openhands) or a local model. The backend seam makes reverting one env var - Budget/usage unification + throughput limits (Phase 5) — worker spend is
attributed to the invoking agent (threaded through the approval args, so
multi-agent configs charge and cap the right budget) and gated by that
agent's monthly budget before an attempt does any work (
block | warnpreserved); per-agent rate/concurrency limits (spec.limits: max_concurrent_tasks, tasks_per_minute) refuse excess tasks at the runtime entry with a tenant-shaped scope key, recorded in the audit trail - Durable workflows — engine + approval-as-wait-node; worker resumes on crash; chat pipeline runs as a workflow (bounded: persisted turn state + idempotent writes; model calls are not replayed on crash)
- Slack async delivery — persisted surface sessions, thinking status + final postbacks, approval/thread-reply continuation, startup reconciler, conversation-history threading (a follow-up turn replays the thread's prior exchanges), idempotency-keyed delivery (a crash between a successful post and recording its id is recovered by key instead of re-posting; best-effort, falls back to at-least-once if the surface lookup can't run), and delivery outside the original request lifecycle
- Cross-process coordination — a distributed lock so that when several
replicas boot together only one leads recovery, re-run on an interval so a
leader that dies mid-sweep is healed by a survivor.
LOCK_BACKEND=auto(default) uses Postgres advisory locks when the deploy already runs Postgres — no extra service — with Redis and process-local backends also available
Scope
This repository is the open-source runtime and control plane — self-hosted, inspectable, and the whole product for now.
It covers the agent runtime, model gateway (LiteLLM), MCP tool gateway + native connectors, channel/thread memory, approval flow, token/cost tracking, Slack async delivery, config-as-code, and Docker Compose deployment.
Security
Agents act on your behalf — treat their credentials like a production service account.
- Least privilege: give each agent the narrowest tool scope it needs; never broad org-wide credentials.
- Approve writes: require human approval for creating issues/PRs, posting externally, or deleting anything.
- Scoped tokens: use fine-grained, per-integration tokens.
- Memory isolation: scope memory per channel so context doesn't leak.
- Secrets: keep real values in the three scoped Doppler projects. Compose
creates the declared
/run/secrets/*files from injected values; trackedconfigs/prd/*.envfiles must contain only non-secret settings and public verification material. - Inspect: self-host the runtime to see exactly what the agent does.
Early-stage software, no warranty. Don't connect sensitive production systems until you've reviewed the threat model for your environment.
Contributing
Early project — shaping the foundations is the most valuable work. Open an issue before anything non-trivial. Help wanted on the runtime, model adapters, MCP connectors, Slack surface, memory layer, and docs. See CONTRIBUTING.md.
License
Metadata
Release files for pyopenloop 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyopenloop-0.1.1.tar.gz | 1.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyopenloop-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.5 MB
Release files / pyopenloop-0.1.1.tar.gz
| Download URL | pyopenloop-0.1.1.tar.gz |
|---|---|
| Size | 1.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3f9ecf127c9d2136e649c4426bf7bc7010410630f68cca7d635d7d5c476b5967
|
|
BLAKE2b-256 checksum How to use checksums |
a88f4fdf4fc1a20a96bacee756e0a5161452c9af5f8134ea9e51b2750e651502
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency logRelease files / pyopenloop-0.1.1-py3-none-any.whl
| Download URL | pyopenloop-0.1.1-py3-none-any.whl |
|---|---|
| Size | 412.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
036d92f4b555636f0edcad609224fca26d042a5bdf6634bea10100e3326b41f9
|
|
BLAKE2b-256 checksum How to use checksums |
5d87b2b8bf9c008008d5b4a3b6738c921706e97d5d131b1f5bc856d950512b9f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 1, 2026.
Transparency log