Skip to content
Open
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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
.worktrees
node_modules
dist
.venv
.env
coverage
*.log
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
DATABASE_URL=postgres://pigeon:replace-me@127.0.0.1:5432/pigeon
PORT=8787
# Comma-separated base64url JSON DeviceIdentity records. Use your secret manager in production.
PIGEON_DEVICES=
PIGEON_SLACK_SIGNING_SECRET=
PIGEON_SLACK_BOT_TOKEN=
PIGEON_SLACK_CLIENT_ID=
PIGEON_SLACK_CLIENT_SECRET=
# Gateway-only values:
PIGEON_RELAY_URL=https://pigeon.example.com
PIGEON_DEVICE_ID=
PIGEON_DEVICE_PRIVATE_KEY=
PIGEON_USER=
PIGEON_TEAMMATES=
PIGEON_CODEX_MODEL=gpt-5.4
PIGEON_WORKSPACES={"pigeon":"/absolute/path/to/pigeon"}
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ permissions:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env: { POSTGRES_USER: pigeon, POSTGRES_PASSWORD: pigeon, POSTGRES_DB: pigeon }
ports: ["5432:5432"]
options: >-
--health-cmd "pg_isready -U pigeon" --health-interval 2s --health-timeout 2s --health-retries 20
env:
PIGEON_TEST_DATABASE_URL: postgres://pigeon:pigeon@127.0.0.1:5432/pigeon
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
Expand All @@ -15,3 +24,4 @@ jobs:
- run: pnpm test
- run: pnpm typecheck
- run: pnpm build
- run: PIGEON_SMOKE_DATABASE_URL=$PIGEON_TEST_DATABASE_URL pnpm smoke:relay
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ dist/
.pigeon/
.venv/
coverage/
.worktrees/
*.log
17 changes: 17 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM node:22-alpine AS build
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:22-alpine
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --prod --frozen-lockfile
COPY --from=build /app/dist ./dist
ENV NODE_ENV=production PORT=8787
EXPOSE 8787
CMD ["node", "dist/relay/main.js"]
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ Pigeon packages an MCP server, an embedded delegation inbox, signed authority en
- Approve triggers MCP `elicitation/create`; cancelling it starts nothing.
- Confirmation capabilities are short-lived and single-use.
- Delegation state transitions and Ed25519 envelopes are tested.
- Slack OIDC enrollment registers device public keys while private keys remain local.
- Approved remote requests launch a sandboxed local Codex app-server task and return its summary.

The included transport is intentionally an in-process evaluation relay. It proves the full approval and execution contract without exposing a machine on the network. A production deployment must replace `InMemoryRelay` with an authenticated durable transport and connect `CodexAdapter` to the local Codex app-server; those boundaries are explicit in `src/gateway.ts`.
The default transport remains an in-process evaluation relay. A Postgres-backed, signed HTTP relay is also included for durable cross-window and cross-machine delivery. See [Company relay operations](docs/company-relay.md).

## Run

Expand All @@ -27,6 +29,8 @@ node dist/server.js

The MCP process uses stdio. Install the repo-local marketplace, then install `pigeon` and start a new Codex task so tools and skills reload.

For company-relay development, run `docker compose up postgres`, then use `PIGEON_TEST_DATABASE_URL=postgres://pigeon:pigeon-local@127.0.0.1:55432/pigeon pnpm test`.

## Approval contract

1. Sender requests the smallest useful scope.
Expand Down
33 changes: 33 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: pigeon
POSTGRES_PASSWORD: pigeon-local
POSTGRES_DB: pigeon
ports: ["55432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U pigeon"]
interval: 2s
timeout: 2s
retries: 20
volumes: ["pigeon-postgres:/var/lib/postgresql/data"]
relay:
build: .
environment:
DATABASE_URL: postgres://pigeon:pigeon-local@postgres:5432/pigeon
PIGEON_DEVICES: ${PIGEON_DEVICES:-}
PIGEON_SLACK_SIGNING_SECRET: ${PIGEON_SLACK_SIGNING_SECRET:-}
PIGEON_SLACK_BOT_TOKEN: ${PIGEON_SLACK_BOT_TOKEN:-}
PIGEON_SLACK_CLIENT_ID: ${PIGEON_SLACK_CLIENT_ID:-}
PIGEON_SLACK_CLIENT_SECRET: ${PIGEON_SLACK_CLIENT_SECRET:-}
ports: ["8787:8787"]
depends_on:
postgres: { condition: service_healthy }
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8787/healthz"]
interval: 5s
timeout: 2s
retries: 20
volumes:
pigeon-postgres:
48 changes: 48 additions & 0 deletions docs/company-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Company relay operations

## Security boundary

The relay stores delegation metadata and an append-only event stream. It never needs OpenAI credentials or local workspace contents. Slack can reject any request and approve only `discuss_only`; `read_only` and `workspace_write` approval must occur through native confirmation in the recipient's Codex.

## Local deployment

1. Start Docker Desktop.
2. Copy `.env.example` to `.env` and keep it untracked.
3. Configure `PIGEON_SLACK_CLIENT_ID` and `PIGEON_SLACK_CLIENT_SECRET` for Sign in with Slack. Static `PIGEON_DEVICES` remains available only for recovery and local fixtures.
4. Run `docker compose up --build`.
5. Confirm `curl http://127.0.0.1:8787/healthz` returns `{ "ok": true }`.

Run the production-component smoke test against the Compose database:

```bash
pnpm build
PIGEON_SMOKE_DATABASE_URL=postgres://pigeon:pigeon-local@127.0.0.1:55432/pigeon pnpm smoke:relay
```

## Slack app

Create an internal Slack app with bot scope `chat:write` and Sign in with Slack scopes `openid profile email`. Enable interactivity and point the request URL to `https://YOUR_RELAY/v1/slack/actions`. Add `https://YOUR_RELAY/v1/enrollment/callback` as an OAuth redirect URL. Store the bot token, signing secret, client ID, and client secret in the deployment secret manager. Pigeon verifies Slack's timestamped HMAC signature over the raw form body before accepting an interaction.

Action messages use DMs. Elevated requests show Reject and Open in Codex; discussion-only requests also show Approve. Channel posting and parsing thread replies as commands are intentionally unsupported.

## Gateway configuration

After building Pigeon, enroll the machine:

```bash
PIGEON_RELAY_URL=https://YOUR_RELAY pnpm enroll
```

The enrollment command generates an Ed25519 key locally, opens Sign in with Slack, waits for the relay to recognize the device, and writes an owner-only credential file to `~/.pigeon/device.json`. The private key never leaves the machine. The plugin reads this file automatically; restart Codex after enrollment.

Set `PIGEON_WORKSPACES` to a JSON object mapping safe relay labels to absolute local roots, for example `{"pigeon":"/Users/me/src/pigeon"}`. `read_only` and `workspace_write` requests fail closed when their label is not mapped. `discuss_only` always runs in a new empty temporary directory. Set `PIGEON_CODEX_MODEL` to override the compatibility default `gpt-5.4`, and use a comma-separated `PIGEON_TEAMMATES` list for recipient discovery. Without relay credentials, Pigeon uses deterministic in-process evaluation mode.

## Production prerequisites

- Put the relay behind managed TLS and company ingress authentication controls.
- Store Postgres credentials, Slack tokens, and device registration data in the company secret manager.
- Back up Postgres, monitor `/healthz`, outbox lag, signature failures, state conflicts, and capability replay attempts.
- Export append-only security events to the company logging system and apply the retention policy in the design spec.
- Exercise device revocation and the organization-wide kill switch before enabling `workspace_write`.

Approved work launches an ephemeral local Codex app-server thread. Pigeon binds it to the approved workspace root, maps `read_only` to the read-only sandbox and `workspace_write` to the workspace-write sandbox, disables further sandbox escalation, and returns the final summary and Codex thread ID through the relay. Keep `workspace_write` limited to allowlisted teams and repositories until company policy enforcement is security-reviewed.
178 changes: 178 additions & 0 deletions docs/superpowers/plans/2026-07-11-company-relay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Pigeon Company Relay Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Ship a deployable, Postgres-backed Pigeon relay with signed device authentication, Slack notifications and low-risk approvals, and a Codex gateway client for durable cross-window delegation.

**Architecture:** A stateless TypeScript HTTP service owns durable delegation state in Postgres and records an append-only event stream plus transactional delivery outbox. The local MCP gateway signs relay commands, polls recipient events, performs elevated approval in Codex, and never sends workspace content to the relay. A Slack adapter posts requests and accepts only reject or `discuss_only` approval actions.

**Tech Stack:** Node.js 20+, TypeScript, Express 5, Postgres 15+, `pg`, Zod, Slack Web API over HTTPS, Vitest, Docker Compose.

## Global Constraints

- Slack may approve only `discuss_only`; `read_only` and `workspace_write` require native Codex confirmation.
- Scope may be narrowed and never widened.
- Relay records must not contain absolute workspace paths, file contents, prompts, tool transcripts, secrets, or model reasoning.
- Capabilities are audience-bound, short-lived, single-use, and stored as hashes.
- Postgres is authoritative; delivery is at least once and command effects are idempotent.
- Do not add Kafka, Redis, or a policy language.

---

### Task 1: Durable protocol and store

**Files:**
- Create: `src/relay/types.ts`
- Create: `src/relay/store.ts`
- Create: `src/relay/memory-store.ts`
- Create: `src/relay/store.test.ts`
- Modify: `src/protocol.ts`

**Interfaces:**
- Produces: `RelayStore`, `RelayCommand`, `RelayEvent`, `DeviceIdentity`, `CreateDelegationInput`, and `MemoryRelayStore`.
- `RelayStore.createDelegation(input, actor)` returns the stored delegation and event atomically.
- `RelayStore.transition(id, expectedVersion, command, actor)` enforces state and scope rules.

- [ ] Write tests for idempotent creation, recipient isolation, narrowing, invalid widening, optimistic concurrency, expiry, terminal states, and redacted workspace labels.
- [ ] Run `pnpm vitest run src/relay/store.test.ts` and verify the new module is missing.
- [ ] Add exact Zod schemas and the `RelayStore` interface; implement the deterministic memory store used by unit tests.
- [ ] Run `pnpm vitest run src/relay/store.test.ts` and verify all store tests pass.
- [ ] Commit with `git commit -m "feat: define durable relay store"`.

### Task 2: Postgres persistence and transactional outbox

**Files:**
- Create: `src/relay/postgres.ts`
- Create: `src/relay/migrations/001_relay.sql`
- Create: `src/relay/postgres.test.ts`
- Modify: `package.json`
- Modify: `pnpm-lock.yaml`

**Interfaces:**
- Consumes: `RelayStore` from Task 1.
- Produces: `PostgresRelayStore`, `migrate(pool)`, `claimOutbox(limit)`, `completeOutbox(id)`, and `failOutbox(id, error)`.

- [ ] Write integration tests gated by `PIGEON_TEST_DATABASE_URL` for atomic create/event/outbox writes, duplicate idempotency keys, concurrent transitions, event cursors, and retry claims.
- [ ] Run the tests without the database variable and verify they skip cleanly; run them against Docker Postgres and verify they fail before implementation.
- [ ] Add `pg`, the migration, parameterized queries, transactions, row locking, uniqueness constraints, and `FOR UPDATE SKIP LOCKED` outbox claims.
- [ ] Run Postgres integration tests and verify all cases pass.
- [ ] Commit with `git commit -m "feat: persist relay state in postgres"`.

### Task 3: Signed relay HTTP API

**Files:**
- Create: `src/relay/auth.ts`
- Create: `src/relay/auth.test.ts`
- Create: `src/relay/app.ts`
- Create: `src/relay/app.test.ts`
- Create: `src/relay/main.ts`
- Modify: `package.json`

**Interfaces:**
- Consumes: `RelayStore` and device public keys.
- Produces: `createRelayApp({ store, devices, clock })`, `verifyDeviceRequest`, and HTTP endpoints `POST /v1/delegations`, `GET /v1/events`, `POST /v1/delegations/:id/approve`, `POST /v1/delegations/:id/reject`, `POST /v1/delegations/:id/start`, `POST /v1/delegations/:id/complete`, plus `GET /healthz`.

- [ ] Write tests for signatures, timestamp skew, nonce replay, tenant/recipient isolation, idempotency, Slack approval restrictions, Codex elevated approval, redacted responses, and stable error codes.
- [ ] Run the focused tests and verify failure before implementation.
- [ ] Implement canonical request signing, in-memory nonce replay protection, Zod request validation, actor/source authorization, and JSON error middleware.
- [ ] Run focused API tests and verify they pass.
- [ ] Commit with `git commit -m "feat: add signed relay api"`.

### Task 4: Slack notifications and safe interactive actions

**Files:**
- Create: `src/slack/client.ts`
- Create: `src/slack/client.test.ts`
- Create: `src/slack/actions.ts`
- Create: `src/slack/actions.test.ts`
- Modify: `src/relay/app.ts`
- Modify: `src/relay/postgres.ts`

**Interfaces:**
- Produces: `SlackClient.postDelegation`, `SlackClient.updateDelegation`, `verifySlackRequest`, `handleSlackAction`, and an outbox delivery handler.
- Slack approval calls the relay transition only when the requested and effective scopes are `discuss_only`; rejection is permitted for every scope.

- [ ] Write tests using a fake Slack HTTP server for Block Kit payload redaction, immediate action acknowledgement, request signature verification, stale actions, rejection, low-risk approval, and elevated-scope denial.
- [ ] Run focused Slack tests and verify failure before implementation.
- [ ] Implement minimal Slack Web API calls, verified interactive routes, opaque IDs, DM request blocks, and outbox retry classification.
- [ ] Run focused Slack and API tests and verify they pass.
- [ ] Commit with `git commit -m "feat: connect slack approval workflow"`.

### Task 5: Codex gateway relay client

**Files:**
- Create: `src/relay/client.ts`
- Create: `src/relay/client.test.ts`
- Modify: `src/server.ts`
- Modify: `src/gateway.ts`
- Modify: `plugins/pigeon/.mcp.json`

**Interfaces:**
- Produces: `RelayClient.createDelegation`, `RelayClient.events`, `RelayClient.approve`, `RelayClient.reject`, and `RelayClient.complete`.
- The existing MCP tools use the remote client when `PIGEON_RELAY_URL`, `PIGEON_DEVICE_ID`, and `PIGEON_DEVICE_PRIVATE_KEY` are present; otherwise they retain the evaluation-only memory relay.

- [ ] Write tests for canonical signing, event cursors, retry-safe commands, remote inbox rendering, Slack-approved discussion work, and mandatory native confirmation for elevated work.
- [ ] Run focused tests and verify failure before implementation.
- [ ] Implement the signed HTTP client and gateway adapter, keeping private keys local and sending only workspace labels.
- [ ] Run gateway, widget, client, and protocol tests and verify they pass.
- [ ] Commit with `git commit -m "feat: connect codex gateway to relay"`.

### Task 6: Deployment, operations, and release verification

**Files:**
- Create: `Dockerfile`
- Create: `docker-compose.yml`
- Create: `.env.example`
- Create: `docs/company-relay.md`
- Create: `scripts/smoke-relay.mjs`
- Modify: `README.md`
- Modify: `.github/workflows/ci.yml`
- Modify: `package.json`

**Interfaces:**
- Produces: `pnpm relay`, `pnpm migrate`, `pnpm smoke:relay`, a health endpoint, container image, local Postgres stack, and operator setup documentation.

- [ ] Add a smoke script that registers two fixture devices, creates a delegation, reads it as the recipient, approves it through the allowed surface, and observes completion.
- [ ] Add container/deployment files with non-secret examples, health checks, migration startup, retention and revocation guidance, and Slack app configuration.
- [ ] Run `pnpm test`, `pnpm typecheck`, `pnpm build`, `pnpm validate:plugin`, and the Docker-backed smoke test.
- [ ] Inspect tracked files and build output for credentials, private keys, absolute personal paths, and raw workspace content.
- [ ] Commit with `git commit -m "docs: ship company relay deployment"`.
- [ ] Push the branch, open a draft pull request against `evalops/pigeon:main`, and report validation evidence and remaining production prerequisites.

### Task 7: Slack OIDC device enrollment

**Files:**
- Create: `src/enrollment/service.ts`
- Create: `src/enrollment/service.test.ts`
- Create: `src/enrollment/slack-oidc.ts`
- Create: `src/enrollment/slack-oidc.test.ts`
- Create: `scripts/enroll-device.mjs`
- Modify: `src/relay/migrations/001_relay.sql`
- Modify: `src/relay/app.ts`
- Modify: `src/relay/main.ts`

**Interfaces:**
- Produces: short-lived enrollment sessions, Slack OIDC authorization/callback endpoints, durable device registration and revocation, and a local enrollment CLI that keeps the private key on the Codex machine.

- [ ] Write failing tests for state hashing, expiry, Slack workspace/user binding, duplicate device rejection, durable lookup, and revocation.
- [ ] Implement OIDC authorization, token exchange, user-info verification, and transactional Postgres enrollment.
- [ ] Add a local CLI that generates an Ed25519 key, completes browser enrollment, and writes an owner-only device credential file.
- [ ] Run focused tests, live Postgres tests, and a two-device enrollment smoke test.
- [ ] Commit with `git commit -m "feat: enroll devices with Slack OIDC"`.

### Task 8: Real Codex app-server execution

**Files:**
- Create: `src/codex/app-server.ts`
- Create: `src/codex/app-server.test.ts`
- Modify: `src/server.ts`
- Modify: `docs/company-relay.md`

**Interfaces:**
- Produces: `CodexAppServerAdapter.run`, which starts an ephemeral Codex thread in the approved local workspace, applies the scope sandbox, starts one turn, streams until terminal completion, and returns a bounded final summary and thread ID.

- [ ] Write failing protocol tests with a deterministic JSON-RPC fixture process.
- [ ] Implement initialize, `thread/start`, `turn/start`, notification handling, timeout, cancellation, and process cleanup.
- [ ] Replace lifecycle-only remote completion with adapter execution before the final relay transition.
- [ ] Run focused tests and a real discuss-only `codex app-server` smoke task.
- [ ] Commit with `git commit -m "feat: execute delegations in codex app-server"`.
Loading
Loading