Metadata-Version: 2.4
Name: loopyard
Version: 0.1.0
Summary: PostgreSQL-backed task backend for coordinating multi-agent autonomous software development
Author: Andrew Grigorev
Author-email: Andrew Grigorev <andrew@ei-grad.ru>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Bug Tracking
Requires-Dist: click>=8.4.2
Requires-Dist: fastapi>=0.139.0
Requires-Dist: mcp[cli]>=1.28.1
Requires-Dist: psycopg-pool>=3.3.1
Requires-Dist: psycopg[binary]>=3.3.4
Requires-Dist: pyjwt[crypto]>=2.13.0
Requires-Dist: pyyaml>=6.0.3
Requires-Dist: uvicorn>=0.51.0
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/ei-grad/loopyard
Project-URL: Repository, https://github.com/ei-grad/loopyard
Description-Content-Type: text/markdown

# loopyard

loopyard is a PostgreSQL-backed task backend for coordinating multi-agent
autonomous software development. All business logic — runnable-set
computation, dependency and status-transition validation, conflict-domain
checks, lock leases with generation fencing — lives in PL/pgSQL functions,
constraints, and triggers, so state stays consistent no matter which client
touches it. The CLI, MCP server, and REST API are thin transports over the
same `api.*` SQL surface; the CLI also speaks vibe-loop's task-source and
lock command contracts directly.

## Relationship to capOS

loopyard is the host-side incarnation of a coordination model that capOS
(`../capos`) plans to self-host in-system in the long term: durable task
records, dependency-derived runnable sets, conflict-domain checks, and
fenced lock leases as typed capability services instead of an external SQL
server. The capOS design records are
`docs/proposals/stateful-task-job-graphs-proposal.md` (the durable
work-graph substrate, Stages E/G) and
`docs/proposals/hosted-agent-swarm-proposal.md` (`AgentTask`,
`ResourceLease`, `ConflictReport`, merge/release queues); both cite loopyard
as the operational precedent. Until that lands, loopyard is the production
task backend for capOS development, and its schema and invariants are the
reference semantics the capOS services are expected to reproduce.

## Quickstart

```sh
cp .env.example .env               # adjust credentials if you like
docker compose up -d               # PostgreSQL 18 on 127.0.0.1:5439
export LOOPYARD_DSN=postgresql://loopyard:loopyard@127.0.0.1:5439/loopyard
uv run loopyard db migrate         # apply migrations + function definitions
```

`loopyard db reset --yes` drops and recreates the `public` schema and
re-migrates (development only — it destroys all data).

## Authentication

The REST API has two modes (`auth_mode` in `.loopyard.toml`, or
`LOOPYARD_AUTH_MODE`):

- `none` (default) — no authentication; intended for localhost
  deployments. Mutating endpoints accept an optional
  `X-Loopyard-Actor: <actor-id>` header for event attribution.
- `oidc` — every `/api/*` request must carry a JWT minted by an OIDC-style
  identity provider or authenticating proxy in front of loopyard. The
  token is validated offline against the provider's JWKS (RS256/ES256
  only; issuer, audience and expiry checked), the identity is mapped to an
  auto-provisioned human `actor`, all mutations are attributed to that
  actor, and `X-Loopyard-Actor` is ignored. `GET /api/whoami` reports the
  resolved identity; identities listed in `admin_identities` may call
  admin endpoints (currently lock force-release — 403 for everyone else).
  The interactive docs and `/openapi.json` are disabled in this mode, and
  the SSE stream validates the token at connect time only (an already-open
  stream is not torn down when the token expires; reconnects re-validate).

Identities are **namespaced with the claim they came from** —
`email:alice@example.com`, `sub:7d5a…` — so values from different claims
can never collide; `admin_identities` entries use the same form. The
`email` claim is trusted only when the token carries
`email_verified: true`; an explicit `email_verified: false` is never
accepted, and a missing `email_verified` counts as unverified unless
`oidc_allow_unverified_email` is enabled (needed for authenticating
proxies that assert the email themselves — see the Cloudflare preset). An
unusable email falls back to `sub`.

Config keys (each also settable via the matching `LOOPYARD_`-prefixed
upper-case env var; list-valued keys take a comma-separated string and
booleans a `true`/`false` string in the env form). In oidc mode
`oidc_issuer` and `oidc_jwks_url` must be `https` URLs — plain `http` is
allowed for localhost only (dev):

| key                           | default      | meaning                                       |
| ----------------------------- | ------------ | --------------------------------------------- |
| `auth_mode`                   | `"none"`     | `none` or `oidc`                              |
| `oidc_issuer`                 | —            | required in oidc mode; exact `iss` match      |
| `oidc_audience`               | —            | required in oidc mode; `aud` check            |
| `oidc_jwks_url`               | discovered   | JWKS endpoint; default from `<issuer>/.well-known/openid-configuration` |
| `oidc_token_sources`          | `["bearer"]` | ordered lookup: `bearer`, `header:<Name>`, `cookie:<name>` |
| `oidc_identity_claim`         | `"email"`    | identity claim; `sub` is always the fallback  |
| `oidc_allow_unverified_email` | `false`      | accept `email` when `email_verified` is ABSENT (explicit `false` never passes) |
| `oidc_http_timeout_ms`        | `5000`       | JWKS/discovery HTTP fetch timeout             |
| `admin_identities`            | `[]`         | namespaced identities allowed on admin endpoints, e.g. `"email:you@example.com"` |

MCP and CLI transports connect to PostgreSQL directly and are unaffected.

### Cloudflare Zero Trust Access preset

Access authenticates the user and forwards a JWT in a header and a cookie;
loopyard just validates it:

```toml
auth_mode = "oidc"
oidc_issuer = "https://<team>.cloudflareaccess.com"
oidc_audience = "<Access application AUD tag>"
oidc_jwks_url = "https://<team>.cloudflareaccess.com/cdn-cgi/access/certs"
oidc_token_sources = ["header:Cf-Access-Jwt-Assertion", "cookie:CF_Authorization"]
oidc_allow_unverified_email = true
admin_identities = ["email:you@example.com"]
```

`oidc_allow_unverified_email` is needed because Access JWTs carry the
`email` Cloudflare authenticated but no `email_verified` claim — Access
asserts the email itself. Without the opt-in every Access user would be
identified by the opaque `sub:<uuid>` instead of their email.

The cookie source is what lets the browser's `EventSource` authenticate
against the SSE live-events endpoint — `EventSource` cannot set request
headers, but Access sets the `CF_Authorization` cookie on the domain.

oauth2-proxy / Keycloak / Authentik and other standard OIDC setups work
with plain bearer tokens: keep the default `oidc_token_sources =
["bearer"]` and set `oidc_issuer` + `oidc_audience` (plus `oidc_jwks_url`
only when issuer discovery is unavailable).

## SQL conventions

SQL ships as package data: `src/loopyard/db/migrations/*.sql` is applied
exactly once per file, `src/loopyard/db/functions/*.sql` holds idempotent
`CREATE OR REPLACE` definitions that are all reapplied on every migrate.
Both are processed in lexicographic filename order — that order is the
contract, so zero-pad migration prefixes (`0001_...`) and encode
inter-function dependencies via filename prefixes. Every file runs in its
own transaction; statements that refuse to run in a transaction block
(e.g. `CREATE INDEX CONCURRENTLY`) are unsupported for now.

## Tests

```sh
uv run -m pytest
```

Tests start a throwaway `postgres:18` [testcontainer](https://testcontainers-python.readthedocs.io/)
and need a working Docker daemon. To use an existing PostgreSQL server
instead (e.g. in CI), set `LOOPYARD_TEST_DSN` to a server the suite may
create and drop databases on:

```sh
LOOPYARD_TEST_DSN=postgresql://loopyard:loopyard@127.0.0.1:5439/loopyard uv run -m pytest
```

The harness migrates a template database once per session and clones a
fresh database per test, so each test gets an isolated schema and may open
multiple concurrent connections.

## Running vibe-loop against this board

This repo self-hosts: its own development dispatch runs through `vibe-loop`
against loopyard's own PostgreSQL-backed board (see `.vibe-loop.toml`).
Project selection is via an environment variable, never a committed file or
baked CLI flag:

```sh
export LOOPYARD_PROJECT=loopyard
vibe-loop run-until-done
```

(`vibe-loop autopilot run` additionally needs an `[autopilot]` block in
`.vibe-loop.toml`, which this wiring does not yet configure.)

Task worktrees for this project live under
`/home/ei-grad/loopyard-worktrees/`. Workers move task status through the
loopyard CLI as they progress — `loopyard task transition <id>
active|review|done` — since vibe-loop only reads the task source and never
writes status itself.
