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:
parent
58c24cff53
commit
a852d3f1ff
47 changed files with 3743 additions and 122 deletions
116
src/secrets_engine/evidence.py
Normal file
116
src/secrets_engine/evidence.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""Non-secret evidence writer.
|
||||
|
||||
Every privileged or noteworthy action emits an evidence record to a local
|
||||
append-only JSONL log and, best-effort, to the State Hub progress API. Records
|
||||
are scrubbed of anything that looks like a secret value before they are written.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from secrets_engine.redact import looks_secret, redact_text
|
||||
|
||||
# Keys that must never carry a value into evidence regardless of nesting.
|
||||
_FORBIDDEN_VALUE_KEYS = {"value", "secret", "token", "password", "raw"}
|
||||
|
||||
|
||||
def _scrub(obj: Any) -> Any:
|
||||
"""Recursively drop secret-looking keys and redact token shapes in strings."""
|
||||
if isinstance(obj, dict):
|
||||
out = {}
|
||||
for k, v in obj.items():
|
||||
if k.lower() in _FORBIDDEN_VALUE_KEYS or looks_secret(k):
|
||||
out[k] = "<omitted: non-secret evidence only>"
|
||||
else:
|
||||
out[k] = _scrub(v)
|
||||
return out
|
||||
if isinstance(obj, list):
|
||||
return [_scrub(v) for v in obj]
|
||||
if isinstance(obj, str):
|
||||
return redact_text(obj)
|
||||
return obj
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvidenceWriter:
|
||||
evidence_dir: Path
|
||||
hub_url: str = ""
|
||||
topic_id: str = ""
|
||||
workstream_id: str = ""
|
||||
author: str = "secrets-engine"
|
||||
actor: str = field(default_factory=lambda: os.environ.get("USER", "unknown"))
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.evidence_dir = Path(self.evidence_dir)
|
||||
|
||||
def _log_path(self) -> Path:
|
||||
self.evidence_dir.mkdir(parents=True, exist_ok=True)
|
||||
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
return self.evidence_dir / f"evidence-{day}.jsonl"
|
||||
|
||||
def record(
|
||||
self,
|
||||
action: str,
|
||||
*,
|
||||
result: str,
|
||||
catalog_id: str = "",
|
||||
stage: str = "",
|
||||
decision_id: str = "",
|
||||
detail: dict[str, Any] | None = None,
|
||||
hub: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Append one non-secret evidence record. Returns the stored record."""
|
||||
record = {
|
||||
"ts": datetime.now(timezone.utc).isoformat(),
|
||||
"action": action,
|
||||
"result": result,
|
||||
"actor": self.actor,
|
||||
"catalog_id": catalog_id,
|
||||
"stage": stage,
|
||||
"decision_id": decision_id,
|
||||
"detail": _scrub(detail or {}),
|
||||
}
|
||||
path = self._log_path()
|
||||
with path.open("a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
if hub and self.hub_url:
|
||||
self._post_hub(action, result, catalog_id, stage, decision_id)
|
||||
return record
|
||||
|
||||
def _post_hub(
|
||||
self, action: str, result: str, catalog_id: str, stage: str, decision_id: str
|
||||
) -> None:
|
||||
"""Best-effort progress note to State Hub. Never raises; never sends values."""
|
||||
if not self.topic_id:
|
||||
return
|
||||
summary = f"secrets-engine {action}: {result}"
|
||||
if catalog_id:
|
||||
summary += f" [{catalog_id}{'/' + stage if stage else ''}]"
|
||||
payload: dict[str, Any] = {
|
||||
"topic_id": self.topic_id,
|
||||
"event_type": "note",
|
||||
"summary": summary,
|
||||
"author": self.author,
|
||||
}
|
||||
if self.workstream_id:
|
||||
payload["workstream_id"] = self.workstream_id
|
||||
if decision_id:
|
||||
payload["detail"] = {"decision_id": decision_id, "catalog_id": catalog_id}
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
self.hub_url.rstrip("/") + "/progress/",
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=3).read()
|
||||
except (urllib.error.URLError, OSError, ValueError):
|
||||
# Hub being offline must never block secret work or leak anything.
|
||||
pass
|
||||
Loading…
Add table
Add a link
Reference in a new issue