Enforce readiness tiers and reconcile blocked workplans
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e75a-fc5c-7913-9dba-9846210c766d
This commit is contained in:
parent
370e1f84c7
commit
36445ae679
14 changed files with 652 additions and 39 deletions
|
|
@ -130,13 +130,13 @@ tooling_contacts:
|
|||
# gate on ADMINISTER @ realm:kubernetes/railiance01 is a quality gate, not
|
||||
# an authorization decision, tiered by the target's railiance-master
|
||||
# ADR-0006 readiness_state. The owner question above is answered: the
|
||||
# Kubernetes API stays a Tooling contact owned by rail-kubernetes.
|
||||
# contact is with realm:kubernetes/railiance01; no layer is assigned.
|
||||
change_gate:
|
||||
decision: the-custodian/docs/kubernetes-change-gate-decision.md
|
||||
decided_by: "Bernd Worsch (founder), GOVERN @ estate"
|
||||
decided_at: "2026-09-21"
|
||||
engine_owner: none
|
||||
tooling_owner: rail-kubernetes
|
||||
realm: "realm:kubernetes/railiance01"
|
||||
tiers:
|
||||
- readiness_state: [declared, installed, verified]
|
||||
path: "direct ADMINISTER @ realm:kubernetes by ops-mason"
|
||||
|
|
@ -156,7 +156,7 @@ tooling_contacts:
|
|||
until: "2026-12-21"
|
||||
rule: "Direct ADMINISTER under activation=APPROVED, each change recorded as production-tier, until ArgoCD onboarding."
|
||||
relies_on_limits: "One expected namespace per plan; Pod and Secret kinds refused; no data or stringData. Widening them is a new decision."
|
||||
enforcement: "Not yet in code: phase 4 does not check readiness_state. Planned in workplans/MASON-WP-0006-readiness-tier-check.md."
|
||||
enforcement: "src/ops_mason/readiness.py: inspect_readiness and resolve_tier; kubernetes_plane.apply refuses before Kubernetes writes. Source/history verification, explicit whitehat placement, dated policy-nexus transition, and recorded BREAK_GLASS."
|
||||
- id: bao-session-grant
|
||||
shape: "5.2"
|
||||
module: scripts/bao-session.sh
|
||||
|
|
|
|||
|
|
@ -59,3 +59,10 @@ dependencies:
|
|||
equals: "true"
|
||||
- path: /spec/ingress/0/from/0/podSelector/matchLabels/whitehat.security~1target
|
||||
equals: audit-core
|
||||
readiness:
|
||||
target: {kind: namespace, namespace: whitehat}
|
||||
source:
|
||||
repo: reef-railiance
|
||||
path: bindings/rapps.yaml
|
||||
revision: e3c5ca2b74f6ae4f3b79f918708f3b573157a0c2
|
||||
sha256: 796007c68f0eda1d50cebddf9e11a2c123a8814ebaf67f739ed5d2af2d6fa3f1
|
||||
|
|
|
|||
91
docs/evidence/2026-09-28-loose-end-review.md
Normal file
91
docs/evidence/2026-09-28-loose-end-review.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Loose-end review — 2026-09-28
|
||||
|
||||
Reviewed every local workplan. MASON-0001 and MASON-WP-0001–0003 are
|
||||
finished; no open task in those files needs implementation. The three proposed
|
||||
workplans, MASON-WP-0004–0006, contain the remaining work. No new task or
|
||||
workplan was opened. Existing uncommitted intake, Telegram construction-plan,
|
||||
and lockfile work was left outside this change.
|
||||
|
||||
## MASON-WP-0006
|
||||
|
||||
T01 is answered by founder decision in
|
||||
`the-custodian/docs/kubernetes-change-gate-decision.md`, communicated by message
|
||||
`b41bd54f-a7a4-41e5-a1ee-aa9b0efa7dc5`: whitehat is explicitly non-production.
|
||||
The same decision and current agent orientation establish that ArgoCD has
|
||||
since been installed; the blanket absence-of-ArgoCD transition is obsolete.
|
||||
|
||||
T02–T05 implemented in the readiness resolver, bundle, CLI, evidence and docs.
|
||||
Validation: 54 tests pass (`.venv/bin/pytest -q`), including a real temporary
|
||||
Git history that promotes then reverts a binding; `git diff --check` passes.
|
||||
Tests exercise approved and refused apply, explicit whitehat placement,
|
||||
binding supersession, stale/missing sources, namespace mapping, historical
|
||||
production approval, deprecation, the December 21 boundary, and emergency
|
||||
reason/evidence. Existing manifest and approval refusals remain covered.
|
||||
Secret-presence verification now requests object names, never Secret JSON.
|
||||
|
||||
A read-only local source check resolves the actual whitehat bundle to
|
||||
non-production under APPROVED, using reef-railiance commit
|
||||
`e3c5ca2b74f6ae4f3b79f918708f3b573157a0c2`, bindings SHA-256
|
||||
`796007c68f0eda1d50cebddf9e11a2c123a8814ebaf67f739ed5d2af2d6fa3f1`.
|
||||
No Kubernetes command or deployment was needed for this implementation.
|
||||
|
||||
T06 stays wait: its review is due 2026-12-21 and requires platform confirmation
|
||||
of policy-nexus GitOps adoption and review of the accepted plan-approval
|
||||
exception. The workplan remains blocked, not finished; the existing task holds
|
||||
this obligation.
|
||||
|
||||
## MASON-WP-0004
|
||||
|
||||
The session check at `http://127.0.0.1:18200` reports no valid caller session
|
||||
and no scoped ops-mason grant. No credential value was requested. The original
|
||||
21-path/17-undescribed inventory has no saved path-level snapshot in this repo;
|
||||
source documents cannot prove the current live path set or completeness.
|
||||
T01–T03 therefore remain wait for an attended scoped metadata grant, current
|
||||
inventory, and owner confirmations. No ownership or recovery story is invented.
|
||||
|
||||
Source-backed candidates for the next T01 inventory comparison:
|
||||
|
||||
| Path or family | Proposed responsible repo | Evidence / unresolved question |
|
||||
| --- | --- | --- |
|
||||
| operators/lldap/admin | net-kingdom | platform-root-custody.md; confirm current consumers and recovery |
|
||||
| operators/privacyidea/pi-admin | net-kingdom | platform-root-custody.md and verify-t06.md; confirm recovery ownership |
|
||||
| operators/forgejo/state-hub-svc | railiance-platform | MASON-WP-0003 construction/delivery record; confirm active metadata |
|
||||
| platform/workloads/railiance/backup/object-storage | railiance-platform | plans/backup-object-storage.md |
|
||||
| platform/workloads/railiance/backup/offsite-lane | railiance-platform | ops-warden catalog railiance-backup-offsite-lane |
|
||||
| platform/workloads/railiance/scaleway/bootstrap | railiance-platform | plans/reef-storage-scaleway-bootstrap.md |
|
||||
| user-engine/runtime and rapp-qonto families | owning consumer plus railiance-platform custody | corresponding plans in this repo; exact current paths need live inventory |
|
||||
| reuse-surface/runtime-secrets | reuse-surface plus railiance-platform custody | cited by T02; current owner procedure still needs confirmation |
|
||||
|
||||
These are candidates, not accepted custom_metadata, and do not enumerate the
|
||||
missing seventeen. Every additional live path needs an owner or a named unknown
|
||||
before T01 can close.
|
||||
|
||||
T04 preparation fixes an actual false-success defect: an unavailable/denied
|
||||
metadata request previously returned an empty list and exit 0. The inventory
|
||||
now exits 2 on missing grant, command failure or malformed response, exits 1
|
||||
for missing descriptions, and reserves 0 for a completed inventory. It refuses
|
||||
to fall back silently to the user's default token. The default Bao address in
|
||||
both helpers now follows the private tunnel. Tests cover failure reporting and
|
||||
metadata-only traversal. Running it without a grant returned the expected
|
||||
explicit error, not a fictitious healthy inventory.
|
||||
|
||||
Scheduled reporting remains blocked on T03 and a named reader plus a
|
||||
non-interactive metadata-only credential. No existing scheduler for this repo
|
||||
was found; a 45-minute interactive grant cannot support a durable schedule.
|
||||
|
||||
## MASON-WP-0005
|
||||
|
||||
The September 9 platform reply (`c0977d84-9a63-421d-8ee1-98587fc60b1b`)
|
||||
and `railiance-platform/docs/credential-lane-designs/fluid-telegram-operator-kv.md`
|
||||
confirm a proposed matrix, not an accepted writer contract. Tenant/path,
|
||||
field/capability acceptance, actual OIDC group/MFA and callbacks, named writer
|
||||
authority, matching platform validator/renderer, and live survey remain open.
|
||||
The existing ops-mason grant does not authorize auth/netkingdom role changes;
|
||||
it must not be widened to bypass this boundary.
|
||||
|
||||
T01–T05 remain wait for those inputs and explicit construction approval.
|
||||
T02 cannot be called done by shipping an unapproved engine shape: the owner
|
||||
requires matching approved CCR and builder contracts before any writer.
|
||||
T06 also waits for verification and routing-owner acceptance; no live lane or
|
||||
resolvable pointer is claimed. Local draft preparation remains in the existing
|
||||
construction plan. The separate adapter demand stays in its existing intake.
|
||||
|
|
@ -46,3 +46,65 @@ ops-mason plane rollback-plan --bundle bundles/<id>.yaml
|
|||
|
||||
Rollback output is a plan, never an action. Review live inventory immediately
|
||||
before using it.
|
||||
|
||||
## Readiness gate
|
||||
|
||||
`apply` checks `ops_mason.readiness.inspect_readiness` after approval/digest
|
||||
checks and before any Kubernetes command. `preflight` reports the result even
|
||||
when direct apply would be refused.
|
||||
|
||||
| Verified target state | Direct apply under APPROVED |
|
||||
| --- | --- |
|
||||
| declared, installed, verified | Allowed with the existing approved-plan checks |
|
||||
| production-approved, including a later evidence lapse | Refused; use the manifest repository and ArgoCD |
|
||||
| deprecated | Retains the previous tier; missing history means production |
|
||||
| missing, unknown, invalid or stale readiness | Production; refused |
|
||||
| whitehat namespace, explicit founder placement of 2026-09-21 | Non-production, until a binding supersedes the placement |
|
||||
| rapp-policy-nexus, production tier | Transition only before 2026-12-21; evidence labels it production |
|
||||
|
||||
ArgoCD now exists on railiance01, so the historical blanket transition for
|
||||
all production targets is not enabled. The policy-nexus transition remains
|
||||
bounded by its review date. The gate does not decide authorization or contact
|
||||
an authorization engine.
|
||||
|
||||
Bundles include a reviewed namespace-to-binding mapping and a source pin:
|
||||
|
||||
```yaml
|
||||
readiness:
|
||||
target: {kind: rapp, rapp_id: rapp-user-engine, namespace: user-engine}
|
||||
source:
|
||||
repo: reef-railiance
|
||||
path: bindings/rapps.yaml
|
||||
revision: <full-40-character-commit-id>
|
||||
sha256: <sha256-of-file-at-that-commit>
|
||||
```
|
||||
|
||||
The approved bundle is the mapping record: the namespace must match its object
|
||||
scope, and any namespace mapping in the source must agree. The only explicit
|
||||
namespace placement is `kind: namespace, namespace: whitehat` for bundle
|
||||
`whitehat-foundational-plane`, using the same pinned source so a newly declared
|
||||
whitehat binding invalidates the placement. Other namespace-only targets and
|
||||
platform objects remain production-tier.
|
||||
|
||||
Use `--readiness-repo /path/to/reef-railiance` on `preflight` or `apply` to
|
||||
locate the source; the default is the sibling checkout. The gate verifies its
|
||||
file digest, commit ancestry, unchanged content through local HEAD, clean source
|
||||
file, and complete Git history. Refresh that checkout before review: the gate
|
||||
checks local HEAD, not an unfetched remote. Missing source/history fails closed.
|
||||
Any production approval in the reachable file history retains production tier;
|
||||
a deliberate re-scope needs a separately reviewed gate change, not just lowering
|
||||
the readiness field. Source pins and mapping changes alter the bundle digest
|
||||
and need fresh review/confirmation. Existing live evidence remains historical.
|
||||
|
||||
For emergency direct apply, keep all normal plan/digest requirements and add:
|
||||
|
||||
```bash
|
||||
ops-mason plane apply --bundle bundles/<id>.yaml \
|
||||
--confirm <approved-plan-id> --expect-digest <digest> \
|
||||
--activation BREAK_GLASS --break-glass-reason '<incident and justification>'
|
||||
```
|
||||
|
||||
The JSON evidence and CLI output record activation, local executing account,
|
||||
time, reason, resolved tier/source, and the obligation to commit the same
|
||||
change to the manifest repository ArgoCD reconciles. The command does not open
|
||||
that change or grant emergency authority itself. An empty reason is refused.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
set -euo pipefail
|
||||
|
||||
export BAO_ADDR="${BAO_ADDR:-https://bao.coulomb.social}"
|
||||
export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}"
|
||||
GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}"
|
||||
GRANT_POLICY="${GRANT_POLICY:-ops-mason-build}"
|
||||
GRANT_TTL="${GRANT_TTL:-45m}"
|
||||
|
|
|
|||
|
|
@ -24,31 +24,42 @@ REQUIRED = ("description", "owner", "used_by", "rotation", "on_loss")
|
|||
DEFAULT_ROOTS = ("operators", "platform/workloads")
|
||||
|
||||
|
||||
def bao(*args: str) -> dict | list | None:
|
||||
class InventoryError(RuntimeError):
|
||||
"""Inventory could not be completed; never report this as an empty store."""
|
||||
|
||||
|
||||
def bao(*args: str) -> dict | list:
|
||||
env = dict(os.environ)
|
||||
env.setdefault("BAO_ADDR", "https://bao.coulomb.social")
|
||||
env.setdefault("BAO_ADDR", "http://127.0.0.1:18200")
|
||||
grant = os.path.expanduser("~/.claude-bao-token")
|
||||
if "BAO_TOKEN" not in env and os.path.exists(grant):
|
||||
with open(grant) as f:
|
||||
env["BAO_TOKEN"] = f.read().strip()
|
||||
if not env.get("BAO_TOKEN"):
|
||||
raise InventoryError("No scoped BAO_TOKEN or ops-mason grant; inventory was not run.")
|
||||
# The Vault/OpenBao CLI rejects flags placed after a positional argument,
|
||||
# so -format=json goes immediately before the path, not at the end.
|
||||
argv = ["bao", *args[:-1], "-format=json", args[-1]]
|
||||
p = subprocess.run(argv, capture_output=True, text=True, timeout=30, env=env)
|
||||
try:
|
||||
p = subprocess.run(argv, capture_output=True, text=True, timeout=30, env=env)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise InventoryError("OpenBao metadata command unavailable or timed out") from exc
|
||||
if p.returncode != 0:
|
||||
return None
|
||||
raise InventoryError(f"OpenBao metadata request failed for {args[-1]}; inventory incomplete")
|
||||
try:
|
||||
return json.loads(p.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
except json.JSONDecodeError as exc:
|
||||
raise InventoryError("Invalid OpenBao metadata response; inventory incomplete") from exc
|
||||
|
||||
|
||||
def walk(prefix: str) -> list[str]:
|
||||
keys = bao("kv", "list", prefix)
|
||||
if not isinstance(keys, list):
|
||||
return []
|
||||
raise InventoryError(f"Invalid metadata listing for {prefix}")
|
||||
out: list[str] = []
|
||||
for k in keys:
|
||||
if not isinstance(k, str) or not k.rstrip("/") or "/" in k.rstrip("/") or k.rstrip("/") in {".", ".."}:
|
||||
raise InventoryError(f"Invalid child in metadata listing for {prefix}")
|
||||
child = f"{prefix.rstrip('/')}/{k.rstrip('/')}"
|
||||
out.extend(walk(child) if k.endswith("/") else [child])
|
||||
return out
|
||||
|
|
@ -62,9 +73,13 @@ def main() -> int:
|
|||
for root in roots:
|
||||
for path in walk(root):
|
||||
total += 1
|
||||
meta = bao("kv", "metadata", "get", path) or {}
|
||||
d = meta.get("data", {}) if isinstance(meta, dict) else {}
|
||||
meta = bao("kv", "metadata", "get", path)
|
||||
if not isinstance(meta, dict) or not isinstance(meta.get("data"), dict):
|
||||
raise InventoryError(f"Invalid metadata response for {path}")
|
||||
d = meta["data"]
|
||||
cm = d.get("custom_metadata") or {}
|
||||
if not isinstance(cm, dict):
|
||||
raise InventoryError(f"Invalid custom metadata for {path}")
|
||||
missing = [k for k in REQUIRED if not cm.get(k)]
|
||||
if missing:
|
||||
incomplete += 1
|
||||
|
|
@ -90,4 +105,8 @@ def main() -> int:
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except InventoryError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections.abc import Sequence
|
||||
|
||||
from ops_mason.kubernetes_plane import (
|
||||
|
|
@ -26,6 +27,8 @@ def _parser() -> argparse.ArgumentParser:
|
|||
for name in ("render", "preflight", "verify", "rollback-plan"):
|
||||
command = commands.add_parser(name)
|
||||
command.add_argument("--bundle", required=True)
|
||||
if name == "preflight":
|
||||
command.add_argument("--readiness-repo", type=Path)
|
||||
|
||||
apply_parser = commands.add_parser("apply")
|
||||
apply_parser.add_argument("--bundle", required=True)
|
||||
|
|
@ -33,6 +36,9 @@ def _parser() -> argparse.ArgumentParser:
|
|||
apply_parser.add_argument(
|
||||
"--expect-digest", required=True, help="exact digest returned by preflight"
|
||||
)
|
||||
apply_parser.add_argument("--readiness-repo", type=Path)
|
||||
apply_parser.add_argument("--activation", choices=("APPROVED", "BREAK_GLASS"), default="APPROVED")
|
||||
apply_parser.add_argument("--break-glass-reason")
|
||||
return parser
|
||||
|
||||
|
||||
|
|
@ -43,7 +49,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
if args.command == "render":
|
||||
result = bundle.render()
|
||||
elif args.command == "preflight":
|
||||
result = preflight(bundle)
|
||||
result = preflight(bundle, readiness_repo=args.readiness_repo)
|
||||
elif args.command == "verify":
|
||||
result = verify(bundle)
|
||||
elif args.command == "rollback-plan":
|
||||
|
|
@ -53,6 +59,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
bundle,
|
||||
confirm_plan_id=args.confirm,
|
||||
expected_digest=args.expect_digest,
|
||||
readiness_repo=args.readiness_repo,
|
||||
activation=args.activation,
|
||||
break_glass_reason=args.break_glass_reason,
|
||||
)
|
||||
else: # pragma: no cover - argparse enforces the command set
|
||||
raise AssertionError(args.command)
|
||||
|
|
|
|||
|
|
@ -8,18 +8,20 @@ and records metadata-only evidence.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import getpass
|
||||
import hashlib
|
||||
import json
|
||||
import subprocess
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import UTC, datetime
|
||||
from datetime import UTC, date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from ops_mason.plan import ConstructionPlan
|
||||
from ops_mason.readiness import inspect_readiness
|
||||
|
||||
|
||||
class PlaneError(RuntimeError):
|
||||
|
|
@ -93,6 +95,7 @@ class PlaneBundle:
|
|||
dependencies: tuple[Dependency, ...]
|
||||
evidence_path: Path
|
||||
documents: tuple[dict[str, Any], ...]
|
||||
readiness: dict[str, Any] | None = None
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str | Path) -> "PlaneBundle":
|
||||
|
|
@ -172,6 +175,7 @@ class PlaneBundle:
|
|||
dependencies=dependencies,
|
||||
evidence_path=local_path(str(raw["evidence_path"])),
|
||||
documents=tuple(documents),
|
||||
readiness=raw.get("readiness"),
|
||||
)
|
||||
bundle.validate()
|
||||
return bundle
|
||||
|
|
@ -185,6 +189,12 @@ class PlaneBundle:
|
|||
return digest.hexdigest()
|
||||
|
||||
def validate(self) -> None:
|
||||
if self.readiness is not None and (
|
||||
not isinstance(self.readiness, dict)
|
||||
or not isinstance(self.readiness.get("target"), dict)
|
||||
or not isinstance(self.readiness.get("source"), dict)
|
||||
):
|
||||
raise PlaneError("readiness needs target and source mappings")
|
||||
actual_refs = tuple(_document_ref(doc, self.allowed_objects) for doc in self.documents)
|
||||
if len(set(actual_refs)) != len(actual_refs):
|
||||
raise PlaneRefused("bundle contains duplicate Kubernetes object identities")
|
||||
|
|
@ -233,6 +243,7 @@ class PlaneBundle:
|
|||
"source_revision": self.source_revision,
|
||||
"implementation_revision": self.implementation_revision,
|
||||
"expected_context": self.expected_context,
|
||||
"readiness": self.readiness,
|
||||
"objects": [asdict(ref) | {"display": ref.display} for ref in self.allowed_objects],
|
||||
"forbidden_kinds": sorted(self.forbidden_kinds),
|
||||
"plan_id": self.plan().id,
|
||||
|
|
@ -363,7 +374,10 @@ def _check_inputs_clean(bundle: PlaneBundle, runner: Runner) -> None:
|
|||
raise PlaneRefused("repository must be committed and clean before Kubernetes mutation")
|
||||
|
||||
|
||||
def preflight(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str, Any]:
|
||||
def preflight(
|
||||
bundle: PlaneBundle, runner: Runner = subprocess_runner, *,
|
||||
readiness_repo: Path | None = None, activation: str = "APPROVED", today: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
context = _run(runner, ["kubectl", "config", "current-context"]).stdout.strip()
|
||||
if context != bundle.expected_context:
|
||||
raise PlaneRefused(
|
||||
|
|
@ -464,6 +478,7 @@ def preflight(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[s
|
|||
if str(item.path.relative_to(bundle.repo_root)) not in server_validated
|
||||
],
|
||||
"dependencies": dependency_evidence,
|
||||
"readiness": inspect_readiness(bundle, runner, readiness_repo, activation, today),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -489,9 +504,9 @@ def verify(bundle: PlaneBundle, runner: Runner = subprocess_runner) -> dict[str,
|
|||
for resource in ("pods", "secrets"):
|
||||
result = _run(
|
||||
runner,
|
||||
["kubectl", "-n", bundle.expected_namespace, "get", resource, "-o", "json"],
|
||||
["kubectl", "-n", bundle.expected_namespace, "get", resource, "-o", "name"],
|
||||
)
|
||||
count = len(json.loads(result.stdout).get("items", []))
|
||||
count = len(result.stdout.splitlines())
|
||||
if count:
|
||||
raise PlaneError(
|
||||
f"negative-scope check failed: {count} {resource} exist in "
|
||||
|
|
@ -538,6 +553,10 @@ def apply(
|
|||
confirm_plan_id: str,
|
||||
expected_digest: str,
|
||||
runner: Runner = subprocess_runner,
|
||||
readiness_repo: Path | None = None,
|
||||
activation: str = "APPROVED",
|
||||
break_glass_reason: str | None = None,
|
||||
today: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
plan = bundle.plan()
|
||||
if not plan.is_approved():
|
||||
|
|
@ -552,8 +571,17 @@ def apply(
|
|||
raise PlaneRefused(
|
||||
f"bundle digest confirmation mismatch: expected {bundle.digest}"
|
||||
)
|
||||
if activation not in {"APPROVED", "BREAK_GLASS"}:
|
||||
raise PlaneRefused("unknown activation")
|
||||
if activation == "BREAK_GLASS" and not (break_glass_reason or "").strip():
|
||||
raise PlaneRefused("BREAK_GLASS requires a non-empty reason")
|
||||
_check_inputs_clean(bundle, runner)
|
||||
before = preflight(bundle, runner)
|
||||
readiness = inspect_readiness(bundle, runner, readiness_repo, activation, today)
|
||||
if not readiness["direct_apply_allowed"]:
|
||||
raise PlaneRefused(f"production tier requires GitOps or BREAK_GLASS: {readiness['reason']}")
|
||||
before = preflight(bundle, runner, readiness_repo=readiness_repo, activation=activation, today=today)
|
||||
if not before["readiness"]["direct_apply_allowed"]:
|
||||
raise PlaneRefused("readiness changed during preflight; refusing mutation")
|
||||
|
||||
server_validated = list(before["server_validated_manifests"])
|
||||
persisted: list[str] = []
|
||||
|
|
@ -607,6 +635,14 @@ def apply(
|
|||
"server_validated_manifests": server_validated,
|
||||
"persisted_manifests": persisted,
|
||||
},
|
||||
"readiness": before["readiness"],
|
||||
"activation": activation,
|
||||
"break_glass": {
|
||||
"reason": break_glass_reason.strip(),
|
||||
"actor": getpass.getuser(),
|
||||
"recorded_at": datetime.now(UTC).isoformat(),
|
||||
"follow_up": "Commit the same change to the manifest repository that ArgoCD reconciles.",
|
||||
} if activation == "BREAK_GLASS" else None,
|
||||
"preflight": before,
|
||||
"verification": verified,
|
||||
"rollback": rollback_plan(bundle),
|
||||
|
|
|
|||
145
src/ops_mason/readiness.py
Normal file
145
src/ops_mason/readiness.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Readiness quality gate; no credential or Kubernetes access."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
TRANSITION_END = date(2026, 12, 21)
|
||||
NON_PRODUCTION = {"declared", "installed", "verified"}
|
||||
|
||||
|
||||
def resolve_tier(
|
||||
state: str | None, target: dict[str, Any], activation: str, today: date,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve already verified source facts. Unknown facts stay production."""
|
||||
tier = "non-production" if state in NON_PRODUCTION or state == "explicit-whitehat" else "production"
|
||||
transition = (
|
||||
tier == "production" and state == "production-approved"
|
||||
and target.get("kind") == "rapp"
|
||||
and target.get("rapp_id") == "rapp-policy-nexus"
|
||||
and today < TRANSITION_END and activation == "APPROVED"
|
||||
)
|
||||
return {
|
||||
"tier": tier,
|
||||
"direct_apply_allowed": activation in {"APPROVED", "BREAK_GLASS"}
|
||||
and (tier == "non-production" or activation == "BREAK_GLASS" or transition),
|
||||
"transition": transition,
|
||||
}
|
||||
|
||||
|
||||
def inspect_readiness(
|
||||
bundle, runner, repo: Path | None, activation: str = "APPROVED",
|
||||
today: date | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Verify pinned source, local freshness and history before trusting a tier.
|
||||
|
||||
A reviewed bundle supplies the namespace-to-rApp mapping. It must identify
|
||||
the namespace explicitly; source mappings, when present, must agree.
|
||||
"""
|
||||
today = today or date.today()
|
||||
block = bundle.readiness or {}
|
||||
target = block.get("target", {})
|
||||
source = block.get("source", {})
|
||||
evidence: dict[str, Any] = {
|
||||
"target": target, "source": source, "activation": activation,
|
||||
"state": None, "reason": "missing or unverifiable readiness",
|
||||
}
|
||||
|
||||
def finish(state=None, reason="unverifiable readiness"):
|
||||
evidence.update(state=state, reason=reason)
|
||||
evidence.update(resolve_tier(state, target, activation, today))
|
||||
return evidence
|
||||
|
||||
if not block or target.get("namespace") != bundle.expected_namespace:
|
||||
return finish(reason="missing readiness or namespace mapping")
|
||||
# A namespace placement never covers other cluster-scoped objects.
|
||||
if any(not ref.namespace and (ref.kind != "Namespace" or ref.name != bundle.expected_namespace)
|
||||
for ref in bundle.allowed_objects):
|
||||
return finish(reason="target includes objects outside the namespace mapping")
|
||||
if target.get("kind") not in {"rapp", "namespace"}:
|
||||
return finish(reason="unmapped platform target")
|
||||
if source.get("repo") != "reef-railiance" or source.get("path") != "bindings/rapps.yaml":
|
||||
return finish(reason="unsupported readiness source")
|
||||
revision = source.get("revision", "")
|
||||
digest = source.get("sha256", "")
|
||||
if not isinstance(revision, str) or not re.fullmatch(r"[0-9a-f]{40}", revision):
|
||||
return finish(reason="source needs a full commit id")
|
||||
if not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest):
|
||||
return finish(reason="source needs a SHA-256 digest")
|
||||
root = repo or bundle.repo_root.parent / "reef-railiance"
|
||||
prefix = ["git", "-C", str(root)]
|
||||
path = source["path"]
|
||||
|
||||
def git(*args):
|
||||
result = runner([*prefix, *args])
|
||||
if result.returncode:
|
||||
raise ValueError("readiness git verification failed")
|
||||
return result.stdout
|
||||
|
||||
def rows(content):
|
||||
data = yaml.safe_load(content)
|
||||
if not isinstance(data, dict) or not isinstance(data.get("bound_rapps"), list):
|
||||
raise ValueError("invalid readiness document")
|
||||
result = data["bound_rapps"]
|
||||
if any(not isinstance(row, dict) or not isinstance(row.get("rapp_id"), str) for row in result):
|
||||
raise ValueError("invalid readiness binding")
|
||||
if len({row["rapp_id"] for row in result}) != len(result):
|
||||
raise ValueError("duplicate readiness binding")
|
||||
return result
|
||||
|
||||
try:
|
||||
if git("rev-parse", "--is-shallow-repository").strip() != "false":
|
||||
return finish(reason="complete readiness history is required")
|
||||
content = git("show", f"{revision}:{path}")
|
||||
if hashlib.sha256(content.encode()).hexdigest() != digest:
|
||||
return finish(reason="readiness digest mismatch")
|
||||
git("merge-base", "--is-ancestor", revision, "HEAD")
|
||||
git("diff", "--exit-code", revision, "HEAD", "--", path)
|
||||
if git("status", "--porcelain", "--", path).strip():
|
||||
return finish(reason="readiness source has uncommitted changes")
|
||||
bindings = rows(content)
|
||||
if target["kind"] == "namespace":
|
||||
# Only the founder's one named placement is compiled into this gate.
|
||||
if bundle.id != "whitehat-foundational-plane" or bundle.expected_namespace != "whitehat":
|
||||
return finish(reason="no explicit tier placement for target")
|
||||
if any(row.get("namespace") == "whitehat" or "whitehat" in row.get("namespaces", [])
|
||||
or row["rapp_id"] == "rapp-whitehat" for row in bindings):
|
||||
return finish(reason="whitehat has a binding; repin using the binding target")
|
||||
return finish("explicit-whitehat", "founder placement 2026-09-21")
|
||||
rapp_id = target.get("rapp_id")
|
||||
matches = [row for row in bindings if row["rapp_id"] == rapp_id]
|
||||
if len(matches) != 1:
|
||||
return finish(reason="rApp target is not listed")
|
||||
row = matches[0]
|
||||
if (row.get("namespace") and row["namespace"] != bundle.expected_namespace) or (
|
||||
"namespaces" in row and bundle.expected_namespace not in row["namespaces"]
|
||||
):
|
||||
return finish(reason="source namespace mapping disagrees with bundle")
|
||||
state = row.get("readiness_state")
|
||||
# Scan all reachable history, including intervening promotions later
|
||||
# reverted. An evidence lapse must never silently lower the tier.
|
||||
history = git("log", "--format=%H", "HEAD", "--", path).splitlines()
|
||||
if not history:
|
||||
return finish(reason="readiness history is missing")
|
||||
previous = []
|
||||
for commit in history:
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", commit):
|
||||
return finish(reason="invalid readiness history")
|
||||
for old in rows(git("show", f"{commit}:{path}")):
|
||||
if old["rapp_id"] == rapp_id:
|
||||
previous.append(old.get("readiness_state"))
|
||||
if "production-approved" in previous:
|
||||
return finish("production-approved", "production tier retained from binding history")
|
||||
if state == "deprecated":
|
||||
state = next((s for s in previous if s != "deprecated"), None)
|
||||
if state not in NON_PRODUCTION | {"production-approved"}:
|
||||
return finish(reason="unknown readiness state or prior tier")
|
||||
return finish(state, "verified pinned binding and history")
|
||||
except (OSError, ValueError, TypeError, subprocess.TimeoutExpired, yaml.YAMLError):
|
||||
return finish(reason="readiness source or history unavailable or invalid")
|
||||
40
tests/test_custody_inventory.py
Normal file
40
tests/test_custody_inventory.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
"""Inventory failures must not be reported as a healthy empty store."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
spec = importlib.util.spec_from_file_location("inventory", Path(__file__).parents[1] / "scripts/custody-inventory.py")
|
||||
inventory = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(inventory)
|
||||
|
||||
|
||||
def test_no_scoped_grant_refuses_instead_of_using_operator_token(monkeypatch):
|
||||
monkeypatch.delenv("BAO_TOKEN", raising=False)
|
||||
monkeypatch.setattr(inventory.os.path, "exists", lambda _: False)
|
||||
with pytest.raises(inventory.InventoryError, match="No scoped"):
|
||||
inventory.walk("operators")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rc,output", [(1, ""), (0, "not-json"), (0, '{}')])
|
||||
def test_listing_errors_never_become_empty_success(monkeypatch, rc, output):
|
||||
monkeypatch.setenv("BAO_TOKEN", "synthetic-token")
|
||||
monkeypatch.setattr(inventory.subprocess, "run", lambda *a, **kw: SimpleNamespace(returncode=rc, stdout=output))
|
||||
with pytest.raises(inventory.InventoryError):
|
||||
inventory.walk("operators")
|
||||
|
||||
|
||||
def test_inventory_reads_only_metadata_and_reports_incomplete(monkeypatch, capsys):
|
||||
calls = []
|
||||
responses = {("kv", "list", "operators"): ["example/"],
|
||||
("kv", "list", "operators/example"): ["admin"],
|
||||
("kv", "metadata", "get", "operators/example/admin"): {"data": {"custom_metadata": {}}}}
|
||||
def bao(*args):
|
||||
calls.append(args)
|
||||
return responses[args]
|
||||
monkeypatch.setattr(inventory, "bao", bao)
|
||||
monkeypatch.setattr(inventory.sys, "argv", ["inventory", "operators", "--undescribed"])
|
||||
assert inventory.main() == 1
|
||||
assert "1 credential path(s), 1 missing" in capsys.readouterr().out
|
||||
assert all(c[:2] == ("kv", "list") or c[:3] == ("kv", "metadata", "get") for c in calls)
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from ops_mason.readiness import inspect_readiness, resolve_tier
|
||||
|
||||
from ops_mason.kubernetes_plane import (
|
||||
CommandResult,
|
||||
PlaneBundle,
|
||||
|
|
@ -16,6 +19,9 @@ from ops_mason.kubernetes_plane import (
|
|||
)
|
||||
|
||||
|
||||
READINESS = "bound_rapps:\n - rapp_id: rapp-test\n readiness_state: verified\n"
|
||||
REVISION = "a" * 40
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
|
|
@ -72,6 +78,11 @@ def _fixture(tmp_path: Path, *, approved: bool = True, kind: str = "Namespace")
|
|||
descriptor = {
|
||||
"schema_version": "ops-mason.kubernetes-plane/v1",
|
||||
"id": "plane",
|
||||
"readiness": {
|
||||
"target": {"kind": "rapp", "rapp_id": "rapp-test", "namespace": "whitehat"},
|
||||
"source": {"repo": "reef-railiance", "path": "bindings/rapps.yaml",
|
||||
"revision": REVISION, "sha256": hashlib.sha256(READINESS.encode()).hexdigest()},
|
||||
},
|
||||
"plan": "../plans/plane.md",
|
||||
"expected_context": "default",
|
||||
"expected_namespace": "whitehat",
|
||||
|
|
@ -148,6 +159,14 @@ class FakeCluster:
|
|||
self.calls.append(command)
|
||||
if command[:4] == ["git", "-C", command[2], "status"]:
|
||||
return CommandResult(0, " M src/ops_mason/kubernetes_plane.py\n" if self.dirty else "")
|
||||
if command[0] == "git":
|
||||
if command[3] == "rev-parse":
|
||||
return CommandResult(0, "false\n")
|
||||
if command[3] == "show":
|
||||
return CommandResult(0, READINESS)
|
||||
if command[3] == "log":
|
||||
return CommandResult(0, REVISION + "\n")
|
||||
return CommandResult(0)
|
||||
if command == ["kubectl", "config", "current-context"]:
|
||||
return CommandResult(0, self.context + "\n")
|
||||
if command[:4] == ["kubectl", "auth", "can-i", "create"]:
|
||||
|
|
@ -176,7 +195,8 @@ class FakeCluster:
|
|||
),
|
||||
)
|
||||
if command[:4] == ["kubectl", "-n", "whitehat", "get"]:
|
||||
return CommandResult(0, json.dumps({"items": []}))
|
||||
assert command[-2:] == ["-o", "name"]
|
||||
return CommandResult(0, "")
|
||||
raise AssertionError(f"unexpected command: {command}")
|
||||
|
||||
|
||||
|
|
@ -264,3 +284,147 @@ def test_rollback_is_generated_but_never_executed(tmp_path: Path) -> None:
|
|||
result = rollback_plan(bundle)
|
||||
assert result["object_scoped_commands"] == []
|
||||
assert result["conditional_namespace_commands"] == ["kubectl delete namespaces whitehat"]
|
||||
|
||||
|
||||
|
||||
class ReadinessCluster(FakeCluster):
|
||||
def __init__(self, content=READINESS, *, old=None, changed=False, shallow=False):
|
||||
super().__init__()
|
||||
self.content, self.old, self.changed, self.shallow = content, old, changed, shallow
|
||||
|
||||
def __call__(self, args):
|
||||
if args[0] == "git" and args[3] != "status":
|
||||
self.calls.append(list(args))
|
||||
if args[3] == "show":
|
||||
return CommandResult(0, self.old if args[4].startswith("b" * 40) else self.content)
|
||||
if args[3] == "log":
|
||||
return CommandResult(0, REVISION + "\n" + ("b" * 40 + "\n" if self.old else ""))
|
||||
if args[3] == "diff":
|
||||
return CommandResult(1 if self.changed else 0)
|
||||
if args[3] == "rev-parse":
|
||||
return CommandResult(0, "true" if self.shallow else "false")
|
||||
return CommandResult(0)
|
||||
return super().__call__(args)
|
||||
|
||||
|
||||
def readiness_bundle(tmp_path, content=READINESS):
|
||||
bundle = PlaneBundle.load(_fixture(tmp_path))
|
||||
bundle.readiness["source"]["sha256"] = hashlib.sha256(content.encode()).hexdigest()
|
||||
return bundle
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["missing", "platform", "unlisted", "digest", "changed", "unknown", "shallow", "namespace"])
|
||||
def test_unverifiable_readiness_refuses_before_kubectl(tmp_path, case):
|
||||
content = READINESS.replace("verified", "mystery") if case == "unknown" else READINESS
|
||||
bundle = readiness_bundle(tmp_path, content)
|
||||
cluster = ReadinessCluster(content, changed=case == "changed", shallow=case == "shallow")
|
||||
if case == "missing":
|
||||
bundle.readiness = None
|
||||
elif case == "platform":
|
||||
bundle.readiness["target"]["kind"] = "platform"
|
||||
elif case == "unlisted":
|
||||
bundle.readiness["target"]["rapp_id"] = "absent"
|
||||
elif case == "digest":
|
||||
bundle.readiness["source"]["sha256"] = "0" * 64
|
||||
elif case == "namespace":
|
||||
bundle.readiness["target"]["namespace"] = "other"
|
||||
with pytest.raises(PlaneRefused, match="production tier"):
|
||||
apply(bundle, confirm_plan_id="plane", expected_digest=bundle.digest, runner=cluster)
|
||||
assert not any(c[0] == "kubectl" for c in cluster.calls)
|
||||
|
||||
|
||||
def test_production_preflight_reports_but_apply_refuses(tmp_path):
|
||||
content = READINESS.replace("verified", "production-approved")
|
||||
bundle = readiness_bundle(tmp_path, content)
|
||||
cluster = ReadinessCluster(content)
|
||||
assert preflight(bundle, cluster)["readiness"]["tier"] == "production"
|
||||
with pytest.raises(PlaneRefused, match="production tier"):
|
||||
apply(bundle, confirm_plan_id="plane", expected_digest=bundle.digest, runner=cluster)
|
||||
assert not cluster.applied
|
||||
|
||||
|
||||
def test_break_glass_requires_reason_and_records_reconciliation(tmp_path):
|
||||
content = READINESS.replace("verified", "production-approved")
|
||||
bundle = readiness_bundle(tmp_path, content)
|
||||
cluster = ReadinessCluster(content)
|
||||
with pytest.raises(PlaneRefused, match="non-empty reason"):
|
||||
apply(bundle, confirm_plan_id="plane", expected_digest=bundle.digest,
|
||||
runner=cluster, activation="BREAK_GLASS", break_glass_reason=" ")
|
||||
assert not cluster.calls
|
||||
result = apply(bundle, confirm_plan_id="plane", expected_digest=bundle.digest,
|
||||
runner=cluster, activation="BREAK_GLASS", break_glass_reason="Incident test")
|
||||
assert result["readiness"]["tier"] == "production"
|
||||
assert result["break_glass"]["reason"] == "Incident test"
|
||||
assert result["break_glass"]["actor"] and result["break_glass"]["recorded_at"]
|
||||
assert "ArgoCD" in result["break_glass"]["follow_up"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("day,allowed", [(20, True), (21, False), (22, False)])
|
||||
def test_policy_nexus_transition_expires_at_review_date(tmp_path, day, allowed):
|
||||
content = READINESS.replace("rapp-test", "rapp-policy-nexus").replace("verified", "production-approved")
|
||||
bundle = readiness_bundle(tmp_path, content)
|
||||
bundle.readiness["target"]["rapp_id"] = "rapp-policy-nexus"
|
||||
cluster = ReadinessCluster(content)
|
||||
kwargs = dict(confirm_plan_id="plane", expected_digest=bundle.digest, runner=cluster, today=date(2026, 12, day))
|
||||
if allowed:
|
||||
result = apply(bundle, **kwargs)
|
||||
assert result["readiness"]["transition"] and result["readiness"]["tier"] == "production"
|
||||
else:
|
||||
with pytest.raises(PlaneRefused, match="production tier"):
|
||||
apply(bundle, **kwargs)
|
||||
assert not cluster.applied
|
||||
|
||||
|
||||
@pytest.mark.parametrize("state,old,tier", [("verified", "production-approved", "production"),
|
||||
("deprecated", "production-approved", "production"), ("deprecated", "verified", "non-production")])
|
||||
def test_lapse_and_deprecation_retain_previous_tier(tmp_path, state, old, tier):
|
||||
content = READINESS.replace("verified", state)
|
||||
bundle = readiness_bundle(tmp_path, content)
|
||||
cluster = ReadinessCluster(content, old=READINESS.replace("verified", old))
|
||||
assert inspect_readiness(bundle, cluster, None)["tier"] == tier
|
||||
|
||||
|
||||
def test_whitehat_explicit_placement_and_binding_supersession(tmp_path):
|
||||
bundle = readiness_bundle(tmp_path)
|
||||
bundle.id = "whitehat-foundational-plane"
|
||||
bundle.readiness["target"] = {"kind": "namespace", "namespace": "whitehat"}
|
||||
assert inspect_readiness(bundle, ReadinessCluster(), None)["tier"] == "non-production"
|
||||
content = READINESS.replace("rapp-test", "rapp-whitehat")
|
||||
bundle.readiness["source"]["sha256"] = hashlib.sha256(content.encode()).hexdigest()
|
||||
assert inspect_readiness(bundle, ReadinessCluster(content), None)["tier"] == "production"
|
||||
|
||||
|
||||
def test_unknown_activation_never_allows_apply():
|
||||
assert not resolve_tier("verified", {}, "anything", date.today())["direct_apply_allowed"]
|
||||
|
||||
|
||||
def test_real_git_history_retains_promotion_even_after_file_reverted(tmp_path):
|
||||
"""A clean file equal to its pin is not proof it was never production."""
|
||||
import subprocess
|
||||
from ops_mason.kubernetes_plane import subprocess_runner
|
||||
|
||||
bundle = readiness_bundle(tmp_path)
|
||||
repo = tmp_path / "reef"
|
||||
repo.mkdir()
|
||||
def git(*args):
|
||||
return subprocess.run(["git", "-C", str(repo), *args], check=True,
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
git("init")
|
||||
git("config", "user.name", "Test")
|
||||
git("config", "user.email", "test@example.invalid")
|
||||
(repo / "bindings").mkdir()
|
||||
source = repo / "bindings/rapps.yaml"
|
||||
def commit(content, message):
|
||||
source.write_text(content)
|
||||
git("add", "bindings/rapps.yaml")
|
||||
git("commit", "-m", message)
|
||||
commit(READINESS, "verified")
|
||||
bundle.readiness["source"]["revision"] = git("rev-parse", "HEAD")
|
||||
assert inspect_readiness(bundle, subprocess_runner, repo)["tier"] == "non-production"
|
||||
commit(READINESS.replace("verified", "production-approved"), "promote")
|
||||
commit(READINESS, "evidence lapse")
|
||||
result = inspect_readiness(bundle, subprocess_runner, repo)
|
||||
assert result["tier"] == "production"
|
||||
assert result["reason"] == "production tier retained from binding history"
|
||||
source.write_text(READINESS + "# uncommitted\n")
|
||||
assert "uncommitted" in inspect_readiness(bundle, subprocess_runner, repo)["reason"]
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ type: workplan
|
|||
title: "Describe every stored credential so the store is navigable"
|
||||
domain: infotech
|
||||
repo: ops-mason
|
||||
status: proposed
|
||||
status: blocked
|
||||
flavor: planning
|
||||
owner: codex
|
||||
topic_slug: custodian
|
||||
created: "2026-08-28"
|
||||
updated: "2026-09-28"
|
||||
related:
|
||||
- MASON-WP-0003
|
||||
- NK-WP-0033
|
||||
|
|
@ -48,7 +49,7 @@ read, and `ops-mason-build` cannot read one.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0004-T01
|
||||
status: todo
|
||||
status: wait
|
||||
priority: medium
|
||||
state_hub_task_id: "5bda75f2-f780-579e-9d44-cc4011662b9a"
|
||||
```
|
||||
|
|
@ -68,7 +69,7 @@ listed as unknown with the question that would settle it.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0004-T02
|
||||
status: todo
|
||||
status: wait
|
||||
priority: medium
|
||||
state_hub_task_id: "a6a944d7-21c5-5043-a78d-b3809de0ee64"
|
||||
```
|
||||
|
|
@ -121,3 +122,9 @@ the report has a reader.
|
|||
|
||||
Acceptance: an undescribed path added today surfaces without anyone
|
||||
remembering to look.
|
||||
|
||||
## Loose-end review — 2026-09-28
|
||||
|
||||
T01–T04 remain wait: no valid scoped metadata grant/current inventory or complete owner/recovery confirmations are available. Inventory failure handling and the private-tunnel defaults are fixed and tested; scheduled reporting still needs its reader and credential.
|
||||
|
||||
Evidence and precise resumption conditions: [review](../docs/evidence/2026-09-28-loose-end-review.md). Existing tasks retain all remaining obligations; no new task or workplan was opened.
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ type: workplan
|
|||
title: "Construct the fluid-telegram operator credential lane"
|
||||
domain: infotech
|
||||
repo: ops-mason
|
||||
status: proposed
|
||||
status: blocked
|
||||
flavor: planning
|
||||
owner: codex
|
||||
topic_slug: helix-forge
|
||||
created: "2026-09-04"
|
||||
updated: "2026-09-04"
|
||||
state_hub_workstream_id: "483e56d6-6601-5377-9066-66225214c046"
|
||||
updated: "2026-09-28"
|
||||
state_hub_workstream_id: "483e56d6-6601-5377-9066-66225214c046"
|
||||
---
|
||||
|
||||
|
|
@ -65,7 +64,7 @@ from the current disk-only survey.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0005-T02
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "69c558d9-664f-5ce0-80b2-d2e7544353a3"
|
||||
```
|
||||
|
|
@ -86,7 +85,7 @@ parent access, and the create-only salt shape.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0005-T03
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "48a2b4ec-8e66-5b98-b039-c17fd8d820ab"
|
||||
```
|
||||
|
|
@ -103,7 +102,7 @@ adapter separation for explicit human approval. Only the plan's
|
|||
|
||||
```task
|
||||
id: MASON-WP-0005-T04
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "0e3d1333-ffc3-5f67-8528-50a9551c4949"
|
||||
```
|
||||
|
|
@ -122,7 +121,7 @@ a provisioner capability.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0005-T05
|
||||
status: todo
|
||||
status: wait
|
||||
priority: high
|
||||
state_hub_task_id: "4c205be7-4f1f-5dd1-aafa-fc112fdd640e"
|
||||
```
|
||||
|
|
@ -147,7 +146,7 @@ operator/platform flow and return metadata-only evidence to ops-mason.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0005-T06
|
||||
status: todo
|
||||
status: wait
|
||||
priority: medium
|
||||
state_hub_task_id: "09eae088-808d-5342-b100-292e13958ee3"
|
||||
```
|
||||
|
|
@ -163,3 +162,9 @@ The adapter lane is tracked separately as `MASON-IN-0003`, tied to
|
|||
`redaction-salt`; it must be denied `operator-app` and `operator-session` and
|
||||
must use the adapter's own runtime identity rather than this attended OIDC
|
||||
role.
|
||||
|
||||
## Loose-end review — 2026-09-28
|
||||
|
||||
T01–T06 remain wait: accepted tenant/matrix, confirmed identity/MFA/callbacks, approved matching writer contracts, live survey, explicit construction approval and verification are outstanding. The platform design remains proposed. No lane was built or activated.
|
||||
|
||||
Evidence and precise resumption conditions: [review](../docs/evidence/2026-09-28-loose-end-review.md). Existing tasks retain all remaining obligations; no new task or workplan was opened.
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ type: workplan
|
|||
title: "Check the target's readiness tier before a direct Kubernetes apply"
|
||||
domain: infotech
|
||||
repo: ops-mason
|
||||
status: proposed
|
||||
status: blocked
|
||||
flavor: implementation
|
||||
owner: claude
|
||||
topic_slug: ops-mason
|
||||
created: "2026-09-21"
|
||||
updated: "2026-09-21"
|
||||
updated: "2026-09-28"
|
||||
state_hub_workstream_id: "1b900161-51ff-544f-8412-ba7fcab64bb5"
|
||||
---
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ policy-nexus rule was used.
|
|||
|
||||
```task
|
||||
id: MASON-WP-0006-T01
|
||||
status: wait
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "7b6fe7b4-08f7-5ad0-899d-a4862b5ddb00"
|
||||
```
|
||||
|
|
@ -120,11 +120,15 @@ question ops-mason can answer for itself. The founder chooses one of:
|
|||
|
||||
T03 must not merge before this is answered.
|
||||
|
||||
**Resolved 2026-09-21; implemented 2026-09-28.** The founder chose explicit
|
||||
non-production placement for namespace whitehat. Decision and inbox receipt
|
||||
are recorded in the September 28 review. This unblocks T03.
|
||||
|
||||
## Extend the bundle schema with the readiness block
|
||||
|
||||
```task
|
||||
id: MASON-WP-0006-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "89d07bd5-5a92-5a06-9ecb-441799c14dd7"
|
||||
```
|
||||
|
|
@ -135,11 +139,15 @@ is included in the bundle digest, as the bundle file already is. Add a
|
|||
`APPROVED`, and a `--readiness-repo` option. `BREAK_GLASS` requires a
|
||||
`--break-glass-reason` string. No existing refusal is relaxed.
|
||||
|
||||
**Done 2026-09-28.** Schema/CLI implemented; the mapping also requires an
|
||||
explicit namespace matching the bundle. Whitehat now pins the source used to
|
||||
check whether a binding supersedes its explicit placement.
|
||||
|
||||
## Enforce the tier in preflight and apply
|
||||
|
||||
```task
|
||||
id: MASON-WP-0006-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "163a807b-29aa-5eb7-a9a8-ef1ac70c89d3"
|
||||
```
|
||||
|
|
@ -163,11 +171,17 @@ Tests, all against the injected runner:
|
|||
- the existing forbidden-kind, `data`/`stringData` and namespace tests still pass
|
||||
unchanged.
|
||||
|
||||
**Done 2026-09-28.** Source digest, ancestry, local freshness and complete
|
||||
binding history are checked. Historical production approval remains production;
|
||||
deprecation retains the last known tier. Unknowns fail closed. The policy-nexus
|
||||
transition stops on December 21 itself. Preflight reports; apply refuses before
|
||||
Kubernetes commands. Covered by injected-runner regression tests.
|
||||
|
||||
## Record BREAK_GLASS and its reconcile-back obligation
|
||||
|
||||
```task
|
||||
id: MASON-WP-0006-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "66349531-09ee-55b9-be47-537842c80f3b"
|
||||
```
|
||||
|
|
@ -177,11 +191,15 @@ record and prints the follow-up that is owed: the same change, committed to the
|
|||
manifest repository that ArgoCD reconciles. ops-mason does not open that change
|
||||
itself.
|
||||
|
||||
**Done 2026-09-28.** Apply evidence and printed JSON include the emergency
|
||||
reason, local account, UTC time and GitOps reconciliation obligation. Empty
|
||||
reasons are refused without invoking the runner.
|
||||
|
||||
## Update the docs and the declaration
|
||||
|
||||
```task
|
||||
id: MASON-WP-0006-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "6d64e7f5-9cff-5bf9-9851-97d318d1df0a"
|
||||
```
|
||||
|
|
@ -190,6 +208,10 @@ Update `docs/kubernetes-plane.md` with the tier table and the readiness block,
|
|||
and change the `enforcement` line of the `kubernetes-plane-apply` entry in
|
||||
`INTENT.md` from "not yet in code" to point at the check.
|
||||
|
||||
**Done 2026-09-28.** Documented tiers, pins, mapping, history, checkout
|
||||
freshness limits and emergency procedure; removed the withdrawn
|
||||
rail-kubernetes ownership/layer assertion from INTENT.md.
|
||||
|
||||
## Review on 2026-12-21
|
||||
|
||||
```task
|
||||
|
|
@ -203,3 +225,9 @@ On 2026-12-21 the policy-nexus transition ends and the plan-approval exception
|
|||
comes up for review. Confirm with railiance-platform that policy-nexus is
|
||||
onboarded to ArgoCD. The date check in T03 ends the transition in code on that
|
||||
date either way.
|
||||
|
||||
## Loose-end review — 2026-09-28
|
||||
|
||||
T01–T05 are done: the founder resolved the whitehat placement and the guarded readiness implementation, tests, emergency evidence and docs are complete. T06 remains wait until the 2026-12-21 platform/exception review; this workplan is blocked on that dated review.
|
||||
|
||||
Evidence and precise resumption conditions: [review](../docs/evidence/2026-09-28-loose-end-review.md). Existing tasks retain all remaining obligations; no new task or workplan was opened.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue