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
99
src/secrets_engine/routing.py
Normal file
99
src/secrets_engine/routing.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""ops-warden routing contract.
|
||||
|
||||
ops-warden routes non-SSH credential needs here. It must NOT vend secret values.
|
||||
A route result is a pointer: catalog id, readiness, decision status, and the safe
|
||||
next command. This module computes that pointer for a lane. No value is read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from secrets_engine.catalog import CatalogEntry
|
||||
from secrets_engine.decisions import Decision, resolve_decision
|
||||
from secrets_engine.errors import DecisionError
|
||||
from secrets_engine.openbao import OpenBaoClient
|
||||
|
||||
|
||||
@dataclass
|
||||
class RouteResult:
|
||||
catalog_id: str
|
||||
owner: str
|
||||
stage: str
|
||||
decision_status: str
|
||||
decision_ref: str
|
||||
review_url: str
|
||||
metadata_applied: bool
|
||||
value_present: bool
|
||||
ready: bool
|
||||
next_command: str
|
||||
missing: str
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
|
||||
def route_lane(
|
||||
entry: CatalogEntry,
|
||||
*,
|
||||
hub_url: str,
|
||||
repo_root: Path,
|
||||
client: OpenBaoClient | None = None,
|
||||
) -> RouteResult:
|
||||
"""Build the front-door routing pointer for a lane. Never reads the value."""
|
||||
decision_status = "n/a (bootstrap-only)"
|
||||
decision_ref = entry.approval.get("decision_ref", "")
|
||||
review_url = ""
|
||||
decision: Decision | None = None
|
||||
if entry.approval_required():
|
||||
try:
|
||||
decision = resolve_decision(
|
||||
hub_url=hub_url, repo_root=repo_root, decision_ref=decision_ref
|
||||
)
|
||||
decision_status = decision.status
|
||||
review_url = decision.review_url
|
||||
except DecisionError:
|
||||
decision_status = "missing"
|
||||
|
||||
metadata_applied = False
|
||||
value_present = False
|
||||
if client is not None and client.is_reachable():
|
||||
metadata_applied = client.read_policy(entry.policy_name) is not None
|
||||
# Presence check uses the engine's own token; reports boolean only.
|
||||
field = entry.fields[0] if entry.fields else ""
|
||||
if field:
|
||||
value_present = client.kv_field_present(entry.mount, entry.path, field)
|
||||
|
||||
approved = decision is None or decision.is_approved()
|
||||
ready = approved and metadata_applied and value_present
|
||||
|
||||
if not approved:
|
||||
missing = f"approved decision for '{decision_ref}'"
|
||||
next_command = f"secrets-engine decision inspect {decision_ref or entry.id}"
|
||||
elif not metadata_applied:
|
||||
missing = "OpenBao policy/role apply"
|
||||
next_command = f"secrets-engine apply {decision_ref or entry.id} --stage {entry.stage}"
|
||||
elif not value_present:
|
||||
missing = "provisioned secret value"
|
||||
next_command = (
|
||||
f"secrets-engine provision {entry.id} --stage {entry.stage} "
|
||||
f"--field {entry.fields[0]} --from-file <path>"
|
||||
)
|
||||
else:
|
||||
missing = ""
|
||||
next_command = f"secrets-engine exec --catalog {entry.id} -- <command...>"
|
||||
|
||||
return RouteResult(
|
||||
catalog_id=entry.id,
|
||||
owner=entry.owner,
|
||||
stage=entry.stage,
|
||||
decision_status=decision_status,
|
||||
decision_ref=decision_ref,
|
||||
review_url=review_url,
|
||||
metadata_applied=metadata_applied,
|
||||
value_present=value_present,
|
||||
ready=ready,
|
||||
next_command=next_command,
|
||||
missing=missing,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue