Review/optimize checklist, executive-summary format, build executor (T02-T04)
docs/review-optimize-checklist.md: six checks (naming, TTL/scoping,
redundancy, compaction, ease of use, posture), applied for real to the
rein-openweights plan's section 4 -- including a genuinely useful
finding (credentials.py already expects this exact path/delivery shape,
zero code changes needed to consume it).
docs/executive-summary-format.md: six fixed fields, no bao syntax, no
restating earlier sections, explicit approve/reject/revise decision.
Rendered for real into the plan's section 5 -- ready for an actual
decision.
src/ops_mason/{plan,executor,audit}.py: the phase-4 build executor for
credential_type openbao-approle-kv. Refuses to run against anything but
an approved plan -- verified the refusal never even calls subprocess.run.
role_id/secret_id (the AppRole's own access credential, not the
downstream secret) land as 0600 files, never logged; the HCL policy
goes over stdin, never argv; the audit trail is metadata-only. 12 tests,
all mocked at the bao boundary (no live OpenBao access from this
session).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
123ccfe20c
commit
0d62ac501d
12 changed files with 645 additions and 12 deletions
0
src/ops_mason/__init__.py
Normal file
0
src/ops_mason/__init__.py
Normal file
48
src/ops_mason/audit.py
Normal file
48
src/ops_mason/audit.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""Metadata-only audit trail for what ops-mason built.
|
||||
|
||||
Same invariant as ops-warden's own audit trail (`warden activity`):
|
||||
object names and plan references only, never secret material.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
_DEFAULT_LOG = Path(__file__).resolve().parents[2] / "audit" / "build-log.jsonl"
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildRecord:
|
||||
plan_id: str
|
||||
plan_path: str
|
||||
approved_by: str
|
||||
approved_at: str
|
||||
objects: dict[str, str]
|
||||
built_at: str
|
||||
|
||||
|
||||
def record_build(
|
||||
*,
|
||||
plan_id: str,
|
||||
plan_path: str,
|
||||
approved_by: str,
|
||||
approved_at: str,
|
||||
objects: dict[str, str],
|
||||
log_path: Path | None = None,
|
||||
) -> BuildRecord:
|
||||
record = BuildRecord(
|
||||
plan_id=plan_id,
|
||||
plan_path=plan_path,
|
||||
approved_by=approved_by,
|
||||
approved_at=approved_at,
|
||||
objects=objects,
|
||||
built_at=datetime.now(UTC).isoformat(),
|
||||
)
|
||||
path = log_path or _DEFAULT_LOG
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("a") as fh:
|
||||
fh.write(json.dumps(asdict(record)) + "\n")
|
||||
return record
|
||||
134
src/ops_mason/executor.py
Normal file
134
src/ops_mason/executor.py
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
"""Phase 4: execute an approved construction plan against OpenBao.
|
||||
|
||||
Narrow on purpose (MASON-WP-0001-T04) — implements only the operations
|
||||
the first real plan (rein-openweights-openrouter-approle,
|
||||
credential_type: openbao-approle-kv) needs: a read-only policy scoped to
|
||||
one KV path, an AppRole bound to it, and role_id/secret_id delivery.
|
||||
|
||||
Never writes to the KV path itself and never touches the downstream
|
||||
secret value (the OpenRouter API key). The KV path a policy references
|
||||
comes into existence later, when the founder does a paste-once-provision
|
||||
through ops-warden's desk (`bao kv put`) — not through this executor.
|
||||
role_id/secret_id are the AppRole's own access credential, not the
|
||||
downstream secret; generating and delivering them is squarely
|
||||
ops-mason's job (INTENT.md: "create ... credentials, tokens"), but they
|
||||
are still handled write-only — never logged, never returned in a
|
||||
printable form beyond confirmation that delivery happened.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from ops_mason.audit import record_build
|
||||
from ops_mason.plan import ConstructionPlan
|
||||
|
||||
|
||||
class BuildRefused(RuntimeError):
|
||||
"""The plan is not approved. This is the one place a bug is a security incident."""
|
||||
|
||||
|
||||
class BuildError(RuntimeError):
|
||||
"""The plan was approved but a bao operation failed."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppRoleKVSpec:
|
||||
policy_name: str
|
||||
kv_path: str
|
||||
approle_name: str
|
||||
kv_capabilities: tuple[str, ...] = ("read",)
|
||||
token_ttl: str = "15m"
|
||||
token_max_ttl: str = "30m"
|
||||
token_num_uses: int = 0
|
||||
secret_id_ttl: str = "0"
|
||||
delivery_dir: Path | None = None
|
||||
bao_bin: str = "bao"
|
||||
audit_log_path: Path | None = None
|
||||
|
||||
|
||||
def _run(bao_bin: str, args: list[str], input_text: str | None = None) -> str:
|
||||
proc = subprocess.run(
|
||||
[bao_bin, *args],
|
||||
input=input_text,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise BuildError(f"`{bao_bin} {' '.join(args)}` failed: {proc.stderr.strip()[:300]}")
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _policy_hcl(kv_path: str, capabilities: tuple[str, ...]) -> str:
|
||||
caps = ", ".join(f'"{c}"' for c in capabilities)
|
||||
return f'path "{kv_path}" {{\n capabilities = [{caps}]\n}}\n'
|
||||
|
||||
|
||||
def build_approle_kv_lane(plan: ConstructionPlan, spec: AppRoleKVSpec) -> dict[str, str]:
|
||||
"""Create the policy + AppRole for an `openbao-approle-kv` plan, deliver role_id/secret_id.
|
||||
|
||||
Refuses unless plan.is_approved(). Returns object names only (no
|
||||
secret material) and appends a metadata-only audit record.
|
||||
"""
|
||||
if not plan.is_approved():
|
||||
raise BuildRefused(
|
||||
f"plan {plan.id!r} is not approved "
|
||||
f"(status={plan.status!r}, approved_by={plan.approved_by!r}, "
|
||||
f"approved_at={plan.approved_at!r}) — refusing to build"
|
||||
)
|
||||
|
||||
policy_hcl = _policy_hcl(spec.kv_path, spec.kv_capabilities)
|
||||
_run(spec.bao_bin, ["policy", "write", spec.policy_name, "-"], input_text=policy_hcl)
|
||||
|
||||
_run(
|
||||
spec.bao_bin,
|
||||
[
|
||||
"write",
|
||||
f"auth/approle/role/{spec.approle_name}",
|
||||
f"token_policies={spec.policy_name}",
|
||||
f"token_ttl={spec.token_ttl}",
|
||||
f"token_max_ttl={spec.token_max_ttl}",
|
||||
f"token_num_uses={spec.token_num_uses}",
|
||||
f"secret_id_ttl={spec.secret_id_ttl}",
|
||||
],
|
||||
)
|
||||
|
||||
role_id = _run(
|
||||
spec.bao_bin, ["read", "-field=role_id", f"auth/approle/role/{spec.approle_name}/role-id"]
|
||||
).strip()
|
||||
secret_id = _run(
|
||||
spec.bao_bin,
|
||||
["write", "-field=secret_id", "-f", f"auth/approle/role/{spec.approle_name}/secret-id"],
|
||||
).strip()
|
||||
|
||||
delivered_to = ""
|
||||
if spec.delivery_dir is not None:
|
||||
spec.delivery_dir.mkdir(parents=True, exist_ok=True)
|
||||
os.chmod(spec.delivery_dir, 0o700)
|
||||
role_id_path = spec.delivery_dir / "role_id"
|
||||
secret_id_path = spec.delivery_dir / "secret_id"
|
||||
role_id_path.write_text(role_id + "\n")
|
||||
secret_id_path.write_text(secret_id + "\n")
|
||||
os.chmod(role_id_path, 0o600)
|
||||
os.chmod(secret_id_path, 0o600)
|
||||
delivered_to = str(spec.delivery_dir)
|
||||
|
||||
objects = {
|
||||
"policy_name": spec.policy_name,
|
||||
"approle_name": spec.approle_name,
|
||||
"kv_path": spec.kv_path,
|
||||
"delivered_to": delivered_to,
|
||||
}
|
||||
record_build(
|
||||
plan_id=plan.id,
|
||||
plan_path=str(plan.path),
|
||||
approved_by=plan.approved_by or "",
|
||||
approved_at=plan.approved_at or "",
|
||||
objects=objects,
|
||||
log_path=spec.audit_log_path,
|
||||
)
|
||||
return objects
|
||||
56
src/ops_mason/plan.py
Normal file
56
src/ops_mason/plan.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"""Parse a construction plan file's YAML frontmatter.
|
||||
|
||||
See docs/construction-plan-format.md. The frontmatter's status field is
|
||||
the phase-4 hard gate: the build executor (executor.py) refuses to run
|
||||
against anything but status: approved with both approved_by/approved_at
|
||||
set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class PlanError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConstructionPlan:
|
||||
id: str
|
||||
status: str
|
||||
approved_by: str | None
|
||||
approved_at: str | None
|
||||
demand_source: str
|
||||
consumer_repo: str
|
||||
credential_type: str
|
||||
path: Path
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path) -> ConstructionPlan:
|
||||
path = Path(path)
|
||||
text = path.read_text()
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 3 or not text.startswith("---"):
|
||||
raise PlanError(f"{path}: missing YAML frontmatter")
|
||||
data: dict[str, Any] = yaml.safe_load(parts[1]) or {}
|
||||
missing = {"id", "status"} - set(data)
|
||||
if missing:
|
||||
raise PlanError(f"{path}: frontmatter missing field(s): {', '.join(sorted(missing))}")
|
||||
return cls(
|
||||
id=data["id"],
|
||||
status=data["status"],
|
||||
approved_by=data.get("approved_by"),
|
||||
approved_at=data.get("approved_at"),
|
||||
demand_source=data.get("demand_source", ""),
|
||||
consumer_repo=data.get("consumer_repo", ""),
|
||||
credential_type=data.get("credential_type", ""),
|
||||
path=path,
|
||||
)
|
||||
|
||||
def is_approved(self) -> bool:
|
||||
return self.status == "approved" and bool(self.approved_by) and bool(self.approved_at)
|
||||
Loading…
Add table
Add a link
Reference in a new issue