FactoryWager Wiki

Proton Integration — Operations Runbook

Token SSOT: all API tokens, bot tokens, R2 keys, and registry secrets are stored in Proton Pass and resolved with pass-cli inject / pass-cli run. Do not mint into shell history or hand-append secrets to ~/.reasonix/.env / .env as the authority path. Derived env files are a cache regenerated from vault templates.

Vault Layout

Vault Purpose Items Agent PAT (.env.pass-tokens)
Personal Personal SSH key id_ed25519 (dev) agent-work
factorywager Monorepo deploy + CF + Telegram (factory bot) R2, Registry, Cloudflare API Token, Telegram bot/webhook, emails PROTON_PASS_FACTORYWAGER_TOKENfactorywager-bot
bet-ticker bet-ticker-worker secrets VPS key, R2, Token, login PROTON_PASS_BET_TICKER_TOKEN
cascade-mover Cascade mover secrets SSH key, Server config PROTON_PASS_CASCADE_TOKEN
cloudflare Optional CF-scoped agent session (agent-only) PROTON_PASS_CLOUDFLARE_TOKEN

Note: factorywager-bot only sees the factorywager vault. Root env.template therefore uses pass://factorywager/... for Telegram as well as CF/R2 (not a separate tenants vault name the agent cannot open).

Cloudflare API token (canonical ref)

pass://factorywager/Cloudflare API Token/password

Env inventory (code ↔ vault)

Map harness Bun.env.* usage to env.template / Proton Pass refs (no secret values printed):

bun run env:inventory              # human summary + dispositions
bun run env:inventory:vault        # vault coverage focus
bun run env:inventory:json         # machine JSON
bun run env:inventory:ratchet      # fail if actionable gaps grow (baseline)
bun run env:inventory:baseline     # lock current gaps after intentional debt
bun run check:env-defaults         # optional config without fallback (harness)
bun run proton:check               # inject proof for all templates

Packages ↔ vault + env inventory (workspace packages/*)

env:inventory scans packages/ alongside lib/config/scripts/tools and emits reverse owners + root/product runtime (claim packages-graph-map-v11):

bun run env:inventory              # includes packages plane section
bun run env:inventory:bake         # → /registry/env-inventory.json
bun run audit:packages:full        # --vault --env
bun run audit:packages:env         # --env --vault-gap + dual bake
bun run audit:packages:vault       # --vault --vault-gap

Policy: scripts/lib/env-secret-policy.ts · baseline: scripts/env-secret-gap-baseline.json

Signal Meaning
Vaulted Code name has pass:// in an env.template
Alias e.g. TELEGRAM_BOT_TOKENTELEGRAM_BOT_FACTORY (already vaulted)
Bun.secrets service label *_SECRETS_SERVICE — service id, not password material
Actionable vault gap Real operator secret used in code with no template/vault ref
env:inventory --ratchet New actionable gaps fail (pre-commit soft; CI can hard-fail)

Secret Injection

# Resolve env.template → project .env (loads agent PAT from .env.pass-tokens)
bun run proton:inject:factorywager

# Same + strip/replace derived keys in ~/.reasonix/.env (MCP / Reasonix)
bun run proton:inject:factorywager:reasonix

# Per-product inject (writes product-local .env, not monorepo root)
bun run proton:inject:cascade
bun run proton:inject:bet-ticker
bun run proton:inject:scanner

# Prove all pass:// refs resolve (no secret values printed)
bun run proton:check
bun run proton:check:list   # refs only

# Inject then run a command with secrets in the environment
bun run proton:run -- factorywager --reasonix -- bun run cloudflare:env:validate
# or package alias:
bun run cloudflare:validate:vault

# Guided CF bootstrap (inject + verify + harness gates)
bash scripts/cloudflare-env-setup.sh

# Full deploy with vault injection
bun run proton:deploy:pages
# or
bun run cloudflare:deploy:vault
Command Script Output .env
proton:inject:factorywager scripts/proton-inject.sh monorepo root .env
proton:inject:factorywager:reasonix same + --reasonix root .env + derived ~/.reasonix/.env
proton:inject:cascade same projects/active/enterprise/cascade-mover-v3/.env
proton:inject:bet-ticker same projects/active/enterprise/bet-ticker-worker-v1.1/.env
proton:inject:scanner same projects/active/analysis/scanner/.env
proton:check scripts/proton-vault-check.sh temp only (deleted)
proton:run scripts/proton-run.sh inject then exec
proton:deploy:pages scripts/proton-deploy.sh root .env then deploy
Agent session scripts/agent-env.sh

Template refs use `` — see root env.template.

Agent Access

source scripts/agent-env.sh <project>
# Projects: factorywager, cloudflare, bet-ticker, cascade-mover
# Always set: PROTON_PASS_AGENT_REASON="why" on pass-cli calls

Agent PATs (pst_…) live in gitignored .env.pass-tokens (or env). They are not the Cloudflare API token — they only authenticate pass-cli to vault shares.

PAT Rotation

PATs expire 2027-07-27. To rotate:

pass-cli pat renew --pat-name <name> --expiration 1y
# Update .env.pass-tokens only (never commit)

SSH Agent

Daemon manages SSH keys from Personal vault.

Orgs

Vault gaps (known)

Template Status Notes
root env.template green bun run proton:check factorywager
bet-ticker green R2 + INITIAL_TOKEN
cascade-mover partial Security items OK; R2 login items not in vault yet — commented in cascade-mover-v3/env.template until R2 cascade-mover bucket exists
scanner green CF token only
Cloudflare Access blocked Dedicated least-privilege Access token must be created in the Cloudflare dashboard, stored as Cloudflare Access API Token, then wired only after proton:check resolves it; see cloudflare-access.md

When adding a secret: create the Proton Pass item first, then add `` to the matching env.template, then bun run proton:check.

After adding vault coverage, re-run bun run env:inventory:vault — the secret should move from “NOT in any env.template” to “used + vaulted”.

Closing actionable gaps

Two planes:

Plane What When
Machine-local mint ~/.factorywager/minted-secrets/<KEY> pass-cli dead / first boot
Proton Pass inject env.template pass://.env multi-host SSOT
bun run vault:gap:status         # Bun.inspect dashboard (offline-capable)
bun run vault:cli status         # same via tools/vault-cli.ts
bun run vault:gap:mint-local     # mint DOD + provision (+ play) to disk
bun run vault:gap:export-minted  # print pass-cli create lines for Terminal.app
bun run vault:gap:mint           # Pass CLI create (needs working pass-cli)
bun run vault:gap:wire           # env.template pass:// when Pass items exist
bun run vault:gap:close          # mint-local (+ Pass if available) + rebaseline
bun run env:inventory:ratchet    # human gaps only (OPENAI, SLACK)
bun run test:secrets             # one-shot secret ratchet tests
bun run test:secrets:watch       # continuous: bun test --watch secret-ratchet
bun run vault:cli:compile        # standalone ./vault binary (bun build --compile)

Continuous validation (tests/secret-ratchet.test.ts):

Env key Pass title Ratchet? Local mint?
DOD_PROOF_SECRET DOD Proof Secret no yes
DOD_ID_ENCRYPTION_KEY DOD ID Encryption Key no yes
PROVISION_ENCRYPTION_KEY Provision Encryption Key no yes
OPENAI_API_KEY OpenAI API Key yes no
SLACK_WEBHOOK_URL Slack Webhook URL yes no
TELEGRAM_CATALOG_RESEARCH_LLM_KEY via OPENAI alias no
PLAY_SIGNING_SECRET Play Signing Secret no yes
REPORT_SIGNING_SECRET Report Signing Secret no yes

Report / compliance HMAC (REPORT_SIGNING_SECRET)

Board integrity and deep-audit reports use lib/security/report-proof.ts. Without a secret the digest is still tamper-detect (sha3-256); HMAC is only present when REPORT_SIGNING_SECRET (or fallback PLAY_SIGNING_SECRET) is injected or mint-local’d.

# Local mint (no Pass create required)
bun run vault:gap:mint-local     # writes ~/.factorywager/minted-secrets/REPORT_SIGNING_SECRET

# Prefer vault SSOT when ready:
#   pass://factorywager/Report Signing Secret/password
# Uncomment in env.template:
#   REPORT_SIGNING_SECRET=
bun run proton:inject:factorywager
bun run compliance:bake:vault    # bake with injected secret → board.integrity.proof.hmac
Surface How HMAC appears
public/registry/compliance-board.json integrity.proof.hmac + check id: hmac
Monitoring / ops-summary slice scoreHint: integrity+hmac when present
Deep audit bun run ops:audit:deep

Full operator loop: compliance-portal.md. Bake ownership on Pages freshness: ops:snapshot (default) or OPS_SNAPSHOT_COMPLIANCE=0 / --no-compliance to skip.

Code path: requireSecret / requireMintableSecret in lib/security/mintable-secret.tsenv inject wins, then local mint.

If pass-cli is Killed: 9 (common in restricted agent hosts), use mint-local + export in Terminal.app / Pass UI.

CI: harness-gates runs bun run env:inventory:ratchet (hard fail on new human gaps only).

Anti-patterns

Do not Do instead
echo 'CLOUDFLARE_API_TOKEN=…' >> ~/.reasonix/.env Update vault item → bun run proton:inject:factorywager:reasonix
Paste tokens into chat / shell history Dashboard → Proton Pass item → inject
Treat .env as source of truth Treat .env as generated; re-inject after rotation
Use /user/tokens/verify for cfat_ tokens Use account verify or bun run cloudflare:env:validate
Point template at vault items that do not exist bun run proton:check before shipping template changes

DNS token & DKIM remediation

Audit-run findings (2026-07-27): the main account token (CLOUDFLARE_API_TOKEN) has no Zone.DNS:Read/Edit (verified 403) and no Access scope (verified 403). The separate vault item Cloudflare API Token (DNS)CLOUDFLARE_DNS_API_TOKEN has Zone.DNS:Read (verified HTTP 200 on dns_records) and Zone.DNS:Edit (verified via invalid-type probe returning 400-validation, not 403 — same result for the duplicate API Token DNS (factory-wager.com) item in the cloudflare vault; both DNS tokens are mutation-capable, so --apply needs no new token). The three Proton DKIM CNAMEs (protonmail[23]._domainkey.factory-wager.com) already exist and resolve publicly — confirmed via the API and DNS-over-HTTPS (plain dig to 1.1.1.1 is blocked on this network; use curl -H 'accept: application/dns-json' https://cloudflare-dns.com/dns-query?name=...&type=CNAME). DKIM values were not needed from the vault; no vault item holds them (they live in the Proton dashboard).

pass-cli incident (2026-07-27, resolved): every new pass-cli exec was instantly SIGKILLed — kernel load code signature error 2 (“embedded signature doesn’t match attached signature”) after the binary was rewritten in place at 03:35; the long-running ssh-agent daemon survived on the old inode. codesign --verify passed on disk, and a copy at a fresh path ran fine → stale code-signature state on the replaced inode. Repair: byte-identical inode refresh (cpmv over ~/.local/bin/pass-cli, backup at /tmp/pass-cli-backup-20260727). If it recurs, reinstall pass-cli from Proton instead.

Human steps if DNS records ever need (re)applying:

  1. Mint/confirm a Cloudflare token with Zone.DNS:Edit scoped to the factory-wager.com zone.
  2. Store it in the Proton vault item Cloudflare API Token (DNS) (field password).
  3. bun run proton:inject:factorywager:reasonix — writes CLOUDFLARE_DNS_API_TOKEN into ~/.reasonix/.env.
  4. Get the three DKIM targets from the Proton dashboard → Domain names → factory-wager.com → DKIM (per-domain, not in the vault). Export as PROTON_DKIM_TARGET_1/2/3 (order: protonmail, protonmail2, protonmail3).
  5. bash scripts/cloudflare-dns-sync.sh (dry-run) → bash scripts/cloudflare-dns-sync.sh --apply.
  6. Verify: dig +short CNAME protonmail._domainkey.factory-wager.com (or the DoH command above where port 53 is blocked).