Metadata-Version: 2.5
Name: pyopenloop
Version: 0.1.1
Summary: The open-source control plane and runtime for team AI agents
Author-email: Artur Maciag <maciag.artur@gmail.com>
License: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: asyncpg>=0.29
Requires-Dist: fastapi>=0.110
Requires-Dist: httpx>=0.27
Requires-Dist: litellm>=1.40
Requires-Dist: pydantic-settings>=2.14.0
Requires-Dist: pydantic>=2.6
Requires-Dist: pyyaml>=6.0
Requires-Dist: slack-bolt>=1.18
Requires-Dist: slack-sdk>=3.33
Requires-Dist: sqlalchemy[asyncio]>=2.0.51
Requires-Dist: uvicorn[standard]>=0.29
Provides-Extra: broker
Requires-Dist: cryptography>=42; extra == 'broker'
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'broker'
Provides-Extra: dev
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: mypy==2.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff==0.16.0; extra == 'dev'
Provides-Extra: githubapp
Requires-Dist: pyjwt[crypto]>=2.8; extra == 'githubapp'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

<div align="center">

# 🔁 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.

<br/>

[![CI](https://github.com/p1c2u/openloop/actions/workflows/ci.yml/badge.svg)](https://github.com/p1c2u/openloop/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/pyopenloop?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/pyopenloop/)
[![Python](https://img.shields.io/pypi/pyversions/pyopenloop?logo=python&logoColor=white)](https://pypi.org/project/pyopenloop/)
[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE)
[![Model-agnostic](https://img.shields.io/badge/models-LiteLLM-7c3aed.svg)](https://github.com/BerriAI/litellm)
[![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md)
[![Status: early/WIP](https://img.shields.io/badge/status-early%2FWIP-orange.svg)](#)

<br/>

[**Why**](#why) ·
[**vs. Claude Tag**](#how-this-differs-from-claude-tag) ·
[**Features**](#what-it-does-mvp) ·
[**How it works**](#how-it-works) ·
[**Quickstart**](#quickstart-preliminary--commands-are-placeholders) ·
[**Capabilities**](#current-capabilities) ·
[**Contributing**](#contributing)

</div>

> **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](https://www.anthropic.com/news/introducing-claude-tag)
> (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

```mermaid
flowchart TB
    subgraph SURFACES["🗣️ &nbsp;Surfaces &nbsp;<sub>(shared team channels)</sub>"]
        direction LR
        S1[Slack] ~~~ S2[Discord] ~~~ S3[Zoom] ~~~ S4[GitHub] ~~~ S5[Linear]
    end

    subgraph SELF["🏠 &nbsp;Self-hosted — your infra, your keys, inspectable"]
        RT["⚙️ &nbsp;Agent Runtime<br/><sub>shared team agent · loads policy · budget</sub>"]
        MG["🧠 &nbsp;Model Gateway<br/><sub>LiteLLM · model-agnostic</sub>"]
        TG["🔧 &nbsp;Tool Gateway<br/><sub>MCP + native · allowlist</sub>"]
        DB[("🗄️ &nbsp;Postgres + pgvector<br/><sub>channel-scoped memory · budget · audit</sub>")]
        HA{{"✋ &nbsp;Human approval"}}
    end

    subgraph MODELS["☁️ &nbsp;Model providers <sub>(external)</sub>"]
        direction TB
        M1[OpenAI] ~~~ M2[Anthropic] ~~~ M3[Gemini] ~~~ M4[Ollama] ~~~ M5[OpenRouter]
    end

    subgraph TOOLS["🛠️ &nbsp;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`](agents/dev-platform.yaml).

```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.

```bash
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.

```bash
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`](ops/compose-secrets.md) for the complete inventory.

### Local development *(preliminary)*

The runtime is a Python/FastAPI app. Local dev uses [mise](https://mise.jdx.dev)
to pin the toolchain and manage a project virtualenv — no global installs.

```bash
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:

1. **Enable Socket Mode** and generate an app-level token with the
   `connections:write` scope → this is your `SLACK_APP_TOKEN` (`xapp-…`).
2. **Add bot OAuth scopes** (OAuth & Permissions → Bot Token Scopes):
   `chat:write`, `app_mentions:read`, `channels:history`, and `files:write`
   (long answers, diffs, and logs use hosted snippets). For private
   channels and DMs, also add `groups:history`, `im:history`, `mpim:history`.
3. **Subscribe to bot events** (Event Subscriptions → Subscribe to bot events) —
   Slack only delivers events you subscribe to:
   - `app_mention` — direct `@bot` mentions.
   - `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.
4. **Install the app** to your workspace, copy the bot token → `SLACK_BOT_TOKEN`
   (`xoxb-…`), and invite the bot to a channel.
5. **Set the runtime secrets**: add a model key, `SLACK_BOT_TOKEN`, and
   `SLACK_APP_TOKEN` to the `openloop-runtime` Doppler project. Then run:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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; `openhands` drives an agentic OpenHands run over the same
  prepared, credential-free workspace (`openhands` dependency group; with
  `CODING_WORKER_SANDBOX=docker` the 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's `per_task_usd` before 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=false` as
  an operational rollback.
- Claude Code worker backend (`CODING_WORKER_BACKEND=claude`) —
  **experimental, personal use only**: drives the `claude` CLI in headless mode
  (`claude -p`) over the same prepared, credential-free workspace, authenticating
  with whatever `claude` is logged into — **including a Pro/Max subscription**.
  Because the subscription dollar signal is unreliable (`total_cost_usd` is an
  API-equivalent estimate, often `0`), the load-bearing **fail-closed** bound is
  resource-based — `--max-turns` **and** a hard wall-clock deadline (the
  subprocess is killed on expiry) — while the spend ledger still records the
  estimate and enforces `per_task_usd` when 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 between `claude` releases. 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 | warn`
  preserved); 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; tracked
  `configs/prd/*.env` files 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](CONTRIBUTING.md).

## License

[Apache-2.0](LICENSE).
