Add deployment: Dockerfile, compose, proxy config, and docs

One container plus a Postgres behind any TLS-terminating proxy. Nothing is
specific to a particular host.

The app and API are served from a SINGLE origin. This is not tidiness: browser
auth sessions live in per-origin storage, so splitting them across two
hostnames makes sign-in loop in a way that presents as a server fault. The
short alias redirects rather than serving a second origin.

Two safety properties verified by running the image, not by reading the code:

- With NODE_ENV=production and no SUPABASE_URL, the process refuses to start
  and says why. Serving the whole CRM unauthenticated is a worse outcome than
  failing to deploy, so the failure is deliberate and loud.
- In production the development auth bypass does not apply: an unauthenticated
  request to /api/dashboard returns 401 rather than adopting the first user in
  the table.

The Dockerfile typechecks all six packages as a build gate, so a deploy that
does not compile fails at build time rather than in front of a user. Runtime
runs unprivileged as `node`, and Postgres is not published to the host.

Docs cover the ontology and why it is shaped this way, agent connection for
Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing
seed data about real people — including how to have your record removed.

Verified: image builds, container reports healthy, serves the SPA, enforces
auth, and the production guard exits non-zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:19:53 -07:00
parent de33a03524
commit c747eb2aa7
7 changed files with 456 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Connecting an agent
PIG is a first-class application for agents. The same MCP server serves every
client, so nobody is asked to use a different tool than the one they already
work in.
## What connects
| Client | How |
|---|---|
| **Claude Code** | `claude mcp add pig -- npx -y @pig/mcp` |
| **Codex** | Add PIG as an MCP server in its config, with the same env vars |
| **prime-agent** | It is an MCP *client*; add PIG through `/mcp` |
| **Buzz** | Agents reach PIG through the ACP bridge's MCP support |
## Setup
Create an API key in PIG under **Settings → API keys**, then:
```bash
export PIG_URL=https://primeintellectgrowth.com
export PIG_API_KEY=pig_...
```
Scope the key to `read` unless the agent genuinely needs to write. An agent
acting for you is a **separate principal** from you: it has its own audit trail
and can be revoked without disturbing your session, and it can never reach
further than you can.
## The tools
| Tool | What it answers |
|---|---|
| `pig_whoami` | Who am I acting for, and which teams am I on? |
| `pig_my_pipeline` | Where are we? What needs attention? |
| `pig_capacity_match` | What have we bought that would serve this customer? |
| `pig_margin_report` | What is each block earning against what it cost? |
| `pig_idle_capacity` | What are we paying for and not selling? |
| `pig_inventory_search` | What could we buy to cover demand we cannot serve? |
| `pig_search` | Find an account |
| `pig_get_account` | Everything about one account |
| `pig_log_activity` | Record a call, meeting or note |
`pig_capacity_match` is the one worth learning. Ask it in plain language:
> "A customer wants 128 H100s with InfiniBand for three months, ceiling $2.80
> per GPU-hour. What have we got?"
It returns ranked matches, preferring blocks that are sitting idle — those
hours are already paid for — and warns explicitly when a match would sell below
break-even.
## Why the surface is small
Nine tools, each doing one thing. A sprawling tool list measurably degrades
model performance, and anything genuinely niche is reachable through
`pig_search` or the HTTP API. If you need something that is not here, it is
probably better added as a service method than as a tenth tool.
## What it cannot do
The MCP server holds an API key and calls the same HTTP API a browser does. It
has no database credentials and no privileged path. There is deliberately no
tool that provisions infrastructure, spends money, or emails a customer.
+106
View File
@@ -0,0 +1,106 @@
# The ontology
Why PIG is shaped the way it is. Read `packages/core/src/ontology.ts` alongside
this — the code carries the same reasoning in comments, and it is the version
that cannot go stale.
## The one table that matters
```
capacity_commitment ──┐
(what we bought, │
at a known cost) │
├──▶ allocation ──▶ margin, utilisation, idle
│ (what we sold,
demand_deal ──┘ at a known price)
(what we sold)
```
Margin, utilisation and idle capacity all fall out of that single join. No
generic CRM can compute any of them, because none has a concept of a
cost-bearing commitment sitting behind the pipeline.
**Cost is charged against the full commitment, not only the hours that sold.**
Unsold hours are already paid for. Charging only the allocated share would
report a healthy margin on a block that is losing money — precisely the failure
this system exists to prevent.
## Three teams
**Supply**, **demand**, and **research**. Research is first-class rather than an
afterthought: internal research burn is real capacity consumption competing with
revenue for the same GPUs, and margin math that cannot see it is wrong.
## Pipelines
**Demand**`qualification → legal → scoping → proposal → procurement → POC →
deployment → expansion`. Note that **legal sits second**. Customers do not hand
workloads to an infrastructure provider before paper is executed. Most CRMs put
contracting at the end of the funnel and are simply wrong about it here.
**Supply**`sourced → qualifying → technical diligence → financial diligence →
pricing → contracting → onboarding → live → renewal`. Qualification is split in
two because accepting capacity is a two-key decision: engineering judges whether
the cluster can do the work, finance judges whether the economics clear. Both
verdicts are recorded attributably.
## Capacity is a shape, not a rectangle
A commitment carries `shape: {intervals[], quantities[]}` — how many GPUs are
held during each interval. Real contracts ramp across tranches and step down at
checkpoints. A single start/end/total flattens that and then reports
availability that does not exist in the month someone wants it.
Availability at any instant is therefore:
```
available(t) = shapeQuantityAt(t) Σ overlapping allocations(t)
```
## Holds reserve; they do not sell
A live hold removes capacity from everyone else's availability — otherwise two
sellers promise the same GPUs — but does not count toward utilisation or
revenue, because it has not sold. Conflating the two is how a pipeline of
optimistic holds comes to look like a full book. Holds expire on a timer so a
stalled deal releases inventory automatically.
## Service levels come in three shapes
A compute aggregator generally **cannot** offer a conventional uptime guarantee
on capacity it resells and does not control, and says so publicly. So `slaKind`
distinguishes:
- `none` — self-serve, no commitment at all
- `credits_policy` — a reliability tier plus service credits. **Not** an uptime
guarantee, and must never be displayed as one
- `negotiated` — a real signed SLA with committed, measurable metrics
Remedies matter as much as targets. `remedyType` includes `fee_abatement`,
where payment obligations are *cancelled* for affected capacity until service is
restored — uncapped in duration and materially better than a capped credit. It
cannot be expressed as a credit percentage, so it gets its own representation.
## Export control is a predicate, not a flag
US controls on advanced computing apply an **ultimate parent** test that reaches
through the corporate tree: an entity can be restricted because of where its
parent is headquartered, even when the entity itself sits somewhere
unrestricted. Country of incorporation is therefore not a valid key.
Compliance is evaluated **on the allocation edge** — this buyer, this beneficial
owner, this physical jurisdiction — recorded with its reasoning and rule
version, and re-evaluated on resale or migration. See
`packages/db/src/schema/compliance.ts`. PIG records and surfaces; it does not
make the legal determination for you.
## Evidence
Agent-derived claims land in `facts` with a confidence score, a band, evidence
and a source URL. Only `verified` claims self-apply; anything weaker waits for a
human. An agent permitted to write unattributed claims will eventually write a
wrong one, and nobody will be able to tell which.
The same principle governs seed data about real people: every record carries a
grade and a citation, authorship is never promoted to employment, and no email
address is ever inferred.
+53
View File
@@ -0,0 +1,53 @@
# Seed data and its provenance
PIG ships with a roster of publicly documented people so the application is
legible on first run. It is public research, not an assertion of fact.
## Rules applied
1. **Every record carries a confidence grade and a source URL.** `confirmed`
means two or more independent sources; `probable` means one good one;
`unverified` means a single weak or self-reported source. The grade is shown
in the interface wherever the record appears — a single-source claim about a
real person must never look as solid as a corroborated one.
2. **No email addresses.** None are published by the subjects. Guessing them
from a name and a domain is unreliable, and when a guess lands it lands on a
real person who did not ask to be contacted.
3. **Authorship is not employment.** People named on papers or in repositories
are recorded with the affiliation actually evidenced — `contributor`,
`resident`, `alumni` — never promoted to `staff` to make the roster look
fuller.
4. **"Not found" is recorded, not invented.** Where a name was supplied but
could not be sourced, it appears in `UNRESOLVED_NAMES` with a note. Absence
is weak evidence: a junior or deliberately non-public employee looks
identical to a failed search.
## Known limitations
- Sourced August 2026. It will go stale — that is what `sourceUrl` is for.
- LinkedIn, Glassdoor and several job boards refuse automated fetching, so some
records rest on search-result summaries rather than a page that was read
end to end. Those are graded accordingly.
- One departure is recorded explicitly (a co-author who now lists a different
company) so the roster does not quietly imply current employment.
- One name in the original brief could not be tied to the company by any source
and is deliberately **not** seeded.
## Removing yourself
If you are seeded here and would rather not be, open an issue and the record
will be removed. Deleting a contact in the application also removes it
permanently.
## Turning it off
Seeding is a separate command and is never automatic:
```bash
npm run db:seed # opt in
```
Skip it and PIG starts empty apart from a development user.