secrets-engine/workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md
tegwick 8a48cb05df Apply GH-DEC-2026-017: INTENT.md governs, layer.yaml is derived, no version
Verified against gate-house's committed ruling (decisions/decisions.md,
GH-DEC-2026-017) and amendments A9-A13, then ops-warden's reference change set
(a70f559, wiki/playbooks/netkingdom-layer-declaration.md). They agree.

layer.yaml: standard_version removed; derived: true and derived_from:
INTENT.md added; declared_by kept. INTENT.md frontmatter never carried
standard_version, but its standard: value was a version-pinned path; it is
de-versioned as the reference instance did. No layer value is re-spelled:
INTENT.md still says Engine and layer.yaml still says engine.

The checker changes in the same commit because it listed standard_version as
a required key: removing the field alone would have made a conforming
declaration exit 2 MALFORMED. It now reads INTENT.md as the governing form,
requires the derived marking, rejects a returning standard_version in either
form, checks both layer values against the closed four-token vocabulary
(Taxonomy included) after an ASCII fold, and reports a post-fold disagreement
between the forms as a finding rather than resolving it by precedence.

Tests assert the fold, not per-file spelling, and cover fold agreement, a
real disagreement, the closed vocabulary and a returning version. Full suite
430 passed.

role:, pep-stance.yaml and schema_version are untouched (not ruled). Still
open: where the removed version lives in a derived conformance record; asked
of gate-house by ops-warden (4220413a), followed rather than chosen here.

Closes the SECRETS-WP-0008 note that waited on the reference form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
2026-09-21 07:37:53 +02:00

23 KiB
Raw Blame History

id type title domain repo status flavor owner topic_slug created updated state_hub_workstream_id
SECRETS-WP-0008 workplan Evolve the Lifecycle engine to the accepted security layer model infotech secrets-engine active implementation grok custodian 2026-08-29 2026-09-21 9c9e5164-b2f5-5ea2-a557-5368d65e9fe0

SECRETS-WP-0008 - Evolve the Lifecycle engine to the accepted security layer model

Goal

Close the gaps between the 2026-08-29 layer declaration (Engine / Lifecycle) and the obligations the accepted NetKingdom Security Layer Model v0.7 places on a PEP-shaped Lifecycle engine.

Source review: history/2026-08-29-layer-model-scope-intent-review.md. Declaration surface already landed this session: INTENT.md frontmatter, layer.yaml, pep-stance.yaml, and tests/test_layer_conformance.py.

This workplan does not restart CLI hardening owned by SECRETS-WP-0007. It adds the layer-model remainder: stance binding, decision-record consumption, audit-core evidence, and the two proposed engine surfaces.

Non-goals

  • Taking ops-warden's SSH access lane or becoming a credential broker.
  • Rendering or caching authorization decisions.
  • Observation in production or automatic containment (companion §10: both are at zero estate-wide).
  • Service mode, runtime tenancy, or a second custody backend.
  • Reopening live production on State Hub status or local fixtures.

Coordination

  • SECRETS-WP-0007-T04 owns the consumer validator and the fail-closed production gate until a durable ActionAuthorization object is served.
  • SECRETS-WP-0007-T05 owns delivery-session self-revocation and the KeyCape scaffold; this workplan owns the layer-model "no standing credential" bar.
  • railiance-platform owns production OpenBao and the JWT role.
  • access-engine / State Hub own serving the decision record.
  • audit-core owns evidence custody.
  • ops-warden owns the SSH Staff PEP and must assent any SSH-CA API contract.
  • kings-guard owns secret-abuse posture and must assent the evidence surface.
  • gate-house owns statute §13.1 inventory of PEP stance maps.

Tasks

Publish stance application as shipped behaviour

id: SECRETS-WP-0008-T01
status: done
priority: high
state_hub_task_id: "945735c6-2d3d-5f72-baf0-692e6b210889"

Completed 2026-08-29. _require_lane_approval loads pep-stance.yaml. SHIPPED_STANCE is pinned equal to the published map. Fail-closed production evidence records stance_stage, stance_failure_mode, and omits stance_decision_id unless a decision was rendered.

pep-stance.yaml is published and tests pin prod: fail_closed to _require_lane_approval. The gate is still hardcoded, and a refusal is recorded as a DecisionError rather than named stance fields.

Load the published map as the source of shipped defaults. When the unreachable-engine residue is applied, record at least stage, failure mode, and decision id present only where a decision was rendered. Keep the three-factor unsafe-demo exception out of the stance table. Assert map equality in tests/test_layer_conformance.py the way ops-warden's stance test does.

Acceptance:

  • _require_lane_approval (or its successor) reads pep-stance.yaml.
  • Changing the YAML without changing the code fails the test, and the reverse.
  • Fail-closed production evidence includes named stance fields, never a secret.
  • Build/test remain fail-open relative to access-engine and still require the existing lane-approval check; that residue stays documented as a gap until T02.

Consume access-engine decision records

id: SECRETS-WP-0008-T02
status: wait
priority: high
state_hub_task_id: "3eb9cff8-1441-5437-9e92-a2b655c82d04"

Progress 2026-09-02. Gate House notice 632bdad9 (GH-DEC-2026-003): this engine is the PEP for FLEX-WP-0017-T05 / OpenBao writes. The shared consume function now lives in src/secrets_engine/approval_consume.py and every live privileged production handler reaches it through _require_lane_approval before OpenBaoClient.resolve. Conflict, unavailability, or a missing binding fail closed with no OpenBao call. Production still also fail-closes on the unpublished durable ActionAuthorization / consume-binding serving path, so this task remains wait.

Correction 2026-09-06. See SECRETS-WP-0007-T04 for the full note. Summary: the consume-before-OpenBao gate was real, but the PIP claim + validate join was a return None stub and validate_action_authorization had no production caller, so this task's "blocked on the durable serving path" framing hid an unimplemented local seam. The join is now implemented and tested; the remaining blocker is genuinely external (approval-engine claim endpoint and access-engine Check, both unreachable as of 2026-09-06).

Progress 2026-09-07. Step 2 is no longer blocked and is proved against a real decision. The 2026-09-06 note above says "approval-engine claim endpoint and access-engine Check, both unreachable"; the second half of that is now out of date and is corrected here rather than left to read as current.

access-engine Check is reachable through the owner-documented access path (loopback kubectl port-forward to the named pod plus a bounded TokenRequest token; docs/pdp-access-path.md). A real CheckRequest for glas-claude-agent-dev-anthropic returned decision:0f9c98f14545c42d, allow, catalog_lane_policy_matched, under package secrets-engine.catalog-lane.lifecycle v2 with binding.tenant: tenant:platform.

Two defects surfaced only because the request was real, both now fixed:

  • Our CheckRequest carried no tenant, which v2 treats as a wrong_tenant denial rather than an omission (80eafaf).
  • validate_decision_envelope required byte-equality between the binding and our unenriched request, which no real decision can satisfy because the evaluator normalizes and enriches from its registry before hashing. Corrected to the published consumer rule in 10baad9.

So the acceptance line "wrong digest, expired lifetime, superseded status, or missing record fail closed before OpenBao" is now exercised against a genuine envelope: the unrefreshed live decision is refused on lifetime, and a restated resource.attributes.stage, a foreign subject.tenant, or an unexpected enrichment field are each refused.

What remains is step 1 only. approval-engine must serve the claim endpoint; APPROVAL-WP-0002-T03 is still wait with no deployed base URL, so resolve_consume_binding returns None and production stays fail-closed. That is one external dependency rather than the two this task previously named.

Also recorded, and not a blocker for this task: flex-auth's response channel is unauthenticated (FLEX-DEC-2026-010). Fail-closed protects against a PDP that is absent, not one that lies. Until FLEX-WP-0024 ships detached signatures, the enforced loopback address shape is what stands in for responder authenticity.

Blocked on the durable ActionAuthorization serving path owned with SECRETS-WP-0007-T04 / State Hub / access-engine.

Once that object is served, a protected side effect holds a decision record identifying the request it was rendered for. Replay is permitted iff the canonical request digest matches and the lifetime holds. Do not cache the verdict. Do not treat State Hub status, a local fixture, or catalog admission as the decision.

Acceptance:

  • Every live privileged production handler passes one shared consume function.
  • Wrong digest, expired lifetime, superseded status, or missing record fail closed before OpenBao.
  • Evidence carries the decision id; a fail-closed residue still records the stance instead.
  • Local fixtures cannot authorize steady-state production.

2026-09-09 published replay-identity contract received

Owner message 83db7e47-ba93-4d3e-ac15-2bcb2bb16a34 supplies FLEX-DEC-2026-012, flex-auth source 0bc624b, and updated docs/request-enrichment.md, docs/canonical-request-digest.md and replay examples. This supersedes the earlier "only external dependency" reading. T02 is progress: the next work is an executable local consumer correction, with no new owner interpretation or approval needed.

Implement binding.submitted_request_digest comparison against the exact submitted CheckRequest. Stop treating enriched request_digest or the consumer's chosen list of benign enrichment fields as the replay identity. Preserve policy pin, lifetime and deny checks. Refresh the owner's replay fixtures without changing their existing request_digest/approval_binding_digest pins.

For the approval join, carry the validated approval claim to the PDP and compare its recorded pdp_digest to the evaluator's approval_binding_digest. Do not recompute either side from unenriched material. Prove changed request, wrong claim/digest, absent new binding field and registry-overridden caller attributes are handled under the published contract before any live native lane activates.

The separate approval identity exchange is implemented in SECRETS-WP-0009-T03 and passes 350 tests plus a real KeyCape/Approval Engine component exercise. That exercise uses a PDP double and does not prove this newer replay contract. The current source has not yet implemented the changes in this return; live PDP rollout/adoption evidence remains explicit too. No synthetic fixture or Hub decision substitutes for a runtime allow and consume.

2026-09-09 replay consumer implementation and real component acceptance

The returned correction is implemented. The PEP now checks the exact submitted request digest including the full carried approval, permits registry-owned attribute replacement, joins the evaluator-origin approval digests and rechecks claim freshness after Check. The consume CAS uses the validated evaluator's request_digest and decision id; an observation alone is not a consume binding.

334 regression tests pass. The disposable KeyCape + actual Approval Engine + compiled Flex Auth exercise passes 13 checks, including real dual-control denial then claim/check/consume, registry override, changed submitted request, wrong action, consumed claim and same/different CAS digest. Independent producer inputs replace response-reconstructed replay fixtures; existing digest pins remain unchanged. Receipt: docs/evidence/2026-09-09-replay-contract-exercise.json. No OpenBao side effect, live credential read or model request occurred.

T02 is wait for the live return: deployed PDP contract adoption and the admitted Approval Engine claim/consume path (APPROVAL-WP-0002-T03 and RPF-WP-0035-T06 / AUDIT-WP-0009-T09). SECRETS-WP-0009-T03 owns native delivery acceptance. The old live decision lacks submitted_request_digest and is explicitly refused; its historical evidence was not rewritten. Local conformance does not close the remaining live task. This supersedes earlier additive-only and claim-free transitive correspondence descriptions above.

Emit evidence under the §9.6 bound

id: SECRETS-WP-0008-T03
status: done
priority: high
state_hub_task_id: "0b54cedf-9335-5fa0-bdcb-1efa9a9cd98d"

Completed 2026-08-29. evidence-classification.yaml is the declaration. Load-bearing records are queued in evidence_dir/outbox/ before JSONL. secrets-engine evidence heartbeat emits a positive claim. Drain never sits on a mutation path and an audit-core outage leaves the queue in place.

Classify each privileged action as load-bearing or attributive. Today's trail is attributive local JSONL plus best-effort State Hub notes; completeness is already not claimed and must stay that way until the destination is audit-core.

Load-bearing classes (at least live destroy, production revoke/suspend of a control, and dual-control provision once those are enabled) require atomic emission via a local outbox and a declared cadence or heartbeat. Do not place a synchronous audit-core dependency inside the state-change transaction. Attributive classes may keep the current non-blocking trade if that trade stays declared.

Acceptance:

  • A machine-readable classification exists next to the declaration.
  • No control in this repository branches on presence or absence of a local evidence record.
  • Load-bearing emission is queued locally; an audit-core outage does not block a revocation.
  • Documentation uses the sound claim: the archive proves records it holds were not altered or truncated after arrival.

Accept the SSH-CA signing engine surface

id: SECRETS-WP-0008-T04
status: done
priority: medium
state_hub_task_id: "581aeee3-3d99-5034-992b-7d42e6542a19"

Completed 2026-08-29. Contract published at docs/ssh-ca-signing-contract.md. No OpenBao SSH-CA write was added. layer.yaml owner_status remains proposed.

ops-warden's ssh-ca-signing-write gap names this engine as intended owner. Accept the engine API, decline the Staff lane. Publish a contract ops-warden can call after an access-engine decision: sign a short-lived certificate for an adm/agt/atm actor under the caller's request, present no additional authority, reconstruct as the caller's action.

Do not implement a replacement client until ops-warden assents the contract. Do not unpark the standalone warden-sign AppRole as break-glass.

Acceptance:

  • A non-secret contract document in docs/ states request, decision binding, lifetime, evidence, and non-responsibilities.
  • ops-warden has a recorded chance to assent or contest.
  • No OpenBao SSH-CA write is added to this CLI ahead of that assent.
  • layer.yaml owner_status remains proposed until the surface ships.

Accept the secret-use evidence surface

id: SECRETS-WP-0008-T05
status: done
priority: medium
state_hub_task_id: "3100d28c-9494-5b21-9d5a-af583cd8aa55"

Completed 2026-08-29. Contract published at docs/secret-use-evidence-contract.md. layer.yaml owner_status remains proposed. route/audit are still not this surface.

2026-09-02: kings-guard assented in its own voice (message 75ebd2cc-a166-4676-94aa-deef2791c0c9). It will not consume the surface as an observation input until the surface ships and publishes its cadence declaration. No Tooling contact will be opened to fill that wait.

2026-09-02 later: shipped secrets-engine secret-use snapshot. Cadence is the declared 1d heartbeat on the snapshot envelope. owner_status stays proposed until kings-guard admits the snapshot as an observation input. route/audit remain distinct operator summaries.

kings-guard's secret-abuse posture is fixture-driven because no engine exposes lease, revocation, mount, rotation, and delivery-session metadata. route and audit are operator summaries over local JSONL and are not that surface.

Publish a read-only engine contract of non-secret metadata, bounded by §9.6: it cannot prove omission at source. Coordinate destination with audit-core. Do not give kings-guard a vault client, and do not return values.

Acceptance:

  • Contract lists fields, freshness, and the evidence bound.
  • kings-guard has a recorded chance to assent or contest.
  • No secret material, token, or accessor is in the surface.
  • layer.yaml owner_status remains proposed until the surface ships.

No standing engine credential

id: SECRETS-WP-0008-T06
status: wait
priority: medium
state_hub_task_id: "d7bc8bdc-a0f8-5058-a640-374ef9859148"

Progress 2026-09-02. Named engine auth is shipped: service-jwt, bootstrap, and env are exclusive providers. --auth service-jwt exchanges the KeyCape service JWT and logs in through OpenBaoClient.login_jwt (JSON file, never argv), then self-revokes. A JWT selection refuses --bootstrap-token-file and does not read BAO_TOKEN on failure. --bootstrap-token-file is a named break-glass provider with auth_break_glass evidence.

The platform-owned OpenBao JWT mount/role is still unpublished, so auto keeps bootstrap/env and this task remains wait. Companion §7 / statute §3.4: an agent holds no long-lived credential of its own. Authority is per task, time-bounded, and attributable to the principal it acts for.

Bootstrap token files remain break-glass, heavily evidenced, never an implicit fallback from service identity.

Acceptance:

  • Steady-state CLI/API auth is the reviewed service identity, not --bootstrap-token-file.
  • Bootstrap is a named break-glass provider with distinct evidence.
  • No fallback from JWT/service-auth failure into bootstrap or AppRole.
  • Issued delivery sessions continue to self-revoke as in WP-0007-T05.

2026-09-21 GH-DEC-2026-017 — section 11 layer declaration ruled

Read from gate-house/decisions/decisions.md, not from message text. secrets-engine is a named affected repository and carries both permitted forms, so all four parts apply here. State Hub decision id 82dd0fea-fabb-4940-92d5-d6e2ea9985f5.

What the ruling settles for this repository:

  1. INTENT.md governs. Our declaration is the layer: Engine key in INTENT.md frontmatter. layer.yaml is a derived artifact: it must be marked as derived, must name INTENT.md as what it derives from, and must agree with it. Our layer.yaml already carries declared_by: INTENT.md and a Voice: INTENT.md (this repository's own declaration, §11) header comment, so we are among the repositories already treating the sidecar as derived in substance. What is missing is the explicit derived marking the ruling now requires as a field rather than a comment.
  2. The §3 vocabulary is case-insensitive and comparison must ASCII case-fold. Our INTENT.md says Engine and our layer.yaml says engine. Under §2 of the ruling that is conforming, it was never a disagreement about a layer, and nothing here is re-spelled. flex-auth withdrew the casing finding itself (message 962bf395) and its validator is the thing that changes. We do not touch either file's spelling, and we do not "align" them.
  3. The vocabulary is closed with four tokensTaxonomy, Tooling, Engine, Staff. Engine is in it; no action.
  4. standard_version comes out of the declaration. Our layer.yaml carries standard_version: "0.7". A declaration asserts a standing property and must not carry a standard version; version-scoped state belongs in the derived conformance record, which must name what it derives from and the version or commit it was derived at.

Deliberately not done in this session. The ruling states that standard_version is not one repository's habit but ops-warden's reference form, which this repository copied along with seven others, and that ops-warden is asked to make the change there rather than each adopter deciding independently. ops-warden is updating that reference form in a parallel session. Inventing our own sidecar shape ahead of it would produce exactly the divergence the ruling exists to close, so we record the requirement and wait for the reference form.

What secrets-engine will need, once the reference form lands:

  • Adopt the reference form's derived-artifact marking in layer.yaml, keeping declared_by: INTENT.md or whatever field the reference form names for it.
  • Remove standard_version: "0.7" from layer.yaml.
  • Move the version the conformance run was derived at into our derived conformance record, per the reference form's field for it. Our conformance checker is scripts/check_layer_conformance.py; it reads layer.yaml today and will need to read INTENT.md as the governing form, read the sidecar as well, and report a disagreement between them as a finding rather than resolve it by precedence. A casing-only difference is not such a disagreement.
  • Keep framework:, layer:, role: and the substantive §3/§5/§6/§9.6 content unchanged. role: is explicitly not ruled and stays as it is.
  • The Framework: header comment in layer.yaml cites security-layer-model_v0.7.md while the ruling is issued against v0.8. That is a citation in a comment, not a declaration version, so it is out of scope for part 4 — but it should be refreshed in the same commit so the two are not confused for each other.

No task is opened here by hand. This is a note against the workplan that already owns layer.yaml / pep-stance.yaml / INTENT frontmatter staying in one voice; the work lands under that exit criterion when ops-warden's form is published.

2026-09-21 GH-DEC-2026-017 applied — reference form published, note closed

ops-warden published the reference form (commit a70f559, wiki/playbooks/netkingdom-layer-declaration.md "Reference-form change set"). Re-read against the ruling in gate-house/decisions/decisions.md and amendments A9A13 before editing; the ruling and the playbook agree.

Applied in one commit, field and checker together:

  • layer.yaml: standard_version: "0.7" removed; derived: true and derived_from: INTENT.md added (declared_by: INTENT.md kept, as the note above required). Header comment marks the file derived and drops the version from the framework citation.
  • INTENT.md frontmatter: never carried standard_version, but its standard: value was a version-pinned path (..._v0.7.md). De-versioned to net-kingdom/canon/standards/security-layer-model, as the reference instance did, because a pinned path in the governing form is a standard version by another name. The field is kept; only the pin is gone.
  • No layer value re-spelled. INTENT says Engine, layer.yaml says engine; both unchanged.
  • scripts/check_layer_conformance.py: standard_version was a required key, so removing the field alone would have exited 2 MALFORMED. It now reads INTENT.md as the governing form, requires the derived marking, rejects a returning standard_version in either form, validates both layer values against the closed four-token vocabulary (Taxonomy included) after an ASCII fold, and reports a post-fold disagreement between the forms as a finding.
  • tests/test_layer_conformance.py: per-file equality on the layer spelling replaced by fold assertions; new tests for fold agreement, a real disagreement (finding, exit 1), the closed vocabulary (surface exit 2, Taxonomy admitted), and a returning standard_version (exit 2). Full suite: 430 passed.

role: untouched (not ruled). pep-stance.yaml untouched (a stance map, not a declaration). schema_version left at 0.1 — ops-warden bumped its own, but the ruling does not call for it.

Still open, not ours to answer: where the removed version lives. A12 says the derived conformance record already MUST carry it; our checker, like ops-warden's, is re-runnable and emits nothing durable. ops-warden asked gate-house (message 4220413a); we follow that answer rather than choose.

Exit criteria

  • The published PEP stance is the shipped default and stance application is recorded by name.
  • Production live side effects require an access-engine decision record, or remain fail-closed with a recorded stance.
  • Evidence classification and destination match §9.6; completeness is not claimed.
  • SSH-CA and secret-use surfaces are either contracted with the declaring repositories or explicitly declined in this repository's own voice.
  • Bootstrap is not the steady-state engine credential.
  • layer.yaml / pep-stance.yaml / INTENT frontmatter stay in one voice.
  • No raw secret values in Git, State Hub, chat, prompts, workplans, evidence, or argv.