Put Piggy on Prime Agent, and let it write to the book
CI / verify (push) Successful in 7m6s
CI / publish (push) Has been skipped

Piggy was a hand-rolled OpenAI tool loop. It is now a Prime Agent session —
Prime Intellect's own harness, embedded as a Node library — answering from
PIG's tools and, for the first time, able to put information into the CRM
rather than only read it out.

The harness is a coding agent, so the first job was taking the coding agent
away from it. `noTools: 'all'` plus an explicit allowlist leaves the model
with PIG's ten `pig_*` tools and no bash, no filesystem, no IPython. That
holds under attack: a hostile extension, a skill and a settings file planted
in the agent's own directory, then `setActiveToolsByName` called with every
built-in, still leaves ten tools, all ours. Both lines are load-bearing —
`noTools` alone registers nothing, and the allowlist is what admits our own.

Writing is gated rather than assumed. A change is proposed, not made: the
tool returns a description, the transcript renders a diff card, and nothing
reaches the database until someone presses Apply. Contracts, commitments,
allocations and compliance always stop for a human whatever the mode. Every
write runs through `executeMutation` as the calling user, so their
capabilities and the audit trail apply exactly as they would to a human's.

Four things about the SDK are wrong in its own documentation and cost a
debugging cycle each: models.json does not resolve an env var name for
`apiKey`, it sends the literal string; there is no built-in prime-inference
provider in 0.84.1; a ResourceLoader you pass in is never reloaded for you;
and the stock system prompt is a coding-assistant prompt that must be
replaced — but replacing it also silently removes the tool list, because the
harness only renders that section when it owns the prompt. AGENTS.md records
all four.

The expensive one was thinking level. The harness defaults to `medium`, and
nemotron spent an entire 4,096-token budget reasoning and returned an empty
answer. `low` was worse; `off` omits the parameter so the endpoint's default
wins. An explicit `reasoning_effort: none` via `thinkingLevelMap` took a turn
from 6,195 output tokens to 149.

And a turn is now bounded. The harness loop is `while (true)` with no
iteration cap; a runaway on a frontier model would have eaten the credit it
is supposed to report on. Ceilings on model calls and tokens, enforced both
through the harness hook and independently from the event stream, plus a
per-user daily spend limit — and the ledger now records spend on turns that
fail, which it previously discarded.

Signing in lands on /piggy, which is a workspace: conversations down one
side, the agent in the middle, what it did and what it cost beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude
2026-08-14 05:26:28 -07:00
parent 99d165b5e5
commit f0173440e4
77 changed files with 28108 additions and 1672 deletions
+122 -18
View File
@@ -106,6 +106,16 @@ is_true() {
esac
}
# The database this deployment actually has, with the same defaults
# docker-compose.yml interpolates. Hardcoding `pig` here is survivable only
# while nobody sets POSTGRES_USER: on a host that does, the pre-migration dump
# would fail and take the deploy with it — fail-safe, but it fails the deploy
# instead of backing up the database, which is not the intent.
PG_USER="$(env_value POSTGRES_USER)"
PG_USER="${PG_USER:-pig}"
PG_DB="$(env_value POSTGRES_DB)"
PG_DB="${PG_DB:-pig}"
# Piggy is part of the release or it is not; there is no half-deployed state
# worth having. Left out of the pull, the build, the `up` and the rollback, it
# runs the PREVIOUS image against the schema this deploy just migrated — the
@@ -142,13 +152,66 @@ else
fi
fi
echo "==> Starting the database"
# Before the dump, not after it. The backup step below `exec`s into this
# container, so on a host where the stack is down — a reboot, a `compose down`,
# a first-ever deploy — the backup was the step that failed, and it took the
# whole release with it. Fail-safe, but it aborted the deploy instead of
# protecting the data, which is the opposite of the intent.
dc up -d db
for _ in $(seq 1 60); do
if dc exec -T db pg_isready -U "$PG_USER" -d "$PG_DB" > /dev/null; then break; fi
sleep 1
done
if ! dc exec -T db pg_isready -U "$PG_USER" -d "$PG_DB" > /dev/null; then
echo "ERROR: database did not become ready within 60 seconds" >&2
exit 1
fi
echo "==> Backing up the database first"
# Cheap insurance. A migration that goes wrong on a database holding real deal
# data is not something to discover without a dump in hand.
# This used to be cheap insurance against a bad migration. It is now the thing
# standing between an agent bug and the book: Piggy can WRITE to the CRM —
# accounts, deals, activity — as the signed-in user, so a release that ships a
# broken write tool can corrupt data no migration ever touched. Restoring from
# here is the only undo that exists.
#
# Taken BEFORE the migration and before the new image starts, which is the only
# ordering that gives a dump of the schema the old code wrote.
#
# How many tables there are, so an empty dump can be told apart from a failed
# one. A first deploy legitimately dumps almost nothing; every later deploy
# dumping almost nothing is a fault.
PUBLIC_TABLES=$(dc exec -T db psql -U "$PG_USER" -d "$PG_DB" -tAc \
"select count(*) from information_schema.tables where table_schema = 'public'" | tr -cd '0-9')
PUBLIC_TABLES="${PUBLIC_TABLES:-0}"
mkdir -p backups
BACKUP="backups/pig-$(date +%Y%m%d-%H%M%S).sql.gz"
dc exec -T db pg_dump -U pig pig | gzip > "$BACKUP"
echo " $BACKUP ($(du -h "$BACKUP" | cut -f1))"
dc exec -T db pg_dump -U "$PG_USER" "$PG_DB" | gzip > "$BACKUP"
# pipefail catches a pg_dump that FAILS. It does not catch a pg_dump that
# succeeds and says nothing — a wrong database name, an empty scratch volume
# adopted by mistake — and gzip turns that silence into a plausible-looking
# 20-byte file. A dump nobody can restore from is worse than a missing one,
# because the deploy log claims a backup was taken. Decompressing the whole
# thing verifies the gzip stream as a side effect, and `wc -c` reads to EOF so
# nothing here dies on SIGPIPE.
BACKUP_BYTES=$(gzip -dc "$BACKUP" | wc -c)
if [ "$PUBLIC_TABLES" -gt 0 ] && [ "$BACKUP_BYTES" -lt 4096 ]; then
echo "ERROR: $PG_DB has $PUBLIC_TABLES tables but $BACKUP decompresses to only" >&2
echo " $BACKUP_BYTES bytes. That is not a usable dump. Refusing to migrate" >&2
echo " without one, because this dump is the only rollback the data has." >&2
exit 1
fi
echo " $BACKUP ($(du -h "$BACKUP" | cut -f1) compressed, $BACKUP_BYTES bytes of SQL, $PUBLIC_TABLES tables)"
if [ "$PUBLIC_TABLES" -eq 0 ]; then
echo " (no tables yet — this looks like a first deploy, so a thin dump is expected)"
fi
# Kept next to the checkout on purpose. backups/ is in .gitignore, so
# scripts/autodeploy.sh's `git checkout --detach --force` leaves it alone — a
# dump that the next deploy deletes is not a backup. Nothing here prunes them
# either; they are small, they are the only copy, and a disk-space problem is a
# better problem than a missing dump. Copy them off this host if the data
# matters as much as the uptime does.
if [ -n "${PIG_IMAGE:-}" ]; then
echo "==> Pulling"
@@ -158,21 +221,23 @@ else
dc build "${DEPLOY_SERVICES[@]}"
fi
echo "==> Starting the database"
dc up -d db
for _ in $(seq 1 60); do
if dc exec -T db pg_isready -U pig -d pig > /dev/null; then break; fi
sleep 1
done
if ! dc exec -T db pg_isready -U pig -d pig > /dev/null; then
echo "ERROR: database did not become ready within 60 seconds" >&2
exit 1
fi
echo "==> Migrating before the schema-dependent app starts"
# A release may query a newly introduced table during startup. Running the
# migration from a one-off container prevents that app from crash-looping
# before an `exec`-based migration can reach it.
#
# This ordering matters more with every migration that a NEW surface depends
# on. 0014 adds piggy_conversations and piggy_messages, which the API writes on
# every agent turn — and the agent turn is paid for BEFORE the insert, so a
# release that starts ahead of its migration takes a user's question, calls a
# model, bills for it and then fails to persist the answer. `dc up` starts app
# and piggy together below, so both are behind this line.
#
# The image runs it, not the host: the migration set that runs is the one baked
# into $IMAGE_REF, so a published image and its migrations can never disagree.
# `--no-deps` because the database is already up, and letting compose start
# dependencies here would start the OLD app container against an unmigrated
# schema, which is the exact race this step exists to avoid.
dc run --rm --no-deps app pnpm exec tsx packages/db/src/migrate.ts
# The image the current container is running, captured before it is replaced.
@@ -190,6 +255,15 @@ fi
# schema, and the dump taken above is the escape hatch for the case where it
# does not. The re-tag makes $IMAGE_REF point back at the old image locally; the
# next successful pull moves it forward again.
#
# Leaving the schema ahead of the image is the deliberate half of that, and it
# is safe for exactly the reason above. 0014 is the current case: it CREATES
# piggy_conversations and piggy_messages and alters nothing, so an image that
# predates it never names those tables and cannot notice they exist. The rule
# this relies on is a rule about migrations, not about this script — a
# migration that drops a column, renames one, or tightens a constraint breaks
# the previous image and makes this rollback a partial outage. Write additive
# migrations, or plan the rollback with the dump in hand.
roll_back() {
echo " !! $1"
dc logs app --tail 40 || true
@@ -266,6 +340,31 @@ if [ "$PIGGY_IN_RELEASE" = '1' ]; then
fi
echo " piggy on $IMAGE_REF"
# Where the harness is allowed to look at the filesystem, read back from the
# container that is actually running rather than from the file that was meant
# to configure it.
#
# The harness discovers extensions, skills and context files from its cwd, and
# Piggy hands it PIGGY_AGENT_DIR as cwd. Inside /app that is the application
# checkout, so a stray bind mount or a hand-edited compose file would put
# source code within reach of a CRM agent's prompt — a leak that every other
# gate here passes cheerfully, because the container is perfectly healthy.
# Empty is fine: an older compose file simply does not set it, and the
# in-code default is ~/.pig/piggy-agent.
PIGGY_AGENT_DIR_LIVE=$(sudo docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' \
"$PIGGY_CID" 2>/dev/null | sed -n 's/^PIGGY_AGENT_DIR=//p' | tail -n1)
case "${PIGGY_AGENT_DIR_LIVE:-}" in
/app | /app/*)
echo " PIGGY_AGENT_DIR IS $PIGGY_AGENT_DIR_LIVE, INSIDE THE CHECKOUT" >&2
echo " The agent harness treats that directory as its cwd and reads" >&2
echo " context files from it, so this puts repository contents inside a" >&2
echo " CRM agent's prompt. Point it at /var/lib/piggy-agent, which the" >&2
echo " image creates for exactly this, and redeploy. Not rolled back:" >&2
echo " the previous image reads the same compose file." >&2
exit "$EXIT_STILL_LIVE"
;;
esac
# `starting` until the first probe answers, so this is a wait, not a poll of
# something already decided. start_period is 20s and the interval 30s, hence
# the longer budget than the app's.
@@ -293,9 +392,14 @@ if [ "$PIGGY_IN_RELEASE" = '1' ]; then
# so restoring it churns the CRM without fixing the agent. The CRM is live
# and well; the agent the operator asked for is not.
echo " The CRM is serving. Piggy is not, and a rollback would not help:" >&2
echo " the previous image reads the same .env. The usual cause is a" >&2
echo " missing or rejected PIGGY_INFERENCE_API_KEY, which crash-loops" >&2
echo " the worker at boot before it can serve anything." >&2
echo " the previous image reads the same .env. Usual causes, in order:" >&2
echo " - PRIME_API_KEY missing or rejected (PIGGY_INFERENCE_API_KEY is" >&2
echo " still accepted as its alias); Piggy exits at boot and" >&2
echo " restart: unless-stopped turns that into a crash loop" >&2
echo " - PIGGY_INTERNAL_TOKEN shorter than 32 characters" >&2
echo " - PIGGY_AGENT_MODEL naming a model that is not in the catalogue" >&2
echo " at apps/piggy/src/agent/models.json" >&2
echo " The log above says which; the config error names the key." >&2
exit "$EXIT_STILL_LIVE"
fi
fi