13dec6b4b8
Seven parallel agents and an adversarial verification pass. The three things worth knowing before reading the diff: RBAC WAS ALREADY BUILT. docs/build-plan.md marks F2 and F3 outstanding and is stale — packages/core/src/permissions.ts and lib/mutation.ts shipped long ago. So this does not rebuild them; it closes the gaps an audit found. The big one is that reads were entirely ungoverned: every GET was "any authenticated member", so a junior demand rep and a research contractor could both pull per-block supplier cost and break-even prices from /api/capacity/margin, and every contract's negotiated terms. For a company whose margin is the business, that was the hole that mattered. Adds book:read / economics:read / team:read, a readGuard middleware, and a `viewer` role below member. THE BUTTON AND THE 403 DISAGREED — the exact thing F3 said must never happen. Contracts.tsx never called can() at all, so its save button was always enabled against a server requiring contract:sign; Capacity.tsx gated commitment creation on deal:write/demand while the server wanted commitment:write/supply. POST /api/activities was the one write bypassing executeMutation: no capability check, and any member could mutate accounts.lastActivityAt as a side effect. It is now a proper mutation() behind activity:write. The shell becomes three panes — a collapsible shadcn sidebar with an account switcher on the Piggy accent, a header with real search, and Piggy docked to the right, page-aware and persistent across navigation. The phone keeps its bottom tab bar, which is the thing this product already beat trycompai/crm on, and gains the sidebar as a sheet. Calendar is a projection over thirteen dated sources rather than a new table, because a table would duplicate dates that already live on contracts, deals and commitments and would drift — and one ledger answering the question is the whole argument. It surfaces export_authorizations and compliance_artifacts, which had indexed expires_at columns, schema comments saying they must be alerted on, and no read endpoint or UI anywhere. Learn carries two tracks. Concepts are members-only; the platform track can be opened with a share code by someone with no account. The code mints a scoped learn-only token and never a Principal — every route here resolves a principal and then checks capabilities, so a principal-minting code would be one missing check away from leaking the book. "Only platform-track rows may be code-visible" is a database CHECK constraint as well as a write-path rule, and a test asserts a valid learn token still gets 401 on /api/dashboard, /api/accounts and /api/contracts — the same invariant scripts/deploy.sh refuses to ship without. CD becomes tag-to-ship. CI publishes an image to the Gitea registry on a release-* tag and cloud-2 pulls it, so no credential on the shared runner can execute anything on production — by construction rather than by policy. Both halves of deploy.sh's original rule survive: nothing on the runner reaches the host, and a human still decides when it ships. deploy.sh gains a rollback and a public-origin check, and PIG_IMAGE now reaches compose through `sudo env`, without which sudo's env_reset silently resolved every release to pig:local. Tests 141 -> 261. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
3.9 KiB
YAML
101 lines
3.9 KiB
YAML
# PIG — self-hosted deployment.
|
|
#
|
|
# docker compose -p pig up -d --build
|
|
#
|
|
# The project name matters. Use something PIG-specific (`-p pig`) so this stack
|
|
# never adopts another application's volumes — a compose project silently
|
|
# inheriting a neighbouring database is a genuinely nasty way to lose data.
|
|
|
|
services:
|
|
db:
|
|
image: postgres:16-alpine
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-pig}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
|
|
POSTGRES_DB: ${POSTGRES_DB:-pig}
|
|
volumes:
|
|
- pig-pgdata:/var/lib/postgresql/data
|
|
# Not published to the host. The application reaches it over the compose
|
|
# network; exposing Postgres publicly is never what you want.
|
|
expose:
|
|
- '5432'
|
|
healthcheck:
|
|
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-pig} -d ${POSTGRES_DB:-pig}']
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
app:
|
|
# `image` alongside `build` means one file serves both paths: with no
|
|
# PIG_IMAGE set, `compose build` tags the local build `pig:local` and
|
|
# nothing changes; with PIG_IMAGE set to a published tag, `compose pull`
|
|
# fetches exactly that image and never builds. scripts/deploy.sh picks.
|
|
#
|
|
# app and piggy MUST carry the same reference. They are the same image
|
|
# running two commands, and a piggy left on an older release talks to the
|
|
# new schema with the old code.
|
|
image: ${PIG_IMAGE:-pig:local}
|
|
build: .
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
environment:
|
|
DATABASE_URL: postgres://${POSTGRES_USER:-pig}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-pig}
|
|
NODE_ENV: production
|
|
PIG_PORT: 8920
|
|
PIG_PUBLIC_URL: ${PIG_PUBLIC_URL:?PIG_PUBLIC_URL must be set}
|
|
SUPABASE_URL: ${SUPABASE_URL:?SUPABASE_URL must be set in production}
|
|
SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
|
|
SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_KEY:-}
|
|
PIG_ADMIN_EMAILS: ${PIG_ADMIN_EMAILS:-}
|
|
PIG_INVITE_CODE: ${PIG_INVITE_CODE:-}
|
|
PRIME_API_KEY: ${PRIME_API_KEY:-}
|
|
PRIME_SYNC_ENABLED: ${PRIME_SYNC_ENABLED:-false}
|
|
PIGGY_ENABLED: ${PIGGY_ENABLED:-false}
|
|
PIGGY_INTERNAL_URL: http://piggy:8931
|
|
PIGGY_INTERNAL_TOKEN: ${PIGGY_INTERNAL_TOKEN:-}
|
|
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN:-}
|
|
SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET:-}
|
|
BUZZ_RELAY_URL: ${BUZZ_RELAY_URL:-}
|
|
NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-}
|
|
NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-}
|
|
NOTION_REDIRECT_URI: ${NOTION_REDIRECT_URI:-}
|
|
BUZZ_PRIVATE_KEY: ${BUZZ_PRIVATE_KEY:-}
|
|
BUZZ_AUTH_TAG: ${BUZZ_AUTH_TAG:-}
|
|
# Bound to loopback: TLS termination belongs to the reverse proxy in front,
|
|
# not to this container.
|
|
ports:
|
|
- '127.0.0.1:${PIG_HOST_PORT:-8920}:8920'
|
|
|
|
piggy:
|
|
profiles: ['piggy']
|
|
# Same reference as `app`, deliberately — see the note there.
|
|
image: ${PIG_IMAGE:-pig:local}
|
|
build: .
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
command: ['npx', 'tsx', 'apps/piggy/src/main.ts']
|
|
environment:
|
|
DATABASE_URL: postgres://${POSTGRES_USER:-pig}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-pig}
|
|
PIGGY_INFERENCE_API_KEY: ${PIGGY_INFERENCE_API_KEY:-}
|
|
PIGGY_INFERENCE_BASE: ${PIGGY_INFERENCE_BASE:-https://api.pinference.ai/api/v1}
|
|
PIGGY_MODEL: ${PIGGY_MODEL:-nvidia/nemotron-3-nano-30b-a3b}
|
|
PIGGY_LEASE_SECONDS: ${PIGGY_LEASE_SECONDS:-300}
|
|
PIGGY_INTERNAL_TOKEN: ${PIGGY_INTERNAL_TOKEN:-}
|
|
PIGGY_CHAT_HOST: 0.0.0.0
|
|
PIGGY_CHAT_PORT: 8931
|
|
PIGGY_CHAT_ALLOW_NON_LOOPBACK: 'true'
|
|
# Private to the Compose network. There is deliberately no `ports` entry.
|
|
expose:
|
|
- '8931'
|
|
|
|
volumes:
|
|
pig-pgdata:
|
|
# Named explicitly so it is obvious which volume holds the data, and so a
|
|
# `docker compose down -v` mistake is at least a legible one.
|
|
name: pig-pgdata
|