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 | 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_TOKEN → factorywager-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-botonly sees the factorywager vault. Rootenv.templatetherefore usespass://factorywager/...for Telegram as well as CF/R2 (not a separatetenantsvault name the agent cannot open).
pass://factorywager/Cloudflare API Token/password
cfat_…) verify at /accounts/{account_id}/tokens/verify — not /user/tokens/verify.CLOUDFLARE_API_TOKEN after inject (MCP, Pages deploy, cloudflare:env:*).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/*)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
lib/harness/packages-vault-map.ts · scripts/lib/env-inventory-compact.ts/registry/packages-graph-map.json (map.vault · map.env) · /registry/env-inventory.json/portal/packages/ · /portal/env/pass:// refs, runtime present/missing booleans, and gap flagsPolicy: 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_TOKEN → TELEGRAM_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) |
# 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.
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.
PATs expire 2027-07-27. To rotate:
pass-cli pat renew --pat-name <name> --expiration 1y
# Update .env.pass-tokens only (never commit)
Daemon manages SSH keys from Personal vault.
~/.ssh/proton-pass-agent.sock~/.ssh/proton-pass-agent.pid~/Library/LaunchAgents/com.proton.pass-cli.ssh-agent.plist| 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”.
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):
~/.factorywager/minted-secrets)getGapList() is empty for new human gaps (within baseline)SECRET_RATCHET_STRICT=1 → fail until OPENAI + SLACK are injected/vaulted| 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_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.ts — env 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).
| 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 |
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 (cp → mv 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:
Zone.DNS:Edit scoped to the factory-wager.com zone.Cloudflare API Token (DNS) (field password).bun run proton:inject:factorywager:reasonix — writes CLOUDFLARE_DNS_API_TOKEN into ~/.reasonix/.env.PROTON_DKIM_TARGET_1/2/3 (order: protonmail, protonmail2, protonmail3).bash scripts/cloudflare-dns-sync.sh (dry-run) → bash scripts/cloudflare-dns-sync.sh --apply.dig +short CNAME protonmail._domainkey.factory-wager.com (or the DoH command above where port 53 is blocked).