ops-mason/docs/construction-plan-format.md
tegwick 2b83324c01 Bind OpenBao builds to approved inputs and secure credential delivery
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e75a-fc5c-7913-9dba-9846210c766d
2026-09-28 11:55:39 +02:00

9 KiB

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

---
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:

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.