Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
env:
# scripts/*.sh (create-project.sh, ratify-*.sh, pg-start/stop.sh, provision*.sh) all default
# PGBIN to a hardcoded Windows path, overridable via this var — set it here rather than
# touch every script, since this override knob already exists for exactly this purpose.
BION_PGBIN: /usr/bin
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 11

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- run: pnpm install

- name: Ensure psql is available at BION_PGBIN
run: command -v psql || sudo apt-get update && sudo apt-get install -y postgresql-client

# This repo's own db:provision/db:provision-test scripts assume a persistent, manually
# initdb'd local cluster (~/.bion-pg) — not a fit for CI's ephemeral postgres service. This
# step is the CI-equivalent: create the same roles the tracked migrations assume exist
# (bion_owner/bion_rw per migrations/0002_grants.sql; bion_desktop_ro is never created by a
# tracked migration — its own role creation predates that convention, directive-72 — but
# 0008/0009 reference it, so it must exist here too), then bion_test + grants, then write
# .env.test (gitignored locally, generated fresh here — never the real local secrets).
- name: Provision bion_test roles + database
env:
PGPASSWORD: postgres
run: |
psql -h 127.0.0.1 -p 5433 -U postgres -c "CREATE ROLE bion_owner LOGIN PASSWORD 'ci_owner_pw';"
psql -h 127.0.0.1 -p 5433 -U postgres -c "CREATE ROLE bion_rw LOGIN PASSWORD 'ci_rw_pw';"
psql -h 127.0.0.1 -p 5433 -U postgres -c "CREATE ROLE bion_desktop_ro LOGIN PASSWORD 'ci_desktop_ro_pw';"
psql -h 127.0.0.1 -p 5433 -U postgres -c "CREATE DATABASE bion_test OWNER bion_owner;"
psql -h 127.0.0.1 -p 5433 -U postgres -d bion_test <<'SQL'
REVOKE ALL ON DATABASE bion_test FROM PUBLIC;
GRANT CONNECT ON DATABASE bion_test TO bion_owner, bion_rw, bion_desktop_ro;
ALTER SCHEMA public OWNER TO bion_owner;
REVOKE CREATE ON SCHEMA public FROM bion_rw;
GRANT USAGE ON SCHEMA public TO bion_rw, bion_desktop_ro;
SQL
cat > .env.test <<'EOF'
BION_DATABASE_URL=postgresql://bion_rw:ci_rw_pw@127.0.0.1:5433/bion_test
BION_MIGRATE_URL=postgresql://bion_owner:ci_owner_pw@127.0.0.1:5433/bion_test
BION_NTFY_URL=
BION_NTFY_TOKEN=
EOF
# test/env.test.ts exercises the real production concern directive-11 exists for (the
# Task-Scheduler-launched daemon must load .env.local regardless of cwd) — it loads this
# file into an isolated object, never real process.env, so a non-functioning placeholder
# ntfy URL is safe here: nothing in the suite makes a real network call against it.
cat > .env.local <<'EOF'
BION_DATABASE_URL=postgresql://bion_rw:ci_rw_pw@127.0.0.1:5433/bion_test
BION_MIGRATE_URL=postgresql://bion_owner:ci_owner_pw@127.0.0.1:5433/bion_test
BION_NTFY_URL=https://ntfy.sh/ci-placeholder-not-real
BION_NTFY_TOKEN=
EOF

- run: pnpm typecheck

- run: pnpm test
66 changes: 50 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
# Bion

A thin, owned **TypeScript coordinator** for a multi-agent workflow. Bion separates
**reasoning** (agents) from **coordination + state** (Bion): it owns project state, task
routing, and agent messaging over rails we already run — Postgres (relational state + full-text
search) and disk + git (the artifact corpus).
A thin, owned **TypeScript coordinator** for a multi-agent workflow (Desktop as architect, Kov as
implementer). Bion separates **reasoning** (agents) from **coordination + state** (Bion): it owns
project state, task routing, agent messaging, and comms over rails it runs itself — Postgres
(relational state + full-text search) and disk + git (the artifact corpus, plus a file-based
mailbox).

Bion does **relational state + FTS only**. It grows no retrieval engine; semantic/vector/graph
memory lives elsewhere.

## Design

- **State store** — a dedicated, isolated local Postgres. Two roles: `bion_owner` (owns the
schema / migration lane) and `bion_rw` (least-privilege runtime). Append-only tables
(`messages`, `events`, `message_consumptions`) have `UPDATE`/`DELETE` revoked from the runtime
role — append-only is *enforced*, not asserted.
- **State store** — a dedicated, isolated local Postgres. Two roles: `bion_owner` (schema/
migration lane) and `bion_rw` (least-privilege runtime), plus a narrow, read-only
`bion_desktop_ro` role for Desktop's own MCP connector. Append-only tables (`messages`,
`events`, `message_consumptions`) have `UPDATE`/`DELETE` revoked from the runtime role —
append-only is *enforced*, not asserted.
- **Coordination primitives** — `record()` (ledger write), `send()` (routed message),
`query_state()` (Postgres FTS + disk grep), `handoff()` (summarize state for the next agent).
- **Mailbox + Comms Protocol v1** — a real disk mailbox
(`.bion/mail/<agent>/{unread,read,flagged}/`), atomic stage-then-rename writes, with the DB as
routing authority (an agent acts only on a packet whose `content_sha256` matches an unconsumed
row). Messages are pointers, not payloads — `intent`/`refs`/`fields`/an optional terse `note`,
never prose. The reserved `escalate` intent fires a durable, at-least-once notification the
moment a packet crosses a standing project gate (real money, mainnet, third-party accounts, the
auto-mode env vars, credential exposure). `bion mail send`/`bion mail poll` is the CLI over it;
`src/mcp/desktopMail.ts` is a narrow stdio MCP server exposing the same two operations for
Desktop's own sandboxed environment, which can't reach the DB directly.
- **Idempotency** — every message and event carries a `dedup_key`; re-delivery is a no-op.
- **Routing authority is the DB** — the mailbox is payload; an agent acts only on a packet whose
`content_sha256` matches an unconsumed row.
- **Daemon + watchers** — a persistent local daemon (`bion daemon`) ticks a dispatch loop,
auto-discovers every git repo under the dev root (`.bionignore`-aware) for commit/test-result
signals, and reacts per `BION_REACTIVE_DISPATCH` mode (`off`/`shadow`/`on`) — bounded,
ratified-backlog-only auto-dispatch, never open-ended.
- **Auto Mode** — a shadow-gated, usage-bounded auto-work loop (`BION_AUTO_MODE`) for Kov's own
idle cycles; off/shadow by default, every gated action still stops at Forces.
- **Cost tracking** — `bion cost` attributes tokens/estimated spend per agent seat from
git-commit and message-send signals.
- **Tasks** — `bion task` (create/list) over a ratified-backlog model; `ratified` is owner/
Forces-lane only, structurally unreachable from the runtime role's own grants.

## Local setup

Expand All @@ -32,14 +51,29 @@ pnpm test # vitest run
```

`pnpm db:start` / `pnpm db:stop` control the cluster. The cluster's data dir lives outside the
repo (`~/.bion-pg`). `.env.local` holds local-dev credentials and is gitignored.
repo (`~/.bion-pg`). `.env.local`/`.env.test` hold local-dev credentials and are gitignored.

Day-to-day CLIs (`tsx src/cli/*.ts`, wired as `pnpm` scripts): `status`, `task`, `mail`, `cost`,
`auto-report`, `check-heartbeat`. `pnpm daemon` runs the persistent loop; `pnpm mcp:desktop-mail`
runs the standalone mail MCP server.

## Layout

```
migrations/ additive SQL (owner lane); tracked in applied_migrations.md
scripts/ cluster provisioning + lifecycle; ratify-task.sh (owner/Forces lane)
src/core/ coordination primitives + data-model types + DAG enforcement
src/db/ connection pool + migration runner
test/ gate specs (round-trip, dedup, append-only, DAG, isolation, ledgers)
migrations/ additive SQL (owner lane); tracked in applied_migrations.md
scripts/ cluster provisioning + lifecycle; ratify-task.sh / ratify-project.sh (owner/Forces lane)
src/adapters/ KovAdapter/DesktopAdapter — mailbox dispatch + poll, DB-as-routing-authority
src/auto/ Auto Mode: usage checks, shadow-gated auto-work selection
src/cli/ status, task, mail, cost, auto-report, checkHeartbeat entry points
src/comms/ Comms Protocol v1 — pointer()/serialize()/parse()/validate()
src/core/ coordination primitives + data-model types + DAG enforcement
src/cost/ per-agent-seat cost attribution (git-commit + message-send signals)
src/daemon/ persistent loop, cluster autostart, single-instance lock, heartbeat
src/db/ connection pool + migration runner + transactional outbox
src/loop/ dispatcher, reactive mode, coordinator, completion reporting
src/mailbox/ disk mailbox — atomic stage-then-rename packet writes
src/mcp/ bion-desktop-mail — narrow stdio MCP server wrapping send/poll mail
src/notify/ ntfy.sh push notifications (dry-run when unconfigured)
src/watchers/ dev-root-wide git + test-result discovery and polling
test/ gate specs — round-trip, dedup, append-only, DAG, isolation, ledgers, ...
```
3 changes: 2 additions & 1 deletion applied_migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ Never edit an applied migration — add a new one.
| 0005 | `migrations/0005_outbox_sending.sql` | Add a pre-send `sending` state so notify is at-least-once (directive-04): claim→sending→send→done; a mid-send crash re-sends on reconcile. Publish unchanged (exactly-once). |
| 0006 | `migrations/0006_projects.sql` | Ordered project list for Auto Mode pivot-on-block (Phase E3): `projects` table (Forces-defined order) + `tasks.project`. Runtime role reads projects; may set a task's project. |
| 0007 | `migrations/0007_cost_attribution.sql` | Cost module Phase 2 (directive-18): extend `events` with `target_seat`, `trigger_class`, `model`, `tokens_in`, `tokens_out`, `est_cost`, `is_approximate` (all nullable — only cost-bearing events populate them). No new grants: cost rows are events, so append-only enforcement (inv 5) already covers them. |
| 0008 | `migrations/0008_desktop_consumption_grant.sql` | Narrow `INSERT` grant on `message_consumptions` for `bion_desktop_ro` (directive-72): lets Desktop's read-only MCP connector write the one consumption row it needs to independently consume its own mail, network-isolated from this repo's CLI. Still append-only (no `UPDATE`/`DELETE`) — additive to `bion_desktop_ro`'s existing SELECT-only footing, mirrors `bion_rw`'s existing SELECT+INSERT on the same table. |
| 0008 | `migrations/0008_desktop_consumption_grant.sql` | Narrow `INSERT` grant on `message_consumptions` for `bion_desktop_ro` (directive-72): lets Desktop's read-only MCP connector write the one consumption row it needs to independently consume its own mail, network-isolated from this repo's CLI. Still append-only (no `UPDATE`/`DELETE`) — additive to `bion_desktop_ro`'s existing SELECT-only footing, mirrors `bion_rw`'s existing SELECT+INSERT on the same table. **Superseded by 0009 — see below.** |
| 0009 | `migrations/0009_revoke_desktop_consumption_grant.sql` | Revert 0008 (directive-75): superseded by D-73's real fix, the `bion-desktop-mail` stdio MCP connector, which reuses `bion_rw` instead. `bion_desktop_ro` returns to SELECT-only everywhere — an unused write grant on a read-designated role is exactly the drift least-privilege exists to prevent. |
17 changes: 17 additions & 0 deletions migrations/0009_revoke_desktop_consumption_grant.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- 0009_revoke_desktop_consumption_grant.sql — revert the bion_desktop_ro INSERT grant on
-- message_consumptions added by 0008 (directive-75 Task 4).
--
-- Superseded: D-73's real fix for Desktop's mail was a narrow, purpose-built stdio MCP connector
-- (src/mcp/desktopMail.ts) that runs on the real machine and reuses bion_rw — not a write grant on
-- Desktop's own read-designated connector role. D-73's own status file already flagged 0008's grant
-- as "unused by this design... landed, harmless." An unused write grant sitting on a
-- read-designated role is exactly the kind of drift least-privilege exists to prevent (the same
-- reasoning D-73's own design review cited from Anthropic's MCP guidance: deny by default, grant
-- the minimum each tier actually needs). bion_desktop_ro returns to SELECT-only everywhere,
-- matching its original, real design intent.

BEGIN;

REVOKE INSERT ON message_consumptions FROM bion_desktop_ro;

COMMIT;
1 change: 0 additions & 1 deletion test/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ describe('env loading is cwd-independent (directive-11)', () => {
process.chdir(elsewhere) // simulate Task Scheduler's foreign cwd
const p = resolveEnvPath()
expect(p).toBe(repoPath('.env.local'))
expect(p.replace(/\\/g, '/')).toMatch(/\/repo\/\.env\.local$/)
expect(existsSync(p)).toBe(true)

// load from that path into a clean object (never touches real process.env)
Expand Down
Loading