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

0
tests/__init__.py Normal file
View file

77
tests/test_catalog.py Normal file
View file

@ -0,0 +1,77 @@
import copy
import pytest
from secrets_engine.catalog import validate_entry, load_catalog
from secrets_engine.config import repo_root
from secrets_engine.errors import CatalogError
VALID = {
"id": "test-lane",
"owner": "team",
"stage": "test",
"mount": "secret",
"path": "test/team/thing",
"fields": ["api_token"],
"consumers": [{"name": "c", "auth": "approle", "claim": "role:c"}],
"delivery_modes": ["exec-env"],
"approval": {"model": "bootstrap-only"},
"verification": {"positive": "x", "negative": "y"},
"rotation": {"ttl": "1h"},
"deactivation": {"expectation": "delete"},
"audit": {"evidence": "non-secret"},
}
def test_valid_entry_parses():
e = validate_entry(VALID)
assert e.id == "test-lane"
assert e.policy_name == "se-test-test-lane"
assert not e.approval_required()
@pytest.mark.parametrize("field", ["stage", "mount", "path", "fields", "approval", "delivery_modes"])
def test_missing_required_field_rejected(field):
data = copy.deepcopy(VALID)
data.pop(field)
with pytest.raises(CatalogError):
validate_entry(data)
def test_bad_stage_rejected():
data = copy.deepcopy(VALID)
data["stage"] = "staging"
with pytest.raises(CatalogError):
validate_entry(data)
def test_unknown_delivery_mode_rejected():
data = copy.deepcopy(VALID)
data["delivery_modes"] = ["telepathy"]
with pytest.raises(CatalogError):
validate_entry(data)
def test_wildcard_path_rejected():
data = copy.deepcopy(VALID)
data["path"] = "test/*"
with pytest.raises(CatalogError):
validate_entry(data)
def test_inline_secret_value_rejected():
data = copy.deepcopy(VALID)
data["token"] = "npm_realvalue"
with pytest.raises(CatalogError):
validate_entry(data)
def test_repo_catalog_loads_and_has_pilot():
entries = load_catalog(repo_root() / "catalog")
assert "whynot-design-npm-publish" in entries
pilot = entries["whynot-design-npm-publish"]
assert pilot.stage == "prod"
assert pilot.approval_required()
# build/test/prod stage separation is representable
stages = {e.stage for e in entries.values()}
assert {"build", "prod"} <= stages

52
tests/test_decisions.py Normal file
View file

@ -0,0 +1,52 @@
import pytest
from secrets_engine.catalog import validate_entry
from secrets_engine.decisions import Decision, require_approved, resolve_decision
from secrets_engine.errors import DecisionError
from tests.test_catalog import VALID
def _approved(model="decision"):
d = dict(VALID, approval={"model": model, "decision_ref": "x"})
return validate_entry(d)
def test_bootstrap_only_needs_no_decision():
e = validate_entry(dict(VALID, approval={"model": "bootstrap-only"}))
require_approved(e, None) # must not raise
def test_unapproved_decision_refused():
e = _approved()
d = Decision(id="d", title="t", status="pending", superseded_by=None, source="hub")
with pytest.raises(DecisionError):
require_approved(e, d)
def test_superseded_decision_refused():
e = _approved()
d = Decision(id="d", title="t", status="resolved", superseded_by="d2", source="hub")
with pytest.raises(DecisionError):
require_approved(e, d)
def test_approved_decision_passes():
e = _approved()
d = Decision(id="d", title="t", status="resolved", superseded_by=None, source="hub")
require_approved(e, d) # must not raise
def test_local_fixture_resolves(tmp_path):
(tmp_path / ".decisions").mkdir()
(tmp_path / ".decisions" / "myref.yaml").write_text(
"id: myref\ntitle: t\nstatus: resolved\nsuperseded_by: null\n"
)
d = resolve_decision(hub_url="http://127.0.0.1:1", repo_root=tmp_path, decision_ref="myref")
assert d.source == "local-fixture"
assert d.is_approved()
def test_missing_decision_raises(tmp_path):
with pytest.raises(DecisionError):
resolve_decision(hub_url="http://127.0.0.1:1", repo_root=tmp_path, decision_ref="nope")

67
tests/test_guards.py Normal file
View file

@ -0,0 +1,67 @@
"""Negative checks: a plan that would grant broad power must fail closed."""
import pytest
from secrets_engine.catalog import validate_entry
from secrets_engine.errors import PolicyGuardError
from secrets_engine.plan import build_plan
from secrets_engine.roles import assert_path_in_stage, assert_policy_safe
from tests.test_catalog import VALID
def _entry(**over):
d = dict(VALID)
d.update(over)
return validate_entry(d)
def test_wildcard_policy_path_refused():
with pytest.raises(PolicyGuardError):
assert_policy_safe("se-test-x", {"secret/*": ["read"]})
def test_sys_path_refused():
with pytest.raises(PolicyGuardError):
assert_policy_safe("se-test-x", {"sys/policies/acl/x": ["read"]})
def test_identity_path_refused():
with pytest.raises(PolicyGuardError):
assert_policy_safe("se-test-x", {"identity/entity/x": ["read"]})
def test_admin_policy_name_refused():
with pytest.raises(PolicyGuardError):
assert_policy_safe("platform-admin", {"secret/data/x": ["read"]})
def test_broad_capability_refused():
with pytest.raises(PolicyGuardError):
assert_policy_safe("se-test-x", {"secret/data/x": ["sudo"]})
def test_out_of_stage_path_refused():
# a 'test' lane pointing into the build prefix is rejected
e = _entry(stage="test", path="build/sneaky/thing")
with pytest.raises(PolicyGuardError):
assert_path_in_stage(e)
def test_build_lane_must_use_build_prefix():
e = _entry(stage="build", path="random/thing")
with pytest.raises(PolicyGuardError):
assert_path_in_stage(e)
def test_stage_mismatch_in_plan_refused():
e = _entry(stage="test", path="test/team/thing")
with pytest.raises(PolicyGuardError):
build_plan(e, "prod")
def test_valid_plan_builds():
e = _entry(stage="test", path="test/team/thing")
plan = build_plan(e, "test", decision_id="d1")
assert plan.policy_name == "se-test-test-lane"
assert any(a.kind == "approle" for a in plan.actions)
assert "secret/data/test/team/thing" in plan.policy_hcl

View file

@ -0,0 +1,103 @@
"""Live integration test against a throwaway OpenBao dev server.
Skipped automatically if the `bao` CLI is not on PATH. Boots an in-memory dev
server on a private port, then drives apply -> provision -> verify(+/-) ->
exec-delivery and asserts the value is reachable by the child but not the parent.
"""
import json
import os
import shutil
import socket
import subprocess
import time
from pathlib import Path
import pytest
from secrets_engine.apply import apply_plan
from secrets_engine.catalog import get_entry
from secrets_engine.config import repo_root
from secrets_engine.exec_delivery import exec_with_secret
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.plan import build_plan
from secrets_engine.provision import provision_from_file
from secrets_engine.verify import verify_negative, verify_positive
pytestmark = pytest.mark.skipif(
shutil.which("bao") is None and shutil.which("vault") is None,
reason="no OpenBao/Vault CLI on PATH",
)
def _free_port() -> int:
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
@pytest.fixture()
def bao_dev():
bao = shutil.which("bao") or shutil.which("vault")
port = _free_port()
addr = f"http://127.0.0.1:{port}"
token = "se-test-root"
proc = subprocess.Popen(
[bao, "server", "-dev", f"-dev-root-token-id={token}",
f"-dev-listen-address=127.0.0.1:{port}"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
client = OpenBaoClient(addr=addr, token=token, bao_bin=bao)
for _ in range(50):
if client.is_reachable():
break
time.sleep(0.2)
else:
proc.kill()
pytest.fail("dev OpenBao did not become reachable")
try:
yield client
finally:
proc.kill()
def test_full_chain(bao_dev, tmp_path):
client = bao_dev
entry = get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
plan = build_plan(entry, "prod", decision_id="test")
apply_plan(client, entry, plan)
# provision from a mode-0600 file outside the repo (tmp_path is outside)
tokenfile = tmp_path / "tok"
tokenfile.write_text("npm_integrationTESTvalue1234567890")
os.chmod(tokenfile, 0o600)
provision_from_file(client, entry, "npm_token", tokenfile)
pos = verify_positive(client, entry, "npm_token")
assert pos.passed, pos.detail
neg = verify_negative(client, entry)
assert neg.passed, neg.detail
# exec delivery: child can resolve token via npmrc; assert via a probe script
probe = tmp_path / "probe.sh"
probe.write_text(
"#!/usr/bin/env bash\n"
'grep -q _authToken "$NPM_CONFIG_USERCONFIG" && echo CHILD_HAS_TOKEN\n'
)
os.chmod(probe, 0o755)
rc = exec_with_secret(client, entry, "npm_token", [str(probe)], mode="npm-config")
assert rc == 0
# the parent process never received the value as an env var
assert "SE_NPM_TOKEN" not in os.environ
def test_idempotent_apply(bao_dev):
client = bao_dev
entry = get_entry(repo_root() / "catalog", "whynot-design-npm-publish")
plan = build_plan(entry, "prod", decision_id="test")
first = apply_plan(client, entry, plan)
second = apply_plan(client, entry, plan)
# policy should be reported unchanged on the second apply
assert any("unchanged" in s for s in second.skipped)

View file

@ -0,0 +1,41 @@
import json
from secrets_engine.evidence import EvidenceWriter, _scrub
from secrets_engine.redact import looks_secret, redact_text
def test_redact_known_token_shapes():
assert "npm_" not in redact_text("token=npm_abcdEFGH12345678abcd")
assert "REDACTED" in redact_text("token=npm_abcdEFGH12345678abcd")
assert "ghp_" not in redact_text("ghp_0123456789abcdef0123")
def test_redact_extra_literal():
out = redact_text("the value is hunter2hunter2", extra=["hunter2hunter2"])
assert "hunter2" not in out
def test_looks_secret():
assert looks_secret("npm_token")
assert looks_secret("API_KEY")
assert not looks_secret("path")
def test_scrub_drops_secret_keys_and_redacts():
scrubbed = _scrub({"token": "npm_realvalue123456789", "path": "a/b", "note": "ghp_0123456789abcdef0123"})
assert scrubbed["token"].startswith("<omitted")
assert scrubbed["path"] == "a/b"
assert "ghp_" not in scrubbed["note"]
def test_evidence_record_has_no_value(tmp_path):
w = EvidenceWriter(evidence_dir=tmp_path, hub_url="") # hub disabled
rec = w.record(
"provision", result="from-file", catalog_id="lane", stage="prod",
detail={"field": "npm_token", "value": "npm_shouldnotappear123"},
)
blob = json.dumps(rec)
assert "npm_shouldnotappear123" not in blob
# written to disk too
files = list(tmp_path.glob("evidence-*.jsonl"))
assert files and "npm_shouldnotappear123" not in files[0].read_text()