feat(mvp): working secrets-engine CLI for the whynot-design npm publish lane

Implements SECRETS-WP-0002 end to end as a uv-managed Python package:

- catalog: non-secret lane registry + strict validator (build/test/prod)
- stage roles + OpenBao ACL policies; guards refuse wildcards, sys/, identity/,
  admin names, and cross-stage paths before any backend call
- plan/apply: dry-run-first, idempotent policy + approle apply, decision-gated
- decisions: State Hub lookup with local-fixture fallback; non-secret evidence
  to JSONL + hub progress, scrubbed of any value
- provision/verify: mode-0600 file import + generated test values; positive/
  negative checks that never print the value
- exec delivery: `exec --catalog ... -- npm publish` injects the token via a
  temp .npmrc for the child only, cleaned up on exit/failure/interrupt
- ops-warden routing contract + hardening backlog docs
- 34 tests incl. live OpenBao integration; scripts/demo-e2e.sh runs the full
  chain against a throwaway bao dev server

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-06-28 12:28:45 +02:00
parent 58c24cff53
commit a852d3f1ff
47 changed files with 3743 additions and 122 deletions

View file

@ -0,0 +1,75 @@
"""Provisioning: get a secret value into OpenBao without it touching coordination
surfaces.
Modes:
- from-file: read a value from a mode-0600 file outside the repo, write it to
OpenBao, and (caller's choice) leave the source file for the operator to shred.
- generate: mint a random non-production value for build/test lanes only.
The value is held only in process memory and passed straight to the backend. It
is never logged, returned, or written to evidence.
"""
from __future__ import annotations
import secrets as _secrets
import string
from pathlib import Path
from secrets_engine.catalog import CatalogEntry
from secrets_engine.errors import ProvisioningError
from secrets_engine.openbao import OpenBaoClient
def _read_value_file(path: Path) -> str:
if not path.exists():
raise ProvisioningError(f"value file not found: {path}")
st = path.stat()
if st.st_mode & 0o077:
raise ProvisioningError(
f"value file {path} is group/other-accessible "
f"(mode {oct(st.st_mode & 0o777)}); must be 0600"
)
for parent in path.resolve().parents:
if (parent / ".git").exists():
raise ProvisioningError(
f"value file {path} is inside a Git worktree ({parent}); "
"keep secret material outside repos"
)
value = path.read_text(encoding="utf-8").strip()
if not value:
raise ProvisioningError(f"value file {path} is empty")
return value
def provision_from_file(
client: OpenBaoClient, entry: CatalogEntry, field: str, file_path: Path
) -> str:
"""Import a value from a strict-permission file. Returns the field name only."""
if field not in entry.fields:
raise ProvisioningError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
value = _read_value_file(Path(file_path))
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field
def provision_generated(client: OpenBaoClient, entry: CatalogEntry, field: str) -> str:
"""Generate a random NON-PRODUCTION value for build/test lanes only."""
if entry.stage == "prod":
raise ProvisioningError(
f"refusing to generate a value for prod lane '{entry.id}'; "
"production values must be provisioned, not generated"
)
if field not in entry.fields:
raise ProvisioningError(
f"field '{field}' not declared in lane '{entry.id}' fields {entry.fields}"
)
alphabet = string.ascii_letters + string.digits
value = "test-" + "".join(_secrets.choice(alphabet) for _ in range(32))
client.ensure_kv_mount(entry.mount)
client.kv_put(entry.mount, entry.path, field, value)
del value
return field