Files
pig/.env.example
T
claude f0173440e4
CI / verify (push) Successful in 7m6s
CI / publish (push) Has been skipped
Put Piggy on Prime Agent, and let it write to the book
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>
2026-08-14 05:26:28 -07:00

348 lines
18 KiB
Bash

# ---------------------------------------------------------------------------
# PIG — environment
# Copy to .env and fill in. Never commit .env.
# ---------------------------------------------------------------------------
# --- Database ---------------------------------------------------------------
# PIG owns this database exclusively. Do not point it at a database shared with
# another application.
DATABASE_URL=postgres://pig:CHANGEME@localhost:5432/pig
#
# Compose only, and REQUIRED there: docker-compose.yml interpolates it with
# `${POSTGRES_PASSWORD:?…}`, so every compose command — including
# `docker compose config` — fails outright until it is set. It is also half of
# the DATABASE_URL compose builds for the containers, which is why running from
# source needs the line above and running in containers needs this one.
# Generate a fresh one; never reuse another service's.
POSTGRES_PASSWORD=CHANGEME
POSTGRES_USER=pig
POSTGRES_DB=pig
# --- Auth (Supabase) --------------------------------------------------------
# PIG uses Supabase for authentication ONLY. It stores no passwords and issues
# no sessions of its own; it verifies incoming JWTs against the project JWKS.
#
# IMPORTANT: authorization does NOT follow from having a Supabase account.
# A user must also have a row in PIG's `users` table. If this Supabase project
# is shared with another application, that application's users get nothing here
# until they are explicitly invited.
SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
SUPABASE_ANON_KEY=
# Service key is only needed for administrative user provisioning. Omit it and
# PIG runs fine in invite-only mode. Treat it as the most powerful secret here.
SUPABASE_SERVICE_KEY=
# --- Auth: on-premises (OIDC) ---------------------------------------------
# Set PIG_OIDC_ISSUER to authenticate against your own identity provider —
# Okta, Entra, Keycloak, Auth0, Authentik, Google Workspace, anything
# standards-compliant. It TAKES PRECEDENCE over the Supabase values above, so
# an on-prem install can leave those in place.
#
# PIG never sees a password. It verifies the token your provider issued and
# reads two things: a stable subject, and an email. Everything else — teams,
# roles, capabilities — is PIG's own data keyed on that subject, so users are
# provisioned in PIG by invite, not by your directory.
PIG_OIDC_ISSUER=
# Optional. Discovered from the issuer's /.well-known/openid-configuration when
# omitted. Set it to skip discovery entirely on an air-gapped network.
PIG_OIDC_JWKS_URI=
# STRONGLY recommended. Without it, a token your provider issued for ANY other
# application in the same tenant is accepted here as a PIG session.
PIG_OIDC_AUDIENCE=
# Comma-separated, in preference order. Defaults to email,preferred_username,upn
# which covers most providers; Entra sometimes needs upn first.
PIG_OIDC_EMAIL_CLAIMS=
# --- Application ------------------------------------------------------------
PIG_PORT=8920
PIG_PUBLIC_URL=http://localhost:8920
NODE_ENV=development
# --- Learn videos, hosted by PIG ---------------------------------------------
# PIG serves its own Learn videos from disk, as a native <video> — no embed
# host, no iframe, and therefore nothing to add to the proxy's frame-src.
#
# PIG_MEDIA_DIR is where the application READS them. Running from source that
# is a path on this machine, relative to the repository root; in the container
# it is always /app/media and docker-compose sets it for you.
PIG_MEDIA_DIR=./media
#
# PIG_MEDIA_HOST_DIR is the HOST directory docker-compose bind-mounts there,
# read-only. Two names for the two sides of the mount, on purpose. It must
# exist before `compose up` — Docker creates a missing bind source as an empty
# root-owned directory, which serves 404s and cannot be written to without
# sudo. On the deployment host this is normally /opt/pig/media.
PIG_MEDIA_HOST_DIR=./media
#
# Filenames are CONTENT-ADDRESSED — `<slug>.<hash>.mp4` — because the files
# themselves are served without authentication while the listing behind
# /api/learn stays code-gated. The hash is what makes a URL unguessable. See
# deploy/README.md, "Learn videos", for the trade this makes and its cost.
# --- Deployment: which image to run -----------------------------------------
# Leave EMPTY to build from the working tree, which is what a development or
# self-hosted-from-source install wants. Set it to a published tag and
# scripts/deploy.sh pulls instead of building, and compose runs exactly that
# image for both the app and Piggy — never one version of each.
#
# Set automatically by scripts/autodeploy.sh; you only put it here to pin a
# specific release by hand.
# PIG_IMAGE=git.karti.ai/pig/pig:release-2026-08-13
PIG_IMAGE=
# The loopback port the app is published on. TLS belongs to the proxy in front.
PIG_HOST_PORT=8920
# Comma-separated emails granted platform-admin rights.
# Every address listed here MUST already have an account. An address listed but
# unregistered is a standing offer of admin to whoever claims it first.
PIG_ADMIN_EMAILS=
# Invite code gating self-serve profile creation. Rotate freely.
PIG_INVITE_CODE=
# Encrypts the credentials an admin types into the settings UI — the Notion and
# Google OAuth secrets in particular, which the API refuses to accept without
# it. Base64-encoded 32 bytes, and NOT interchangeable with any other secret
# here:
#
# openssl rand -base64 32
#
# Rotating it does not re-encrypt what is already stored; anything written
# under the old key has to be entered again.
PIG_SETTINGS_ENCRYPTION_KEY=
# --- Prime Intellect API key ------------------------------------------------
# ONE key, two consumers, and it is worth knowing both before you scope it:
#
# - the API syncs GPU availability into `inventory_listings` from
# api.primeintellect.ai;
# - Piggy calls models on api.pinference.ai, which bills the same account.
#
# Mint it at https://app.primeintellect.ai/dashboard/tokens with the NARROWEST
# scope that works: `Availability -> Read`, plus inference if Piggy is on. PIG
# never provisions infrastructure and must not hold a key that could. Set an
# expiry.
#
# Piggy accepts PIGGY_INFERENCE_API_KEY as an alias for this value, so a .env
# written before Piggy moved onto Prime Inference keeps working untouched. They
# are the same key now; set one of them, not two different ones.
#
# SET IT OR COMMENT IT OUT — do not leave it blank once Piggy is on. Blank is
# harmless to the API, which treats it as absent, but the piggy container is
# handed the empty string and Piggy's config refuses it:
#
# Invalid Piggy configuration:
# PRIME_API_KEY: String must contain at least 1 character(s)
#
# ...followed by a crash loop. The same applies in reverse to the alias below.
# Measured, not theorised: an empty line is not an absent one.
PRIME_API_KEY=
PRIME_API_BASE=https://api.primeintellect.ai
# Rate limits are undocumented upstream; the sync backs off empirically.
PRIME_SYNC_ENABLED=false
PRIME_SYNC_INTERVAL_MINUTES=30
# --- Piggy (the in-app agent) ----------------------------------------------
# Piggy is a Prime Agent session — Prime Intellect's own agent harness, run as
# a library inside PIG — holding PIG's CRM tools and NOTHING else. The harness
# is constructed with every built-in tool disabled and an explicit allowlist on
# top, so the model has no shell, no filesystem and no Python; the running tool
# list is compared with the allowlist at session start, and a mismatch is a
# startup failure rather than a surprise.
#
# It drains a leased queue, serves chat on an authenticated internal listener,
# and — new, and the reason the settings below matter — it can WRITE to the CRM.
# Generate one internal token and give the same value to API + Piggy. Never
# publish the Piggy listener or put this token in a URL.
#
# THREE keys turn the agent on, and all three are required together:
#
# PIGGY_ENABLED=true the API offers the chat surface
# PRIME_API_KEY the model credential (see above; Piggy also
# accepts the legacy PIGGY_INFERENCE_API_KEY)
# PIGGY_INTERNAL_TOKEN 32+ characters, the same value for API and Piggy
#
# (PIGGY_INTERNAL_URL is the fourth thing the API needs, and docker-compose.yml
# already sets it to http://piggy:8931. Only a Piggy running outside Compose
# needs the line below.)
#
# Under Compose that is the whole configuration: scripts/deploy.sh reads
# PIGGY_ENABLED from this file and adds `--profile piggy` to the pull, the
# build, the `up` and the rollback, so the agent ships with the app rather than
# being started by hand and then quietly left on an old image. Everything else
# below has a working default and exists to be lowered.
#
# EVERY Piggy setting is read from this environment ONCE, at Piggy's boot. None
# of it is admin-selectable at runtime: changing a budget, a mode or the DEFAULT
# model means editing this file and restarting the container. The one thing a
# user picks for themselves is which model answers a given conversation, and
# even that is a choice between the five in apps/piggy/src/agent/models.json —
# a file in the image, not a setting here.
PIGGY_ENABLED=false
# The legacy spelling of PRIME_API_KEY, kept as an alias so a deployment that
# predates the harness swap keeps starting. Set PRIME_API_KEY above instead and
# leave this COMMENTED OUT; uncomment it only if that is the name your host
# already has, and then comment PRIME_API_KEY out.
#
# Commented rather than blank, and that is the whole point of the line. A blank
# `PIGGY_INFERENCE_API_KEY=` is passed to the container as the empty string,
# which fails Piggy's minimum-length check *even when PRIME_API_KEY is set
# correctly* — so the agent crash-loops with a message about the key you did
# not use. With neither name set, the error is the honest one:
# "PRIME_API_KEY ... is required."
# PIGGY_INFERENCE_API_KEY=
# Required to turn the agent on. 32 characters minimum; anything shorter is
# refused at boot rather than accepted as weak.
# openssl rand -hex 32
PIGGY_INTERNAL_TOKEN=
# Where the API reaches the chat server. Under Compose this is set for you to
# http://piggy:8931; set it here only when running Piggy from source.
PIGGY_INTERNAL_URL=http://127.0.0.1:8931
# Model and host for the QUEUE worker, which still calls the endpoint directly.
# Read by both the API (to display) and Piggy (to call).
PIGGY_MODEL=nvidia/nemotron-3-nano-30b-a3b
PIGGY_INFERENCE_BASE=https://api.pinference.ai/api/v1
# --- The agent itself -------------------------------------------------------
# All four have working defaults in apps/piggy/src/config.ts. Uncomment one only
# to change it, and read the note on PIGGY_AGENT_THINKING before you change the
# model — the two are related in a way that is not obvious from the outside.
#
# Which model the agent answers with when a user has expressed no preference in
# the picker. It must be one of the five in apps/piggy/src/agent/models.json:
# anything else is not registered with the harness and is rejected at boot,
# which is deliberate — the alternative is a model that 404s on the first turn.
# PIGGY_AGENT_MODEL=nvidia/nemotron-3-nano-30b-a3b
#
# What Piggy may do to the CRM. `confirm` is the shipped default and the one to
# run in production:
#
# read_only the pre-agent behaviour; Piggy answers and never writes
# confirm a write is PROPOSED as a card and applied when a person clicks
# auto Piggy writes directly, as the signed-in user
#
# Whatever this says, contracts, commitments, allocations and compliance records
# ALWAYS require a click — `auto` does not buy them. Every write runs as the
# calling user's own principal, so Piggy can never reach a record its user
# could not.
# PIGGY_AGENT_MODE=confirm
#
# How hard the model thinks before answering. Leave this alone unless you have
# changed the model, and read this paragraph if you have: it is the single
# setting most likely to make a working deployment look broken.
#
# The harness's own default is `medium`, tuned for a coding agent. On nemotron
# that produced 6,195 output tokens of reasoning and an EMPTY answer — the turn
# hit its token ceiling mid-thought and came back with finish_reason `length`.
# `low` was worse. `off` maps, for that model, to the endpoint's
# `reasoning_effort: none` and answered the same question correctly in 149
# output tokens.
#
# The mapping is PER MODEL and lives in `thinkingLevelMap` in
# apps/piggy/src/agent/models.json. A model with no entry (deepseek, opus,
# gpt-5.6) sends no reasoning parameter at `off` and gets the endpoint's own
# default, which may be verbose. So: empty answers, exhausted budgets and
# surprising bills after a model change are this setting, not a broken agent.
# PIGGY_AGENT_THINKING=off # off | minimal | low | medium | high | xhigh | max
#
# Output tokens one agent turn may spend, reasoning included. Clamped down to
# the chosen model's own ceiling, so raising it cannot ask for more than the
# endpoint will return.
# PIGGY_AGENT_MAX_TOKENS=4096
#
# Where the harness keeps its state — the models.json it reads, and anything
# else it writes. docker-compose.yml pins it to /var/lib/piggy-agent, a
# directory the image creates owned by the unprivileged runtime user, and there
# is no reason to set it here for a Compose deployment.
#
# If you do set it, on a bare-metal install: it MUST NOT be the checkout or any
# directory holding code. The harness discovers extensions, skills and context
# files from its cwd, and Piggy points the harness's cwd here. The default is
# ~/.pig/piggy-agent for exactly that reason.
# PIGGY_AGENT_DIR=/var/lib/piggy-agent
PIGGY_CHAT_HOST=127.0.0.1
PIGGY_CHAT_PORT=8931
# Only containers on a private network need this; never combine it with a
# published Piggy port. Compose sets it to true for the container, because the
# API calls Piggy across the Compose network.
PIGGY_CHAT_ALLOW_NON_LOOPBACK=false
# Tuning. COMMENTED OUT ON PURPOSE, and worth understanding before you
# uncomment one: an empty line here is not the same as an absent one. Compose
# passes `PIGGY_MAX_TOKENS=` through as the empty string, which coerces to 0 and
# fails Piggy's positive-integer check at boot. Leave a setting commented to get
# the default from apps/piggy/src/config.ts; give it a value or nothing at all.
#
# PIGGY_LEASE_SECONDS=300 # queue lease, renewed at half the interval
# PIGGY_POLL_INTERVAL_MS=2000 # how often an idle worker looks for a task
# PIGGY_MAX_TOKENS=1024 # per queued task
# PIGGY_CHAT_MAX_TOKENS=2048 # per interactive answer; tools return tables
# PIGGY_MAX_TURNS=4 # model calls per chat turn, tool round trips included
# PIGGY_WORKER_ID= # defaults to hostname:pid; only set it if you run two
#
# Reasoning is off. The default model thinks aloud when asked to, reasoning
# tokens bill like any other, and the chat panel is on every page — so the
# volume is set by how often people type. Raise it to make the UI's reasoning
# panel reachable while chasing a wrong figure, not in normal operation.
# PIGGY_REASONING_EFFORT=none # none | low | medium | high
#
# Model price in CENTS PER MILLION TOKENS, which is what makes the recorded cost
# of a run exact integer arithmetic. These are the published prices of the
# default model and must be changed with it: a stale price is worse than none,
# because it still looks like a measurement.
# PIGGY_PRICE_INPUT_CENTS_PER_MTOK=5
# PIGGY_PRICE_OUTPUT_CENTS_PER_MTOK=20
# --- Deployment: the release poller -----------------------------------------
# Only relevant on a host running scripts/autodeploy.sh. These belong in
# /etc/pig/autodeploy.env (read by the systemd unit), not here — they are
# listed here so the whole deployment surface is in one file to read.
#
# The registry credential is NOT an environment variable. It is a file, mode
# 0600, holding a pull-only token and nothing else:
#
# /etc/pig/registry-token
#
# Mint it in Gitea as a token with `read:package` scope ONLY. A token that can
# write packages, or push to the repository, defeats the point: the reason CI
# cannot deploy to production is that no build-side credential should be able
# to change what production runs, and a write-capable token here reintroduces
# exactly that from the other end.
#
# PIG_REGISTRY_USER=pig-deploy # the Gitea user that owns the token
# PIG_REGISTRY=git.karti.ai
# PIG_IMAGE_REPO=pig/pig # Gitea lowercases the owner
# PIG_REGISTRY_TOKEN_FILE=/etc/pig/registry-token
# PIG_REPO_DIR=/opt/pig
# PIG_RELEASE_TAG_PREFIX=release-
#
# The public origin deploy.sh checks AFTER the container is healthy, to catch a
# proxy that is answering 200 with an empty body. Defaults to PIG_PUBLIC_URL
# above, then to the production origin.
# PIG_DEPLOY_PUBLIC_URL=https://primeintellectgrowth.com
# A string the real application always renders. Change it only if index.html's
# mount point changes.
# PIG_DEPLOY_PUBLIC_MARKER=<div id="root">
# --- Slack ------------------------------------------------------------------
SLACK_BOT_TOKEN=
SLACK_SIGNING_SECRET=
SLACK_APP_TOKEN=
# --- Buzz (https://github.com/block/buzz) -----------------------------------
# Buzz agents reach PIG through the MCP server, so no PIG-specific credential is
# required. These are only for PIG pushing notifications into a Buzz relay.
BUZZ_RELAY_URL=
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
NOTION_REDIRECT_URI=http://localhost:8920/api/imports/notion/oauth/callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Must use the PIG_PUBLIC_URL origin and exact /oauth/google/callback path.
GOOGLE_REDIRECT_URI=http://localhost:8920/oauth/google/callback
BUZZ_PRIVATE_KEY=
# Optional NIP-OA owner attestation JSON for an agent identity.
BUZZ_AUTH_TAG=