ops-mason/docs/construction-plan-format.md

188 lines
9 KiB
Markdown
Raw Normal View History

# Construction plan format
The artifact phase 1 (`INTENT.md`) produces. One markdown file per plan,
stored at `plans/<plan-id>.md` in this repo — plain, versioned, reviewable
in a diff, not a database. `<plan-id>` is kebab-case and traceable to the
demand that triggered it (usually a workplan task id from the requesting
repo, e.g. `rein-openweights-openrouter-approle` for
`glas-harness/GLAS-WP-0002-T02`).
A plan file is the single source of truth across all four phases: phase 1
writes it, phase 2 edits it in place (self-review, not a separate
document), phase 3's executive summary is generated *from* it (not a
parallel artifact that can drift), and phase 4 reads the same file to
execute and appends a result section when done.
## Frontmatter
```yaml
---
id: rein-openweights-openrouter-approle
demand_source: glas-harness/workplans/GLAS-WP-0002-T02
consumer_repo: rein-openweights
credential_type: openbao-approle-kv
status: draft # draft -> reviewed -> approved -> built -> catalogued
approved_by: null # founder identity string, set only at phase 3 -> 4
approved_at: null # ISO date, set alongside approved_by
created: "2026-07-27"
updated: "2026-07-27"
---
```
`status` is the phase tracker for the plan itself, distinct from any
State Hub task status — it's what the build executor (`MASON-WP-0001-T04`)
checks before doing anything: **refuses to execute unless
`status: approved` and both `approved_by`/`approved_at` are set.** No
other field or file can substitute for this — that's the one hard gate.
## Body sections
### 1. Demand
Plain description: who needs what, why, and what they need it *for* (one
paragraph — this feeds the executive summary's plain-language framing
directly, so write it for the founder, not for `bao`).
### 2. Existing-structure survey
What already exists that might cover this, checked *before* proposing
anything new:
- Relevant `bao policy list` / `bao auth list` / `bao secrets list`
entries (paste the actual output, not a paraphrase)
- Relevant `ops-warden/registry/routing/catalog.yaml` entries (grep by
`owner_repo`/`subsystem`/`need_keywords`)
- Explicit answer: **does an existing lane already satisfy this demand?**
If yes, the plan's proposed changes should be "bind to existing policy
X" or "reuse existing path Y," not "create new." (See
`binky-control/integrations/executor-worker-secrets.md`'s own Lane 1 —
reused the existing `llm-connect-provider-secrets` secret instead of
minting a new one, because the scoping already covered the new
consumer.)
### 3. Proposed changes
A list, each item tagged `create` / `modify` / `tear-down`, each with a
one-line reuse-vs-new rationale referencing section 2. Every item names
exactly what OpenBao object it touches (policy name, AppRole name, KV
path) — no vague "provision access" line items.
### 4. Review notes (phase 2)
Filled in during the self-review pass, before a human ever sees the
plan: naming/TTL/scoping convention check against existing lanes (e.g.
the `agent-harness-binky-mail` shape — `token_ttl=15m`,
`token_max_ttl=30m`, bounded `token_num_uses`), redundancy check, and any
compaction opportunity spotted along the way. If this section is empty,
the plan is not ready for phase 3.
### 5. Executive summary (phase 3, generated from sections 1-4)
See `docs/executive-summary-format.md` (`MASON-WP-0001-T03`) for the
exact rendering. Lives in the same plan file so there is one document to
read, not two to cross-reference.
### 6. Build result (phase 4, appended after execution)
What was actually created/modified/torn down (object names, not values),
the ops-warden catalog entry proposed (id + PR/commit reference once
filed), and the exact `paste_once_provision` step the founder still needs
to run (path, field) if the plan involved a new secret value. This
section does not exist until phase 4 has run.
## Worked example (also the input to `MASON-WP-0001-T05`)
See `plans/rein-openweights-openrouter-approle.md` once T01 is
implemented against this format — the first real plan, not a synthetic
one, per `MASON-WP-0001`'s own scoping.
## Binding OpenBao approval to execution inputs
The AppRole and Kubernetes-auth builders additionally require two frontmatter
fields: `build_spec` and `approved_spec_sha256`. `build_spec` is the complete
value-free output of `ops_mason.executor.build_spec_document(spec)`. It includes
the engine, exact policy/role/path and identity bindings, token bounds, policy
reuse and content pin, delivery destination, audit destination, and executable.
`credential_type` must match its `engine`.
During drafting, render the specification and its digest for review:
```python
from pathlib import Path
from ops_mason.executor import AppRoleKVSpec, build_spec_document, build_spec_digest
spec = AppRoleKVSpec(
policy_name="workload-kv-read-example",
kv_path="platform/workloads/example/runtime",
approle_name="example",
token_num_uses=8,
delivery_dir=Path("/home/consumer/.local/example/approle"),
audit_log_path=Path("/home/builder/ops-mason/audit/build-log.jsonl"),
)
document = build_spec_document(spec) # put this mapping under build_spec
candidate_digest = build_spec_digest(document) # present alongside the review
```
The digest is SHA-256 of UTF-8 JSON with sorted keys, compact separators and
no NaN values. Paths render as absolute strings; tuples render as lists. All
specification fields, including explicit defaults and null values, participate.
After the founder approves that exact specification, record the digest as
`approved_spec_sha256` alongside `approved_by`, `approved_at` and
`status: approved`. Quote approval dates in YAML. A candidate digest alone
is not approval, and the digest is an integrity marker, not a signature.
Before any OpenBao command, the builder reloads the plan from disk, checks
approval and engine, validates the specification, and compares both the
frontmatter mapping and approved digest to the actual inputs. Duplicate YAML
keys, missing build specifications, changed scope or changed destinations are
refused. Editing the mapping without renewing its approval digest cannot reuse
the previous approval. The plan file remains the trust boundary: this does not
protect against someone authorized to rewrite the approval record itself.
Historical plans stay historical. They are not automatically given new
specifications or approvals. Before re-executing one, draft and review the exact
new inputs through the existing four-phase process. No live lane was changed
by this format migration.
### Input and policy constraints
The existing AppRole engine creates only exact KV-v2 read lanes: one literal
entry path, `read` on data and metadata, no wildcard, empty/parent path segment,
HCL injection, or write capability. OIDC matrix writers remain a separate,
blocked contract under MASON-WP-0005. Role and policy names must be single
literal names; Kubernetes service-account names/namespaces must be explicit,
nonempty, unique DNS labels. Token use counts must be explicit nonnegative
integers (zero is deliberately unlimited when approved); TTLs are positive
integer durations in `s`, `m` or `h`, with maximum TTL at least initial TTL.
`secret_id_ttl: "0"` remains an explicit reviewable choice.
For `AppRoleKVSpec(reuse_policy=True)`, also provide
`reuse_policy_sha256`. For `KubernetesKVSpec`, provide `policy_sha256`, since
that builder always binds an existing policy. During the scoped structure
survey, hash the exact UTF-8 output of `bao policy read <name>`, including its
trailing newline, and review that policy's full scope. The builder reads only
policy text, checks its hash before auth-role writes, and refuses drift. A
reused policy may deliberately cover more than the AppRole's nominal KV path;
its reviewed contents, not that path field alone, define the grant. Rechecking
a pin is not an atomic lock against later administrative policy changes.
### Credential delivery and failed builds
AppRole delivery requires an absolute directory. Every directory component is
opened without following symlinks; missing directories are created privately.
An existing final directory must belong to the executing account with mode
0700. Both credential files are reserved exclusively at mode 0600 before any
policy/auth write or credential issuance. Existing files, hardlinks and symlink
paths are refused rather than overwritten or permission-repaired. Delivery
uses the held file descriptors and flushes credentials to disk.
OpenBao error and timeout output is suppressed so a failed credential command
cannot echo sensitive material. Audit output contains object names, the
approved specification digest and approval attribution, never credentials.
A failed build can leave private empty or partially delivered files, and may
have changed policy/auth structure or issued a credential before failing.
Do not blindly retry or delete them: inspect structure and audit metadata,
resolve/revoke any partial issuance through the scoped custody procedure, and
review a fresh delivery destination before retrying. The executor does not
claim transactional rollback or automatically widen its revocation authority.