# secrets-engine CLI ## Terminology (Gitea is overloaded — be explicit) | Term we use | Means | Example | Not to be confused with | | --- | --- | --- | --- | | **org** | the Gitea organisation | `coulomb` | the npm scope | | **repo** | the Gitea repository / product | `whynot-design` | an org; a Gitea "project" board | | **npm scope** | the `@`-prefix npm name | `@whynot` | the org or the repo | | **npm package** | the published artifact | `@whynot/design` | the repo it's built from | | **lane / catalog id** | a secrets-engine secret lane | `whynot-design-npm-publish` | — | A catalog entry carries `org` + `repo` explicitly (never a bare "owner"), and the npm registry/scope live in `delivery_config.npm` as data — the engine never hardcodes a registry. The pilot publishes `@whynot/design` from the `coulomb/whynot-design` repo to `https://forgejo.coulomb.social/api/packages/coulomb/npm/`. ## Install ```bash uv venv && uv pip install -e ".[dev]" source .venv/bin/activate secrets-engine --version ``` ## Environment | Var | Default | Purpose | | --- | --- | --- | | `BAO_ADDR` | `http://127.0.0.1:8200` | OpenBao address | | `BAO_TOKEN` | _(unset)_ | OpenBao token (or use `--bootstrap-token-file`) | | `SECRETS_ENGINE_HUB_URL` | `http://127.0.0.1:8000` | State Hub for decisions + evidence (empty to disable) | | `SECRETS_ENGINE_CATALOG` | `./catalog` | catalog directory | | `SECRETS_ENGINE_EVIDENCE` | `./.evidence` | local non-secret evidence log | ## Commands ```text secrets-engine catalog list secrets-engine catalog show secrets-engine decision inspect secrets-engine plan --stage secrets-engine apply --stage [--dry-run] [--bootstrap-token-file F] secrets-engine provision --stage --field NAME (--from-file F | --generate) secrets-engine verify [--field NAME] [--positive] [--negative] secrets-engine handoff --stage --role-id-file F --secret-id-file F secrets-engine exec --catalog [--field NAME] [--mode auto|npm-config|exec-env] -- CMD... secrets-engine policy publication secrets-engine route [--json] secrets-engine revoke [--dry-run] ``` `policy publication` resolves a lane's effective publication scope and the env var the token is injected under, per the netkingdom publication-scope policy (see [docs/publication-scope-policy.md](publication-scope-policy.md)). `` is a catalog id or a decision/CCR ref (matched against `approval.decision_ref`). `plan` and `apply --dry-run` never mutate OpenBao. For decision-gated lanes they may render with `decision: ` when the approval object is not reachable; non-dry-run `apply` remains decision-gated. `handoff` is for `kind: auth-capability` lanes such as `warden-sign`. It mints a fresh AppRole `secret_id` and writes `role_id` plus `secret_id` to caller-chosen mode-0600 files outside Git worktrees. It never prints the `secret_id`; use the resulting files only for attended out-of-band delivery. ## Exit codes | Code | Meaning | | --- | --- | | 0 | success | | 2 | catalog error (missing/invalid lane) | | 3 | decision error (unapproved / superseded / missing) | | 4 | policy guard error (out-of-stage / wildcard / broad admin) | | 5 | backend error (OpenBao unreachable / failed) | | 6 | provisioning error (bad file mode / inside repo / missing field) | | 7 | verification failed | | 8 | delivery error | ## End-to-end demo ```bash SECRETS_ENGINE_HUB_URL="" bash scripts/demo-e2e.sh ``` Boots a throwaway in-memory OpenBao dev server and runs the whole pilot chain: plan → apply → provision → verify(+/-) → exec (npm-config injection) → route → revoke. Nothing is persisted; the token is a throwaway local string. ## npm publish wiring (dry-run) ```bash bash scripts/npm-publish-demo.sh ``` Boots a throwaway OpenBao, applies + provisions the lane with a *fake* token, and runs a **real `npm publish --dry-run`** through `secrets-engine exec` against a scratch package. Proves npm in the child resolves its auth token from the temp `.npmrc` secrets-engine injected, builds the tarball, and reaches the publish step — while the parent shell never holds the token. For a real publish, provision a real npm automation token the same way and drop `--dry-run`. ## Auth-capability: warden-sign ```bash # Preview the non-KV policy/AppRole lane; no mutation. SECRETS_ENGINE_HUB_URL="" secrets-engine apply warden-sign --stage prod --dry-run # Live apply and handoff require an approved decision and an out-of-repo # bootstrap token file. Values are written to files, not stdout. BAO_ADDR=https://bao.coulomb.social \ secrets-engine handoff warden-sign --stage prod \ --bootstrap-token-file ~/.secrets-engine/bootstrap/prod-warden-sign.token \ --role-id-file ~/.secrets-engine/handoff/warden-sign.role_id \ --secret-id-file ~/.secrets-engine/handoff/warden-sign.secret_id ``` See [warden-sign-auth-capability.md](warden-sign-auth-capability.md) for the full runbook and State Hub pointer payload. ## Pilot closeout: whynot-design real publish ```bash scripts/whynot-real-publish-preflight.sh ``` The real publish path is documented in [whynot-design-real-publish-closeout.md](whynot-design-real-publish-closeout.md). ## Pilot: whynot-design npm publish ```bash # 1. inspect the approved decision secrets-engine decision inspect whynot-design-npm-publish # 2. preview the OpenBao changes (no mutation) secrets-engine plan whynot-design-npm-publish --stage prod # 3. apply policy + approle secrets-engine apply whynot-design-npm-publish --stage prod # 4. provision the token from a mode-0600 file OUTSIDE the repo secrets-engine provision whynot-design-npm-publish --stage prod \ --field npm_token --from-file ~/.secrets-engine/whynot.token # 5. prove access without printing the value secrets-engine verify whynot-design-npm-publish --field npm_token --positive --negative # 6. publish with the token injected into the child only secrets-engine exec --catalog whynot-design-npm-publish -- npm publish ```