diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index ee357fb..6f6cde3 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -16,8 +16,6 @@ distinction that matters here. | `ADR-0006` | Enforcement is zone-scoped, never a global flag | | `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade | | `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after | -| `ADR-0009` | Adopt security-zones v0.1 as a consumer; membership is compiled, never inferred | -| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; doctrine belongs to gate-house | ### Owned versus inherited — check `owner:` before changing anything diff --git a/.claude/rules/credential-routing.md b/.claude/rules/credential-routing.md index c3e999d..54216e0 100644 --- a/.claude/rules/credential-routing.md +++ b/.claude/rules/credential-routing.md @@ -8,12 +8,6 @@ ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Eve other credential need belongs to another subsystem. **Do not** message `ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key. -Secrets practice follows SecurityCanon `SEC-DSM` 1.0.0 -(`../security-canon/infospace/standards/devops-secrets-management/SecurityCanonDevOpsSecretsManagementStandard.md`). -Use its preference hierarchy (§5) and decision procedure (§37) before proposing -a new lane. SSH certificates are the ephemeral credential this repo issues. -Static secrets stay with their owner. This repo routes to that owner. - ### Lookup (do this first) **Always plan before drafting any founder credential step** (WARDEN-WP-0029): diff --git a/.custodian-brief.md b/.custodian-brief.md new file mode 100644 index 0000000..a0eb681 --- /dev/null +++ b/.custodian-brief.md @@ -0,0 +1,34 @@ + +# Custodian Brief — ops-warden + +**Domain:** infotech +**Last synced:** 2026-08-25 18:17 UTC +**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)* + +## Active Workstreams + +### Native lane handoff — review secrets-engine's catalog admission, and fix what it exposed +Progress: 5/6 done | workplan_id: `4627d89b-4b00-562a-81e9-76e96f90fa7e` + +**Open tasks:** +- ! Tasks `5acac140` + +### Tamper-resistant credential governance + mass rotation/lockdown (Strand B) +Progress: 2/3 done | workplan_id: `21528e8d-a049-523d-9ae1-da7a27cb8bbf` + +**Open tasks:** +- ► Task: Graded lockdown / break-glass with explicit trust-root `cae498ee` + +## Inbox Hygiene + +**Stale unread:** 9 message(s) older than 3 day(s) — triage at session start. +**Missing thread_id:** 7 unread message(s) lack supersession chains. +- ! secrets-engine: Re: Need the warden-sign live apply/handoff (FLEX-WP-0007 T4) `80456912` + +--- +## MCP Orientation (when available) + +If the state-hub MCP server is reachable, call: +`get_domain_summary("infotech")` +This provides richer cross-domain context. +If the MCP call fails, use this file as your orientation source. diff --git a/.generated-work-records.json b/.generated-work-records.json deleted file mode 100644 index f815db4..0000000 --- a/.generated-work-records.json +++ /dev/null @@ -1 +0,0 @@ -{"version": 1, "repo_slug": "ops-warden"} diff --git a/.generated-work-records.mk b/.generated-work-records.mk deleted file mode 100644 index 8ff134b..0000000 --- a/.generated-work-records.mk +++ /dev/null @@ -1,8 +0,0 @@ -# Explicit opt-in recipe; no environment files or external make includes. -CUSTODIAN_SOURCE ?= /home/worsch/the-custodian -GENERATED_REPO_SLUG ?= ops-warden -.PHONY: generated-work-records generated-work-records-check -generated-work-records: - python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" -generated-work-records-check: - python3 "$(CUSTODIAN_SOURCE)/scripts/generate_staged_work_records.py" --repo . --repo-slug "$(GENERATED_REPO_SLUG)" --check diff --git a/.gitignore b/.gitignore index 4c89b5b..ddae26a 100644 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,3 @@ cython_debug/ *.swp .claude/ralph-loop.local.md - -# Local State Hub orientation snapshot; authored work records remain tracked. -/.custodian-brief.md diff --git a/.repo-manager/index.json b/.repo-manager/index.json deleted file mode 100644 index ae60c11..0000000 --- a/.repo-manager/index.json +++ /dev/null @@ -1,2360 +0,0 @@ -{ - "schema": "repo_manager.index.v1", - "slug": "ops-warden", - "repo_root": "/home/worsch/ops-warden", - "head_sha": "529feeac49a8cde94f68776fc45557c90e2a5a5a", - "observed_at": "2026-08-31T22:59:28.638497Z", - "source_fingerprint": "6f60b2dbcb864a3f8e45a97c247ab142a765487187e67edf52bdb57027087bd4", - "source_files": [ - ".repo-classification.yaml", - "INTENT.md", - "intakes/intakes.md", - "workplans/ADHOC-2026-06-27.md", - "workplans/ADHOC-2026-06-29.md", - "workplans/ADHOC-2026-08-11.md", - "workplans/ADHOC-2026-08-17.md", - "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "workplans/WARDEN-WP-0017-access-front-door-discoverability.md", - "workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md", - "workplans/WARDEN-WP-0019-route-to-secrets-engine.md", - "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "workplans/WARDEN-WP-0027-credential-governance-lockdown.md", - "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "workplans/WARDEN-WP-0030-delegation-register.md", - "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "workplans/WARDEN-WP-0032-security-zones.md", - "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md", - "workplans/WARDEN-WP-0036-attended-login-openbao-output.md", - "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "workplans/archived/260515-WARDEN-WP-0002-correctness-and-completeness.md", - "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "workplans/archived/260617-WARDEN-WP-0005-openbao-doc-alignment.md", - "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "workplans/archived/260623-WARDEN-WP-0009-flex-auth-policy-gate-production.md", - "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "workplans/archived/260707-ADHOC-2026-07-07.md" - ], - "work_records": [ - { - "kind": "workplan", - "id": "WARDEN-WP-ADHOC-2026-06-27", - "status": "finished", - "title": "Ad Hoc Tasks \u2014 2026-06-27", - "source_path": "workplans/ADHOC-2026-06-27.md", - "uuid": "a222c91f-3bb5-58a4-b6b2-f0fb18cdd5c3", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-06-27-T01", - "status": "done", - "title": "T01 \u2014 Fix stale `warden` CLI install + make it usable outside the repo", - "source_path": "workplans/ADHOC-2026-06-27.md", - "uuid": "9176b560-8ca5-5143-888d-479857fe60f0", - "parent_id": "WARDEN-WP-ADHOC-2026-06-27", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-ADHOC-2026-06-29", - "status": "finished", - "title": "Ad Hoc Tasks \u2014 2026-06-29", - "source_path": "workplans/ADHOC-2026-06-29.md", - "uuid": "13fa845f-852e-55ec-a2a5-2296996e0216", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-06-29-T01", - "status": "done", - "title": "T01 \u2014 Joint-smoke mode for the deployed flex-auth (assist FLEX-WP-0007 T4)", - "source_path": "workplans/ADHOC-2026-06-29.md", - "uuid": "62540533-f4ca-5176-9237-32adbeb292ee", - "parent_id": "WARDEN-WP-ADHOC-2026-06-29", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-ADHOC-2026-08-11", - "status": "finished", - "title": "Ad Hoc Tasks \u2014 2026-08-11", - "source_path": "workplans/ADHOC-2026-08-11.md", - "uuid": "9f99cc64-4682-5f20-b13e-89af2b6f7c70", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-11-T01", - "status": "done", - "title": "T01 \u2014 Repair stale `rapp-qonto-keycape-client` wiki anchor (restore green routing suite)", - "source_path": "workplans/ADHOC-2026-08-11.md", - "uuid": "0771d121-278c-556e-9509-841cf6e657c3", - "parent_id": "WARDEN-WP-ADHOC-2026-08-11", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-11-T02", - "status": "done", - "title": "T02 \u2014 Triage the stale ops-warden inbox (11 unread, C-28/C-29)", - "source_path": "workplans/ADHOC-2026-08-11.md", - "uuid": "0ed58145-732f-5102-b6a8-b931d9b6ba08", - "parent_id": "WARDEN-WP-ADHOC-2026-08-11", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-11-T03", - "status": "done", - "title": "T03 \u2014 warden-sign AppRole: PARKED pending WP-0027 break-glass + ops-bridge cutover", - "source_path": "workplans/ADHOC-2026-08-11.md", - "uuid": "337ae793-c6b0-59e9-8a07-3a7ccba237aa", - "parent_id": "WARDEN-WP-ADHOC-2026-08-11", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-ADHOC-2026-08-17", - "status": "finished", - "title": "Ad Hoc Tasks \u2014 2026-08-17", - "source_path": "workplans/ADHOC-2026-08-17.md", - "uuid": "5c6c2bbb-b944-5afd-b89c-20d865518849", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-17-T01", - "status": "done", - "title": "T01 \u2014 Answer flex-auth: how should `/v1/check` authenticate its callers?", - "source_path": "workplans/ADHOC-2026-08-17.md", - "uuid": "04a2f8f9-e70b-5eed-ad87-343c8f9ef501", - "parent_id": "WARDEN-WP-ADHOC-2026-08-17", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-17-T02", - "status": "done", - "title": "T02 \u2014 user-engine: USER_ENGINE_PROXY_SECRET stays railiance-apps; record consumer-only", - "source_path": "workplans/ADHOC-2026-08-17.md", - "uuid": "0e815282-2fad-5c8d-be34-398e492737d0", - "parent_id": "WARDEN-WP-ADHOC-2026-08-17", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-17-T03", - "status": "done", - "title": "T03 \u2014 key-cape: `rapp-qonto-keycape-client` interim accepted; refresh the blocker", - "source_path": "workplans/ADHOC-2026-08-17.md", - "uuid": "e21781d9-a35d-5916-b335-d12131f97a22", - "parent_id": "WARDEN-WP-ADHOC-2026-08-17", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-08-17-T04", - "status": "done", - "title": "T04 \u2014 Session hygiene", - "source_path": "workplans/ADHOC-2026-08-17.md", - "uuid": "b15724e0-c27a-5260-a810-4dd25bff2228", - "parent_id": "WARDEN-WP-ADHOC-2026-08-17", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0016", - "status": "finished", - "title": "ops-bridge cert_command pilot \u2014 readiness gate + handoff", - "source_path": "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "uuid": "a56da8db-38bc-4bbe-8671-823360ec9245", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0016-T01", - "status": "done", - "title": "T1 \u2014 Read-only `cert_command` readiness preflight", - "source_path": "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "uuid": "fea84495-dbec-480a-b42b-90e39f414b78", - "parent_id": "WARDEN-WP-0016", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0016-T02", - "status": "done", - "title": "T2 \u2014 Offline cert_command contract smoke", - "source_path": "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "uuid": "e34ae1a8-2ba9-4324-8d1a-005d61dae478", - "parent_id": "WARDEN-WP-0016", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0016-T03", - "status": "done", - "title": "T3 \u2014 Playbook gate + ops-bridge handoff", - "source_path": "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "uuid": "330e01f4-4927-4280-b0e0-49d35b4416d6", - "parent_id": "WARDEN-WP-0016", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0016-T04", - "status": "done", - "title": "T4 \u2014 INTENT/SCOPE alignment", - "source_path": "workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md", - "uuid": "4726f5bb-4ffd-484f-8674-91ee5658434f", - "parent_id": "WARDEN-WP-0016", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0017", - "status": "finished", - "title": "Access front-door discoverability \u2014 stop reading as SSH-only", - "source_path": "workplans/WARDEN-WP-0017-access-front-door-discoverability.md", - "uuid": "cf8b392e-7624-4585-8935-a85e29202935", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0017-T01", - "status": "done", - "title": "T1 \u2014 CLI discoverability: route role + access framing", - "source_path": "workplans/WARDEN-WP-0017-access-front-door-discoverability.md", - "uuid": "6e98df42-b5b4-49f8-a444-3c6346c8abd7", - "parent_id": "WARDEN-WP-0017", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0017-T02", - "status": "done", - "title": "T2 \u2014 Agent rule + SCOPE reframe", - "source_path": "workplans/WARDEN-WP-0017-access-front-door-discoverability.md", - "uuid": "6e2a7067-1afc-4f38-8d99-4d5c36a4661c", - "parent_id": "WARDEN-WP-0017", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0017-T03", - "status": "done", - "title": "T3 \u2014 Federated capability registration", - "source_path": "workplans/WARDEN-WP-0017-access-front-door-discoverability.md", - "uuid": "7199625b-e78e-4495-8ca0-076100ae9f08", - "parent_id": "WARDEN-WP-0017", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0018", - "status": "finished", - "title": "Activate whynot-design npm publish lane + resolvable readiness flag", - "source_path": "workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md", - "uuid": "1256aca2-5979-4d21-818e-0de42c5d811b", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0018-T01", - "status": "done", - "title": "T1 \u2014 Concrete catalog entry + playbook", - "source_path": "workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md", - "uuid": "189d0883-22b9-42dc-bda0-89460509a87d", - "parent_id": "WARDEN-WP-0018", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0018-T02", - "status": "done", - "title": "T2 \u2014 `resolvable` readiness flag + stable-id resolution", - "source_path": "workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md", - "uuid": "b5dc1013-5334-43ff-afd6-1f99d521358f", - "parent_id": "WARDEN-WP-0018", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0018-T03", - "status": "done", - "title": "T3 \u2014 Close the loop", - "source_path": "workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md", - "uuid": "95b00ef8-477a-4f0d-bd71-6154fba401f5", - "parent_id": "WARDEN-WP-0018", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0019", - "status": "finished", - "title": "Route secret-exec lanes to secrets-engine (route-primary, proxy fallback)", - "source_path": "workplans/WARDEN-WP-0019-route-to-secrets-engine.md", - "uuid": "5e49abb6-497f-4640-a484-2da5f39a7c4e", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0019-T01", - "status": "done", - "title": "T1 \u2014 Catalog + CLI: surface the owner-native exec front door", - "source_path": "workplans/WARDEN-WP-0019-route-to-secrets-engine.md", - "uuid": "ea153605-7a14-4db7-8bce-d780ea143f8a", - "parent_id": "WARDEN-WP-0019", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0019-T02", - "status": "done", - "title": "T2 \u2014 Agent rule, SCOPE, playbook", - "source_path": "workplans/WARDEN-WP-0019-route-to-secrets-engine.md", - "uuid": "96059b8a-8938-4763-b3d0-cc5a0eb2465c", - "parent_id": "WARDEN-WP-0019", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0020", - "status": "finished", - "title": "ops-warden worker \u2014 autonomous coordination via llm-connect", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "c906ba1d-f991-4fb0-b113-59432ddf87c0", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0020-T01", - "status": "done", - "title": "T1 \u2014 Worker scaffold (llm-connect-independent, safe)", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "979c2d9b-0803-442f-aa2e-acb02bac07e9", - "parent_id": "WARDEN-WP-0020", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0020-T02", - "status": "done", - "title": "T2 \u2014 llm-connect brain", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "52d281b2-7d48-44f5-b77e-80e3ed500b5f", - "parent_id": "WARDEN-WP-0020", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0020-T03", - "status": "done", - "title": "T3 \u2014 Action dispatch + guardrails (full-auto in-scope)", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "3a71965e-42d5-4258-9761-aced804c88e7", - "parent_id": "WARDEN-WP-0020", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0020-T04", - "status": "done", - "title": "T4 \u2014 Scheduled trigger", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "7f77ea6d-c281-42c5-ad25-2a0bb9fd68de", - "parent_id": "WARDEN-WP-0020", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0020-T05", - "status": "done", - "title": "T5 \u2014 Docs / SCOPE / INTENT", - "source_path": "workplans/WARDEN-WP-0020-ops-warden-worker.md", - "uuid": "6e7ae317-7f8b-468a-bb5c-b08093ed43a0", - "parent_id": "WARDEN-WP-0020", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0021", - "status": "finished", - "title": "Enable the scheduled worker tick \u2014 conservative inbox triage, unattended", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "8c487014-b630-4016-a4f0-31b971a473d2", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0021-T01", - "status": "done", - "title": "T1 \u2014 Scheduler install + enablement + kill switch", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "10451fe6-7fab-4ae0-8494-e6cfdfbcf8cf", - "parent_id": "WARDEN-WP-0021", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0021-T02", - "status": "done", - "title": "T2 \u2014 Scheduled-run robustness (graceful degradation)", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "1f35f816-1af5-46ff-b48c-1715f3ae5784", - "parent_id": "WARDEN-WP-0021", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0021-T03", - "status": "done", - "title": "T3 \u2014 Operator visibility (see new drafts)", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "3c7f6423-8db0-4bc6-b67d-078d9d929c6d", - "parent_id": "WARDEN-WP-0021", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0021-T04", - "status": "done", - "title": "T4 \u2014 Review\u2192send loop (`warden worker approve`)", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "dabc9fc0-abb1-4e9d-b87e-5f0c5950693c", - "parent_id": "WARDEN-WP-0021", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0021-T05", - "status": "done", - "title": "T5 \u2014 Runbook + SCOPE", - "source_path": "workplans/WARDEN-WP-0021-enable-scheduled-worker-tick.md", - "uuid": "9915da96-1b33-4d0f-b752-408ea8d43333", - "parent_id": "WARDEN-WP-0021", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0022", - "status": "finished", - "title": "Audit trail + `warden activity` \u2014 one place to see what ops-warden did", - "source_path": "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "uuid": "fc8afa28-68a7-4250-a19e-9754829f0cd5", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0022-T01", - "status": "done", - "title": "T1 \u2014 Unified audit event log", - "source_path": "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "uuid": "7f8f768a-4c62-4096-bad8-912cea0f35a7", - "parent_id": "WARDEN-WP-0022", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0022-T02", - "status": "done", - "title": "T2 \u2014 Instrument the actions", - "source_path": "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "uuid": "e7ae4037-ca79-4557-81f0-bfb8478ff647", - "parent_id": "WARDEN-WP-0022", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0022-T03", - "status": "done", - "title": "T3 \u2014 `warden activity` command", - "source_path": "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "uuid": "4439bdd8-1461-47df-8b0b-048df7384a68", - "parent_id": "WARDEN-WP-0022", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0022-T04", - "status": "done", - "title": "T4 \u2014 Tests, runbook, SCOPE", - "source_path": "workplans/WARDEN-WP-0022-audit-trail-and-activity.md", - "uuid": "bdfb8703-7a79-43e7-913b-19d61722f164", - "parent_id": "WARDEN-WP-0022", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0023", - "status": "finished", - "title": "INTENT\u2013SCOPE Alignment Closeout", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "7bad1ec4-a7c2-4980-b8f9-49a7f5408574", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T01", - "status": "done", - "title": "T01 \u2014 Persist gap analysis", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "52485c90-87fe-40b1-9db5-a51ebb957dd5", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T02", - "status": "done", - "title": "T02 \u2014 Refresh INTENT.md", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "9a9b3631-8948-45af-ace1-c19ee74ace4d", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T03", - "status": "done", - "title": "T03 \u2014 Production integration coordination pack", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "26f23798-494b-45fc-baa8-af27bdffa038", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T04", - "status": "done", - "title": "T04 \u2014 `warden sign` broker hint when `VAULT_TOKEN` unset", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "85e324f9-273d-4740-a202-9c4e8fb122ae", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T05", - "status": "done", - "title": "T05 \u2014 Catalog draft-lane promotion checklist", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "82608692-2845-41e1-a498-90ed53780748", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T06", - "status": "done", - "title": "T06 \u2014 SCOPE and workplan consistency", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "79ca7b9a-554e-4952-9393-a29b100f6190", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0023-T07", - "status": "done", - "title": "T07 \u2014 Sequence WP-0022 audit implementation", - "source_path": "workplans/WARDEN-WP-0023-intent-scope-alignment-closeout.md", - "uuid": "1f3b3b33-974e-49bf-be4a-9d50b702c2a4", - "parent_id": "WARDEN-WP-0023", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0024", - "status": "finished", - "title": "Experiential Memory Across Worker, Agent Sessions, And OpenRouter", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "5d9fafb3-f9b6-43bf-b259-5f5301daa2e9", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T01", - "status": "done", - "title": "T01 - Canonical memory store and discovery", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "6305f1bc-c016-4298-adc2-a07d52b6aca5", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T02", - "status": "done", - "title": "T02 - Session recording hooks in CLI commands", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "242a4d9a-5375-4df8-8d43-063b0491d202", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T03", - "status": "done", - "title": "T03 - Memory-aware worker tick", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "176fcae1-e4e5-481f-9a83-e9a7000fac1a", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T04", - "status": "done", - "title": "T04 - Agent session activation helper", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "16501557-6cde-44ea-bc6f-1726cb7ec070", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T05", - "status": "done", - "title": "T05 - Cross-runtime continuity", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "55ae679c-c08f-4afd-8646-9f5f3019f86e", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T06", - "status": "done", - "title": "T06 - OpenRouter efficiency layer", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "fc2dffcf-7184-4f3d-8653-d26dc18a9afc", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0024-T07", - "status": "done", - "title": "T07 - Operator and agent documentation", - "source_path": "workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md", - "uuid": "ca2aaf23-833f-49a6-a49b-a0b659208f5f", - "parent_id": "WARDEN-WP-0024", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0025", - "status": "finished", - "title": "Forgejo admin PAT OpenBao lane (CCR-2026-0006)", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "70c11222-d8e7-5936-99f7-7d626a4a5deb", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0025-T01", - "status": "done", - "title": "T1 \u2014 Draft CCR + policy metadata", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "2c288bf0-b39c-5f5b-ad25-eed3da826dc3", - "parent_id": "WARDEN-WP-0025", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0025-T02", - "status": "done", - "title": "T2 \u2014 ops-warden catalog + playbook", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "01b81595-f9e5-5746-b0dd-2075189cb00e", - "parent_id": "WARDEN-WP-0025", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0025-T03", - "status": "done", - "title": "T3 \u2014 Platform-operator approval + metadata apply", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "279b74d7-3240-5ca0-897d-b1ddffd23c4e", - "parent_id": "WARDEN-WP-0025", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0025-T04", - "status": "done", - "title": "T4 \u2014 Attended PAT provision + verification", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "4e21232c-62a1-5115-acce-edfbcfa84e48", - "parent_id": "WARDEN-WP-0025", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0025-T05", - "status": "done", - "title": "T5 \u2014 Notify downstream consumers", - "source_path": "workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md", - "uuid": "f8c7c70b-b9b6-5980-a25b-7f11033b81a2", - "parent_id": "WARDEN-WP-0025", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0026", - "status": "finished", - "title": "Credential disclosure hygiene + rotation guidance (Strand A)", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "331c7620-bd34-5acd-9135-591985b568e5", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T01", - "status": "done", - "title": "Task: Capabilities-based lane verification", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "1cb22a40-b7c6-560a-a805-7766a5786dcc", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T02", - "status": "done", - "title": "Task: Safe access transport (no stdout values)", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "bcb7da96-0a28-5484-bf3e-06e97acf5873", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T03", - "status": "done", - "title": "Task: Masking display filter (defense-in-depth)", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "d90b0628-fa1d-527d-99c3-28a7ed933e52", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T04", - "status": "done", - "title": "Task: Agent read-boundary on high-risk lanes", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "827fa67d-5f69-5fac-bdce-9903b1b909fb", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T05", - "status": "done", - "title": "Task: EXPOSED taint convention", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "09ef8727-31da-59ac-aac7-2d47924569fe", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T06", - "status": "done", - "title": "Task: Rotation / re-establishment guidance registry", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "a2e1544e-e501-57eb-a40e-9a2147cef12a", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0026-T07", - "status": "done", - "title": "Task: Incident lessons + first worked lane (CCR-2026-0004)", - "source_path": "workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md", - "uuid": "62d8286f-7954-52a8-bce6-6a16072e5246", - "parent_id": "WARDEN-WP-0026", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0027", - "status": "active", - "title": "Tamper-resistant credential governance + mass rotation/lockdown (Strand B)", - "source_path": "workplans/WARDEN-WP-0027-credential-governance-lockdown.md", - "uuid": "21528e8d-a049-523d-9ae1-da7a27cb8bbf", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0027-T01", - "status": "cancel", - "title": "Task: Executable mass rotation driver", - "source_path": "workplans/WARDEN-WP-0027-credential-governance-lockdown.md", - "uuid": "b5691939-9d84-5115-9618-0f8839010d14", - "parent_id": "WARDEN-WP-0027", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0027-T02", - "status": "progress", - "title": "Task: Graded lockdown / break-glass with explicit trust-root", - "source_path": "workplans/WARDEN-WP-0027-credential-governance-lockdown.md", - "uuid": "cae498ee-6307-5d32-9f1b-a471cfcc2536", - "parent_id": "WARDEN-WP-0027", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0027-T03", - "status": "cancel", - "title": "Task: Tamper-evident policy governance + reconcile", - "source_path": "workplans/WARDEN-WP-0027-credential-governance-lockdown.md", - "uuid": "7dbedcdc-dd5a-551c-bff1-0702fea0a9cf", - "parent_id": "WARDEN-WP-0027", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0028", - "status": "finished", - "title": "Tenant secret custody \u2014 NetKingdom pattern for client/tenant secrets", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "6b66228a-199a-5b85-b5e2-a7afeaab903b", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T01", - "status": "done", - "title": "T01 \u2014 Canon note: tenant secret path + ownership", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "9982a884-7a50-593e-861e-cc8d2343a0ba", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T02", - "status": "done", - "title": "T02 \u2014 Align binky-control integration plan to production path", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "d7d7ee9d-2ecf-5492-8a13-0b747d899456", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T03", - "status": "done", - "title": "T03 \u2014 Enable `tenants` mount + extend CCR tooling + policy/role", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "e0b675ef-9c08-5f89-8f13-ef4249ca2364", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T04", - "status": "done", - "title": "T04 \u2014 ops-warden catalog + playbook + rotation", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "9405b13d-4045-519b-8e3f-79a891323397", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T05", - "status": "done", - "title": "T05 \u2014 Founder provision (Red) + first scan evidence", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "f17bba95-bc53-5c2a-8b44-7df9e42b2341", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T06", - "status": "done", - "title": "T06 \u2014 Generalize \"tenant secret onboarding\" playbook", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "76bd01a0-1d03-5ff9-8f59-a1b44763979d", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0028-T07", - "status": "done", - "title": "T07 \u2014 secrets-engine alignment decision (record only)", - "source_path": "workplans/WARDEN-WP-0028-tenant-secret-custody.md", - "uuid": "46c8f8a9-3bbe-568f-8a45-07b690874273", - "parent_id": "WARDEN-WP-0028", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0029", - "status": "finished", - "title": "Policy front door: posture-aware access planning + founder interaction surface", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "bbb3d9ec-d88d-5088-b4c0-55bfba0a10cf", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0029-T02", - "status": "done", - "title": "T02 \u2014 Declared organization posture (build phase)", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "6c213024-6601-5116-b52f-d6711dc0587d", - "parent_id": "WARDEN-WP-0029", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0029-T05", - "status": "done", - "title": "T05 \u2014 Catalog freshness + agent guidance", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "c82d8745-4af4-5b89-ab49-06d8a902e592", - "parent_id": "WARDEN-WP-0029", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0029-T01", - "status": "done", - "title": "T01 \u2014 `warden plan` decision front door", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "34db38fa-cb99-5ced-9a12-85176a7f2b44", - "parent_id": "WARDEN-WP-0029", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0029-T04", - "status": "done", - "title": "T04 \u2014 Retire file-drop patterns from playbooks", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "717f57da-fbc4-5a4d-9f8c-c1c3fa75e0ee", - "parent_id": "WARDEN-WP-0029", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0029-T03", - "status": "done", - "title": "T03 \u2014 Founder interaction surface (local web approval page)", - "source_path": "workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md", - "uuid": "47c781d2-768b-5777-b971-b5227fe41f5c", - "parent_id": "WARDEN-WP-0029", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0030", - "status": "finished", - "title": "Delegation register \u2014 make gap-covering interim, visible, and retirable", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "da3367d5-890c-52c6-aa54-1bdd0f277342", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0030-T01", - "status": "done", - "title": "T01 \u2014 Interim custodianship doctrine", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "6f88876e-434c-5718-9c8d-ec6bf64ae4aa", - "parent_id": "WARDEN-WP-0030", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0030-T02", - "status": "done", - "title": "T02 \u2014 `delegation:` metadata + backfill", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "b02e8da6-57ca-5f9f-9405-9b0624498e3a", - "parent_id": "WARDEN-WP-0030", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0030-T03", - "status": "done", - "title": "T03 \u2014 `warden route gaps` + conformance test", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "f5e0f5af-45d8-5c82-9de2-d640d7d0a1f7", - "parent_id": "WARDEN-WP-0030", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0030-T04", - "status": "done", - "title": "T04 \u2014 Promotion gate", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "b808a749-e1d7-5708-aabf-91732dd76abd", - "parent_id": "WARDEN-WP-0030", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0030-T05", - "status": "done", - "title": "T05 \u2014 Publish the register to the owners", - "source_path": "workplans/WARDEN-WP-0030-delegation-register.md", - "uuid": "b0188ec4-4860-57c8-8030-45904a132190", - "parent_id": "WARDEN-WP-0030", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0031", - "status": "finished", - "title": "Calling-side identity for flex-auth, so policy.enabled can flip", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "739bad25-2345-5f4f-aaa3-cc4cd8c71f6c", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0031-T01", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "d3b7c701-bcdd-53f9-aa72-6f289bf5909b", - "parent_id": "WARDEN-WP-0031", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0031-T02", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "b77b3c80-a168-564a-9b7f-3063aefc3c2e", - "parent_id": "WARDEN-WP-0031", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0031-T03", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "4245155e-6c71-5425-b574-11f61e1d4461", - "parent_id": "WARDEN-WP-0031", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0031-T04", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "f3834af7-2a08-51dd-bf31-8ce8550de699", - "parent_id": "WARDEN-WP-0031", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0031-T05", - "status": "cancel", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0031-policy-caller-identity.md", - "uuid": "3f6dc609-89db-52eb-a6f9-d2fd271be821", - "parent_id": "WARDEN-WP-0031", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0032", - "status": "finished", - "title": "Adopt security zones as a consumer \u2014 retire the global policy.enabled", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "38c6a5f3-fb0d-5230-be85-f9e3ffc850f6", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T01", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "b03a5caa-0bb5-5cdd-bb99-32c694b0da29", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T02", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "b3f41c85-a293-58ad-ac27-9f8110c51266", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T03", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "d04a737b-ecdf-5747-ba25-20dadd99d3bc", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T04", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "c7879127-3dba-55c0-853c-a10775736873", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T05", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "1d968627-83f4-59cd-84ca-0f9f35e435ff", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T06", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "4294084c-bf3f-5aa6-b84d-5173121882ff", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0032-T07", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0032-security-zones.md", - "uuid": "6b4bbbed-2864-5fe9-82be-22ff9543f6f4", - "parent_id": "WARDEN-WP-0032", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0033", - "status": "finished", - "title": "Native lane handoff \u2014 review secrets-engine's catalog admission, and fix what it exposed", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "4627d89b-4b00-562a-81e9-76e96f90fa7e", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T01", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "154f2f03-387d-5fe6-a0f5-1929c46a2bd8", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T02", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "6996d07f-63bb-5708-a171-68c4b1bbddde", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T03", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "a736f983-94da-5a4a-aaf9-5114485518a6", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T04", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "5acac140-a586-5db3-b231-bbf236710786", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T05", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "75051d17-399b-5129-860b-ae00dae91c47", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0033-T06", - "status": "done", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0033-native-lane-handoff.md", - "uuid": "94f77f5a-f919-5328-832c-ba1d24c6431b", - "parent_id": "WARDEN-WP-0033", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0034", - "status": "ready", - "title": "Layer model v0.7 conformance \u2014 state the deadline, bind the agent boundary, steward the estate's newest rule", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "ae3ff76f-883d-5e2f-b6aa-144d61e8fdef", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0034-T01", - "status": "todo", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "8b3bdb9f-d2c2-5b3e-89e2-417bf3e37484", - "parent_id": "WARDEN-WP-0034", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0034-T02", - "status": "todo", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "3318ee1a-b5d9-5d39-baf7-9c42a8bc7b55", - "parent_id": "WARDEN-WP-0034", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0034-T03", - "status": "todo", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "a891b32c-b0a7-59f6-a5cd-977be65c09ca", - "parent_id": "WARDEN-WP-0034", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0034-T04", - "status": "todo", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "94e73daa-f74d-51fd-8639-68896a4066ee", - "parent_id": "WARDEN-WP-0034", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0034-T05", - "status": "todo", - "title": "Tasks", - "source_path": "workplans/WARDEN-WP-0034-layer-model-v07-conformance.md", - "uuid": "7d1b3c82-9b96-5087-a53a-496212909029", - "parent_id": "WARDEN-WP-0034", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0035", - "status": "finished", - "title": "Register the Policy Nexus Forgejo source-read route", - "source_path": "workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md", - "uuid": "45aec8d3-94b3-586e-b019-a47e656efafa", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0035-T01", - "status": "done", - "title": "Register the exact high-risk lane", - "source_path": "workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md", - "uuid": "dd84f2be-0143-540c-9c16-74f0fd129260", - "parent_id": "WARDEN-WP-0035", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0035-T02", - "status": "done", - "title": "Verify routing and governed use", - "source_path": "workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md", - "uuid": "1fa8f778-3e46-5f44-86c4-cab8628b7e60", - "parent_id": "WARDEN-WP-0035", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0036", - "status": "finished", - "title": "Accept contained OpenBao login output only after helper persistence", - "source_path": "workplans/WARDEN-WP-0036-attended-login-openbao-output.md", - "uuid": "d844c96e-152d-53fa-bff6-e072125ef66c", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0036-T01", - "status": "done", - "title": "Repair attended-login handoff", - "source_path": "workplans/WARDEN-WP-0036-attended-login-openbao-output.md", - "uuid": "7eb8b9c9-1285-5ada-a17b-1d5bfbb8ba59", - "parent_id": "WARDEN-WP-0036", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0036-T02", - "status": "done", - "title": "Verify live contained operation", - "source_path": "workplans/WARDEN-WP-0036-attended-login-openbao-output.md", - "uuid": "d22bab05-c38b-561f-95de-6c146ce7c6cf", - "parent_id": "WARDEN-WP-0036", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0001", - "status": "archived", - "title": "OpsWarden Initial Implementation", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "c3118cc6-adfb-428c-a9c6-edd0ee152ae6", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T1", - "status": "done", - "title": "T1 \u2014 Repository bootstrap", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "6d643e9d-5e97-4224-9d82-87267b5ba6bc", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T2", - "status": "done", - "title": "T2 \u2014 Models and config", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "c66fc65a-0b16-4ba2-9e70-a83d875572ec", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T3", - "status": "done", - "title": "T3 \u2014 LocalCA backend", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "a5a41e58-1c6d-42a9-9b11-2088f17c29b5", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T4", - "status": "done", - "title": "T4 \u2014 VaultCA backend", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "b2067ee6-c9ce-423b-9d60-0d28069fb304", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T5", - "status": "done", - "title": "T5 \u2014 Principals inventory", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "6d13f8cd-1850-44c9-b769-b21250348319", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T6", - "status": "done", - "title": "T6 \u2014 CLI commands", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "656a4615-92bb-4b5d-9406-e86d24fa15d0", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T7", - "status": "done", - "title": "T7 \u2014 Scorecard runner", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "7818bcc5-f40e-4793-b117-d36f653ffeed", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T8", - "status": "done", - "title": "T8 \u2014 ops-ssh-wrapper script", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "e9c28152-5785-4995-83a5-439985ed3db9", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T9", - "status": "done", - "title": "T9 \u2014 Tests", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "950139ab-cc17-4f1d-9a17-d5744e402ddf", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0001-T10", - "status": "done", - "title": "T10 \u2014 Documentation", - "source_path": "workplans/archived/260515-WARDEN-WP-0001-initial-implementation.md", - "uuid": "271d6759-e359-41ce-80e4-76c574634a87", - "parent_id": "WARDEN-WP-0001", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0002", - "status": "archived", - "title": "OpsWarden Correctness and Operational Completeness", - "source_path": "workplans/archived/260515-WARDEN-WP-0002-correctness-and-completeness.md", - "uuid": "5a9fba2c-6161-49a4-a231-e750fa4ab572", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0002-T1", - "status": "done", - "title": "T1 \u2014 TTL max enforcement per ActorType", - "source_path": "workplans/archived/260515-WARDEN-WP-0002-correctness-and-completeness.md", - "uuid": "b0d0b5f7-a181-4590-be26-c48ae28cd964", - "parent_id": "WARDEN-WP-0002", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0002-T2", - "status": "done", - "title": "T2 \u2014 Stale cert cleanup command", - "source_path": "workplans/archived/260515-WARDEN-WP-0002-correctness-and-completeness.md", - "uuid": "aeeefbad-c0bd-4ae8-a3fe-9f72321b4caa", - "parent_id": "WARDEN-WP-0002", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0002-T3", - "status": "done", - "title": "T3 \u2014 Outgoing signatures log", - "source_path": "workplans/archived/260515-WARDEN-WP-0002-correctness-and-completeness.md", - "uuid": "0194d24f-a8fe-4f6d-88e6-addea3542c0e", - "parent_id": "WARDEN-WP-0002", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0003", - "status": "archived", - "title": "OpsWarden Test Coverage and Code Quality", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "cb2bbf3c-848a-4af6-ba64-8361e64cd4d7", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T1", - "status": "done", - "title": "T1 \u2014 VaultCA tests", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "eff074ce-c027-4df5-8006-0990296592ac", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T2", - "status": "done", - "title": "T2 \u2014 LocalCA.generate_keypair tests", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "ddfe5331-0a3b-4783-bdf4-f5ebcdf7965c", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T3", - "status": "done", - "title": "T3 \u2014 CLI tests", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "040ce3a1-0efb-4816-a2d9-357162dd1612", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T4", - "status": "done", - "title": "T4 \u2014 Real ssh-keygen integration test", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "434fb008-103f-410c-85fd-e77b33e61fe4", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T5", - "status": "done", - "title": "T5 \u2014 File permissions enforcement (mode 600)", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "ac146fe6-d1fd-4186-91bd-6f098de72449", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0003-T6", - "status": "done", - "title": "T6 \u2014 warden status --state-dir override", - "source_path": "workplans/archived/260515-WARDEN-WP-0003-test-coverage-and-quality.md", - "uuid": "1c9f1987-7b11-43c1-a5e3-c2fd8d1c1589", - "parent_id": "WARDEN-WP-0003", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0004", - "status": "archived", - "title": "OpsWarden Repo Hygiene and Hub Sync", - "source_path": "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "uuid": "3c4b6e68-550a-4fc6-a804-95f1f68936c3", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0004-T01", - "status": "done", - "title": "T1 \u2014 Update orientation docs", - "source_path": "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "uuid": "f9d3926c-8637-411c-a477-2960b754704c", - "parent_id": "WARDEN-WP-0004", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0004-T02", - "status": "done", - "title": "T2 \u2014 Fill agent rules", - "source_path": "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "uuid": "86c764a5-62fc-45fe-a8d2-332d6554a976", - "parent_id": "WARDEN-WP-0004", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0004-T03", - "status": "done", - "title": "T3 \u2014 Archive finished workplans", - "source_path": "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "uuid": "d3e54e63-ce98-4632-bc08-0e2667f19f12", - "parent_id": "WARDEN-WP-0004", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0004-T04", - "status": "done", - "title": "T4 \u2014 Sync State Hub", - "source_path": "workplans/archived/260617-WARDEN-WP-0004-repo-hygiene-and-hub-sync.md", - "uuid": "51729695-262f-4fe4-9c38-f99ee046d32a", - "parent_id": "WARDEN-WP-0004", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0005", - "status": "archived", - "title": "OpsWarden OpenBao-First Documentation Alignment", - "source_path": "workplans/archived/260617-WARDEN-WP-0005-openbao-doc-alignment.md", - "uuid": "57f6ebf8-0ef3-4686-9a73-3f9d38288be9", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0005-T01", - "status": "done", - "title": "T1 \u2014 OpsWardenConfig.md", - "source_path": "workplans/archived/260617-WARDEN-WP-0005-openbao-doc-alignment.md", - "uuid": "bbbc4dda-9634-4c04-86e5-94b96c021b43", - "parent_id": "WARDEN-WP-0005", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0005-T02", - "status": "done", - "title": "T2 \u2014 Cross-reference updates", - "source_path": "workplans/archived/260617-WARDEN-WP-0005-openbao-doc-alignment.md", - "uuid": "6391cb82-896e-405a-a59b-36640e6480ba", - "parent_id": "WARDEN-WP-0005", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0006", - "status": "archived", - "title": "NetKingdom Alignment and Operational Access Stewardship", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "a5c9f24b-1ad4-46da-bc8e-b99897f8e302", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T01", - "status": "done", - "title": "T1 \u2014 Credential routing runbook", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "ffc6a0c2-4312-4584-be7a-c8411cb01899", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T02", - "status": "done", - "title": "T2 \u2014 Actor inventory patterns", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "3816463d-7dfd-469d-9324-fd7880b50608", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T03", - "status": "done", - "title": "T3 \u2014 NetKingdom cross-links (ops-warden side)", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "f158366a-5746-48b8-acce-472dce8f925e", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T04", - "status": "done", - "title": "T4 \u2014 NetKingdom canon patch (coordination)", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "e40e4395-8f01-4f79-a539-d0de8e427321", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T05", - "status": "done", - "title": "T5 \u2014 OpenBao SSH engine operational checklist", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "a94e20a2-970b-4a0c-bd23-8510b841b938", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T06", - "status": "done", - "title": "T6 \u2014 Policy-gated signing design (design only)", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "b10a4b4d-bfa1-4f49-b6a5-f339f1e6a2e1", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0006-T07", - "status": "done", - "title": "T7 \u2014 Re-assess INTENT \u2194 SCOPE", - "source_path": "workplans/archived/260617-WARDEN-WP-0006-netkingdom-alignment-and-access-stewardship.md", - "uuid": "ef8b5c57-2343-4cfc-9fee-48db1e56f69a", - "parent_id": "WARDEN-WP-0006", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0007", - "status": "archived", - "title": "Policy Gate and Production OpenBao Verification", - "source_path": "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "uuid": "3718ac07-2fa2-47d0-a02a-c9a7b83a5ba9", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0007-T01", - "status": "done", - "title": "T1 \u2014 Production OpenBao verification evidence", - "source_path": "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "uuid": "344540ad-5912-4118-b406-450b96e13c40", - "parent_id": "WARDEN-WP-0007", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0007-T02", - "status": "done", - "title": "T2 \u2014 Policy config and flex-auth client", - "source_path": "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "uuid": "05424ddf-5fe9-43a1-a2f8-c47235a012c8", - "parent_id": "WARDEN-WP-0007", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0007-T03", - "status": "done", - "title": "T3 \u2014 Wire policy gate into sign/issue", - "source_path": "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "uuid": "f5ae8e6e-8cce-4526-b18c-0452a135af49", - "parent_id": "WARDEN-WP-0007", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0007-T04", - "status": "done", - "title": "T4 \u2014 Tests and docs", - "source_path": "workplans/archived/260617-WARDEN-WP-0007-policy-gate-and-production-verify.md", - "uuid": "ea921d56-033b-4619-8032-61af7992e610", - "parent_id": "WARDEN-WP-0007", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0008", - "status": "finished", - "title": "Production SSH Path and Stewardship Closeout", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "a174963a-4ff1-4565-b19f-896cd4ff14a0", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0008-T01", - "status": "done", - "title": "T1 \u2014 Post-WP-0007 INTENT/SCOPE reassessment", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "05379da4-79d0-4742-8638-9e9565cccf72", - "parent_id": "WARDEN-WP-0008", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0008-T02", - "status": "done", - "title": "T2 \u2014 Production OpenBao end-to-end sign verification", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "b1a1831d-b2b3-4204-95f6-04dc7f29f67c", - "parent_id": "WARDEN-WP-0008", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0008-T03", - "status": "done", - "title": "T3 \u2014 State Hub task status canon migration", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "876827c4-4a86-4e58-9a1f-ac87045dc903", - "parent_id": "WARDEN-WP-0008", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0008-T04", - "status": "done", - "title": "T4 \u2014 Production config example and archive hygiene", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "75b9f366-3d7a-419d-98ad-bc10ab90a697", - "parent_id": "WARDEN-WP-0008", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0008-T05", - "status": "cancel", - "title": "T5 \u2014 flex-auth policy gate production readiness (coordination)", - "source_path": "workplans/archived/260618-WARDEN-WP-0008-production-ssh-path-and-stewardship-closeout.md", - "uuid": "03b412a5-5b99-42df-a154-733dd4156000", - "parent_id": "WARDEN-WP-0008", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0009", - "status": "archived", - "title": "flex-auth Policy Gate Production Readiness", - "source_path": "workplans/archived/260623-WARDEN-WP-0009-flex-auth-policy-gate-production.md", - "uuid": "9213b262-e2f5-480e-a5bc-56635d5eb4c9", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0009-T01", - "status": "done", - "title": "T1 \u2014 flex-auth policy package confirmation", - "source_path": "workplans/archived/260623-WARDEN-WP-0009-flex-auth-policy-gate-production.md", - "uuid": "f988ed2e-0f63-4e89-abc4-183a7f23ddc2", - "parent_id": "WARDEN-WP-0009", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0009-T02", - "status": "done", - "title": "T2 \u2014 Production enablement and smoke", - "source_path": "workplans/archived/260623-WARDEN-WP-0009-flex-auth-policy-gate-production.md", - "uuid": "9d0fabc2-10ef-426d-a3d2-d4970d377029", - "parent_id": "WARDEN-WP-0009", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0010", - "status": "archived", - "title": "Access Routing \u2014 Charter and Pointer Catalog", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "e93de9fd-0192-4d02-bb7c-5e859fb76b9b", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0010-T01", - "status": "done", - "title": "T1 \u2014 INTENT wording", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "589081a6-d1f5-47b4-bec0-e82d9c3444f4", - "parent_id": "WARDEN-WP-0010", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0010-T02", - "status": "done", - "title": "T2 \u2014 Routing-role wiki page", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "9ac333f7-5fc4-4fa2-82f3-d5ece8ff0d92", - "parent_id": "WARDEN-WP-0010", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0010-T03", - "status": "done", - "title": "T3 \u2014 Pointer catalog schema + seed", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "59e0f480-694a-482a-b35e-b7bc4930aa41", - "parent_id": "WARDEN-WP-0010", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0010-T04", - "status": "done", - "title": "T4 \u2014 Routing index in CredentialRouting.md", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "aabd28c0-db2d-4267-be98-95be272c687d", - "parent_id": "WARDEN-WP-0010", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0010-T05", - "status": "done", - "title": "T5 \u2014 Registry and repo-boundary alignment", - "source_path": "workplans/archived/260624-WARDEN-WP-0010-access-routing-charter.md", - "uuid": "3335a689-922c-4319-98d0-4263ab13790b", - "parent_id": "WARDEN-WP-0010", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0011", - "status": "archived", - "title": "Routing Lookup CLI", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "0a520f8e-01b4-48f1-9af3-2f3f69fd0672", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0011-T01", - "status": "done", - "title": "T1 \u2014 Catalog loader and models", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "55b8422c-ad3c-4084-9e00-acaa4c360906", - "parent_id": "WARDEN-WP-0011", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0011-T02", - "status": "done", - "title": "T2 \u2014 `warden route list` and `show`", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "60b679c5-79bd-4186-b5a6-ac576931f06c", - "parent_id": "WARDEN-WP-0011", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0011-T03", - "status": "done", - "title": "T3 \u2014 `warden route find`", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "d307701f-0117-44f0-80fd-ca6f7ae06f42", - "parent_id": "WARDEN-WP-0011", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0011-T04", - "status": "done", - "title": "T4 \u2014 Tests", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "00a76e0f-8ab6-4f9a-ac6a-00eae633342c", - "parent_id": "WARDEN-WP-0011", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0011-T05", - "status": "done", - "title": "T5 \u2014 Doc consistency + drift guard", - "source_path": "workplans/archived/260624-WARDEN-WP-0011-routing-guide-cli.md", - "uuid": "bf848375-eca7-4116-bb1d-fb7df6395c70", - "parent_id": "WARDEN-WP-0011", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0013", - "status": "archived", - "title": "Production Integration & Stewardship Closeout", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "4678c41a-c1d0-48cd-9988-4ea0380e8258", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T01", - "status": "done", - "title": "T1 \u2014 Post-gap reassessment and SCOPE refresh", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "de46f9a2-bf11-4651-a23c-430c63f396c8", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T02", - "status": "done", - "title": "T2 \u2014 Archive hygiene (WP-0010, WP-0011)", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "1b35321d-63ad-40da-a1aa-0b66190a0733", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T03", - "status": "done", - "title": "T3 \u2014 ops-bridge cert_command migration playbook", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "ad8588b2-9ae9-4f94-bd77-8025851a38f5", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T04", - "status": "done", - "title": "T4 \u2014 Operator OpenBao token hygiene runbook", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "5cb35829-32eb-4d59-97a1-f4d92ce8e239", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T05", - "status": "done", - "title": "T5 \u2014 Principals inventory drift check", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "4025cd32-89f8-42c3-b1e8-eaf78497d91f", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0013-T06", - "status": "done", - "title": "T6 \u2014 Policy gate production enablement checklist", - "source_path": "workplans/archived/260624-WARDEN-WP-0013-production-integration-and-stewardship-closeout.md", - "uuid": "51663f65-79cb-4108-87c8-9721f9476259", - "parent_id": "WARDEN-WP-0013", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0012", - "status": "finished", - "title": "Routing Scenario Playbooks", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "a7e712a0-02f8-4f83-944e-6b207e77bc4c", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0012-T01", - "status": "done", - "title": "T1 \u2014 issue-core ingestion key playbook", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "830bb512-0288-4dba-9dd4-ccfd28a4921f", - "parent_id": "WARDEN-WP-0012", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0012-T02", - "status": "done", - "title": "T2 \u2014 Inter-Hub and bootstrap lanes", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "7726a703-6e00-4e49-9380-ed3fb3268827", - "parent_id": "WARDEN-WP-0012", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0012-T03", - "status": "done", - "title": "T3 \u2014 ops-bridge tunnel migration", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "9fb397f0-0abb-48f5-bb62-7e77edae93bb", - "parent_id": "WARDEN-WP-0012", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0012-T04", - "status": "done", - "title": "T4 \u2014 Platform secret scenarios (LLM, STS, DB)", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "edcf4ed7-f18d-4a92-a42d-8cc7ca0ab792", - "parent_id": "WARDEN-WP-0012", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0012-T05", - "status": "done", - "title": "T5 \u2014 Drift review cadence", - "source_path": "workplans/archived/260627-WARDEN-WP-0012-routing-scenario-playbooks.md", - "uuid": "db98d655-8551-487b-9413-41bf97fc06e1", - "parent_id": "WARDEN-WP-0012", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0014", - "status": "finished", - "title": "Operator Access Assist \u2014 warden access front door", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "3c30b2ed-6ede-4b95-a438-fde6da6f6633", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0014-T01", - "status": "done", - "title": "T1 \u2014 Catalog schema: structured handoff fields", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "abb0e722-6524-4224-8638-6ee1573ed3e0", - "parent_id": "WARDEN-WP-0014", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0014-T02", - "status": "done", - "title": "T2 \u2014 `warden access` advisory surface", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "c1497263-7124-459f-b63a-d0c0c7005c86", - "parent_id": "WARDEN-WP-0014", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0014-T03", - "status": "done", - "title": "T3 \u2014 OpenBao proxy lane (`--fetch` / `--exec`)", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "6d3eb0e4-309c-4065-893e-6c4053fb0db2", - "parent_id": "WARDEN-WP-0014", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0014-T04", - "status": "done", - "title": "T4 \u2014 key-cape / login orchestration lane", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "481997e4-193d-4724-84a6-61cbc2940153", - "parent_id": "WARDEN-WP-0014", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0014-T05", - "status": "done", - "title": "T5 \u2014 Docs, security model, and INTENT/SCOPE alignment", - "source_path": "workplans/archived/260627-WARDEN-WP-0014-operator-access-assist.md", - "uuid": "a5eb616e-4edf-42db-a4fb-bf296cdb92bc", - "parent_id": "WARDEN-WP-0014", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-0015", - "status": "finished", - "title": "Workload Security Posture \u2014 env posture \u00d7 maturity + conformance", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "99f4a0e1-853c-456f-8aa7-8ff0f318ea65", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0015-T01", - "status": "done", - "title": "T1 \u2014 Author the two-axis Workload Security Posture standard (canon-bound)", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "85aeb676-a593-4056-986a-db14d4c5209f", - "parent_id": "WARDEN-WP-0015", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0015-T02", - "status": "done", - "title": "T2 \u2014 Machine-readable posture descriptors (both axes)", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "011fb0af-154d-40f4-a03e-3172c325321a", - "parent_id": "WARDEN-WP-0015", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0015-T03", - "status": "done", - "title": "T3 \u2014 Conformance checker (incl. secret-flow lattice)", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "c1a0e987-19d0-478e-ac08-2dbe98e64e09", - "parent_id": "WARDEN-WP-0015", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0015-T04", - "status": "done", - "title": "T4 \u2014 Dev-tier contract-double fixture library", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "e556fd2e-4e39-4c7d-bd94-b4330e4bef45", - "parent_id": "WARDEN-WP-0015", - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-0015-T05", - "status": "done", - "title": "T5 \u2014 INTENT/SCOPE alignment + canon contributions", - "source_path": "workplans/archived/260627-WARDEN-WP-0015-secret-lifecycle-tiering.md", - "uuid": "298c9b09-4a5a-41bf-a3bd-6c572385236b", - "parent_id": "WARDEN-WP-0015", - "extra": {} - }, - { - "kind": "workplan", - "id": "WARDEN-WP-ADHOC-2026-07-07", - "status": "finished", - "title": "Ad Hoc Tasks \u2014 2026-07-07", - "source_path": "workplans/archived/260707-ADHOC-2026-07-07.md", - "uuid": "90568b1e-8395-5c67-9c69-851ed08ff3d3", - "parent_id": null, - "extra": {} - }, - { - "kind": "task", - "id": "WARDEN-WP-ADHOC-2026-07-07-T01", - "status": "done", - "title": "T01 \u2014 Roll out proxy pipe fix (be3b4a2)", - "source_path": "workplans/archived/260707-ADHOC-2026-07-07.md", - "uuid": "bf985c95-bea6-5057-94f9-9cfa7e1c9dd8", - "parent_id": "WARDEN-WP-ADHOC-2026-07-07", - "extra": {} - }, - { - "kind": "intake", - "id": "WARDEN-IN-0001", - "status": "closed", - "title": "Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation", - "source_path": "intakes/intakes.md", - "uuid": "01a049ed-bbbc-7520-bc7c-6b0912ca534a", - "parent_id": null, - "extra": { - "record": { - "id": "WARDEN-IN-0001", - "kind": "intake", - "title": "Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation", - "status": "closed", - "outcome": "assented", - "origin": "cross-repo", - "origin_ref": "gate-house GH-DEC-2026-001", - "priority": "medium", - "owner": "ops-warden", - "requested_by": "gate-house", - "standard": "net-kingdom/canon/standards/security-layer-model_v0.1.md", - "description": "gate-house asks ops-warden to assent to three boundary items. (1) ops-warden is Staff, bound by the rule that Staff acts only through Engine APIs and never touches Tooling directly (standard section 5). (2) Doctrine versus runbook: the NetKingdom Security Literacy section in ops-warden INTENT is evidence the security curriculum had no owner; it now has one in gate-house. Proposal is that doctrine and curriculum move to gate-house and that section becomes lane-specific runbooks referencing gate-house doctrine rather than restating it. ops-warden keeps the lanes it stewards and everything operational about them. (3) The access lane/rule demarcation, normative in standard section 8: ops-warden and ops-mason own access lanes \u2014 how a worker reaches a host; access-engine owns access rules \u2014 whether they may. This demarcation is the condition attached to renaming flex-auth to access-engine, so ops-warden effectively holds a veto on that name. Also requested: add gate-house to the Security Literacy and routing tables \u2014 currently every plane is listed and gate-house appears nowhere \u2014 routing doctrine and authority-model questions there while continuing to route policy decisions to access-engine. If moving the curriculum out leaves ops-warden unable to instruct its own workers, say so; the boundary is wrong if it does.", - "notes": "Assented to all three items in ADR-0010, with reasoning in history/2026-08-28-security-layer-model-assent.md. (1) Staff accepted; the section 5 binding rule exposed a real non-conformance \u2014 src/warden/vault.py is a direct OpenBao client performing a write, as is warden desk's bao kv put. Declared in INTENT.md as an engine gap with intended owner secrets-engine and blocker \"no engine exposes an SSH-CA surface\", not negotiated as an exemption; taint.py declared under the read-only allowance; warden access proxies run under the caller's identity. An amendment is offered back to gate-house: a second sanctioned shape in section 5 for a declared engine gap carrying intended owner, blocker and review date, machine-readable so section 10 can tell a tracked gap from an undeclared violation. (2) Doctrine versus runbook accepted; the literacy section is now a lane routing runbook referencing gate-house doctrine. Answering gate-house's test question: it does not leave ops-warden unable to instruct its workers, because what instructs them is warden plan / warden route and .claude/rules/credential-routing.md, which stays inline by design. (3) The lane/rule demarcation assented unconditionally and the access-engine veto not exercised \u2014 ops-warden already consumes decisions and renders none. One request on sequencing only: a deprecation window in which both names resolve (598 references across 82 files here). gate-house added to the routing tables in INTENT.md and SCOPE.md.", - "created": "2026-08-28T19:30:28.087109Z", - "updated": "2026-08-28T21:05:00Z", - "state_hub_intake_id": "01a049ed-bbbc-7520-bc7c-6b0912ca534a" - } - } - }, - { - "kind": "intake", - "id": "WARDEN-IN-0002", - "status": "open", - "title": "Review requested: security layer model v0.3 \u2014 and does maturity-engine absorb warden route gaps?", - "source_path": "intakes/intakes.md", - "uuid": "01a04d97-94cd-7b49-8019-a91c7fce8adb", - "parent_id": null, - "extra": { - "record": { - "id": "WARDEN-IN-0002", - "kind": "intake", - "title": "Review requested: security layer model v0.3 \u2014 and does maturity-engine absorb warden route gaps?", - "status": "open", - "origin": "cross-repo", - "origin_ref": "net-kingdom security-layer-model_v0.3", - "priority": "medium", - "owner": "ops-warden", - "requested_by": "gate-house", - "description": "v0.3 is proposed and changes sections 4, 9 and 13 only; the v0.2 assent record stands. Two new engines: approval-engine (section 9.4) and maturity-engine (section 9.5). THE QUESTION FOR YOU concerns section 5.3, which exists because you offered the amendment. v0.3 gives declared gaps an owner: maturity-engine takes the gap register with intended_owner, blocked_on and review dates, and section 13 now says the register in the standard is interim and should not outlive that engine. You offered warden route gaps and the 27 delegation catalog entries as reusable prior art. So the question is whether that machinery should MOVE, be MIRRORED, or STAY. Our tentative reading, which we want tested rather than accepted: routing is yours and stays yours \u2014 warden route find answers where a credential need goes, and that is lane knowledge, not maturity. What might move is the readiness half: whether a declared gap is still within its review date, and whether an intended owner has an engine surface yet. If splitting those creates two sources for one fact, that is worse than either option and we would rather hear it now. Your SSH-CA signing write would be tracked in maturity-engine as a declared gap with intended owner secrets-engine and a review date \u2014 that is reporting your own non-conformance to an engine, so we would rather you assent to it than discover it. Also note approval-engine (section 9.4): it owns the approval object, not the approval workflow, so ops-warden lanes needing approval consume a claim rather than implementing one. Assent, revision, or rejection acceptable.", - "created": "2026-08-28T20:40:24.957468Z", - "updated": "2026-08-28T20:40:24.957468Z", - "state_hub_intake_id": "01a04d97-94cd-7b49-8019-a91c7fce8adb" - } - } - } - ], - "events": [ - { - "type": "repo.reconciled", - "workplan_count": 41, - "task_count": 183, - "source": "repo-manager", - "emitted_at": "2026-08-31T22:59:28.638627Z" - } - ] -} diff --git a/AGENTS.md b/AGENTS.md index dea050c..fdcd749 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,33 +190,6 @@ get wrong. -## Secrets practice guide - -From 2026-10-11, secrets practice follows SecurityCanon `SEC-DSM` 1.0.0: - -`../security-canon/infospace/standards/devops-secrets-management/SecurityCanonDevOpsSecretsManagementStandard.md` - -Gate House teaches that standard. This repo operates lanes inside it. SSH -certificates (`warden sign`) are the short-lived credential this repo issues. -Every other credential is routed, not stored here. - -Before adding or changing a lane, apply §5 and §37: - -- Prefer native or federated identity, then an ephemeral credential, then an - automatically rotated secret, then a vaulted static secret. -- Do not become the vault (`DSM-AP-002`, `DSM-AP-006`, `DSM-AP-015`). OpenBao - remains custody. This repo remains the conduit. -- Do not have a human carry a secret (`DSM-HUM-001`). Desk paste-once and - `warden access --out` / `--exec` / `--wrap` stay the sanctioned transports. -- Do not write secret values into audit, chat, workplans, or stdout - (`DSM-AUDIT-002`). -- A tool that can read a secret is agent authority (`DSM-AP-020`, - `DSM-AGENT-008`). High-risk lanes stay behind the agent read boundary. - -A residual static secret that cannot be eliminated is still someone else's -custody. The catalog entry points at the owner and records rotation and -revocation. It does not hold the value. - ## Experiential memory (WARDEN-WP-0024) ops-warden shares a **phase-memory** store across worker ticks, coding agent @@ -248,19 +221,6 @@ Install bundled memory: `make install-all` then `make check-memory` from ## Workplan Convention (ADR-001) -### State Hub reads and caller identity - -Use `GET /workplans/{id}` and `GET /tasks/?workplan_id=` for reads. -Do not send the legacy `workstream_id` query parameter or call `/workstreams/` -routes (410 Gone). Keep `state_hub_workstream_id` in file frontmatter unchanged. -Send `X-StateHub-Component: ops-warden` on direct HTTP requests so operational -reviews can attribute callers. Example: - -```bash -curl -fsS -H 'X-StateHub-Component: ops-warden' \ - 'http://127.0.0.1:8000/tasks/?workplan_id=' -``` - Work items originate as files in this repo — not in the hub. The hub is a read/cache/index layer that rebuilds from files. @@ -318,25 +278,3 @@ To create a new workplan: 1. Write the file following the format above 2. Notify the custodian operator to run `make fix-consistency REPO=ops-warden` (or send a message to the hub agent via `POST /messages/`) - - -## NetKingdom situational knowledge pilot - -For security-related work, read the shared -[Emergent Security orientation](../emergent-security/knowledge/cards/start.md) -and the task's card in that package. From this repository root, validate and read: - -```bash -PYTHONPATH=../emergent-security/src python3 -m emergent_security.knowledge --repositories .. show start --audience generic -``` - -Use the matching provider audience when known and retrieve the relevant card. -Record package version, digest and source status; reload after resume/compaction -or a repository switch. A withheld card requires source review; an offline -snapshot is orientation only. If the sibling checkout is absent, use the trusted -installed `emergent-knowledge` package or an explicitly reviewed exported bundle; -do not silently download or execute a replacement. - -This pilot adds situational knowledge, not authority, live readiness or a change -to this repository's owner contracts. Work: `emergent-security/EMERGENT-WP-0002`. - diff --git a/CLAUDE.md b/CLAUDE.md index e6f4503..a812bb4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,25 +11,3 @@ @.claude/rules/credential-routing.md @.claude/rules/finding-routing.md @.claude/rules/agents.md - - -## NetKingdom situational knowledge pilot - -For security-related work, read the shared -[Emergent Security orientation](../emergent-security/knowledge/cards/start.md) -and the task's card in that package. From this repository root, validate and read: - -```bash -PYTHONPATH=../emergent-security/src python3 -m emergent_security.knowledge --repositories .. show start --audience generic -``` - -Use the matching provider audience when known and retrieve the relevant card. -Record package version, digest and source status; reload after resume/compaction -or a repository switch. A withheld card requires source review; an offline -snapshot is orientation only. If the sibling checkout is absent, use the trusted -installed `emergent-knowledge` package or an explicitly reviewed exported bundle; -do not silently download or execute a replacement. - -This pilot adds situational knowledge, not authority, live readiness or a change -to this repository's owner contracts. Work: `emergent-security/EMERGENT-WP-0002`. - diff --git a/INTENT.md b/INTENT.md index c643f16..0f0919e 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,77 +1,5 @@ ---- -# THE layer declaration (§11, GH-DEC-2026-017 §1). `layer.yaml` is derived from -# this frontmatter and must agree with it; it does not govern. Comparison of the -# §3 vocabulary is ASCII case-insensitive, so `Staff` here and `staff` there are -# the same value and neither is re-spelled (GH-DEC-2026-017 §2). -layer: Staff -role: null # Engines only: PDP | PIP | Evidence | Lifecycle -pep_shaped: true # §6.4 — issuing a certificate is a protected side effect -# No `standard_version` here or in the sidecar: GH-DEC-2026-017 §5 / A12 — -# the declared layer is a standing property that does not change when the -# standard is revised, so a version in the declaration reads as a validity -# condition it is not. Version-scoped state belongs in the derived conformance -# record. The version ops-warden assented at is recorded with the assent -# (ADR-0010), which is where a version-at-a-boundary belongs. -standard: net-kingdom/canon/standards/security-layer-model -companion: net-kingdom/SECURITY-COMPANION.md -declaration: layer.yaml # derived form; this frontmatter governs -pep_stance: pep-stance.yaml -assent: docs/adr/ADR-0010 ---- - # INTENT -> **ops-warden is Staff, and PEP-shaped.** Declared here in its own voice, per -> `security-layer-model_v0.8` §11 — a layer stated *about* a repository by another -> repository is not a declaration. The standard is **accepted**; its operative form -> is `net-kingdom/SECURITY-COMPANION.md`. ops-warden's assent is `ADR-0010`, and its -> reviews of v0.4, v0.6 and v0.7 are in `history/`. -> -> **Staff** because ops-warden's core function is judgement, not computation: it -> decides which lane a need belongs to and stewards the paths through the estate's -> rules. Its artifacts are runbooks, routing decisions, workplans. **PEP-shaped** -> because issuing a certificate is a protected side effect — a shape, not a layer -> (§6.4). ops-warden renders no authorization decision and never will; it consumes -> them from `access-engine`. -> -> **The estate's front door for paths.** The companion routes the whole estate here: -> *"For how to get something done — which lane, which credential, which route — ask -> `ops-warden`. This document says what the rules are; ops-warden stewards the paths -> through them."* That is a standing obligation, not a compliment: every rule -> gate-house writes needs a path someone can actually walk, and ops-warden owes the -> estate that path. -> -> **The declarations are files, not this note** — prose cannot distinguish a -> declaration from a transcribed review (§11): -> -> | File | Declares | Enforced by | -> | --- | --- | --- | -> | `layer.yaml` | every Tooling contact, mapped to a §5.1/§5.2/§5.3 shape, plus the non-Tooling clients so the check is total | `scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py` | -> | `pep-stance.yaml` | the unreachable-engine stance map (§6.4 obl. 3), total per zone | test asserts the published map **equals** the shipped default | -> | `tenancy.yaml` | tenancy posture, and `z1-operational` zone membership | `ADR-0009` | -> -> Both are cited in the standard as the estate's reference forms (§11, §6.4, §13.1). -> -> **Two declared engine gaps (§5.3), tracked non-conformance and not conformance.** -> `VaultCA` signs over a direct OpenBao client and `warden desk` shells `bao kv put`. -> Intended owner **secrets-engine**; blocked on no engine exposing an SSH-CA or -> attended-provisioning surface; reviewed quarterly; registered in statute §13. -> ops-warden keeps signing while the gap is open — refusing would remove production -> host access to close a documentation gap. -> -> **The agent principal (§3.4).** ops-warden is operated by agents as well as people, -> and they share the layer but not the blast radius. No standing credential; tool use -> is a conduit or an engine API and there is no third route; **tool availability is -> not permission**, which is exactly what `ADR-0004`'s read-boundary enforces; agent -> memory is not a state plane. Session semantics belong to `glas-harness`, not here. -> -> **Evidence (§9.6).** ops-warden's audit trail is **attributive**, not load-bearing: -> no control branches on the presence of a signing record. Emission is deliberately -> non-atomic so an audit-store failure cannot remove production host access — a trade -> the standard sanctions, declared in `wiki/AuditTrail.md`, registered in §13. If any -> future control ever gates on this trail, that trade must be revisited before it -> ships. - > This file captures **why this repository exists**, the **direction it is > moving toward**, and the **kind of system it is meant to become**. > It is intentionally **aspirational and stable**, not a description of @@ -130,15 +58,14 @@ owns one lane and points at the rest: (secrets-engine `exec`, railiance-platform `credential exec`) ops-warden routes to the owner's front door — it does not mint tokens or run the owner's tool itself. This is the assist layer, not a universal broker: custody stays in OpenBao / - secrets-engine / the platform broker; authorization in access-engine - (repository `flex-auth`). + secrets-engine / the platform broker; authorization in flex-auth. 3. **Steward workload security posture conformance.** Author the ops-security slice for environment posture (`dev/test/prod`) and workload maturity (`M0-M3`), then ship descriptors and read-only checks that identify whether a secret-flow blocker is real, owner-routed, or removable with a contract double. Runtime enforcement - remains access-engine; custody remains OpenBao. + remains flex-auth; custody remains OpenBao. 4. **Align** runbooks, wiki, inventory patterns, and scorecard checks with - NetKingdom canon as the platform evolves (OpenBao-first, access-engine policy, + NetKingdom canon as the platform evolves (OpenBao-first, flex-auth policy, key-cape IAM Profile, railiance deployment layers). 5. **Issue** short-lived SSH certificates for `adm` / `agt` / `atm` actors when host or ops reachability requires the SSH lane — via `warden sign`, @@ -150,52 +77,30 @@ owns one lane and points at the rest: --- -## Lane routing — who owns which need +## NetKingdom Security Literacy -**This is a runbook, not doctrine.** Security doctrine, the authority model, and the -security curriculum are **gate-house's** (`security-layer-model_v0.7` §8). -ops-warden references them and does not restate them. What follows is lane -stewardship: which subsystem owns which need, and what ops-warden does about it. +ops-warden should be fluent in the platform architecture documented in +`net-kingdom` — especially: -The machine-readable form is `registry/routing/catalog.yaml`, and the executable form -is `warden plan ""` / `warden route find`. Prefer either over this table — it is -orientation, and the catalog is the source of truth (`ADR-0001`). - -| Component | Layer | Owns | ops-warden relationship | -| --- | --- | --- | --- | -| **gate-house** | Staff | Security doctrine, invariants, authority ceilings, authority context, conformance review, curriculum | **Route doctrine and authority-model questions here.** Not policy decisions — those go to access-engine | -| **access-engine** (`flex-auth`) | Engine | **The policy decision** — whether an actor may act. The only decision point in NetKingdom | Consume decisions; caller-side pre-sign gate. ops-warden never renders or caches one | -| **key-cape / Keycloak** | Tooling | Identity — who the actor is, MFA, IAM Profile claims | Instruct the identity path; do not re-implement OIDC | -| **OpenBao** | Tooling | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; proxy reads as the caller when `exec_capable`. Direct client use is the declared exception above | -| **secrets-engine** | Engine | Credential abstraction, custody, lifecycle; owner-native exec | Route provisioned exec lanes (e.g. npm publish). **Intended owner of the SSH-CA surface** | -| **tenant-engine** | Engine | Tenant/client secret custody and front door | Route tenant lanes once fronted; current tenant proxies are interim (section 9) | -| **user-engine** | Engine | Users, accounts, memberships | No ops-warden lane today; route rather than absorb | -| **zone-engine** | Engine | Zone identity and membership | Consume compiled membership; ops-warden declares `z1-operational` (`ADR-0009`) | -| **railiance-platform** (broker) | — | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens | -| **ops-mason** | Staff | Building and tearing down access routes and perimeters | Peer lane owner; same lane/rule demarcation applies | -| **ops-warden** | Staff | **Operational access lanes** — short-lived SSH certificates, routing, stewardship, runbooks | **Own and issue** the SSH lane | -| **ops-bridge** | Staff | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration | -| **railiance-infra** | — | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible | -| **kings-guard** | Staff | Adaptive defence, observation, containment; publishes posture | Posture may reduce authority, never manufacture it | - -### Access lane versus access rule - -Normative, per `security-layer-model_v0.7` §8 and assented to in `ADR-0010`: - -- **access lane** — ops-warden and ops-mason. *How* a worker reaches a host. -- **access rule** — access-engine. *Whether* they may. - -ops-warden owns the route and never the decision. A question about whether an actor -may do something is not an ops-warden question, however it arrives. +| Plane / component | Role in access | ops-warden relationship | +| --- | --- | --- | +| **key-cape / Keycloak** | Identity — who is the actor, MFA, IAM Profile claims | Instruct identity path; do not re-implement OIDC | +| **flex-auth + Topaz** | Authorization — may this actor perform this action | Caller-side policy gate shipped (opt-in); production flip is flex-auth's | +| **OpenBao** | Runtime secrets — API keys, dynamic creds, leases, audit | Instruct custody paths; SSH engine is signing backend only; proxy reads as caller when `exec_capable` | +| **secrets-engine** | Owner-native secret-exec (`secrets-engine exec`) | Route provisioned exec lanes (e.g. npm publish); ops-warden does not hold tokens | +| **railiance-platform** (credential broker) | Scoped lease grants (`credential exec`) | Route `warden-sign` token needs; ops-warden does not mint OpenBao tokens | +| **tenant-engine** | Tenant/client secret custody and front door | Route tenant lanes once fronted; ops-warden's current tenant proxies are interim (§9) | +| **user-engine** | End-user identity and account lifecycle | No ops-warden lane today; route rather than absorb | +| **ops-warden** | Operational SSH certificates — short-lived host access | **Own and issue** this lane | +| **ops-bridge** | Tunnel transport — consumes certs via `cert_command` | Primary consumer; document integration | +| **railiance-infra** | Host principals, force-command, SSH hardening | Instruct host-side deployment; do not own Ansible | +| **railiance-platform** (deploy) | OpenBao/K8s/platform service deployment | Instruct production endpoints; do not deploy clusters | Canonical references: -- `net-kingdom/SECURITY-COMPANION.md` — the operative form; start here -- `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted; §5 shapes, §6.4 PEP, §8 vocabulary) - `net-kingdom/docs/platform-identity-security-architecture.md` - `net-kingdom/docs/responsibility-map.md` - `wiki/AccessManagementDirective.md` (ops SSH actor model) -- `.claude/rules/credential-routing.md` (agent-facing runbook — stays inline by design) --- @@ -218,9 +123,8 @@ Canonical references: | Need | Route to | | --- | --- | | OIDC login, MFA, human identity claims | key-cape / Keycloak (NetKingdom IAM Profile) | -| Security doctrine, invariants, authority model | gate-house | -| Policy decision — may actor X access resource Y | access-engine (`flex-auth`) | -| API keys, provider secrets, DB creds, object-storage STS | OpenBao (+ access-engine policy where required) | +| Policy decision — may actor X access resource Y | flex-auth | +| API keys, provider secrets, DB creds, object-storage STS | OpenBao (+ flex-auth policy where required) | | Inter-Hub operator keys, LLM provider credentials | OpenBao or approved operator secret store | | Tunnel lifecycle, port forwarding | ops-bridge | | `/etc/ssh/auth_principals/`, host hardening | railiance-infra | @@ -343,7 +247,7 @@ ops-warden (issue SSH; route / assist the rest) +-- Generic API / DB / provider secret? -> OpenBao path | (warden access proxies as caller when exec_capable) | - +-- Authorization required? ------------> access-engine decision + +-- Authorization required? ------------> flex-auth decision | (caller-side gate on sign + access when policy.enabled) | +-- Identity / MFA required? -------------> key-cape / Keycloak path @@ -372,7 +276,7 @@ for one security lane plus operational guidance. ops-warden should appear in NetKingdom responsibility and pattern material as the **operational SSH credential authority**, not as a replacement for -OpenBao or access-engine. +OpenBao or flex-auth. --- @@ -399,8 +303,8 @@ ops-warden is succeeding when: ## Non-goals - Universal credential broker for all secret types -- Runtime enforcement of the workload secret-flow lattice (access-engine owns that) -- Replacing OpenBao, access-engine, key-cape, or railiance deployment ownership +- Runtime enforcement of the workload secret-flow lattice (flex-auth owns that) +- Replacing OpenBao, flex-auth, key-cape, or railiance deployment ownership - Storing Inter-Hub, LLM provider, or other long-lived API keys - Host-side SSH configuration deployment - **Duplicating or restating another subsystem's procedure** — routing material @@ -417,7 +321,7 @@ ops-warden is succeeding when: The repository shipped the SSH CA CLI first (WARDEN-WP-0001–0003). The stewardship and NetKingdom-alignment mission is the **next stratum** — docs, routing canon, inventory standards, production OpenBao SSH engine alignment, -access-engine integration design, and NetKingdom cross-links — without collapsing +flex-auth integration design, and NetKingdom cross-links — without collapsing platform boundaries. See `wiki/CredentialRouting.md` for worker-facing routing, diff --git a/Makefile b/Makefile index 4463c9e..0a71247 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ # ops-warden development and install targets ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) -UNIT_PYTHON ?= $(ROOT)/.venv/bin/python PHASE_MEMORY_REPO ?= $(abspath $(ROOT)/../phase-memory) .DEFAULT_GOAL := help @@ -25,12 +24,12 @@ sync: uv sync install-warden: - uv tool install . --force --reinstall-package ops-warden + uv tool install . --force install-memory: @test -d "$(PHASE_MEMORY_REPO)" || (echo "ERROR: phase-memory not found at $(PHASE_MEMORY_REPO). Clone it or set PHASE_MEMORY_REPO."; exit 1) @test -f "$(PHASE_MEMORY_REPO)/pyproject.toml" || (echo "ERROR: $(PHASE_MEMORY_REPO) is not a phase-memory checkout."; exit 1) - uv tool install . --with-editable "$(PHASE_MEMORY_REPO)" --force --reinstall-package ops-warden + uv tool install . --with-editable "$(PHASE_MEMORY_REPO)" --force install: install-warden @@ -44,14 +43,7 @@ verify-memory: check-memory @warden memory status test: - "$(UNIT_PYTHON)" -I scripts/run_unit_tests.py --phase-memory-root "$(PHASE_MEMORY_REPO)" $(TEST_ARGS) + PYTHONPATH=src:$(PHASE_MEMORY_REPO)/src uv run pytest lint: - uv run ruff check . - -# CUST-WP-0079: explicit staged work-record generation, using the owner renderer. -.PHONY: generated-work-records generated-work-records-check -generated-work-records: ## Generate and stage WORK-RECORDS from staged sources - $(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records -generated-work-records-check: ## Verify staged index without writes or publication - $(MAKE) --no-print-directory -f .generated-work-records.mk generated-work-records-check + uv run ruff check . \ No newline at end of file diff --git a/README.md b/README.md index 101a345..cd4e10c 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,6 @@ uv sync uv tool install . --with-editable ../phase-memory --force ``` -For a working checkout, an editable install keeps the `warden` CLI in step -with `git pull`. This avoids stale-wheel builds: - -```bash -uv tool install -e . --with-editable ../phase-memory --force -``` - Or run without installing: ```bash diff --git a/SCOPE.md b/SCOPE.md index 7aab85e..ae19ee4 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -21,9 +21,8 @@ access guidance aligned with NetKingdom canon. ops-warden **issues short-lived SSH certificates and routes every other credential need to the subsystem that owns it.** SSH signing is **production-verified** on -Railiance OpenBao (`warden sign`, host CA trust deployed). Since public -`bao.coulomb.social` was retired, production signing reaches OpenBao through -the ops-bridge tunnel (`http://127.0.0.1:18200`). +Railiance OpenBao (`warden sign` against `https://bao.coulomb.social`, host CA trust +deployed). **Access routing** is shipped: `wiki/AccessRouting.md`, credential routing wiki, NetKingdom security map, machine-readable pointer catalog @@ -86,28 +85,6 @@ cutover, flex-auth publishing the zone-aware pre-sign stance package, the owner-driven WP-0015 canon landing, and — newly named — the missing owner front doors that keep ops-warden holding interim lanes (secrets-engine, tenant-engine). -### Layer-model conformance (v0.7, accepted) - -ops-warden declares **Staff**, **PEP-shaped**, in `INTENT.md` frontmatter and in its -own voice — `security-layer-model_v0.7` §11. Shipped declaration artifacts, both -cited in the standard as the estate's reference forms: - -| Artifact | Declares | Status | -| --- | --- | --- | -| `layer.yaml` | 5 Tooling contacts mapped to §5.1/§5.2/§5.3 shapes + non-Tooling clients so the check is total; **derived** from `INTENT.md`, which governs (`GH-DEC-2026-017`) | shipped; named reference form (§11) | -| `pep-stance.yaml` | unreachable-engine stance map, total per zone | shipped; registered in statute §13.1 (§6.4 obl. 3) | -| `scripts/check_layer_conformance.py` | every direct Tooling client maps to a declared shape | shipped; CI-enforced | -| `tests/test_layer_conformance.py` | the §5.2 no-authority property, and published stance map **equals** shipped default | shipped, 11 tests | - -Conformance state under §11: **declared gap** — tracked non-conformance, not -conformance. Two §5.3 contacts (`VaultCA` signing write, `warden desk` `bao kv put`), -intended owner `secrets-engine`, registered in statute §13. - -Four ops-warden findings have been adopted into the standard: §9.1's two marks -(`pending` vs `declared-gap`), §5's Tooling scope rule, §6.4 obligation 1's second -limb, and §13.1's existence. Reviews: `history/2026-08-29-layer-model-v04-review.md`, -`-v06-review.md`, `-v07-scope-intent-assessment.md`. - ### Issue vs route ops-warden executes exactly one lane with its own authority and routes/assists the rest. @@ -179,7 +156,6 @@ superseding ADR, never an in-place edit. | `ADR-0007` | Build-stage permissiveness stops at credential disclosure; every lane carries an explicit `risk` grade | | `ADR-0008` | A lane's risk grade covers every field its path discloses, not just the field it is named after | | `ADR-0009` | Adopt security-zones v0.1 and compile explicit workload membership; PEP failure mode is per zone | -| `ADR-0010` | ops-warden is Staff and PEP-shaped — it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap, not an exemption | Rules we follow but do not own — NetKingdom canon, the IAM profile, the credential-management standard, the-custodian's ADR-001 workplan convention — are @@ -415,11 +391,6 @@ Remaining production distance is also in other repos' lanes (see Known gaps). repos' lanes (and retiring interim covers as those owners ship front doors) - **Integration docs:** cert_command migration, token hygiene (broker-first), principals drift (`wiki/playbooks/`) -- **Per-actor issuance limits:** WARDEN-WP-0045. The issuance contract carries a - per-actor extension allow-list and a `max_ttl_hours` ceiling. The new profile - `agt-custodian` (12h, `permit-pty` only) has the attended lane - `agt-custodian-ssh-cert`. Its first issuance was done by the founder on - 2026-10-20 for the supervised interactive agents (CUST-WP-0073). - **Latest assessment:** `history/2026-08-11-delegation-surface-assessment.md` - **Latest workplans:** WP-0029 (policy front door) shipped July 2026; WP-0030 (delegation register) shipped August 2026 @@ -462,12 +433,11 @@ Downstream: `ops-bridge` (primary), kaizen agents, CI automations, human operato | Repo | Relationship | | --- | --- | -| `gate-house` | Owns the security layer model, doctrine, invariants, authority context, and conformance review. ops-warden routes doctrine questions there, and the companion routes the estate's *path* questions back to ops-warden (`ADR-0010`) | | `net-kingdom` | Canonical security architecture; ops-warden aligns to it | | `ops-bridge` | Primary cert_command consumer | | `railiance-infra` | Host-side SSH principals and hardening | | `railiance-platform` | OpenBao deployment and platform secrets | -| `flex-auth` | Authorization — ruled name `access-engine`; the only policy decision point. Policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 | +| `flex-auth` | Authorization; policy package shipped (FLEX-WP-0006); runtime deploy FLEX-WP-0007 | | `key-cape` | Identity / IAM Profile lightweight mode | | `secrets-engine` | Owner-native secret-exec front door (`secrets-engine exec/route`); ops-warden routes provisioned secret lanes to it (WP-0019) and holds 6 more as interim proxies pending its front doors | | `tenant-engine` | Intended owner of tenant/client secret front doors; ops-warden holds 3 tenant lanes as interim proxies (WP-0028 pattern, WP-0030 register) | @@ -484,9 +454,7 @@ type: security title: SSH certificate issuance description: Issues short-lived CA-signed SSH certificates for adm/agt/atm actors via a pluggable cert_command interface; documents NetKingdom operational access routing; - supports local CA and OpenBao/Vault-compatible SSH engine backends. Per-actor - extension allow-lists and max_ttl_hours ceilings bound each profile (e.g. - agt-custodian: 12h, permit-pty only). + supports local CA and OpenBao/Vault-compatible SSH engine backends. keywords: [ssh, certificate, ca, credential, warden, ops-warden, pki, openbao, vault, netkingdom] ``` @@ -518,9 +486,6 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l | `wiki/CredentialRouting.md` | Which subsystem for each credential need | | `wiki/WorkloadSecurityPosture.md` | Secret-store posture, workload maturity, and blocker triage | | `registry/routing/catalog.yaml` | Machine-readable routing pointer catalog | -| `net-kingdom/SECURITY-COMPANION.md` | **The estate's operative security rules — start here** | -| `layer.yaml` | Derived layer declaration: every Tooling contact and its §5 shape (`INTENT.md` frontmatter governs) | -| `pep-stance.yaml` | Unreachable-engine stance map (§6.4); equals shipped behaviour by test | | `tenancy.yaml` | Declared tenancy posture (`I1 A1 E0 P n/a R n/a V0`) and why each axis sits where it does | | `wiki/NetKingdomSecurityMap.md` | Platform security component map | | `examples/warden.production.example.yaml` | Production warden.yaml template | @@ -542,24 +507,3 @@ keywords: [access, credential, secret, npm, token, api-key, openbao, key-cape, l | `history/2026-06-18-access-routing-intent-shift-assessment.md` | Routing charter decision | | `history/2026-06-23-flex-auth-policy-gate-production-smoke.md` | Policy gate smoke evidence | | `net-kingdom/docs/platform-identity-security-architecture.md` | Platform security canon | - -## Staged work-record generation — 2026-10-19 - -The repository explicitly opts into the shared staged work-record generator via -`.generated-work-records.json` and its dedicated `.generated-work-records.mk`. -`make generated-work-records` uses the existing State Hub renderer through the -Custodian adapter, stages only `WORK-RECORDS.md`, and refuses unreconciled -unstaged index edits. `make generated-work-records-check` checks without writes. -The shared commit hook retains its generated-only guard and repository-specific -scanner routing; meaningful source commits regenerate the index before any -configured scan. -Generation performs no Hub calls, commits, pushes, credential access or live -changes. Source inventories and other generated artifacts retain their owners. - -## Local orientation brief policy — 2026-10-19 - -`.custodian-brief.md` is a local, ignored State Hub orientation snapshot. Its -existing bytes remain available for offline session startup; refreshing it does -not require a Git commit. Authored workplans and the generated WORK-RECORDS index -retain their existing source and staged-generation contracts. No runtime or -credential change follows from this repository hygiene policy. diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index eacd7d9..0bc6497 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -8,12 +8,10 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | -| workplan | WARDEN-WP-ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md | -| workplan | WARDEN-WP-ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md | -| workplan | WARDEN-WP-ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md | -| workplan | WARDEN-WP-ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md | -| workplan | WARDEN-WP-ADHOC-2026-09-08 | finished | — | workplans/ADHOC-2026-09-08.md | -| workplan | WARDEN-WP-ADHOC-2026-10-03 | finished | — | workplans/ADHOC-2026-10-03.md | +| workplan | ADHOC-2026-06-27 | finished | — | workplans/ADHOC-2026-06-27.md | +| workplan | ADHOC-2026-06-29 | finished | — | workplans/ADHOC-2026-06-29.md | +| workplan | ADHOC-2026-08-11 | finished | — | workplans/ADHOC-2026-08-11.md | +| workplan | ADHOC-2026-08-17 | finished | — | workplans/ADHOC-2026-08-17.md | | workplan | WARDEN-WP-0016 | finished | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | | workplan | WARDEN-WP-0017 | finished | — | workplans/WARDEN-WP-0017-access-front-door-discoverability.md | | workplan | WARDEN-WP-0018 | finished | — | workplans/WARDEN-WP-0018-whynot-design-npm-lane-activation.md | @@ -25,41 +23,22 @@ | workplan | WARDEN-WP-0024 | finished | — | workplans/WARDEN-WP-0024-experiential-memory-and-agent-sessions.md | | workplan | WARDEN-WP-0025 | finished | — | workplans/WARDEN-WP-0025-forgejo-admin-api-token-lane.md | | workplan | WARDEN-WP-0026 | finished | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | -| workplan | WARDEN-WP-0027 | blocked | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| workplan | WARDEN-WP-0027 | active | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | | workplan | WARDEN-WP-0028 | finished | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | | workplan | WARDEN-WP-0029 | finished | — | workplans/WARDEN-WP-0029-policy-front-door-and-founder-surface.md | | workplan | WARDEN-WP-0030 | finished | — | workplans/WARDEN-WP-0030-delegation-register.md | | workplan | WARDEN-WP-0031 | finished | — | workplans/WARDEN-WP-0031-policy-caller-identity.md | | workplan | WARDEN-WP-0032 | finished | — | workplans/WARDEN-WP-0032-security-zones.md | -| workplan | WARDEN-WP-0033 | finished | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | -| workplan | WARDEN-WP-0034 | active | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| workplan | WARDEN-WP-0035 | finished | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | -| workplan | WARDEN-WP-0036 | finished | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | -| workplan | WARDEN-WP-0037 | blocked | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | -| workplan | WARDEN-WP-0038 | finished | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | -| workplan | WARDEN-WP-0039 | finished | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | -| workplan | WARDEN-WP-0040 | active | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | -| workplan | WARDEN-WP-0041 | finished | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | -| workplan | WARDEN-WP-0042 | finished | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | -| workplan | WARDEN-WP-0043 | finished | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | -| workplan | WARDEN-WP-0044 | active | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | -| workplan | WARDEN-WP-0045 | finished | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md | -| task | WARDEN-WP-ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | -| task | WARDEN-WP-ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | -| task | WARDEN-WP-ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | -| task | WARDEN-WP-ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md | -| task | WARDEN-WP-ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md | -| task | WARDEN-WP-ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md | -| task | WARDEN-WP-ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md | -| task | WARDEN-WP-ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md | -| task | WARDEN-WP-ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T01 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T02 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T03 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T04 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T05 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-09-08-T06 | done | — | workplans/ADHOC-2026-09-08.md | -| task | WARDEN-WP-ADHOC-2026-10-03-T01 | done | — | workplans/ADHOC-2026-10-03.md | +| workplan | WARDEN-WP-0033 | active | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | ADHOC-2026-06-27-T01 | done | — | workplans/ADHOC-2026-06-27.md | +| task | ADHOC-2026-06-29-T01 | done | — | workplans/ADHOC-2026-06-29.md | +| task | ADHOC-2026-08-11-T01 | done | — | workplans/ADHOC-2026-08-11.md | +| task | ADHOC-2026-08-11-T02 | done | — | workplans/ADHOC-2026-08-11.md | +| task | ADHOC-2026-08-11-T03 | done | — | workplans/ADHOC-2026-08-11.md | +| task | ADHOC-2026-08-17-T01 | done | — | workplans/ADHOC-2026-08-17.md | +| task | ADHOC-2026-08-17-T02 | done | — | workplans/ADHOC-2026-08-17.md | +| task | ADHOC-2026-08-17-T03 | done | — | workplans/ADHOC-2026-08-17.md | +| task | ADHOC-2026-08-17-T04 | done | — | workplans/ADHOC-2026-08-17.md | | task | WARDEN-WP-0016-T01 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | | task | WARDEN-WP-0016-T02 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | | task | WARDEN-WP-0016-T03 | done | — | workplans/WARDEN-WP-0016-ops-bridge-tunnel-cert-pilot.md | @@ -113,7 +92,7 @@ | task | WARDEN-WP-0026-T06 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | | task | WARDEN-WP-0026-T07 | done | — | workplans/WARDEN-WP-0026-credential-disclosure-hygiene.md | | task | WARDEN-WP-0027-T01 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | -| task | WARDEN-WP-0027-T02 | todo | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | +| task | WARDEN-WP-0027-T02 | progress | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | | task | WARDEN-WP-0027-T03 | cancel | — | workplans/WARDEN-WP-0027-credential-governance-lockdown.md | | task | WARDEN-WP-0028-T01 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | | task | WARDEN-WP-0028-T02 | done | — | workplans/WARDEN-WP-0028-tenant-secret-custody.md | @@ -147,46 +126,6 @@ | task | WARDEN-WP-0033-T01 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | | task | WARDEN-WP-0033-T02 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | | task | WARDEN-WP-0033-T03 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | -| task | WARDEN-WP-0033-T04 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | +| task | WARDEN-WP-0033-T04 | wait | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | | task | WARDEN-WP-0033-T05 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | | task | WARDEN-WP-0033-T06 | done | — | workplans/WARDEN-WP-0033-native-lane-handoff.md | -| task | WARDEN-WP-0034-T01 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0034-T02 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0034-T03 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0034-T04 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0034-T05 | wait | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0034-T06 | done | — | workplans/WARDEN-WP-0034-layer-model-v07-conformance.md | -| task | WARDEN-WP-0035-T01 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | -| task | WARDEN-WP-0035-T02 | done | — | workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md | -| task | WARDEN-WP-0036-T01 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | -| task | WARDEN-WP-0036-T02 | done | — | workplans/WARDEN-WP-0036-attended-login-openbao-output.md | -| task | WARDEN-WP-0037-T01 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | -| task | WARDEN-WP-0037-T02 | done | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | -| task | WARDEN-WP-0037-T03 | wait | — | workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md | -| task | WARDEN-WP-0038-T01 | done | — | workplans/WARDEN-WP-0038-plan-mutation-intent.md | -| task | WARDEN-WP-0039-T01 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | -| task | WARDEN-WP-0039-T02 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | -| task | WARDEN-WP-0039-T03 | done | — | workplans/WARDEN-WP-0039-explicit-policy-refusal.md | -| task | WARDEN-WP-0040-T01 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | -| task | WARDEN-WP-0040-T02 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | -| task | WARDEN-WP-0040-T03 | wait | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | -| task | WARDEN-WP-0040-T04 | done | — | workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md | -| task | WARDEN-WP-0041-T01 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | -| task | WARDEN-WP-0041-T02 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | -| task | WARDEN-WP-0041-T03 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | -| task | WARDEN-WP-0041-T04 | done | — | workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md | -| task | WARDEN-WP-0042-T01 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | -| task | WARDEN-WP-0042-T02 | done | — | workplans/WARDEN-WP-0042-openbao-structure-survey-route.md | -| task | WARDEN-WP-0043-T01 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | -| task | WARDEN-WP-0043-T02 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | -| task | WARDEN-WP-0043-T03 | done | — | workplans/WARDEN-WP-0043-platform-supplier-credentials.md | -| task | WARDEN-WP-0044-T01 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | -| task | WARDEN-WP-0044-T02 | done | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | -| task | WARDEN-WP-0044-T03 | progress | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | -| task | WARDEN-WP-0044-T04 | wait | — | workplans/WARDEN-WP-0044-security-case-follow-through.md | -| task | WARDEN-WP-0045-T01 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md | -| task | WARDEN-WP-0045-T02 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md | -| task | WARDEN-WP-0045-T03 | done | — | workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md | -| intake | WARDEN-IN-0001 | closed | — | intakes/intakes.md | -| intake | WARDEN-IN-0002 | closed | — | intakes/intakes.md | -| intake | WARDEN-IN-0003 | closed | — | intakes/intakes.md | diff --git a/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md index 5069487..08e5516 100644 --- a/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md +++ b/docs/adr/ADR-0004-agent-read-boundary-on-high-risk-lanes.md @@ -5,15 +5,15 @@ title: "ADR-0004 — High-risk lanes refuse raw value streaming to agent session domain: infotech repo: ops-warden status: accepted -version: "1.1" -revision: "2" +version: "1.0" +revision: "1" owner: ops-warden binds: "ops-warden; any agent runtime calling warden access" created: "2026-07-20" -updated: "2026-09-04" -last_reviewed: "2026-09-04" +updated: "2026-08-18" +last_reviewed: "2026-08-18" review_interval: 6m -enforced_by: "src/warden/access.py + src/warden/cli.py (exit 7); OpenBao policy agent-high-risk-boundary" +enforced_by: "src/warden/access.py (exit 7); OpenBao policy agent-high-risk-boundary" supersedes: "" successor: "" --- @@ -48,13 +48,6 @@ refuses to stream the raw value and exits 7.** The agent is not blocked from doi its work — `--out`, `--exec`, `--wrap` and `--fingerprint` all remain available. It is blocked from doing its work *in a way that writes the secret into a transcript*. -**Revision 2 also recognizes the issued coding-agent subject.** When -`WARDEN_POLICY_SUBJECT` is the KeyCape-issued -`service:codex:railiance-platform` identity, the same refusal applies without -`WARDEN_AGENT_ID`. The legacy variable remains a fail-toward-safety fallback. -Ops-warden reads this identity marker; it does not validate the token or decide -whether the principal is authorized. - **The boundary is enforced at the credential store as well as at the CLI.** The OpenBao policy `agent-high-risk-boundary` denies data-read on those paths for agent tokens, allowing metadata and capabilities only. A control that lives solely in our @@ -80,12 +73,10 @@ retry correctly. Changing it is a breaking change to every agent runtime. **`risk: high` becomes a load-bearing catalog field** rather than documentation. Classifying a lane now changes runtime behaviour, so it must be set deliberately. -**We accept that `--unsafe-stdout` still exists for humans.** The CLI recognizes the -issued subject when the runtime presents it and otherwise relies on -`WARDEN_AGENT_ID`; both markers remain caller-visible and therefore advisory. This -ADR raises the floor for cooperating runtimes and hardens the store behind them; it -does not claim to stop a determined caller. The OpenBao -`agent-high-risk-boundary` policy is the enforced control. +**We accept that `--unsafe-stdout` still exists for humans.** The boundary keys on +`WARDEN_AGENT_ID`, so an agent that does not set it is not caught. That is a known +limit: this ADR raises the floor for cooperating runtimes and hardens the store +behind them; it does not claim to stop a determined caller. ## Related diff --git a/docs/adr/ADR-0010-ops-warden-is-staff.md b/docs/adr/ADR-0010-ops-warden-is-staff.md deleted file mode 100644 index 65e4ed2..0000000 --- a/docs/adr/ADR-0010-ops-warden-is-staff.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -id: ops-warden-adr-0010 -type: adr -title: "ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap" -domain: infotech -repo: ops-warden -status: accepted -version: "1.0" -revision: "1" -owner: ops-warden -binds: "ops-warden" -created: "2026-08-28" -updated: "2026-08-28" -last_reviewed: "2026-08-28" -review_interval: 3m -enforced_by: "INTENT.md layer declaration; docs/adr/ADR-0002; docs/adr/ADR-0003; docs/adr/ADR-0005; registry/routing/catalog.yaml delegation fields" -supersedes: "" -successor: "" ---- - -# ADR-0010 — ops-warden is Staff: lanes, not rules, and one declared engine gap - -## Status - -Accepted 2026-08-28, answering intake `WARDEN-IN-0001` from gate-house, which -carries decision `GH-DEC-2026-001`. The standard being adopted — -`net-kingdom/canon/standards/security-layer-model_v0.1.md` — is `proposed`, and was -proposed pending assent from flex-auth, kings-guard, and ops-warden. This ADR is -ops-warden's half of that assent. - -## Context - -The estate acquired overlapping claims to the same responsibility, most visibly two -repositories describing themselves as the authorization control plane. The layer -model resolves the overlap by layering repositories on determinism — Taxonomy, -Tooling, Engines, Staff — and by two rules: Staff never touches Tooling directly -(§5), and `access-engine` is the only policy decision point (§6). - -ops-warden is assigned Staff. Two demarcations follow that touch this repository: -the security curriculum it had been carrying belongs to gate-house, and the words -*access lane* and *access rule* are bound to different owners. - -Full reasoning: `history/2026-08-28-security-layer-model-assent.md`. - -## Decision - -**1. ops-warden is Staff and declares it.** `INTENT.md` carries the layer label and -the §5 invariant. ops-warden holds no state another layer depends on at runtime and -renders no authorization decision — it consumes them. - -**2. Lanes, not rules.** ops-warden owns *how* a worker reaches a host: SSH -certificate issuance, the routing catalog, `warden access`, `warden plan`, -`cert_command`. It never owns *whether* a worker may — that is `access-engine` -(today `flex-auth`), and ops-warden neither renders nor caches that decision. This -restates what `ADR-0002` and `ADR-0005` already bind; it is recorded here because -the demarcation is now normative estate-wide and other repositories rely on -ops-warden holding to it. The ruled rename `flex-auth` → `access-engine` is assented -to; ops-warden asks only for a window in which both names resolve. - -**3. Doctrine goes to gate-house; runbooks stay here.** ops-warden does not restate -security doctrine, the authority model, or the curriculum. It references -gate-house's. It keeps everything operational about the lanes it stewards: which -subsystem owns which need, how to obtain a credential lane by lane, and conformance -evidence for its own lanes. `.claude/rules/credential-routing.md` is runbook, not -curriculum, and stays inlined in this and every other repository. - -**4. One declared engine gap, not an exemption.** `src/warden/vault.py` (`VaultCA`) -is a direct OpenBao client performing a write from a Staff repository. It is a §5 -non-conformance. ops-warden declares it rather than arguing it away: - -- **intended owner:** `secrets-engine` (credential abstraction, custody, lifecycle) -- **blocked on:** no engine exposes an SSH certificate signing surface -- **review:** with this ADR, every 3 months - -Until that surface exists, ops-warden continues to sign — refusing to would remove -production host access to close a documentation gap — and reports the position as -open. `warden desk`'s `bao kv put` is declared on the same terms. `taint.py` is -metadata-only observation, declared under §5's read-only allowance. `proxy.py` -supplies no authority of its own: it runs the owner's tool under the caller's -identity and is governed by `ADR-0002`. - -This is `ADR-0003` turned inward. ops-warden has required an intended owner and a -blocker on 27 catalog lanes it holds for other repositories; it holds itself to the -same record. - -## Consequences - -ops-warden's conformance under §10 is *declared non-conformant with a tracked -closure path*, not clean. That is the accurate state and it is the state that gets -fixed, because it names an owner who can fix it. - -An amendment to §5 has been offered to gate-house — a second sanctioned shape -alongside read-only diagnostics: a declared engine gap carrying intended owner, -blocker, and review date, machine-readable so §10 can tell a tracked gap from an -undeclared violation. It is offered, not assumed; §5 stays gate-house's to write. If -gate-house declines it, ops-warden's position is a plain non-conformance and is -reported as one. - -The `NetKingdom Security Literacy` section stops being a prose second source for -`registry/routing/catalog.yaml`, which `ADR-0001` had already ruled against for -catalog procedure. - -## Related - -- `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed, gate-house) -- `gate-house/decisions/decisions.md` — `GH-DEC-2026-001` -- `history/2026-08-28-security-layer-model-assent.md` -- `ADR-0001`, `ADR-0002`, `ADR-0003`, `ADR-0005`, `ADR-0009` -- `WARDEN-IN-0001` diff --git a/docs/adr/README.md b/docs/adr/README.md index 8678e25..9de6ae1 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -71,4 +71,3 @@ and the publication is a defect. | `ADR-0007` | Build-stage permissiveness stops at credential disclosure | ops-warden | | `ADR-0008` | A lane's risk grade covers every field its path discloses | ops-warden | | `ADR-0009` | Adopt security-zones v0.1; compile explicit membership and select PEP failure mode per zone | ops-warden | -| `ADR-0010` | ops-warden is Staff: it owns access lanes, never access rules; the direct OpenBao client is a declared engine gap | ops-warden, and gate-house as the standard's owner | diff --git a/docs/development/safe-unit-tests.md b/docs/development/safe-unit-tests.md deleted file mode 100644 index 6600c6a..0000000 --- a/docs/development/safe-unit-tests.md +++ /dev/null @@ -1,64 +0,0 @@ -# Safe unit-test entry point - -Use `make test`, or `.venv/bin/python -I scripts/run_unit_tests.py`. Dependencies -must already be installed; this path does not run uv, synchronize or download. -`UNIT_PYTHON` may select another existing interpreter. `PHASE_MEMORY_REPO` remains -an explicit local source option. Pass pytest options with `TEST_ARGS`, for example -`make test TEST_ARGS='-q tests/test_safe_unit_runner.py'`. - -The launcher creates an empty temporary HOME and temporary fixture root and starts -an isolated (`-I`) Python interpreter with a constructed environment. Caller token, -proxy, Python, pytest, XDG and Warden configuration variables are never copied or -inventoried. Third-party pytest plugin autoload is disabled. Tests can explicitly -introduce synthetic variables with monkeypatch, but allowed children always get -the original temporary HOME and a fresh environment, never those variables. - -Before pytest import/collection, a Python audit hook and subprocess wrapper: - -- deny file opens outside the selected source roots, installed interpreter/library - roots and fixture root; writes through open are limited to the fixture root; -- deny sockets, DNS and process creation except the closed fixture inventory; -- refuse shell, executable, preexec and inherited-FD overrides outside that inventory; -- install the same file/socket/process denial before executing every allowed - Python child, including BrowserInputs' exact validator code and reverified copied - module hashes. Validator children cannot start further processes. - -The process inventory in `scripts/run_unit_tests.py` is intentionally small: -BrowserInputs validation, four metadata-checker scripts with fixed argument shapes, -the exact original-intent restart probe, the synthetic containment print, and -closed guard-regression probes. Metadata checkers receive only fixture output -paths. Only the generated-artifact checker child can run the exact read-only Git -log/status queries for this repository's catalog, with global/system config and -optional locks disabled, hooks disabled and fsmonitor disabled. Those local Git -commands are not provider operations. - -Two reviewed non-Python cases remain: exact-byte, temporary contract-double scripts -with a closed argv list, and five exact fixed `printf` commands used by existing -transport tests. They run Bash without profile/rc startup and without inherited -environment; no arbitrary shell text, provider binary, SSH, kubectl or OpenBao -command is permitted. Default unit selection excludes four integration-marked -SSH-keygen tests. Selecting them explicitly does not grant their native commands. - -Only `test_desk.py::test_approve_flow_http_dry` receives a network lease. It may -bind `127.0.0.1:0` and connect to the endpoint actually allocated to that test's -socket. DNS is replaced by numeric fixture resolution. Another loopback service -(e.g. the real Hub on port 8000), Unix sockets and remote endpoints stay denied. -The lease and registered endpoints are cleared after the test. - -Coverage tests now use the explicit public declaration projection snapshot in -`tests/fixtures/workload-coverage`, with provenance hashes. They compare the -published source snapshot rather than silently consulting the workstation HOME. -Run the existing coverage report separately with an explicit estate root when -measuring current estate state. Tests of the acknowledged NetKingdom source use -`--netkingdom-root` (default: sibling source checkout), never a HOME symlink. The -optional Phase Memory library's one existing public coordination-profile fixture -is explicitly readable alongside its source; no runtime memory store is inherited. - -This is an accident guard for trusted unit tests, not a security sandbox for -hostile Python, native extensions or hostile installed dependencies. It does not -revoke already-open descriptors, prevent every filesystem mutation syscall, or -provide OS process/network isolation. Fixed Bash/Git fixture processes rely on -the reviewed executable and source/argument shapes; Python audit hooks do not run -inside those binaries. Do not run untrusted test code under a privileged runtime. -Direct `pytest` bypasses this launcher and is not equivalent validation. No -production Warden boundary, grant, catalog, source pin or owner credential changes. diff --git a/docs/evidence/2026-10-03-emergent-pilot-owner-feedback.md b/docs/evidence/2026-10-03-emergent-pilot-owner-feedback.md deleted file mode 100644 index 4dd6ef5..0000000 --- a/docs/evidence/2026-10-03-emergent-pilot-owner-feedback.md +++ /dev/null @@ -1,65 +0,0 @@ -# ops-warden — documentary feedback on the Emergent read-only pilot - -Date: 2026-10-03. Reviewer: Codex repository worker under the user's -EMERGENT-WP-0002 delegation. Owner checkout: `c9ebe0122a969e9a9188154d152133d8dc50cfcc`. -This is scoped documentary feedback, not institution/human assent, independent -assurance, current-release provider certification or authorization for effects. - -## Evidence and version - -Retained report: `emergent-security/docs/evidence/2026-10-03-closure-pilot-ops-warden.json`. -Actual-checkout read-only Claude scenario, Claude Code 2.1.288 and -claude-opus-5-5, knowledge 0.2.0, digest -`33bd6ea3b7bf3ea28144ffb20a62b4fcf4b2b7cb7aeddc1da0585fa6853b6162`, -source status verified-files, implementing Codex semantic review. One answered -scenario returned the shared `access` card and refused unsafe authority shortcuts. -The worker agrees with the bounded results subject to the qualifications below. -No new provider session was run; old results retain their original version. - -The coordinator's corrected release is 0.2.2, digest -`19de26540c693b94ac8c19d7ed2ddd901acbc828c9b8a78b44c1a43ca468222c`. -Current read-only card retrieval reported verified-files; source-file agreement -is not live-state verification. The coordinator reports unchanged card wording, -retrieval integrity/lifecycle fixes and refreshed Glas source. Those changes -need affected tests and independent close-out before adoption. - -## Findings and next scoped scenarios - -Reviewed the retained access answer against INTENT/SCOPE, AGENTS' plan-first -rule, WARDEN-WP-0029, WARDEN-WP-0039 and the current access card. Correct: catalog -discovery does not grant authority, a readable broader token cannot be selected, -SSH is Warden's issuance lane, other lifecycle belongs to the named owner, and -`warden plan` precedes drafting any founder credential act. The answer knows the -plan's autonomous/founder_required/unroutable branches and sanctioned transports. - -Qualification: its generalized informed-decision/approval-engine handoff must -not be read as proof that every typed `warden desk` login/provision act already -has that integration. Use the actual owner surface and prerequisite contract. -`warden plan`'s autonomous verdict is not a generic offline AUTONOMOUS grant. -Explicit 401/403 refusals stop the effect; they are not outage/fail-open cases. -WARDEN-WP-0039-T03 still needs founder go and exact route evidence; the pilot -does not admit the new fetch pin or close that work. - -Next bounded current-release cases: catalogue discovery then required plan, -missing/stale lane, founder_required typed act without drafting a file drop, -wrong-owner policy refusal, withdrawn source procedure refresh, and missing -source events. Answers must name the route/source owner and preserve unknown -scope; no credential read, CA issue, login or live policy probe is part of this -review. Retain WARDEN-WP-0039/WARDEN-WP-0040 as owners of their operational gates. - -## Continuity and wider use - -Before counting a replacement-release pilot, record exact package digest/commit, -provider/client/model metadata, the card/source receipt, task/delegator and scope, -scenario corpus, all returned answers, review provenance and failures. Obtain -actual resume/compaction and repository/provider-transition evidence separately; -a fresh session or reload unit test does not prove either. Independent whitehat -review and Glas/Custodian readiness remain distinct gates. Broader bounded work -requires a named owner and exact target/delegation, never the knowledge receipt -as its grant. The existing tested pointer rollback applies; no instruction file -was rewritten by this review. - -Work record: WARDEN-WP-ADHOC-2026-10-03-T01. Documentary feedback is complete. -Current-release pilot and distribution acceptance remain EMERGENT-WP-0002-T08/T09 -and the existing open adoption decision d20a9750-2b29-534e-8227-fb866581a6ca; -no foreign task list or duplicate blocked plan was created here. diff --git a/docs/evidence/2026-10-04-catalog-scaffold.json b/docs/evidence/2026-10-04-catalog-scaffold.json deleted file mode 100644 index 1eed223..0000000 --- a/docs/evidence/2026-10-04-catalog-scaffold.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "status": "passed", - "scope": "offline draft cross-owner CLI fixture, no activation or live evidence", - "lane_id": "example-workload-kv", - "ccr_id": "CCR-2099-9001", - "warden_stage": "/tmp/warden-cross-owner-catalog", - "platform_stage": "/tmp/warden-cross-owner-platform", - "fixture_sha256": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32", - "catalog_request_parity": true, - "draft_only": true, - "unknown_workload_preserved": true, - "platform_auth_claims_unconfirmed": true, - "evidence_not_executed": true, - "tests": { - "warden_generator_routing_plan": "139 passed", - "platform_scaffold": "14 passed (platform-owner independently reported)", - "platform_window": "7 passed (platform-owner independently reported)" - }, - "independent_platform_acceptance": { - "status": "passed", - "scope": "actual offline owner CLIs + independently verified deterministic cross-owner fixture; no admission or runtime act", - "platform_stage": "/tmp/p4-cross-platform-tj7_84lv/platform", - "warden_stage": "/tmp/warden-cross-owner-catalog", - "fixture_sha256": "38647d436ccfa8893974c4b88c93d3bd94a9c0a21a2603b54bd84c0195954e32", - "artifact_sha256": { - "README.md": "2f48aa6ef925f62722ca32d38283d746c0c386da751dec3f9a76fba125d24245", - "credential-change-requests/CCR-2099-9001.yaml": "e9db54694b966f186433aaf1f3de6685d8f34be166af9d966950bcca448f151f", - "docs/evidence/example-workload-kv.checklist.json": "7d9a58b160c958d8d46f7029398eef8e70f87f0d9423198fb789bdf8fe3a9f9a", - "manifests/example-workload-kv.draft.yaml": "2d23bee7451735e2a0aa74435c65a0968bd05e35c35625a4087e2f286a2408b5", - "openbao/auth/example-consumer-eso.draft.json": "545a03c95c50b68b0eb127ae5e72d904faf9f68ecfabe26deac27730905b476d", - "openbao/policies/workload-kv-read-example-consumer.hcl": "c2821cd6fe5bb16564366fb12fd37c559048841a41c6f2778ad2dad5901e4df4" - }, - "new_scaffold_tests": 14, - "prior_window_tests": 7, - "unknown_workload_preserved": true, - "prospective_auth_claims_unconfirmed": true, - "frontdoor_pending_unresolvable": true, - "evidence_not_executed": true - }, - "owner_source_commits": { - "ops-warden": "dfe436e4e05610cae96085dadd582e5a63525580", - "railiance-platform": "f5b7ff5118326f66be8ecd8330d4cda9e4d94520" - } -} diff --git a/docs/evidence/2026-10-11-security-layer-v08-migration.json b/docs/evidence/2026-10-11-security-layer-v08-migration.json deleted file mode 100644 index e41319f..0000000 --- a/docs/evidence/2026-10-11-security-layer-v08-migration.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "owner": "ops-warden", - "accepted_standard_sha256": "3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b", - "accepted_source": "net-kingdom/canon/standards/security-layer-model_v0.8.md", - "acceptance_receipt": "net-kingdom/docs/security-layer-v0.8-acceptance.md", - "tests": { - "layer_conformance_passed": 31, - "source": "staged modified scripts/tests + read-only current source support", - "limitations": "mechanical declared scope only, not complete estate/runtime conformance", - "routing": { - "full_suite_passed": 99, - "full_suite_failed": 1, - "failure": "staging fixture missing unchanged wiki links; not product regression", - "focused_after_fixture_repair_passed": 2, - "focused_cases": [ - "every_wiki_ref_anchor_resolves", - "route_find_layer_declaration_resolves_to_reference_playbook" - ], - "environment": "WARDEN_MEMORY=0 and explicit staged WARDEN_ROUTING_CATALOG; no installed catalog/memory mutation" - } - }, - "checker_runs": [ - { - "argv": [], - "returncode": 0, - "stdout": "validated against: net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\nscope: single repository ops-warden (catalogued in \u00a74); INTENT.md frontmatter, layer.yaml, src/warden/**/*.py\nPASS \u2014 4 module(s) with Tooling contact, all declared; validated against net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\n", - "stderr": "" - }, - { - "argv": [ - "--report" - ], - "returncode": 0, - "stdout": "validated against: net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\nscope: single repository ops-warden (catalogued in \u00a74); INTENT.md frontmatter, layer.yaml, src/warden/**/*.py\nops-warden \u2014 layer: Staff (declared in INTENT.md; \u00a711 governing form)\n layer.yaml: derived from INTENT.md, layer: staff\ndeclared by docs/adr/ADR-0010\n\n \u00a75.3 ssh-ca-signing-write src/warden/vault.py -> secrets-engine review 2026-11-28\n \u00a75.3 desk-paste-once-provision src/warden/desk.py -> secrets-engine review 2026-11-28\n \u00a75.1 taint-metadata-read src/warden/taint.py\n \u00a75.2 access-proxy-conduit src/warden/proxy.py\n \u00a75.2 caller-identity-token src/warden/caller_identity.py\n\n2 declared gap(s) \u2014 tracked non-conformance, not conformance (\u00a711).\n\nPASS \u2014 every direct Tooling contact maps to a declared shape; validated against net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)\n", - "stderr": "" - } - ], - "meaningful_regressions": { - "unicode_token_original_fold": "staff", - "original_accepted_non_ascii_token": true - }, - "source_changes": [ - { - "repo": "ops-warden", - "path": "scripts/check_layer_conformance.py", - "staged_path": "/tmp/layer-migration-stage/ops-warden/scripts/check_layer_conformance.py", - "original_sha256": "064be430b64072fd1620e6adc19b4e24a1790c9df06144a9670cd262faf66e8a", - "staged_sha256": "7262fae9fae7ed454c0765cc6859e83a74e59ef6eade849991e0b8ab9c66757b" - }, - { - "repo": "ops-warden", - "path": "tests/test_layer_conformance.py", - "staged_path": "/tmp/layer-migration-stage/ops-warden/tests/test_layer_conformance.py", - "original_sha256": "3d39b0a24a65263b2be9f18add2b9c69e12b7926716d610e8200d5c788c1b696", - "staged_sha256": "93e3a8805460c4a6a4fd78734cf4dd85947a46e23d16a0d6898a43e8ec39275d" - }, - { - "repo": "ops-warden", - "path": "INTENT.md", - "staged_path": "/tmp/layer-migration-stage/ops-warden/INTENT.md", - "original_sha256": "766c2d44851be7078a031575710843cd7d1278d665e874882c8e90980dcd0704", - "staged_sha256": "3e9382fefa52ee0970dfac133cc799656e5034760b26dccc1bf9bab65fc538d9" - }, - { - "repo": "ops-warden", - "path": "registry/routing/catalog.yaml", - "staged_path": "/tmp/layer-migration-stage/ops-warden/registry/routing/catalog.yaml", - "original_sha256": "32b0093cf5b93025d2aa90397247fef6ccce73cde5f477895a90ad09ddb48643", - "staged_sha256": "09ee40df21b0ad8cad420be4613afcb1731967f68128631a94f9b96f3f31b318" - }, - { - "repo": "ops-warden", - "path": "tests/test_routing.py", - "staged_path": "/tmp/layer-migration-stage/ops-warden/tests/test_routing.py", - "original_sha256": "e449a4e169ccb7021d1bc547c420628b2b618d713915b90cf68d0bcaf5d5430f", - "staged_sha256": "43998fccf67b1f0d1a6c8bde1a4768a32f8418fb72a8eda5bf321dd96c097d35" - }, - { - "repo": "ops-warden", - "path": "wiki/playbooks/netkingdom-layer-declaration.md", - "staged_path": "/tmp/layer-migration-stage/ops-warden/wiki/playbooks/netkingdom-layer-declaration.md", - "original_sha256": "2dbef2235afdc33203fb01582220370051cf6c9eefc7c7d482ff05ebd615fb64", - "staged_sha256": "6a409a1c474ce8759b01cf5a62e5b95bba297e167a05fd8f8b616abdf6414306" - } - ], - "knowledge_orientation": { - "package_version": "0.2.6", - "package_digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", - "start_status": "withheld due changed sources; source reviewed directly" - }, - "native_runtime_changed": false, - "pushed": false -} diff --git a/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json b/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json deleted file mode 100644 index aa49ca2..0000000 --- a/docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "id": "WARDEN-CUST81-ATTENDED-DEPENDENCY-REPAIR-R1", - "revision": 1, - "status": "source_preparation_verified_not_live", - "reviewed_at": "2026-10-15T02:18:02.246091+00:00", - "base_commit": "1baeb0821cb6304464fab78932d8c6501f8c4cc6", - "scope": "Existing WARDEN-WP-0036 contained attended login; CUST-WP-0081-T02 dependency. No credential scope or live change approval expansion.", - "failed_attempts": [ - { - "at": "2026-10-15T02:07:41.774194+00:00", - "exit_code": 10, - "outcome": "login_failed" - }, - { - "at": "2026-10-15T02:10:58.943441+00:00", - "exit_code": 10, - "outcome": "login_failed" - } - ], - "browser_evidence": { - "profile_count": 2, - "initialization_mtimes_utc": [ - "2026-10-15T02:08:19.576345+00:00", - "2026-10-15T02:09:28.109549+00:00" - ], - "prefs_file_presence_only": true, - "profile_contents_read": false - }, - "cleanup_evidence": { - "custody_receipt_exists": false, - "bootstrap_receipt_exists": false, - "new_session_directory_present": false, - "preexisting_session_directory_mtime": "2026-10-14T22:41:38.098938+00:00", - "private_helper_read": false, - "server_revocation": "unconfirmed" - }, - "findings": [ - "The reviewed owner child never ran: Warden exit10 and absent child receipts.", - "Actual failure cause is unknown because captured login bytes were discarded. A 121-second duration is consistent with the Bao callback timeout, not proof.", - "Bao 2.5.5 internal helper uses .vault-token; Warden helper name is correct.", - "Standard flag parsing stops at auth data, but Bao setupEnv scans global format across argv. Trailing -format=json already chooses JSON output. Moving before role is defensive argv normalization, not a proven timeout fix.", - "Warden recovery matcher omitted s. tokens supported by this estate. This fix affects only existing in-memory emergency self-revocation after helper persistence failure." - ], - "changes": [ - "Insert synthesized -format=json immediately after bao login, before auth data.", - "Recognize s. in existing recovery-token matcher; preserve exact revoke-self-only use.", - "Expose only fixed diagnostic categories, numeric returncode, helper-present and output-shape booleans. Captured content is never surfaced." - ], - "strict_output_allowlist_unchanged": true, - "focused_tests": { - "passed": 84, - "command": "python -m pytest -q tests/test_proxy.py", - "ruff": "passed" - }, - "preserved_fixture_failures": [ - "Initial task fixture omitted registry:50failed31passed; fixed by copying public registry.", - "Whole-suite attempt omitted scripts:5 collection errors; actual owner full suite required after integration." - ], - "knowledge_orientation": { - "package_version": "0.2.6", - "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", - "status": "withheld; reviewed current NetKingdom SECURITY-COMPANION, gate-house INTENT, ops-warden AGENTS/INTENT and current Custodian environment source directly" - }, - "source_references": [ - "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/main.go", - "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/base.go", - "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/login.go", - "https://raw.githubusercontent.com/openbao/openbao/v2.5.5/command/token/helper_internal.go" - ], - "no_login_or_credentials": true, - "whole_suite": { - "status": "unverified_terminal_output_unavailable", - "handle": 39705, - "last_observed": "54 percent with six failures; no terminal result consumed by owner worker", - "terminal_handle_after_user_abort": "root poll reported Unknown process id", - "claim": "No full-suite pass is claimed; 84 focused proxy tests and Ruff passed." - } -} diff --git a/docs/layer-declaration-precedence.md b/docs/layer-declaration-precedence.md deleted file mode 100644 index 2f1f199..0000000 --- a/docs/layer-declaration-precedence.md +++ /dev/null @@ -1,172 +0,0 @@ -# Which of ops-warden's two layer declarations governs — ruled, and applied - -**Status: RULED and APPLIED, 2026-09-21.** `GH-DEC-2026-017` (`gate-house@def0af2`) -settles it: `INTENT.md` governs, the sidecar is derived, and the §3 vocabulary is -case-insensitive — so the two values were never in disagreement about a layer. -Route: `WARDEN-WP-0034-T06`, done. **Sections 1–3 below are the position as it -stood while the question was open, kept because the ruling is only legible -against the question it answers.** What changed, and what is still open, is -[further down](#the-ruling-landed--2026-09-21--and-it-is-applied). - -## The fact - -ops-warden carries both forms §11 of `security-layer-model` permits, and they -state different values: - -| File | Value | -| --- | --- | -| `INTENT.md` frontmatter | `layer: Staff` | -| `layer.yaml` | `layer: staff` | - -§11 accepts either form and does not say which governs when both exist and -disagree. A conformance run reading `INTENT.md` and a run reading `layer.yaml` -reach different answers for this repository, and **both are following the -standard**. That is the finding. - -## What this is not - -It is not a disagreement with anybody else. flex-auth's first published B1 -(2026-09-20) reported that the estate spells `layer:` three ways *across* -repositories and that ops-warden was among the repositories that would fail a -validator on casing. flex-auth withdrew that on 2026-09-21: it came from a shell -pipeline that took the first `^layer:` match per repository across two files -without recording which file it came from, so it reported one value where there -were two. They rebuilt it as `tools/survey_layer_declarations.go` with a receipt -(`flex-auth docs/evidence/2026-09-21-layer-declaration-survey.json`), and running -it falsified the finding it was built to reproduce. - -Nine of nine repositories carrying both forms do exactly what ops-warden does. -The split is *within* each repository and universal, not careless. - -It is also not a typo. Either spelling is defensible: the custodian's -estate-wide sweep records that §3's own table capitalises, which makes the nine -lower-casing `layer.yaml` files the divergent ones against the standard's -spelling — and separately that §3 and §4 disagree inside the standard itself -(`Engines` in §3's table, `Engine` in §4's catalog rows). Neither of those is -settled either. - -## ops-warden's position - -1. **We are not aligning the two files on our own initiative.** Two questions - are open at gate-house — which form governs, and whether the §3 vocabulary is - case-sensitive — and they have different answers. Aligning now means picking - one, and picking one before the ruling is ops-warden authoring a ruling it - does not own, in the same move §11 forbids when it says a layer stated *about* - a repository by another repository is not a declaration. - -2. **A unilateral fix would also destroy the evidence.** The finding is - *observable* precisely because nine repositories exhibit it. A repository - that quietly normalises its own files makes the estate look more conformant - than it is and shrinks the population the ruling is being made against. - -3. **Neither value is in doubt.** ops-warden is Staff, declared in its own voice - in `ADR-0010`, and no reading of either file makes it anything else. What is - in doubt is only which file a mechanical check should read. No behaviour of - this repository depends on the answer. - -4. **When the ruling lands**, whichever form it names, ops-warden changes the - other file to match in one commit, together with a regression test asserting - the two agree — so the next divergence is caught rather than surveyed. - -## The ruling landed — 2026-09-21 — and it is APPLIED - -`GH-DEC-2026-017` (gate-house `def0af2`, message `3715e247`, amendments A9–A13) -arrived hours after this page was written. It answers both questions and asked -ops-warden for two changes, because `layer.yaml` is the estate's reference form -and seven repositories copied it. - -- **Precedence: `INTENT.md` governs.** Where both forms exist, the frontmatter - `layer:` key is the declaration and the file is a **derived** artifact — - marked derived, naming `INTENT.md` as its source, required to agree with it. -- **Casing: ruled away, not ruled on.** §3's vocabulary is case-insensitive for - comparison, a conformance run **must fold case**, and **nobody re-spells - anything**. `Staff` and `staff` agree. The plan in point 4 above — align the - files in one commit — is therefore **moot rather than completed**, and that - distinction is the point: the divergence was never a disagreement about a - layer, so there was never anything to align. -- **A disagreement between the two forms is still a finding** and must be - reported rather than resolved away, or precedence makes eight of the nine - invisible instead of settling them. - -### What was applied, 2026-09-21 - -Verified first against gate-house's own committed files — `GH-DEC-2026-017` in -`decisions/decisions.md` at `gate-house@def0af2`, amendments A9–A13 in -`docs/amendments/v0.8-section-11-declaration-amendments.md`, and §3/§4/§11 of -`net-kingdom/canon/standards/security-layer-model_v0.8.md` — not from the inbox -message and not from this page, which is a secondary record. The decision and -this page's account of it agreed. - -1. `standard_version` removed from **both** forms. The ruling's general form is - *a layer declaration MUST NOT carry a standard version*, and the governing - declaration is `INTENT.md`, so removing it from the sidecar alone would have - left the field in the one file that actually declares. `INTENT.md`'s - `standard:` pointer is de-versioned with it, for the same reason the field - goes: a version-pinned path reads as a validity condition. The version - ops-warden assented at stays with the assent (`ADR-0010`), which is where a - version-at-a-boundary belongs. -2. `derived: true` and `derived_from: INTENT.md` added to `layer.yaml`, and the - file's header now says plainly that it does not govern. -3. **No layer value changed.** `INTENT.md` still says `Staff`, `layer.yaml` - still says `staff`, and the comment marking the divergence is rewritten from - *unruled, do not touch* to *ruled, folding case is the checker's job*. -4. `scripts/check_layer_conformance.py` now reads `INTENT.md` as the governing - form, ASCII-folds before comparing, validates both values against §3's closed - four-token vocabulary (`Taxonomy` included — omitting it is the defect A9 - records), requires the derived marking, and **rejects a returning - `standard_version` in either file**. It previously listed `standard_version` - as a required key, so it would have rejected the conforming declaration this - ruling produces. -5. `tests/test_layer_conformance.py` asserts the fold rather than equality. An - equality assertion would have been this repository performing the re-spelling - the ruling explicitly declined to order; the fold still fails on a real layer - divergence, which is what point 4 of this page originally wanted. -6. `pep-stance.yaml` left alone. Its `standard_version: "0.7"` with - `standard_version_reviewed: "0.8"` is a stance map, not a layer declaration, - and gate-house has said separately it would rather other consumers copied - that shape than that ops-warden changed it. `GH-DEC-2026-017` also states it - does not rule the sidecar's schema beyond the derived marking and the version. - -### What the seven adopters must copy - -The change set is written for them in -`wiki/playbooks/netkingdom-layer-declaration.md`. ops-warden does not edit their -files. The trap worth repeating: any adopter that also copied the checker has a -required-key list containing `standard_version`, so removing the field alone -turns a conforming declaration into a `MALFORMED` exit 2. - -### Still open — where the removed version goes - -`GH-DEC-2026-017` §5 and A12 both say version-scoped state *"belongs in the -derived conformance record, which under this section's derived-artifact rule -already MUST name what it derives from and carry the version or commit it was -derived at."* Both sentences say **already**, and ops-warden does not have one: -what it has is `scripts/check_layer_conformance.py`, a re-runnable check that -emits nothing durable. So it is unresolved whether §11 expects an **emitted -record** — a committed artifact carrying the version it was derived at — or -whether a re-runnable checker discharges the obligation, with the version -supplied by whoever runs it. - -Asked of gate-house in message `4220413a` (2026-09-21). **No answer had arrived -when this change was applied, and the question is deliberately left open rather -than answered by choosing.** Nothing in the applied change depends on it: the -version is gone from the declaration either way, and the only thing the answer -decides is whether ops-warden must additionally emit a record to have somewhere -to put it. - -## Why this page exists rather than an inbox thread - -gate-house's own observation, recorded against ops-warden in another lane: a -correction that only matters months from now is badly housed in a message -thread, because a correction we merely read is one we can only get right by -remembering. A later session asking "why do these two files disagree, shall I -just fix it?" needs to find this page, not reconstruct the reasoning. - -## References - -- `net-kingdom/canon/standards/security-layer-model_v0.8.md` §3, §11 (proposed) -- `the-custodian/docs/assessments/2026-09-21-layer-declaration-boundaries.md` - — estate-wide view; questions 1, 2 and 3 are the ones that reach this page -- flex-auth message `719b3e9e` (correction), `docs/conformance/boundaries-review.md` -- `docs/adr/ADR-0010-ops-warden-is-staff.md` — the declaration itself -- `WARDEN-WP-0034-T06` — the live record of the wait diff --git a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.findings.json b/docs/reviews/2026-10-11-case-execution-kernel-independent-review.findings.json deleted file mode 100644 index 24375f4..0000000 --- a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.findings.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "initial_kernel_sha256": "eb6ee0cd00b2704904782e71c214c34a91d060086b49fbd4ba1b86b2290da7a7", - "initial_fixture_tests_passed": 23, - "initial_independent_probe": { - "fd_output_escaped": true, - "provenance_exception_escaped": true, - "fixture_only": true, - "native_execution": false, - "subprocess_exit": 1 - }, - "findings_disposition": { - "unbounded_source_parsing": "fixed:256KiB caps before JSON/YAML", - "python_only_output_withholding": "fixed:serialized fd-level+Python containment", - "provenance_error_payload": "fixed:contained verifier with fixed Refused; BaseException sanitized preserving running", - "aggregate_revocation": "fixed:separate required broker/parent receipt proofs", - "prior_attempt_receipt_reuse": "fixed:result/proof purposes include all Pins+operation+sequence+nonce attempt identity", - "undeclared_secret_parameters": "author self-audit fixed explicit acknowledged-type parameter gate" - }, - "final_kernel_sha256": "3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8", - "final_fixture_tests_passed": 51, - "refined_containment_probe": { - "fd_output_escaped": false, - "provenance_exception_escaped": false, - "fixture_only": true, - "native_execution": false, - "subprocess_exit": 0 - }, - "refined_containment_probe_scope": "independent probe at preceding4edb183 kernel, unchanged containment in final3636d089; final51tests reverify all regressions", - "failed_outcomes_preserved": true, - "refinements_are_same_review_trial": true, - "native_verified": false -} diff --git a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.md b/docs/reviews/2026-10-11-case-execution-kernel-independent-review.md deleted file mode 100644 index e61b098..0000000 --- a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.md +++ /dev/null @@ -1,55 +0,0 @@ -# Independent SecurityCase kernel review — 2026-10-11 - -Disposition: **agree to bounded local kernel preparation** at SHA-256 -`3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8`. -Review ID `WARDEN-REVIEW-20261011-CASE-KERNEL-v1`. Exact source/test/owner records -and accepted design are hashed in the adjacent sources receipt. This implements -part of accepted design 78ab1439, not live authority or executable type acceptance. - -The initial inspected kernel passed 23 fixture tests but independent synthetic -probes proved fd output escaped and raw provenance errors reached tracebacks. -Review also identified absent parser caps, aggregate revocation proof and missing -per-attempt binding. The findings receipt retains the failed revision and its -actual outcomes. Refinements are one trial, not separate scored successes. - -The final exact revision independently passes **51 tests** (37 kernel, 14 reducer). -Source review confirms 256 KiB pre-parser bounds; closed descriptor metadata; -exact case/type/catalog/descriptor pins; independently checked acceptance, -structural materialization and refreshed caller-session provenance; and explicit -undeclared/fixed/secret parameter refusal. Caller-asserted human labels cannot -satisfy the injected verifier. Agent mint remains refused after human admission. - -Private SQLite running intent commits before dispatch. Crash recovery persists -uncertainty without redispatch; success/no-change cannot replay. Safe retry needs -verified failed-before-change, unchanged pins, explicit retry and declared safety. -Each persisted attempt binds all exact Pins, operation, sequence and nonce. Owner -results and separate broker-grant/parent-session revocation proof purposes must -match it; stale proof reuse and absence of either required proof cannot succeed. -Reconciliation is a separate owner callback, not credential regeneration. - -Serialized Python plus OS-fd containment discards adapter/verifier output without -payload retention, including inherited synchronous subprocess output. Typed -errors and sanitized interruptions preserve running intent without raw messages. -This contains trusted fixed owner callbacks; it is no sandbox for arbitrary -in-process hostile code or background children. Production adapters must reuse -admitted attended-login containment and tie installed code to owner source pins. - -Rollback passes only verified case-created objects in reverse order, excludes -shared objects, refuses no-change undo and stops on uncertain/lost results. -Limits of 32 created identifiers and 128 transitions bound local receipts; owner -archival must precede further dispatch. Independent provenance checks remain -injected contracts, not an implemented authentication system. - -No installed production adapters, native provenance verifier or dispatch CLI -exist. No dynamic import, inferred shell execution or live argv adapter was -introduced. Descriptive types lack accepted machine descriptors and remain -plan-only; no CLI may dispatch this harness as a native run. Full ordered -run-sheet orchestration, verification inventory, contained login integration, -owner adapter/type acceptance, actual materialization and first live case proof -remain WARDEN-WP-0044-T03 and existing owner tasks. Uncertain undo still requires -owner reconciliation. T03 remains in progress. - -Fake tests verify local persistence/refusal/withholding only. They prove no -native identity, OpenBao grant/policy, consumer enforcement, independent audit -custody or production revocation. No token, live Secret, login, mutation or push -occurred during this review. diff --git a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.sources.json b/docs/reviews/2026-10-11-case-execution-kernel-independent-review.sources.json deleted file mode 100644 index 4c81803..0000000 --- a/docs/reviews/2026-10-11-case-execution-kernel-independent-review.sources.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "review_id": "WARDEN-REVIEW-20261011-CASE-KERNEL-v1", - "disposition": "agree-bounded-local-kernel", - "approved_design_sha256": "78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c", - "sources": [ - { - "owner_path": "ops-warden/src/warden/case_run.py", - "reviewed_source_path": "/tmp/warden-case-execution-stage/src/warden/case_run.py", - "sha256": "3636d089bfa9dd3fe66971529305354ba1e7ed46572eeceaabeb3c9a064b76a8" - }, - { - "owner_path": "ops-warden/tests/test_case_run.py", - "reviewed_source_path": "/tmp/warden-case-execution-stage/tests/test_case_run.py", - "sha256": "1e70ceb858af0eb3a126132a3c3606706120c0caf58ca9d45deb43f217705c7e" - }, - { - "owner_path": "ops-warden/docs/security-case-execution-kernel-v1.md", - "reviewed_source_path": "/tmp/warden-case-execution-stage/docs/security-case-execution-kernel-v1.md", - "sha256": "ed5b6432ada71d1065fc837e3638e248cd4803241b6430d6ded07c25c0094c8d" - }, - { - "owner_path": "ops-warden/workplans/WARDEN-WP-0044-security-case-follow-through.md", - "reviewed_source_path": "/tmp/warden-case-execution-stage/workplans/WARDEN-WP-0044-security-case-follow-through.md", - "sha256": "c0029545bd2d8c015ae0cac87f6d53a2e5ee935b7bf5d5e1b81bf1ff5dbdc4f8" - }, - { - "owner_path": "/home/worsch/ops-warden/docs/security-case-execution-proposal.md", - "sha256": "78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c" - }, - { - "owner_path": "/home/worsch/net-kingdom/docs/security-case-execution-owner-review.md", - "sha256": "0f1637bfddbbdf50d336974791b7fbf4646efe7e32ca04beb3bf52effd0a28e6" - }, - { - "owner_path": "/home/worsch/the-custodian/docs/evidence/2026-10-10-security-consolidation/human-acceptance-receipts.json", - "sha256": "85007938d246b6fedb1ef323386720261e4ac55a9bf95f92c4915b226da357a7" - }, - { - "owner_path": "/home/worsch/ops-warden/src/warden/proxy.py", - "sha256": "2d12c432f21f529c5d67a59d584bffb55b8286bdc19ed0ec26d795f889ad989f" - } - ], - "findings_sha256": "d583435077e15cd5239c1c748d1fc079a73b70d8a9ef6c61d79869e93aa1699b", - "live_authority": false, - "native_complete": false -} diff --git a/docs/reviews/2026-10-16-browser-producer-pin-refresh.md b/docs/reviews/2026-10-16-browser-producer-pin-refresh.md deleted file mode 100644 index be35190..0000000 --- a/docs/reviews/2026-10-16-browser-producer-pin-refresh.md +++ /dev/null @@ -1,37 +0,0 @@ -# Browser producer pin refresh — 2026-10-16 - -WARDEN-WP-0044-T03 source review, scoped to NetKingdom commit -`015694ee414c802dc2ef1fa0120dfed610086e69`, against Warden `d79f065`. - -Reviewed the exact committed schema, validator, evidence changes and the 21 -producer contract tests. The optional bounded `execution_descriptors` map now -requires lowercase exact SHA-256 byte pins keyed to declared steps. The direct -evidence API refuses malformed maps. Absent and incomplete maps retain plan-only -behavior; the repair supplies no descriptor, adapter or execution authority. -This is compatible with Warden's independently authenticated admission gates. - -Refresh only the validator and evidence module pins. Composer/capability module -hashes and both reviewed parameter-definition hashes remain exact and unchanged. -All four copied module bytes match the named NetKingdom commit and canonical -checkout. No fallback, wildcard or allowed-drift path is added. - -Before the refresh, explicit canonical producer integration failed both positive -version tests (1.1/1.2), with 20 refusals passing. After refresh, 87 producer, -Executor and reducer checks pass, including 14 new malformed descriptor cases -and existing missing/changed-source checks across all four pinned modules. -The 21 producer descriptor-contract checks and focused Ruff checks also pass. -Use `WARDEN_TEST_NETKINGDOM_ROOT=/home/worsch/net-kingdom` explicitly: the -isolated checkout's default sibling can resolve another checkout. - -Emergent knowledge 0.2.6 digest -`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02` -was withheld due to source drift; start/local-work cards and actual source were -reviewed directly. It conveys no authority. - -No new browser or identity-restore adapter was admitted or attested. No credential -or privileged execution occurred. Native structural receipts, authenticated owner -provenance, descriptor assent, attended sessions, ordered verification and the -first native case run remain open. T03 stays progress; T04 stays wait. - -Exact pin hashes, tests and publication identity are retained under the Custodian -`docs/changes/netkingdom-target-2026-10-16/warden-browser-pin-refresh/` directory. diff --git a/docs/security-case-execution-kernel-v1.md b/docs/security-case-execution-kernel-v1.md deleted file mode 100644 index b9b87d2..0000000 --- a/docs/security-case-execution-kernel-v1.md +++ /dev/null @@ -1,192 +0,0 @@ -# Closed case execution kernel v1 — 2026-10-11 - -Owner implementation record for WARDEN-WP-0044-T03. The architectural proposal -at SHA256 78ab1439e4c5beb205aeccf15b1b2e706511be55ab195c5a3aa682f326c7c77c -was accepted through memo `infd-20261010-security-case-execution`, version 1, -approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca, confirmed disposition -disp-cf44dacb-2117-4de7-8f90-cfa99f84f325 at 2026-10-10T23:23:31Z. The original -approved proposal remains byte-identical. That decision accepts source design; -it grants no execution session, credential or deployment authority. - -`src/warden/case_run.py` advances the pure reducer into an executable injected -owner harness with private durable receipts. Descriptor version -`warden.case.execution.v1` is a closed local implementation seam, not an accepted -NetKingdom type schema. It contains fixed owner/adapter/source digest, actor, -lane, target parameter, effect, reviewed created-object identifiers declared -retry safety and explicit required revocation scopes (parent-session and broker-grant). -A mint descriptor cannot omit either scope. Unknown fields, free shell text and secret parameters are refused. -An acknowledged type must additionally pin the descriptor bytes under its -`execution_descriptors` map and independent owner evidence must authenticate -that exact machine-seam revision. Existing descriptive types lack this map and -remain plan-only. Their acknowledged labels do not authorize this new seam. - -There are no installed owner adapters, native provenance verifier or dispatch CLI -in this change. Callables are injected only through a fixed adapter registry, -with immutable owner reference/source pins. A real integration must supply -independent owner/native verification or explicit attended manual evidence; -a bare JSON receipt identifier is insufficient. Fixture verifiers in tests -are deliberately fake and attest no native case. Caller sessions are independently -verified for exact principal/actor/lane at admission and again before dispatch; -input actor labels alone are never sufficient. Agent mint is refused, including -when WARDEN_AGENT_ID appears after initial human admission. - -Each admission pins exact case/type/descriptor/catalog bytes, acceptance reference -and run-sheet identity. Actual ops-mason materialization evidence is required -against the case source, separately from its status label. Case parameters supply -an exact metadata target. Source/session changes cannot inherit journal success. -Owner results have a closed metadata shape, result evidence fingerprint and -separate independently authenticated grant/parent-session revocation evidence -fingerprints. An aggregate revoked boolean alone cannot satisfy either scope. Unknown result payloads, unverified evidence, absent revocation and -created objects outside the reviewed scope become uncertain, never success. -Python and OS stdout/stderr, including inherited child-process output, are -discarded without buffering or retention under a serialized fd containment -context. Independent provenance verification uses the same containment and -sanitizes exceptions. All source inputs are capped at 256 KiB before parsing. -Error text is not copied to the journal. KeyboardInterrupt/SystemExit and other -owner interruptions are rethrown with a fixed message while retaining running -intent for crash recovery. - -The journal uses an owner-private 0700 directory and 0600 files, refuses symlinks -and insecure modes, uses SQLite FULL synchronization and commits running intent -before calling the adapter. A separate process lock prevents concurrent mutation. -A crash between dispatch and final receipt leaves running; the next lock holder -persists uncertain before refusing replay. Both intent and outcome remain in -history. Each persisted attempt has a unique identity bound to all exact pins, -operation (run/reconcile/rollback), sequence and nonce. Adapter results and both -revocation proof purposes carry that identity. Prior-attempt evidence cannot -verify a safe retry, even if a caller rewrites result labels. Descriptor-created -objects are capped at 32 and histories at 128 transitions, with dispatch stopped -before archival is required. A throw or lost response is not failed-before-change. A safe retry needs -verified failed-before-change, unchanged pins, descriptor retry-safe and explicit -retry. Success and no-change cannot replay. - -Reconciliation is an explicit fixed owner callback, never the mutation callback. -It may verify an already-present native CAS result as no-change/success, or prove -failed-before-change; it never tells Warden to regenerate a credential. Uncertain -or failed-after-change requires this owner-native inspection before any retry. -Owner proof and revocation remain mandatory. Rollback visits only objects in a -verified creation receipt, in reverse order; no-change has no undo and shared -objects are excluded. Unknown/lost undo blocks another rollback. - -The kernel operates individual admitted steps. Full ordered run-sheet orchestration, -verification inventory, contained login integration, native owner adapters and -CLI activation remain WARDEN-WP-0044-T03; no overall native-complete result is -implemented. Current Forgejo source is not materialized and has no acknowledged -machine descriptors. Its first native run remains an acceptance requirement, -with real materialization, separately authenticated broker-grant and parent-session -revocation receipts, session/owner receipts and consumer enforcement. This -implementation neither promotes a type nor changes any case status. - -Validation: 37 new kernel tests plus 14 reducer tests pass (51 total). Tests open -an independent SQLite connection inside dispatch to prove running was committed, -crash after mutation and reopen a fresh journal to prove no duplicate invocation, -lose a response then reconcile without mint, require explicit retry, check exact -pins/session expiry, withhold malicious output and unconfirmed revocation, and -prove created-only reverse undo plus uncertain-undo refusal. Ruff formatting and -checks pass. No live token or target action occurred. - -## Browser input projection — 2026-10-16 source preparation - -An explicitly configured `BrowserInputs(netkingdom_root)` now validates browser -cases before admission. It calls the real NetKingdom type/case validator over -verified producer bytes in a disposable Python process (`-I`, empty environment, -timeout, no inherited output). The validator, composer, capability contract and lazy evidence -modules have exact source pins; missing or changed code refuses. There is no -automatic discovery or download. A producer upgrade needs reviewed pin changes. - -Only the exact reviewed 1.1/1.2 browser parameter definitions qualify. A digest -of each complete parameter definition list prevents reclassifying secrets as -metadata, loosening constraints, or adding fields under a recognized version. -The complete case passes producer validation, including required fields, -constraints and credential-shape checks. Explicit fixed/default overrides, -unknown fields and secret references refuse. Security-sensitive browser metadata -can therefore be present without being passed to an owner adapter: the only -projection is its descriptor's exact public/operational target identifier. -No raw parameter values are added to invocations or persisted receipts. - -`require_implemented=False` validates the acknowledged type's source, not its -execution authority. Existing descriptor/type/source/catalog/caller provenance -gates stay mandatory. Neither historical browser type contains the required -exact machine-descriptor map, and neither is promoted by this change. There are -still no installed native owner adapters, authenticated provenance verifier, -contained session/ordered run orchestration or live CLI dispatch. A journal digest -is a fingerprint and cannot authenticate native evidence. Completion still needs -owner-admitted descriptors and actual structural, mint/delivery, consumer and -revocation/rollback evidence. Local fixture success is not native acceptance. - -Tests use the actual producer checkout (override sibling location with -`WARDEN_TEST_NETKINGDOM_ROOT`), the real Executor, SQLite journal and injected -test-only owner adapter/evidence verifier. Their descriptor extensions and case -materialized labels exist only in memory. Canonical case/type bytes stay intact. - -Producer integration refinement: the accepted NetKingdom browser-case patch -`d32a7f51cbf3012bee3e4bb329f4259dea867eff1c59a2b01a65d72774488d05` -is now the tested producer revision. Its evidence module is imported lazily only -by `check-evidence`, outside this admission path; Warden nevertheless pins and -copies it with the complete reviewed producer set. Missing or modified bytes in -any of the four modules refuse admission. The test covers every dependency. - - -## Original-intent consistency boundary — 2026-10-17 - -`OriginalIntent` and the optional additive -`reconcile_original(ReconcileContext(original, attempt))` callback retain the -original run separately from a fresh reconciliation attempt. Existing one-argument -reconcile adapters remain source-compatible. Intent and pre-dispatch receipt are -committed in one SQLite transaction; lookup now requires their exact attempt, -actor, step/owner/target, source and pin correspondence. Missing legacy context, -an orphan receipt, changed context or an invalid bounded field contract refuses; -there is no backfill or new runtime holder. - -Original intent serialization accepts only the fixed reviewed Invocation/Pins -field contract and bounded operational identifiers. Duplicate, oversized and -malformed original records refuse. Current Executor.admit leaves tenant and realm -empty: those reserved fields are not admitted tenant/realm provenance, and future -nonempty coordinates need an authoritative owner contract. Raw owner payloads, -credentials, parameters and caller output are not part of this context contract. - -Local dispatch correspondence is not independent owner proof. A real owner must -still authenticate the exact original created-only result and original revocation, -current native revision and the fresh attempt's session/grant evidence before -recovery can count. No actual original holder/verifier or native adapter is added. -NetKingdom's current registration adapter still has no reconciliation callback; -source tests do not authorize its post-restart undo, replay or a changed caller/ -session/run-sheet migration. WARDEN-WP-0044-T03 remains a native acceptance gate. - -## Current caller versus original session reference — 2026-10-18 source - -`Executor.admit` now accepts the optional `original_session_reference` for an -exact existing original-run continuation. It is metadata, never current -authentication. The normal `session_evidence` is independently verified as the -current caller's principal/actor/lane proof, including after restart, and remains -the evidence rechecked immediately before every journal write/owner callback. - -The optional reference preserves the original run-sheet digest rather than -replacing immutable provenance with the new login. Before admitting it, under -the existing journal lock, the kernel requires an existing matching step journal -and `Journal.original_intent` correspondence: the original running receipt, -attempt, owner/target/principal/actor/lane, all six pins and exact adapter source. -Missing/orphan/legacy context, a changed reference or caller and a stale current -session refuse. The reference must differ from the current evidence identifier; -the actual session owner must additionally establish distinct authenticated -token/session identity, not just two labels. No new original context is created, -no completed operation can replay, and uncertainty is not cleared by readmission. - -Omitting this option retains the old admission/pin semantics. Supplying a fresh -session alone still creates a different run sheet and cannot reuse a previous -journal. No existing descriptor, Invocation, Pins or receipt schema changes. -The new source seam does not implement changed-source/catalog/caller migration -or loss of rollback result; those still require the existing owner reconciliation -and recovery evidence. It is not general session management or another issuer. - -A source integration fixture runs the actual producer-owned KeyCape status CLI -over HTTPS, the actual Warden kernel and a separately compiled generated Go -consumer. It verifies revoked-parent refusal before writes, distinct inverse -authentication in another Python process, original journal pin retention, -no business replay, exact synthetic target restore and independent read-only -negative cleanup observation. Case/type/catalog/owner attestations and the -target are synthetic; this is not Forgejo/browser/native acceptance or a real -approval. KeyCape's proposed online profile remains unadmitted. The private -unsigned CLI observation is not portable execution authority; actual owner -custody and original grant ownership are still mandatory before an inactive -response can establish revocation. No native verifier or adapter is installed. diff --git a/docs/security-case-execution-proposal.md b/docs/security-case-execution-proposal.md deleted file mode 100644 index f061d43..0000000 --- a/docs/security-case-execution-proposal.md +++ /dev/null @@ -1,136 +0,0 @@ -# SecurityCase execution contract proposal — 2026-10-10 - -Status: proposed owner review; not an accepted runtime protocol or authority. -Owner: ops-warden. Related: WARDEN-WP-0044-T03, NK-WP-0048, MASON-WP-0010. - -`warden case plan` now gives a valid descriptive run, but the type's -`entry_point`, `auth`, verification `check` and rollback `action` fields are -prose. No `warden case run` or `rollback` implementation exists. A case becoming -`materialized` cannot turn those descriptions into executable commands. - -The proposal preserves caller authority and routes by owner. It introduces no -new credential, policy grant, shell command inferred from prose, or authority -for Warden to configure a consumer. Architectural contract acceptance belongs to -net-kingdom and adapter interfaces to their owners. Founder review is needed -only if the acknowledged type's authorization/session boundary changes; the -current absence of a machine contract is not an excuse to fabricate one. - -## Proposed machine seam - -Keep human descriptions. Add a separately validated `execution` reference to -steps and checks, resolving to owner-provided closed adapters. An adapter has: - -- immutable reference (owner repo, interface id, version, source digest); -- closed parameter schema (no secret-sensitivity fields; no free shell text); -- declared actor mode and catalog lane, which must agree with the case plan; -- explicit side-effect class and target identifier deriving from case parameters; -- idempotency/reconciliation contract and metadata-only receipt schema; -- supported rollback reference or explicit owner-only undo. - -Warden does not invent an argv contract for ExternalSecrets, group membership, -KeyCape registration or Forgejo configuration. Owner adapters may internally -call their reviewed tools. Warden invokes only the fixed entry point provided by -that adapter, with validated separate argv fields, and refuses unknown adapters -or templates. `entry_point` prose never goes to `shell=True` or `sh -c`. - -An owner may initially supply a receipt-only adapter: the run sheet records that -step as pending until the owner performs it and provides independent evidence. -This gives useful resumable orchestration before every live adapter exists, -without implying native completion or transferring ownership. - -## Run sheet and receipts - -The run sheet pins exact case bytes (`case-ref`), accepted type bytes/version, -owner adapter digests, catalog revision, acceptance reference and ordered step -identities. Changed pins require a newly reviewed sheet; never inherit previous -success into a different exact revision without explicit reconciliation. - -Each attempt yields an immutable metadata receipt containing these pins, step -id, owner, target, attempt id, result enum and evidence-reference fingerprints. -Allowed results: pending-owner, not-started, running, verified-success, -verified-no-change, failed-before-change, failed-after-change, uncertain. -Neither subprocess output nor arbitrary owner payload is copied into receipts. -Adapter error strings are not evidence and must not be logged verbatim. - -Stop on failure or uncertainty. A thrown exception or lost process receipt is -uncertain, not failed-before-change. Uncertain/failed-after-change blocks retry -until the owner reconciles target state. `verified-success` and -`verified-no-change` are not repeated. Retry after failed-before-change requires -the same pins and adapter's explicit retry-safe classification; a replay-safe -CAS0 receipt is reconciled, not retried into secret generation. State persistence -must write running before dispatch and result afterwards; interruption between -those writes leaves uncertain on resume. Outbox failures remain visible and do -not get rewritten as verified audit delivery. - -Owner receipt ingestion must authenticate provenance or be explicitly manual -attended evidence, never a bare trusted JSON argument. Source/local receipts are -attributive preparation until their owner contract defines independent native -verification. Overall native completion requires every applicable step and -verification plus confirmed session revocation; pending-owner is never success. - -## Sessions, values and rollback - -Reuse `proxy_attended_login_exec` containment/output suppression/revocation -rather than add a second login helper. A key-cape login lane and the OpenBao -broker issuer role are distinct grants; the executor must represent both and -never silently share/inherit a token. Grouping is permitted only within the -same declared identity/lane and preserves follow-through order. An agent -principal cannot dispatch generated-in-custody mint-secret. The broker continues -to rederive the exact case-ref independently. - -Rollback traverses successful step receipts in reverse order and only invokes -an owner's reviewed undo for target changes that receipt proves were created -by this case. Existing shared groups, membership and policies cannot be deleted -by interpreting a type rollback sentence literally. 'No change' yields no undo; -failed/uncertain states require reconciliation first. Do not restore a prior -secret value by extracting it into Warden; custody rollback is owner-native. - -## Owner acceptance requests to prepare - -1. net-kingdom: accept the closed machine seam separately from descriptive type - fields, document materialized = structural acceptance, and avoid gating that - state on mint-secret which is itself a follow-through step. Pin the revised - schema/type and keep type acceptance provenance explicit. -2. railiance-platform: provide a generate adapter over existing `credential.py` - exact `--grant`, `--client-id`, `--case-file`, `--case-ref` and receipt result - codes; clarify reconcile semantics for already-present CAS0 and grant/session - revocation evidence. No new credential is required by this proposal. -3. key-cape/net-kingdom: provide registration/group/env adapters and verify/undo - contracts in the existing attended boundary; do not hand Warden a user-engine - runtime token or a new broad Kubernetes credential. -4. railiance-apps/Forgejo: provide consumer OIDC + groups enforcement adapter or - receipt-only step, exact auth binding for CCR-2026-0042, and source/native - group-denial verification. This stays consumer ownership. -5. ops-mason: give a hash-pinned materialization receipt and list of objects - created vs pre-existing, sufficient for collision checks and scoped rollback. - -No coordination messages have been sent by preparation of this proposal. - -## Implementation and verification after owner seam acceptance - -Implement `case_run.py` with injected typed adapters and receipt storage, then -CLI run-sheet/receipt import/resume/rollback commands. Keep case_plan read-only. -Add type machine-seam validation without turning valid descriptive planning -into an unreviewed source-breaking schema migration. - -Required tests: rejected changed pins; unaccepted/unmaterialized cases; unsupported -adapter/argv refusal; agent mint refusal; pending owner keeps native gate open; -verified success never replayed; lost receipt resumes as uncertain; crash after -running persistence prevents duplicate side effect; failed-before-change retries -only with declared retry-safe adapter; failed-after-change needs owner reconcile; -malicious/secret output excluded; inability to revoke remains unverified; -rollback visits created successful objects only, preserves shared memberships, -and stops on uncertain undo. Inject runners; no live login or target required. - -WARDEN-WP-0044-T03 remains open. This packet resolves the precise source contract -deficit; it is not case materialization, a native run, or a type decision. - - -## Local reducer prototype - -`src/warden/case_receipt_state.py` is proposed local source preparation, not an -accepted wire format. Its pure immutable transitions can be tested without any -owner adapter or shell dispatch. Persist the returned running intent before an -external side effect; on a lost result, recover marks uncertainty and blocks -replay. No receipt provenance, durable storage, CLI dispatch or live action is -implemented. `tests/test_case_receipt_state.py` covers the failure boundaries. diff --git a/examples/inventory.seed.yaml b/examples/inventory.seed.yaml index 4c12450..49ce69f 100644 --- a/examples/inventory.seed.yaml +++ b/examples/inventory.seed.yaml @@ -24,24 +24,6 @@ actors: applicability: applicable workload_id: codex-interhub-bootstrap - # CUST-WP-0073-T02 / WARDEN-WP-0045: supervised interactive agent account - # (Codex, Claude Code, Grok Build) on host account custodian-agt @ Railiance01. - # Issued only by the supervisor from the operator account; the agent distro - # (custodian-agents) holds no warden config, CA key or Vault token. - # No source-address: workstation egress IP is not stable (see wiki). - agt-custodian: - type: agt - principals: - - agt-custodian - ttl_hours: 12 - max_ttl_hours: 12 - extensions: - - permit-pty - description: "Supervised interactive custodian agent (custodian-agt on Railiance01)" - zone_subject: - applicability: applicable - workload_id: custodian-agents - adm-example: type: adm principals: diff --git a/examples/warden.production.example.yaml b/examples/warden.production.example.yaml index 0b28ea9..91e7aa7 100644 --- a/examples/warden.production.example.yaml +++ b/examples/warden.production.example.yaml @@ -4,9 +4,7 @@ backend: vault vault: - # OpenBao via the ops-bridge tunnel (railiance01). bao.coulomb.social is no - # longer public; it serves a notice, not the API. - addr: http://127.0.0.1:18200 + addr: https://bao.coulomb.social mount: ssh role_map: adm: adm-role @@ -26,9 +24,6 @@ policy: # flex-auth.flex-auth.svc Service does not exist. From a workstation, reach it # through a port-forward or tunnel and point this at that local address. flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080 - # Fetch-check pin (CUST-WP-0075), used only by `warden access` fetches. - # No fallback to flex_auth_url; unset means every fetch is refused. - fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080 zone_registry_path: registry/flex-auth/production_registry_snapshot.json failure_modes: z0-experimental: fail_open diff --git a/history/2026-08-28-security-layer-model-assent.md b/history/2026-08-28-security-layer-model-assent.md deleted file mode 100644 index 286b855..0000000 --- a/history/2026-08-28-security-layer-model-assent.md +++ /dev/null @@ -1,171 +0,0 @@ -# Security layer model — ops-warden's assent (WARDEN-IN-0001) - -**Date:** 2026-08-28 -**Intake:** `WARDEN-IN-0001` -**Requested by:** gate-house, ratified as `GH-DEC-2026-001` -**Standard:** `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed) -**Outcome:** assent to all three items; one declared non-conformance and one -proposed amendment to the standard. - ---- - -## What was asked - -gate-house asked ops-warden to assent to three boundary items: - -1. **ops-warden is Staff**, bound by §5 — Staff acts only through Engine APIs and - never holds a direct Tooling client. -2. **Doctrine versus runbook** — the security curriculum moves to gate-house; the - `NetKingdom Security Literacy` section in `INTENT.md` becomes lane-specific - runbooks that reference gate-house doctrine rather than restating it. -3. **The access lane / access rule demarcation** (§8) — ops-warden and ops-mason own - *lanes* (how a worker reaches a host); access-engine owns *rules* (whether they - may). This is the condition attached to renaming flex-auth to access-engine, so - ops-warden effectively holds a veto on that name. - -Plus: add gate-house to the literacy and routing tables, and say so if moving the -curriculum out leaves ops-warden unable to instruct its own workers. - ---- - -## Item 1 — Staff, and the §5 problem it exposes - -**Assent to the layer.** Staff is the right assignment and not a demotion. -ops-warden's artifacts are exactly what §3.4 describes: routing decisions, -workplans, runbooks, an audit trail. Its one production lane is non-deterministic -in the sense that matters — it is an operator front door, not a contract. - -**But §5 is violated today, and by the one lane ops-warden permanently owns.** -The rule is deliberately greppable, so grepping is the honest response: - -| Path | Tooling contact | Kind | Whose credential | -| --- | --- | --- | --- | -| `src/warden/vault.py` (`VaultCA.sign`) | `POST /v1//sign/` | **write** | broker-issued token held by ops-warden's process | -| `src/warden/desk.py` | `bao kv put` (paste-once provisioning) | **write** | founder's, at the desk | -| `src/warden/taint.py` | `bao kv metadata get` | read (metadata only, never data) | caller's | -| `src/warden/proxy.py` (`warden access --fetch/--exec/--wrap`) | catalog `fetch_command` | read | **the caller's own** | - -Two of these are not really ops-warden's clients. `proxy.py` runs the owner's tool -under the caller's identity and supplies no authority of its own — that is -`ADR-0002`, conduit not broker, and it is arguably outside §5's target. `taint.py` -reads metadata only, and fits §5's read-only-observation allowance once declared. - -**`VaultCA` does not have that defence.** It is a direct OpenBao client, in a Staff -repository, performing a write, presenting a token from its own environment. It is -production-verified and it is the SSH lane — the single thing ops-warden owns -permanently. Under §5 as written, adopting this standard puts ops-warden's core -lane in violation on the day it is adopted. - -The escape hatch §5 offers does not fit: it covers *read-only observation for -diagnostics*, and signing is a write. The route §5 prescribes does fit — - -> *A Staff repository needing a capability no engine exposes MUST raise that as an -> engine gap, not solve it locally.* - -— and no engine exposes SSH certificate signing. `secrets-engine` owns credential -abstraction, custody and lifecycle, which is the layer this belongs in, but it -fronts no SSH-CA API today. - -**So ops-warden assents and declares the non-conformance rather than negotiating an -exemption.** `VaultCA` is recorded in `INTENT.md` as a declared §5 exception with a -named intended owner (`secrets-engine`), a blocker (no SSH-CA engine surface), and a -review date. That is `ADR-0003` — cover gaps, never silently own them — applied to -ops-warden itself instead of to someone else's lane. - -### Proposed amendment to the standard - -§5 has exactly one shape for a Staff repository that legitimately touches Tooling: -read-only diagnostics. That shape is too narrow to describe the estate as it exists, -and a rule with no lane for a real, sanctioned case gets satisfied by relabelling -rather than by closing the gap. - -Recommend §5 gain a second shape: a **declared engine gap** — a Staff repository MAY -hold a Tooling client for a capability no engine exposes, provided it is declared in -`INTENT.md` with an intended owner, the blocker, and a review date, and provided the -declaration is machine-readable so the conformance check in §10 can distinguish a -tracked gap from an undeclared violation. - -ops-warden already runs this machinery for other repositories' lanes: 27 catalog -entries carry `delegation:` with `intended_owner` and `blocked_on`, and -`warden route gaps` lists them (WP-0030). It is offered, not imposed — the standard -is gate-house's. - ---- - -## Item 2 — Doctrine versus runbook - -**Assent.** The `NetKingdom Security Literacy` section is what gate-house says it is: -evidence that the curriculum had no owner, so it accreted in whatever `INTENT.md` -was open. That is the same failure `risk-nexus` names for findings and the same one -`ADR-0001` prevents for catalog procedure. ops-warden has argued this rule twice -against other repositories; it applies here. - -The boundary, drawn precisely: - -| Moves to gate-house | Stays with ops-warden | -| --- | --- | -| Why the planes are separated; the authority model | Which subsystem owns which credential need | -| What "posture", "zone", "authority ceiling" mean | How to obtain a cert, a lease, a login — per lane | -| The security curriculum a worker is taught | The runbook a worker executes | -| Doctrine a lane must conform to | Evidence of conformance for ops-warden's lanes | - -**gate-house's test question, answered: no, it does not leave ops-warden unable to -instruct its workers — and the reason is worth recording.** What actually instructs -an ops-warden worker is not the prose in `INTENT.md`. It is `warden plan ""`, -`warden route find`, and `.claude/rules/credential-routing.md`, which is inlined into -every repository's agent instructions precisely because credential routing is -high-frequency and high-risk. That surface is executable, lane-specific, and -unambiguously runbook. It does not depend on the literacy table, and moving doctrine -out does not weaken it. - -If anything the move improves it: the literacy table has been a second, prose copy of -what `registry/routing/catalog.yaml` states machine-readably, which is the -double-source failure `ADR-0001` exists to stop. - -**One thing must not move with it.** `.claude/rules/credential-routing.md` stays -inline in this repository and in every other. It is not doctrine and not a -curriculum; it is the anti-pattern list an agent needs *before* it acts, and a -reference to a document in another repository would not be read in time. - ---- - -## Item 3 — Access lane versus access rule - -**Assent, unconditionally, and the veto on `access-engine` is not exercised.** - -ops-warden is already built this way. `ADR-0005` implements one lane narrowly and -routes everything else; `ADR-0002` makes it a conduit that never decides; `ADR-0009` -has ops-warden compile membership attributes and apply a zone's failure mode while -flex-auth owns the stance. ops-warden consumes decisions; it has never rendered one. -The demarcation costs nothing because it describes what is already true. - -`access-engine` is also the better name. ops-warden's own routing table has had to -say "authorization" for the decision and "access" for the route for a year, and the -collision is visible in every playbook. - -**One operational condition, on execution rather than on the ruling.** The rename is -598 references across 82 files in this repository alone — catalog `owner:` fields, -`registry/flex-auth/`, `src/warden/policy.py`, the production registry snapshot -builder, playbooks, and the `.claude/rules/` files that other repositories inline. -Ops-warden asks for a deprecation window in which both names resolve, rather than a -flag day; ops-warden will do its own migration inside that window. This is a request -about sequencing, not a reservation about the name. - ---- - -## Item 4 — gate-house is missing from every table - -Correct, and fixed in this pass. gate-house is added to the literacy/routing table -in `INTENT.md` as the owner of doctrine, invariants, authority context, and -conformance review — with the routing rule stated explicitly: **doctrine and -authority-model questions go to gate-house; policy decisions continue to go to -access-engine.** Those are different questions and the distinction is the whole -point of §6. - ---- - -## Recorded as - -- `ADR-0010` — ops-warden is Staff; lanes not rules; the declared §5 exception -- `INTENT.md` — layer declaration, reworked routing table, gate-house row -- `WARDEN-IN-0001` — closed, outcome `assented` diff --git a/history/2026-08-29-layer-model-v04-review.md b/history/2026-08-29-layer-model-v04-review.md deleted file mode 100644 index a548399..0000000 --- a/history/2026-08-29-layer-model-v04-review.md +++ /dev/null @@ -1,136 +0,0 @@ -# Security Layer Model v0.4 — ops-warden's review - -**Date:** 2026-08-29 -**Reviewed:** `net-kingdom/canon/standards/security-layer-model_v0.4.md` (accepted) -**Prior position:** `ADR-0010`, assent to v0.1 (`WARDEN-IN-0001`) -**Outcome:** no objection to the ruling; three findings, one of them against ops-warden. - ---- - -## What v0.4 did with ops-warden's amendment - -Both §5 asks from `ADR-0010` were adopted. - -**§5.3 declared engine gap** is the amendment ops-warden offered, adopted with the -four fields intact (`capability`, `intended_owner`, `blocked_on`, `review`), the -rationale preserved — *a rule offering no lane for a real sanctioned case gets -satisfied by relabelling rather than by closing the gap* — and the framing that -matters most kept explicit: **a declared gap is tracked non-conformance, not -conformance**. ops-warden's delegation machinery is cited as prior art. - -**§5.2 conduit** resolves the question ops-warden flagged rather than assumed. The -test is the supplied-authority property, which is the right test: it turns on what -the repository presents, not on what it touches. *"A conduit that presents its own -token is not a conduit"* is a sharper statement of `ADR-0002` than `ADR-0002` makes. - -**This created an obligation ops-warden had not met.** §5.3 requires the fields -*machine-readably* and §11 makes the mapping a mechanical check; ops-warden's -declaration was prose in `INTENT.md`. Fixed in this pass: `layer.yaml`, -`scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py`. The -checker found three undeclared modules on first run, all false positives — help -text, a docstring, and the doubles library that *simulates* `bao` — which is why -it now matches invocation shapes rather than the word. - ---- - -## Finding 1 — §9.1 and §5.3 disagree, and ops-warden's §4 row is the instance - -§9.1: *a Staff repository MUST NOT be catalogued in §4 as owning a capability that -requires a Tooling contact no engine exposes*; where intended but unbuilt, the -entry **MUST be marked pending** and the gap declared under §5.3. - -ops-warden's §4 row reads `operational access lanes, stewardship, runbooks; SSH -certificate issuance` — with no pending mark. And §13 lists *SSH-CA signing write -(`VaultCA`, `bao kv put`) — declared by ops-warden — intended owner secrets-engine*. - -So the catalog asserts ownership of a capability that requires a Tooling contact no -engine exposes, unmarked. By §9.1's own text that is a defect. But the available -fix is worse than the defect: **marking it pending would be false.** SSH issuance is -production-verified and in daily use. `pending` would tell a reader ops-warden does -not yet do the one thing it demonstrably does. - -The root cause is that §9.1 collapses two different states: - -| State | Example | Capability today | -| --- | --- | --- | -| No route exists at all | kings-guard containment (§9.2) | **zero** | -| Route exists via a declared §5.3 gap | ops-warden SSH issuance | **working, tracked** | - -§5.3 exists precisely to sanction the second. §9.1 was written for the first — it -was raised by kings-guard, about containment, and correctly fixed *for that case*. -Applied to the adjacent case it produces a false catalog. - -**Recommendation:** give §9.1 two marks rather than one — `pending` where no route -exists, and `declared-gap` where the capability is discharged under §5.3 and -registered in §13. Both are honest; today's binary forces a choice between a false -label and an unmarked violation. - -This is the §12 loop working as designed, and §12 already says so: a finding that a -rule is unsatisfiable is a success of the loop. - ---- - -## Finding 2 — §5's scope is undefined for infrastructure §4 does not catalogue - -§5 forbids *a direct client for a Tooling-layer system*. §4 catalogues the security -estate, and only `key-cape` and `OpenBao` are Tooling rows. - -ops-warden holds an HTTP client for the **State Hub** and for **llm-connect** -(`src/warden/worker.py`). Neither appears in §4. Both are infrastructure a Staff -repository holds a direct client for. - -The question is not rhetorical, because the answers diverge sharply: - -- **If they are Tooling**, then every Staff repository in the estate is in - undeclared violation on adoption day — they all write progress events — and - §11's second mechanical check fails estate-wide. -- **If they are not**, §5 should say so, because *"a Tooling-layer system"* reads - considerably broader than *"a repository in the §4 Tooling rows"*. - -ops-warden has recorded both under `non_tooling_clients` in `layer.yaml` with the -reasoning stated, rather than resolving it unilaterally. The scope is gate-house's -to set. - ---- - -## Finding 3 — §9.6 lands on ops-warden, and ops-warden does not satisfy it - -This is the one against us, and it is the most consequential item in the review. - -§9.6 consequence 1: *any system whose evidence is load-bearing MUST make emission -atomic with the state change it records. An archive cannot retrofit completeness.* - -**ops-warden's audit emission is deliberately non-atomic.** `src/warden/ca.py:90` -carries `pass # audit must not block signing`, and `wiki/AuditTrail.md` states the -trail *"never blocks the primary action"*. If the audit append fails, the -certificate is still issued and the event is simply lost — a suppressed event that -leaves the chain perfectly intact, which is the exact failure §9.6 describes. - -That was a considered availability choice: an audit-disk problem should not remove -production host access. §9.6 now makes it a conformance question, and the trade is -real in both directions: - -- make emission atomic → an audit write failure fails the sign, and the estate's - operational access lane acquires a new dependency on its own evidence store; -- leave it → signing evidence cannot be treated as complete, and anything reasoning - from *"there is no record of a sign"* is unsound. - -**ops-warden has not changed it, and is not going to decide this alone** — §9.6 is -estate doctrine and the question is whether SSH signing evidence is load-bearing in -gate-house's sense. What ops-warden can say is that the second horn is currently -true and undocumented: `wiki/AuditTrail.md` does not warn that absence of a record -is not evidence of absence. That correction is ops-warden's regardless of the -ruling, and is the smaller half of the fix. - -Note also that §5.2 requires a conduit action to be *"reconstructable as the -caller's action in audit"* — an audit-dependent claim, and therefore bounded by -§9.6. Worth a cross-reference so the two rules do not drift apart. - ---- - -## Offered - -`layer.yaml` + `check_layer_conformance.py` + `test_layer_conformance.py` is a -working reference implementation of §5.3 and of §11's second mechanical check. Eight -of fifteen estate repositories have yet to declare (§14). If it is useful as a -pattern to point them at, it is offered — as the delegation machinery was. diff --git a/history/2026-08-29-layer-model-v06-review.md b/history/2026-08-29-layer-model-v06-review.md deleted file mode 100644 index 857a78a..0000000 --- a/history/2026-08-29-layer-model-v06-review.md +++ /dev/null @@ -1,156 +0,0 @@ -# Security Layer Model v0.6 — ops-warden's review - -**Date:** 2026-08-29 -**Reviewed:** `security-layer-model_v0.6.md` (proposed), plus v0.5 and the companion -**Prior positions:** `ADR-0010` (v0.1 assent); `history/2026-08-29-layer-model-v04-review.md` -**Outcome:** no objection; one conformance action taken, two findings, one accepted SHOULD. - ---- - -## Disposition of ops-warden's v0.4 findings - -All three were acted on, two of them exactly as recommended. - -| Finding | Outcome | -| --- | --- | -| §9.1 forces a false `pending` onto working capability | **Adopted** — v0.5 split it into `pending` and `declared-gap`, credited to ops-warden | -| §5 scope undefined for uncatalogued infrastructure | **Adopted** — "Tooling-layer system" now means a §4 Tooling row; the State Hub case is recorded, not policed | -| §9.6 atomicity lands on ops-warden's signing lane | **Ruled** — the load-bearing / attributive distinction, with ops-warden's `# audit must not block signing` named as the estate's live example | - -The §9.6 ruling deserves a note, because it went in ops-warden's favour and that is -a reason to check it rather than accept it. The test is *"no control branches on -its presence"*. Verified: the only consumer of `audit.jsonl` is `warden activity` -(`cli.py`), which displays. Nothing gates on a signing record — not the agent -read-boundary, not `warden plan`, not the scorecard. The lane is genuinely -attributive and the trade is legitimate on the standard's own terms. The two -obligations that attach — declare it, never claim completeness — were already met -in `wiki/AuditTrail.md`, now updated to record the ruling rather than the open -question. **If a future ops-warden control ever gates on this trail, the trade has -to be revisited before that ships**, and that is recorded there. - -`layer.yaml` is named in §11 as the estate's reference declaration form, including -the "record non-Tooling clients so the check is total" property. Offered again to -the repositories that have yet to declare. - ---- - -## Conformance action taken — the stance map was not published - -§6.4 obligation 3 requires a declared unreachable-engine stance that is total, per -zone, with no implicit default, *"published rather than held in code comments"* — -and §6.4 requires **every** PEP-shaped consumer to publish its map so the maps can -be inventoried. `ADR-0009` is named as the reference shape. - -ops-warden was not doing this. The map lived in `PolicyConfig.failure_modes`, a -dataclass default in `src/warden/config.py`. That is not a code *comment*, but it -is not published either — it is merely written down, and a consumer of the estate -had no way to read ops-warden's stance without reading ops-warden's source. - -Published as `pep-stance.yaml`, with the property that makes publishing worth -anything: `tests/test_layer_conformance.py` asserts the published map is **equal to -the shipped default**. A published map that may drift from the code is worse than -no map, because it invites reliance it cannot support. The file also records the -obligation-2 position (verdict never cached; input claims cached under their own -freshness rules) and the obligation-4 bound (§9.6 attributive). - ---- - -## Finding 1 — §6.4 obligation 1 contradicts obligation 3, and ops-warden is the instance - -> **1. No side effect without a decision record.** A PEP MUST NOT perform the -> protected action unless it holds a decision from `access-engine` identifying the -> request it was rendered for. - -> **3. A declared unreachable-engine stance (§9.3):** total, per zone... `ops-warden` -> `ADR-0009` is the reference shape. - -These cannot both be absolute. ops-warden's declared stance — blessed by §9.3 as -*"the only thing left"* when there is no engine to ask — is `fail_open` for `z0`–`z2` -and `unknown`. Applying it means issuing a certificate **without holding a -decision**, which obligation 1 forbids without qualification. - -So the same section names ops-warden as the reference shape for obligation 3 while -obligation 1 makes ops-warden's shipped behaviour a violation. §9.3 settled the -substance; §6.4 restates it in a form that takes it back. - -**Recommendation.** Bound obligation 1 by obligation 3: - -> A PEP MUST NOT perform the protected action unless it holds a decision from -> `access-engine` identifying the request it was rendered for, **or its declared -> §9.3 stance for the applicable scope permits proceeding without one and the -> application of that stance is recorded in place of the decision**. - -This is not a weakening. It is stricter than today's text in the case that matters: -it makes the *recorded application of the stance* mandatory, rather than leaving -"no decision record" as a silent state. ops-warden already does this — `ca.py` -writes `policy_zone`, `policy_failure_mode` and `policy_decision_id` (present only -where a decision was rendered) into both the signatures log and `audit.jsonl`, per -`ADR-0009` rule 4: *a fail-open signing result is metadata, not silence*. - -This is the same shape as the v0.4 §9.1 finding: a rule written for the clean case, -correct there, producing a false result on the adjacent case the standard has -already sanctioned elsewhere. - ---- - -## Finding 2 — §6.4 creates a register that §13 does not implement - -§6.4: *"Every PEP-shaped consumer MUST publish its stance map, and those maps MUST -be inventoried — in `maturity-engine` once it exists, **in §13 until then**."* - -§13 contains no stance-map rows. It records declared contacts and unowned -capabilities; there is no column, row, or section for a PEP stance. So the -obligation names a register that does not exist yet, and the failure mode §6.4 -itself warns about — *"`z0`–`z2` and unknown fail open" becoming the estate's real -policy without anyone having compiled it* — is exactly what the missing register -permits. - -**Recommendation.** Either add a stance-map table to §13 with the same -state/owner-status discipline the gap table has, or state that the inventory waits -for `maturity-engine` and mark the obligation pending under §9.1's own logic — a -requirement whose register does not exist is a capability catalogued without a -surface. ops-warden's row is ready to paste: - -| PEP | Protected action | Scope | Stance | Published | -| --- | --- | --- | --- | --- | -| `ops-warden` | SSH certificate issuance | security-zone | open `z0`–`z2`+unknown, closed `z3`/n-a | `ops-warden/pep-stance.yaml` | - -The second half matters more than the first: **ops-warden is currently the only -PEP that has published one**, so an inventory today would contain one row and that -is itself the finding. `ops-mason` is named PEP-shaped in the same paragraph. - ---- - -## Accepted, not yet done — §9.6 emission cadence - -§9.6: *"A source SHOULD declare an expected emission cadence, and a drop below it -SHOULD become a finding in its own right."* - -ops-warden declares none. This is a genuine SHOULD and the reasoning behind it is -sound — it converts the suppression blind spot into something detectable without -any Tooling contact, because the source publishes its own stream. It is not done -here because a cadence asserted without evidence is worse than none: ops-warden's -signing volume is operator-driven and bursty, and a fabricated baseline would -generate findings that mean nothing. Deriving one from the existing trail is -tractable and is recorded as ops-warden's to do, not gate-house's to chase. - ---- - -## On the pace - -Six versions in two days, with four repositories' findings absorbed and credited, -is the §12 loop working at a rate the estate has not seen before. Two cautions, -offered as an interested consumer rather than as objections: - -1. **§13 already says it should not be statute, and it is right.** The register has - grown every version. Moving it to `maturity-engine` is the stated plan; until - that exists, each version of the standard is also a snapshot of a backlog, and - the two have very different review intervals. -2. **The standard is `proposed` again at v0.6**, and the four repositories that - assented did so to v0.1. ops-warden's `ADR-0010` assent covers the three - boundary items, and nothing in v0.2–v0.6 has disturbed them — the layer, the - lane/rule demarcation, and doctrine-versus-runbook all stand. But the - `assented_by` list carries assent forward across five revisions, and a reader - could take it as assent to the current text. Worth distinguishing *assented to - the boundary* from *reviewed the current revision*; ops-warden has now done - both, and this note is the second. diff --git a/history/2026-08-29-v07-scope-intent-assessment.md b/history/2026-08-29-v07-scope-intent-assessment.md deleted file mode 100644 index f8a2196..0000000 --- a/history/2026-08-29-v07-scope-intent-assessment.md +++ /dev/null @@ -1,157 +0,0 @@ -# v0.7 conformance — INTENT vs SCOPE gap assessment - -**Date:** 2026-08-29 -**Standard:** `security-layer-model_v0.7.md` (**accepted**) + `SECURITY-COMPANION.md` v0.2 -**Prior:** `ADR-0010`; v0.4 and v0.6 reviews in `history/` -**Method:** each v0.7 obligation checked against shipped code, not against intent. - ---- - -## Summary - -ops-warden is **conformant on every obligation it can discharge alone except three**, -and holds two declared §5.3 gaps that are tracked, registered and owned elsewhere. -The three genuine gaps are §9.7.2 (no stated revocation visibility deadline — a MUST), -§3.4 rule 1 (the agent read-boundary keys on an honour-system marker rather than an -issued identity), and §9.6's cadence, which is a SHOULD for an attributive source and -remains undone for an honest reason. - -One new obligation is not a rule at all but a role: the companion routes the entire -estate to ops-warden for *how to get something done*. Nothing in the repo answers a -layer or declaration question today, and that is now a discoverability gap. - -Four ops-warden findings were adopted into the standard between v0.4 and v0.7 — §9.1's -two marks, §5's Tooling scope rule, §6.4 obligation 1's second limb, and §13.1's -existence. That is the conformance loop working; it is not a reason to assume the next -pass finds nothing. - ---- - -## Obligation-by-obligation - -### Conformant, shipped, evidenced - -| Obligation | Evidence | -| --- | --- | -| §11 declare layer in own voice, machine-readably | `INTENT.md` frontmatter (`layer: Staff`, `pep_shaped: true`) + `layer.yaml` — cited in §11 as the estate's reference form | -| §5 every Tooling contact maps to a shape; non-Tooling recorded so the check is total | `layer.yaml` 5 contacts + 2 exclusions; `scripts/check_layer_conformance.py` | -| §5.2 conduit supplies no authority | `proxy.py::_caller_env`; `tests/test_layer_conformance.py::test_conduit_supplies_no_authority_of_its_own` | -| §6.4 obl. 1 no side effect without a decision **or a recorded stance** | `ca.py` writes `policy_zone`, `policy_failure_mode`, `policy_decision_id` (present only where rendered). ops-warden is the named reference for limb two | -| §6.4 obl. 2 no verdict recaching | `policy.py` caches nothing — verified by inspection, not by claim | -| §6.4 obl. 3 stance map published, at a path named in the declaration, equal to shipped behaviour, asserted by test | `pep-stance.yaml`, named in `layer.yaml`; test asserts equality with `PolicyConfig().failure_modes`; registered in statute §13.1 | -| §9.3 stance total, per zone, no implicit default | 7 rows covering every zone plus `unknown` and `not-applicable` | -| §9.6 evidence claims bounded | `wiki/AuditTrail.md` declares the attributive trade and states absence is not evidence of absence | -| §9.7.1 every allow has an explicit lifetime | TTL enforced per `ActorType` — `adm` 48h, `agt` 24h, `atm` 8h | -| §3.4 rule 2 tool use is a conduit or engine API | `warden access` is the conduit; `ADR-0004` enforces that tool availability is not permission | - -### Declared gaps — tracked non-conformance, owned elsewhere - -Both registered in statute §13, intended owner `secrets-engine`, reviewed quarterly. -Neither is closable by ops-warden: closing them means another repository shipping a -surface. - -- **`VaultCA` signing write** — no engine exposes SSH-CA signing. -- **`warden desk` `bao kv put`** — no engine exposes attended provisioning. - -Nothing in v0.7 changes their status. The right ops-warden behaviour is to keep them -declared, keep the review dates honest, and not quietly grow a third. - ---- - -## The three real gaps - -### G1 — §9.7.2: no stated revocation visibility deadline (MUST) - -> *A **PEP** has one boundary and MUST state one deadline… an unstated deadline is -> an unbounded replay window.* - -ops-warden states none, and the honest answer is uncomfortable: **the effective -window is the certificate TTL — up to 48 hours.** A certificate issued under an allow -stays valid for its full TTL even if the decision that authorized it is revoked or -superseded the next minute. ops-warden has no revocation channel for an issued cert: -there is no CRL, no KRL distribution, and host-side `auth_principals` is -`railiance-infra`'s. - -This is not a documentation gap. It is a design property that has never been written -down, and §9.7.2 exists precisely to force it into the open. Two things follow: - -1. The deadline must be **stated** — `adm` 48h / `agt` 24h / `atm` 8h — in - `pep-stance.yaml`, as what it is rather than as an aspiration. -2. Whether 48h is *acceptable* is a separate question, and it is partly - `railiance-infra`'s (KRL distribution) and partly ours (TTL policy). Stating it is - ours and is cheap; shortening it is a joint change. - -Stating a bad number is better than stating none: an unstated deadline is an -unbounded replay window, and this one is bounded and already implemented. - -### G2 — §3.4 rule 1: the agent boundary rests on an honour-system marker - -> *No standing credential. Authority is issued per task, time-bounded under §9.7, -> and attributable to the principal on whose behalf it acts.* - -`ADR-0004`'s read-boundary triggers when `WARDEN_AGENT_ID` is set — an environment -variable the agent sets **about itself**. An agent that does not set it is not -recognised as an agent. ops-warden has known this (`WARDEN-WP-0033-T04` recorded it -as "an honour-system marker on the ops-warden side"), and it was tolerable while no -issued agent identity existed. - -One now does. `key-cape` accepted issuance ownership in `KEY-WP-0009-T03`: -`codex-railiance-platform`, subject `service:codex:railiance-platform`, role -`coding-agent`, scope `openbao:login`, 15-minute lifetime. The OpenBao side is -enforced by `railiance-platform`'s policy, which is the half that actually holds. - -So the gap is narrower than it looks and worth stating precisely: **the OpenBao-side -boundary is real; the ops-warden-side boundary is advisory.** ops-warden should key -its read-boundary on the issued identity where one is present, and treat -`WARDEN_AGENT_ID` as a fallback that fails *toward* the boundary rather than away -from it. That is a change in this repo and does not need another repo to move. - -### G3 — §9.6 emission cadence (SHOULD, for an attributive source) - -Unchanged from the v0.6 review and still honest: ops-warden declares no expected -cadence because its signing volume is operator-driven and bursty, and a fabricated -baseline generates findings that mean nothing. v0.7 makes cadence a **MUST for -load-bearing sources**; ops-warden's trail is attributive, so it remains a SHOULD. - -Deriving a real baseline from the existing trail is tractable and is ops-warden's to -do. It should be derived and declared, or explicitly deferred with a reason — not -left silent, which is what it is today. - ---- - -## The role the companion assigns, and what it costs - -> *"For how to get something done in NetKingdom — which lane, which credential, which -> route — ask `ops-warden`. This document says what the rules are; ops-warden stewards -> the paths through them."* - -This is the largest change in ops-warden's INTENT surface and it is not a rule, so it -does not appear in any conformance check. The estate has been told to come here. - -**Today the repo answers credential questions and no others.** `warden route` and -`warden plan` cover lanes, owners and acts. Nothing answers *"which layer am I"*, -*"how do I declare"*, *"I am PEP-shaped, what do I owe"* — the questions the companion -and the standard's adoption status (eight of fifteen repositories undeclared) actually -generate. - -ops-warden has already built the reference artifacts those repositories need, and the -standard points at them by name in §11 and §6.4. What is missing is the path: a -discoverable route from *"I read the companion"* to *"here is the file to copy and the -check to run"*. That is exactly the stewardship ops-warden claims, applied to the -estate's newest rule rather than to its credential lanes. - -Also worth noting, and not ops-warden's to fix: §13.1's register has one row, and -`ops-mason` — catalogued PEP-shaped in the same paragraph — has published nothing. -The standard says one row is itself the finding. - ---- - -## What does not need doing - -- **No new ADR.** `ADR-0010` holds: Staff, lanes not rules, declared gaps not - exemptions. v0.2–v0.7 refined the rules around it and disturbed none of its three - positions. The reviews extend it; a superseding record would add ceremony without - changing a decision. -- **No change to the two §5.3 gaps.** They are correctly declared and owned elsewhere. -- **No re-assent.** ops-warden assented to the boundary in `ADR-0010` and has now - reviewed three revisions on their merits, which is the stronger position. diff --git a/history/2026-09-09-layer-model-v08-review.md b/history/2026-09-09-layer-model-v08-review.md deleted file mode 100644 index ac4ddd8..0000000 --- a/history/2026-09-09-layer-model-v08-review.md +++ /dev/null @@ -1,175 +0,0 @@ -# security-layer-model v0.8 — ops-warden review - -**Date:** 2026-09-09 -**Reviewing:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (status -`proposed`, net-kingdom@66eeaba), circulated for assent by gate-house -**Prior reviews:** `2026-08-29-layer-model-v04-review.md`, `-v06-review.md`, -`-v07-scope-intent-assessment.md` - -## The cell in question - -v0.8 §6.4 obligation 3 adds: *unknown is not a zone and MUST resolve to -`fail_closed`.* `pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`. -§13.1 records the cell as non-conformant. - -## Finding 1 — assent. The rule is right and our falsifier check fails. - -gate-house wrote the falsifier into GH-DEC-2026-009's reversal: a scope that is -genuinely unknown **and** genuinely low-consequence, where failing closed costs -availability with no security gain. Their expectation is that any such path is a -§5.1 read-only diagnostic carrying no PEP obligation at all. - -We looked for one on our side and do not have it. The stance map governs exactly -one protected action — `warden sign`, SSH certificate issuance. That is a -credential-issuing side effect, not a diagnostic; §5.1 does not reach it. So the -predicted escape hatch is absent here, and the argument stands unrebutted on its -own terms: - -- §9.3 licenses trading availability for openness **per zone**, knowingly, for a - named scope. Where the scope is unknown that trade was never made for the - request in hand, so resolving permissively does not extend a considered - decision — it invents one. -- Unknown is the cheapest state to induce. Our own `unknown` reasons are - `workload_resolution_absent` and `catalog_applicability_absent` — an - unregistered workload or a missing declaration, neither of which requires a - credential to arrange. - -We accept that being unclassifiable must not buy permissiveness. The cell is -wrong as doctrine. - -## Finding 2 — the rule is right and adopting it today would be a global fail-closed flag in all but name - -This is the finding we would like recorded, and it is about adoption cost, not -about the rule. - -The stance map applies only when the engine is unreachable, so the cell's bite is -proportional to how much of the target population sits in `unknown`. Measured -today: - -| Population | Resolved to a zone | `unknown` | `not-applicable` | -| --- | --- | --- | --- | -| Signing targets (actor resources in the flex-auth snapshot) | **0** | **3** | 1 | -| Routing catalog lanes | 3 | 18 | 12 | - -Every signing target that resolves at all resolves to `unknown`. So -`unknown: fail_closed` adopted today does not fail closed on an edge case; it -fails closed on **essentially every certificate ops-warden issues** whenever -flex-auth is unreachable. - -That is the precise configuration `ADR-0006` rejected and `ADR-0009` replaced: one -value making the decision engine a uniform dependency of every signing path, -*including the continuity paths needed to repair that dependency*. If flex-auth is -unreachable and the operator needs an SSH certificate to reach the host and -restore it, `unknown: fail_closed` denies the certificate — and the target is -`unknown` precisely because nobody has classified the repair path. - -The zone model already answers this: classify continuity paths `z2-continuity`, -whose stance is `fail_open`, and the deadlock does not arise. We accept that as -the right answer. The point is that it is *work not yet done*, and the reason it -is not done is not ours to fix — 18 of our 18 unknown lanes are unknown because -another repo has not published an authoritative workload identity declaration. -`ADR-0009` rule 3 forbids us from closing that gap with inference, correctly. - -So as written, v0.8 §6.4 obligation 3 transfers an availability cost onto the -consumer for a classification gap the consumer cannot close alone, and it does so -at the moment of assent rather than at the moment the gap closes. - -**What we ask for**, in preference order: - -1. **§6.4 obligation 3 names a transition.** A consumer may declare - `unknown: fail_open` as a dated, published transitional state with a - classification-coverage figure attached, converting to `fail_closed` on - coverage rather than on calendar. This keeps the doctrine and prices the - adoption honestly. -2. Failing that, **§13.1 records classification coverage alongside stance.** A - register row reading `unknown: fail_closed` while 100% of that consumer's - targets are unknown is conformant and materially misleading; a reader cannot - tell a strict consumer from an unclassified one. This is the same defect - §11's *published map equals shipped behaviour* rule exists to prevent, one - level up: the map is now accurate about itself and inaccurate about its - effect. - -## Finding 3 — §12's derived-artifact rule, small and in agreement - -Six instances in one week matches our experience. `registry/generated/high-risk-data-paths.yaml` -carries `catalog_revision` and a `catalog_dirty` flag for exactly the reason §12 -now generalises: a derived artifact whose revision does not describe the file it -came from is worse than one with no revision, because the stale one is trusted. -No change requested; recording the agreement since the rule is being generalised -from other repos' incidents. - -## What ops-warden is doing about it - -Not flipping the cell on a `proposed` standard, and not flipping it before -coverage exists — that ordering is `ADR-0006`'s whole holding. `WARDEN-WP-0040` -records the adoption sequence: raise signing-target classification coverage -first, then convert the cell, with the deadlock case classified before rather -than after. If v0.8 is accepted with obligation 3 unchanged, ops-warden's cell -remains a **declared gap** under §11 — tracked non-conformance with a named -owner and a route, which is the mark §9.1 exists for and which we argued for in -the v0.6 round. - -## Axis declaration (second change touching us) - -`pep-stance.yaml` already carries `scope: security-zone`. No change needed. We -agree with rejecting a forced migration onto zones — secrets-engine scoping by -catalog stage is an accurate statement about their control, and making them -assert a zone they cannot know would trade a real fact for a conformant fiction. -The unavailability of cross-axis aggregation is better recorded than papered -over, and §13.1 now says so. - ---- - -## Ruling — GH-DEC-2026-011, 2026-09-09 - -gate-house closed the assent round the same day: **ask 1 declined, ask 2 -adopted**, standard amended at `net-kingdom@64394e9`, dispositioned in -`gate-house/docs/conformance/2026-09-06-v08-assent-round.md`. - -**Ask 1 (a dated transitional `unknown: fail_open`) — declined**, and the reason -is better than the ask. A sanctioned transitional `fail_open` is -*indistinguishable at runtime* from the stance the rule forbids, and it would make -the rule optional at the moment of adoption — the only moment it costs anything. -Dating an intention to stop being permissive does not make `unknown` any more -expensive for an attacker to induce while the transition runs. Accepted without -reservation; the argument defeats the ask on its own terms. - -**Ask 2 (coverage published beside stance) — adopted.** §13.1 now carries a -Coverage column and this repo's figures are its first entries: *signing targets -0/3 resolved; routing lanes 3/21 resolved (2026-09-09, self-measured)*. Three -guards came with it, all of which we would have wanted: coverage does not soften -the stance, does not gate it, and never makes a non-conformant cell conformant; a -blank means "not reported" and never "complete"; and the decision carries an -explicit reversal condition — if a row is ever argued to be conformant *because* -its coverage is low, the column is doing harm and comes out. - -That reversal condition is the right shape and it binds us first, since the column -exists because we asked. Our row reads non-conformant **and** low-coverage; those -are two facts, and the second is not a defence of the first. - -**The deadlock was recorded rather than routed around.** §6.4 obligation 3 now -states that a stricter stance is not a licence to manufacture the membership that -makes it survivable — where a scope is unknown because another repository has -published no workload-identity declaration, the consumer MUST NOT infer one. That -is `ADR-0009` rule 3 promoted into the standard at exactly the point where the -pressure lands, which is more than we asked for. - -### What we did in response - -- `pep-stance.yaml` marks the `unknown` cell as a **declared gap** inline, with - the assent, the measured reason for not flipping, the declined ask, and - `WARDEN-WP-0040` as its route. The stance value is unchanged — v0.8 is still - `proposed` and v0.7 remains in force, so `standard_version` stays `0.7` with a - new `standard_version_reviewed: 0.8`. -- The coverage figures are published in `pep-stance.yaml` and, because we asked - for the column, **measured rather than hand-counted**: - `scripts/report_coverage.py` derives both populations from the artifacts the - runtime uses, reusing `report_workload_join.build` rather than re-deriving the - join. `tests/test_layer_conformance.py` asserts the published block equals the - measured one, and asserts the cell stays marked while it is `fail_open`. - - A hand-counted figure in a register that explicitly does not recompute it would - decay silently, and a stale number beside a marked cell is worse than the blank - the other four rows carry — a blank at least reads as "not reported". -- `WARDEN-WP-0040` T04 is closed; T01–T03 stand unchanged. gate-house confirmed - they are not asking for the cell to be flipped before coverage exists. diff --git a/history/2026-09-21-gate-house-a16-a17-response.md b/history/2026-09-21-gate-house-a16-a17-response.md deleted file mode 100644 index 3c4b72d..0000000 --- a/history/2026-09-21-gate-house-a16-a17-response.md +++ /dev/null @@ -1,110 +0,0 @@ -# A-16 and A-17 — response from a repository that bears the cost - -Date: 2026-09-21 -Subject: gate-house `a5a1bcf`, INTENT.md Core Rules 16–17, ArchitectureBlueprint -A-16/A-17. No assent was requested. This is the "if it would cost you something -we have not counted" reply. - -Both rules are right. Two things are worth sending back: a third instance of -A-16 that predates the rule, and one refinement to A-17 that our own declined -request exposes. - -## A-16 — distinguishable routes - -The rule as stated: where one observable state is reachable by two routes that -differ in security meaning, the record must say which route produced it; the -routes usually must behave *identically*, and safely. - -**A third instance, built before the rule and without knowing it.** -`pep-stance.yaml`'s `on_apply` block records `policy_decision_id` **only where a -decision was actually rendered**, alongside `policy_zone`, `policy_failure_mode` -and `outcome`. A certificate issued because access-engine allowed it and a -certificate issued because the engine was unreachable and the stance said -`fail_open` are the same observable state — a signed cert in the signatures log -— reached by two routes that differ entirely in security meaning. The absent -`policy_decision_id` is what tells them apart. - -We did not derive that from a principle. We built it because §6.4 obligation 1 -requires a decision record for a protected side effect and there is no decision -to hold when the engine is unreachable, so we recorded the *application of the -stance* instead. That it lands exactly on A-16 is the useful part: the rule -reproduces a choice a consumer made independently under local pressure, which is -better evidence for graduation than our agreeing with it would be. - -**The cost of A-16 here is small and we would pay it again.** One extra field on -an audit record. The failure it prevents is not small: without it, "the -signatures log shows 3 issuances, all allowed" reads as a statement about -authorization when it may be a statement about reachability. - -## A-17 — fail-closed transitions, and the one thing we would add - -We are one of the two cases. Our dated `unknown: fail_open` was declined, and -the decline was correct. The distinguishing case is *engine unreachable and -target zone unknown* — precisely where a transitional `fail_open` fails open. So -it is a permission, and a register that held it would be holding a permission -under a name that reads as a gap. We argued the other way at the time and were -wrong about it; A-17 states why in one sentence, which is more than our -objection did. - -**The refinement.** A-17 as written tests *direction of failure at the -distinguishing case*. Our case shows that direction alone is not sufficient for -the thing a register is for, because a deviation that fails closed can still be -**unobservable**, and then nobody can tell when the transition has ended. What -actually made our non-conformance holdable was not a date and not the direction -— it was the marking plus a *measurement*: `0 of 3` signing targets resolve to a -zone, `3 of 23` routing lanes do, measured from the artifacts the runtime uses -rather than hand-counted. - -So we would state the corollary explicitly, and it is A-16 applied to A-17: -**where transitional relief is admitted, the record must distinguish an outcome -produced under the relief from the same outcome produced conformantly** — -otherwise the transition has no detectable end, and a relief with no detectable -end is a permanent rule acquired by a different route. Two rules, one shape, -which is the thing you said you had not noticed until three variants sat -together. - -**And a corollary on what replaces relief when it is refused.** When A-17 -declines a transitional deviation, what the register should acquire instead is a -*measurement*, not a *date*. A date says when someone intends to look again; a -coverage figure says how far the conformant behaviour is from being affordable, -and it moves on its own when the work happens. Ours is the worked example of the -substitution — we asked for relief, were declined, and got a Coverage column -instead — and we would rather A-17 named that remedy than left each declined -consumer to invent it. - -**Is two cases thin?** Yes, and here is the honest limit from inside one of -them: our case and key-cape's differ in more than direction of failure. Ours is -a *runtime* stance about an unreachable engine; theirs is an *assertion -provenance* question about a claim. A-17 reads as though the two are the same -kind of thing because both are transitional, and we cannot tell from one case -whether the rule survives a transition that is neither a runtime failure mode -nor a claim. We would not block graduation on that, but we would not count -ourselves as two independent data points either. - -## One thing today's work adds, unprompted - -You wrote that a register which does not recompute its figures makes every entry -a hand-count in a place that cannot detect staleness. Our published coverage -block went stale within twelve days: the routing-lane denominator moved from 30 -to 38 as lanes were added, so `unknown` went 18 → 20 and `not_applicable` 12 → -15 while `resolved` stayed at 3. Nothing about our classification changed. The -only reason it was caught is the test asserting the published block equals what -`scripts/report_coverage.py` measures — the test you called the better half of -the pair, and it earned that today rather than in principle. Corrected figures -sent separately. - -Classification did not regress. The denominator moved. That distinction is -itself an A-16 case: `unknown: 20` reached by "we classified less" and -`unknown: 20` reached by "there is more to classify" are the same number with -different meanings, and a Coverage column that carries only the number cannot -tell them apart. §13.1 may want the two populations dated, or the delta -attributed, if the column is to be read over time rather than at a point. - -## References - -- gate-house `a5a1bcf` — INTENT.md Core Rules 16–17, ArchitectureBlueprint A-16/A-17 -- GH-DEC-2026-011 — the declined transitional `fail_open`; §13.1 Coverage column -- `pep-stance.yaml` — the marked cell, `on_apply`, `classification_coverage` -- `docs/adr/ADR-0009` — stance map rule of record -- `WARDEN-WP-0040` — the route out of the marked cell -- `history/2026-09-09-layer-model-v08-review.md` — the v0.8 review this follows diff --git a/history/2026-10-08-oidc-rp-follow-through-review.md b/history/2026-10-08-oidc-rp-follow-through-review.md deleted file mode 100644 index f254ee4..0000000 --- a/history/2026-10-08-oidc-rp-follow-through-review.md +++ /dev/null @@ -1,161 +0,0 @@ -# Review — `oidc-browser-relying-party` 1.0 `follow_through` (WARDEN-WP-0044-T01) - -Reviewed: net-kingdom `security-case-types/oidc-browser-relying-party/type.yaml` -(1.0, proposed), against canon `security-case-types_v0.1` and ops-warden -`ADR-0001`, `ADR-0002`, `ADR-0004`, `ADR-0005`, `ADR-0008`, and `ADR-0010`. -Reviewer: ops-warden, 2026-10-08. Feeds net-kingdom `NK-WP-0048-T05`. - -**Verdict:** the step order, the attended grouping, and the rollback are sound. -Before ops-warden can plan a run of this type (`WARDEN-WP-0044-T02`), the schema -needs the changes R1–R3. R4–R7 are refinements. - -## Required - -**R1 — Bind each step to a catalog lane.** Add a `lane: ` field to -every follow-through step. `warden case plan` resolves each step through the -catalog, and it refuses a step whose lane is missing or not reviewed. That is -the T02 acceptance test. Today the steps name their auth only in prose, so -ops-warden cannot tell which lane applies. Two lanes do not exist yet: - -| Step | Lane needed | Owner | -| --- | --- | --- | -| `ensure-groups` | identity-provisioner caller credential for `POST /v1/groups/ensure` / `revoke` | net-kingdom | -| `mint-secret` | broker grant that **generates a value in custody** at `{secret_ref}` | railiance-platform | - -`key-cape-oidc-login` already covers the attended operator session. - -**R2 — Define `warden_executes: true` narrowly.** It should mean: ops-warden -invokes the owner's declared entry point, through the existing caller proxy and -under the caller's identity. It must not mean that ops-warden holds a credential. -For `ensure-groups`, the "identity-provisioner service token … via the ops-warden -proxy" has to be a token the caller obtains through its own lane. If ops-warden -held a standing service token, it would be a broker, which `ADR-0002` rules out. -Please write this definition into the canon field description, not only into -this type. - -**R3 — Generated secrets must never pass through the run.** `mint-secret` has to -generate the value inside the broker and custody. It must never be generated in -the run and written to custody afterwards. Add `value_handling: none | -generated_in_custody` per step, and refuse any step that would put a value on -ops-warden's stdout, argv, or receipt (`ADR-0004`, `ADR-0008`). -`custody-allowed-to-consumer` remains a capabilities-only check. That is correct. - -## Refinements - -**R4 — Per-case catalog entries.** The `catalog-entry` materialization item -(`oidc-client-{client_id}`) creates one route per case. `WARDEN-WP-0044-T04` and -`ADR-0001` point the other way: the type contributes one reviewed, parameterized -entry, and each case adds values, not routes. Proposal: drop `catalog-entry` -from `materialization`, and let the type declare a single `oidc-client` lane -whose path template uses `{client_id}`. The existing hand-built -`rapp-qonto-keycape-client` entry would later become a case of the type. - -**R5 — Make ESO delivery a gate.** `bind-env` needs the delivered Secret to -exist, and KeyCape fails closed at start when it does not. Add a per-step -`ready_check` (or a verification item) after `deliver-secret`: ExternalSecret -`sso/keycape-{client_id}-client` reports `Ready=True`. This checks status only -and never reads the value. Without it, the attended session can start before -the Secret has been delivered. - -**R6 — Membership comes only from the accepted record.** In `ensure-groups`, the -case controls the members (`initial_members`), and it runs unattended. The run -must pass exactly the memberships in the accepted case record and refuse -anything else. Group membership is an access grant. - -**R7 — Missing verifications.** Add `mfa-pinned` (when `mfa_required == "true"`, -the KeyCape registration carries the MFA pin, checked through the non-secret -client view) and `secret-delivered` (R5). Every item keeps -`reads_secret_value: false`. - -## Not in scope for ops-warden - -`bind-env` and `register-client` are attended, and their owner is net-kingdom. -The justification is accepted: the live registry is a Kubernetes Secret. -ops-warden groups those steps into one `key-cape-oidc-login` session and does -not execute them. `hand-over` belongs to the consumer. - -## Addendum 2026-10-09: R4 reconciled with ops-mason item 5 - -net-kingdom pointed out that R4 conflicts with MASON-WP-0010-T01 item 5 -(per-case `{consuming_service}-keycape-client`). On reflection, R4 was wrong -as written. **R4 is withdrawn and replaced with R4′.** - -A single parameterized entry would have to carry `workload_ref: not-applicable`, -as the generic `openbao-api-key` template does. That would drop the consumer's -workload reference, and `ADR-0009` needs that reference to select the zone and -PEP failure mode for this exact client. A per-case entry carries real -per-consumer data. It is not a second copy of a procedure, so `ADR-0001` is not -engaged. - -**R4′:** keep one catalog entry per case, named -`{consuming_service}-keycape-client` (ops-mason's item 5). Template its -**body** in the type, as item 3 now requires for policies, so that cases add -values and never route design (WARDEN-WP-0044-T04). The body should contain: - -- `owner_repo: key-cape`, `warden_executes: false`, `risk: high`, - `status: active` once verification passes -- `workload_ref: {applicability: applicable, name: {consuming_service}, - declaration_ref: }`. If the consumer has not published - a declaration, the field takes the explicit unknown form, never an inferred one. -- `path_template: platform/workloads/keycape/{client_id}`, field `CLIENT_SECRET` -- `delegation: {mode: native, intended_owner: key-cape}` -- `rotation` steps referencing the type's mint-secret and verification, with - `automatable: false` until a run has been proven - -Correction to R4: the existing `rapp-qonto-keycape-client` is a -client-credentials workload client at a different path. It is not a browser -relying party, so it would **not** become a case of this type. Only the naming -precedent carries over. - -## Re-check 2026-10-09: revised 1.0 against R1–R7 - -`warden case check-type` run on the revised type: `schema_ok=True`. R1 (`lane`), -R2 (canon definition of `warden_executes`), R3 (`value_handling`), R4′ -(templated `catalog-entry` body), R5 (`ready_check` on `deliver-secret`), R6 -(exact memberships in `ensure-groups`) and R7 (`secret-delivered`, -`mfa-pinned`) are all present. Before T02 can plan a run, two lanes must -become reviewed catalog entries: `identity-provisioner-groups` (owner -net-kingdom) and `keycape-client-secret-generate` (owner railiance-platform). -ops-warden has no further required changes to the type. - -## Addendum 2026-10-09: R8, `ensure-groups` has no caller-identity path - -NK-WP-0048-T08 handed the ops-warden route for the provisioner's service token to -WARDEN-WP-0044-T01. That route cannot be written as the type declares it. - -The `ensure-groups` step says `warden_executes: true`, auth "the caller's own -identity-provisioner credential through its lane; ops-warden invokes it via the -caller proxy and holds no token". The deployed provisioner has no per-caller -credential: - -- `identity-provisioner/server.py` compares every `POST` bearer against one - `PROVISIONER_SERVICE_TOKEN` -- that token is user-engine's runtime secret - (`platform/workloads/user-engine/runtime`, property `PROVISIONER_TOKEN`, - delivered by `sso-mfa/k8s/user-engine/openbao-runtime.yaml`) -- NetworkPolicy `sso/identity-provisioner` admits port 8080 only from - user-engine pods, plus traefik for the `/setup/password` Ingress - -A catalog lane over this would have to fetch user-engine's workload secret and -reach the service from outside its admitted peers. The operator would be acting -as user-engine, not as themselves. That breaks canon §3's narrow -`warden_executes` (caller identity, no standing credential) and ADR-0002. So -ops-warden adds no `identity-provisioner-groups` entry in this form. - -**R8:** net-kingdom chooses one of these: - -1. **Attended, in the existing session (recommended).** Run `groups ensure` in - the attended session that already runs `register-client`, through the same - contained kubectl (an exec into the provisioner pod, or a contained one-shot - job), with `warden_executes: false`, `attended: true` and lane - `key-cape-oidc-login`. This adds no attended session, because the type - already needs one, and nothing new has to be built. -2. **Give the provisioner a caller credential.** Add an operator-scoped - credential separate from user-engine's, such as an OIDC caller token - verified against KeyCape or an OpenBao-issued per-operator token, plus an - admitted ingress path for it. Then `identity-provisioner-groups` becomes a - real caller-proxy lane and `warden_executes: true` holds. This is more work, - and it is the shape the type describes today. - -Until one of these lands, `warden case check-type` keeps reporting the lane -missing, which is correct. diff --git a/history/2026-10-31-oidc-service-client-follow-through-review.md b/history/2026-10-31-oidc-service-client-follow-through-review.md deleted file mode 100644 index a391c9e..0000000 --- a/history/2026-10-31-oidc-service-client-follow-through-review.md +++ /dev/null @@ -1,202 +0,0 @@ -# Review — `oidc-service-client` 0.1 `follow_through` (canon §8 owner review) - -Reviewed: net-kingdom `security-case-types/oidc-service-client/` (0.1, -proposed, net-kingdom commit `5d5f15d`): `type.yaml`, `README.md`, -`examples/nk-bind-executor-approval.yaml` and `decision/packet.md` (memo -`nk-wp-0056-t14-oidc-service-client-r1`, hub decision `f8044f81`). Compared -against `oidc-browser-relying-party` 1.1 and this repo's earlier review -(`history/2026-10-08-oidc-rp-follow-through-review.md`, R1–R8, R4′). Also checked -against the routing catalog (`registry/routing/catalog.yaml`), the -`keycape-client-secret-generate` playbook, and railiance-platform -`scripts/keycape_client_secret_generate.py` (source read only). -Reviewer: ops-warden, 2026-10-31. Request: hub message `44400f30`. - -**Verdict: change needed before acknowledgement.** The step order, the -value handling, and the custody design are sound, and they follow the browser -type's reviewed shape. A native per-call read with no Secret copy beside the -consumer is better than ESO delivery into the consumer's namespace. Three type -changes (R5, R6, R7) are needed. One external blocker (R1) must be recorded in -the type and memo, because `mint-secret` cannot run today. R8–R10 are -questions for net-kingdom, and the founder should see their answers in memo r2. - -## Lanes and value handling - -**R1 — `mint-secret` lane does not admit this type yet. Verdict: change needed -(record it as a blocker; the fix belongs to railiance-platform).** The lane -`keycape-client-secret-generate` is active, but the broker behind it refuses -this type in two places: - -- `ACKNOWLEDGED_TYPES` contains only `oidc-browser-relying-party@1.1`, pinned by - source hash and parameter fingerprint. Any `oidc-service-client@0.1` case - exits 2 with `case_type_unacknowledged`. -- `check_case` requires `parameters.client_type == "confidential"`. This type - has no `client_type` parameter, because every service client is confidential. - The case therefore exits 2 with `case_client_not_confidential`, even after the - type is admitted. - -railiance-platform needs a separately reviewed admission binding for -`oidc-service-client@0.1`. That binding needs: - -- the type's pin and parameter fingerprint; -- a confidential check that follows from the type, not from a parameter. - -The broker's own comment already asks for this ("a future … entry requires real -acknowledgment and a separately reviewed admission binding"). This should not be -worked around by adding a fixed `client_type` parameter to the type just to -satisfy the check. - -Until that binding lands, the type, README and memo should state that -`mint-secret` is blocked on railiance-platform admission. Once railiance-platform -confirms, ops-warden will add this type to the catalog entry's `need_keywords` -and to "Used by" in the playbook. - -**R2 — `mint-secret`: `warden_executes: true`, `attended: false`, -`generated_in_custody`. Verdict: ok, with one clarification.** This matches the -narrow canon §3 definition (R2 of the browser review). ops-warden starts the -owner's broker command as a child process in the caller's own environment and -holds no token. The value is generated inside custody, written once with CAS 0, -and verified from metadata only. - -The clarification: `attended: false` means "no interactive change to a live -system". It does not mean "no human". The issuer role needs a -`net-kingdom-admins` aal2 OIDC login, and the broker refuses `WARDEN_AGENT_ID` -and root. So under NK-WP-0056, the bounded executor cannot mint its own client -secret. That is correct, and the README should say it. - -**R3 — `deliver-secret`. Verdict: ok.** The ESO role is bound to -`external-secrets/external-secrets` only, with a 15m TTL, one policy, and no -default policy. It reads one path, with no metadata or list access. The -`ready_check` reads status only, which matches browser R5. The step has no -`lane` field, the same as browser 1.1. The draft catalog lane -`platform-secret-delivery` (CATALOG-1, 2026-10-17) belongs to the browser type's -1.3 work. Adopt it here only when browser 1.3 does. - -**R4 — `bind-env` and `register-client` attended, in one `key-cape-oidc-login` -session. Verdict: ok for 0.1; see R9 for the forward path.** Both justifications -are accepted for the same reasons as the browser type: - -- The live client registry is a Kubernetes Secret. -- KeyCape resolves the secret at start and fails closed. -- `bind-env` runs before `register-client` in one session. -- The run goes in a quiet window. -- `keycape check-config` runs before the restart. - -ops-warden groups these steps and does not execute them. The type also lists the -consumer's possible actors (another workload in the namespace, a coding agent, -a compromised consumer), and it attends exactly the steps that touch a Secret. - -## Catalog entry - -**R5 — Rotation in the catalog-entry body points at a lane that refuses -rotation. Verdict: change needed.** The body says `rotation: mint-secret then -verification`, and the README says "Rotation is mint-secret, then the consumer's -next exchange". But `keycape-client-secret-generate` covers first generation -only. It refuses an existing path (exit 3), and its own catalog entry routes -rotation to railiance-platform as a task that does not exist yet. - -Change the body to: - -- `rotation: {method: rotate, owner: railiance-platform, automatable: false}`; -- a step that says no rotation lane exists yet, and that a rotation is a CAS - write at the custody path plus an attended KeyCape restart (KeyCape reads the - secret at start). - -Correct the README limit to match. Writing a lane name that does not exist into -every per-case entry is the failure mode ADR-0001 warns about. - -**R6 — Entry name `{consuming_service}-keycape-{client_id}`, and the missing -reader fields. Verdict: ok on the name; change needed on the body.** - -*Name.* Adding `{client_id}` is right for this type. The type allows one -audience per case, so one consumer can hold several cases. The browser shape -`{consuming_service}-keycape-client` would collide on the second one. The -example yields `bounded-executor-keycape-nk-bind-executor-approval`, which is -unique and readable. `workload_ref` can point at the published declaration -`net-kingdom/tools/service-identity/declarations/bounded-executor.yaml`, so the -explicit unknown form is not needed for this case. - -*Body.* The body omits the fields that tell `warden route` / `warden access` -how the consumer reads the secret, and that there is no human fetch path. Add: - -- `lane: secret` -- `auth_method: "Kubernetes auth role workload-kv-read-keycape-{client_id}-{consuming_service}, ServiceAccount {consumer_namespace}/{consumer_service_account}, audience openbao, 15m; KeyCape reads through ESO role external-secrets-keycape-{client_id}"` -- `exec_capable: false` and no `fetch_command`. Only the workload reads the - secret. Without this, a catalog consumer could infer an operator - `bao kv get` route, the way `rapp-qonto-keycape-client` has one. -- `need_keywords` drawn from the case: `{consuming_service}`, `{client_id}`, - `{audience}`, `client-credentials`, `service-token`. Keep them case-specific, - as with the `keycape-client-secret-generate` lesson: broad phrases outrank - named lanes. -- `subsystem: KeyCape + OpenBao` - -`owner_repo: key-cape` and `delegation: native, intended_owner key-cape` are -consistent with browser R4′. Keep them. - -## Verification - -**R7 — The claim "no Secret copy in the consumer's namespace" does not hold for -the first case, and nothing verifies it. Verdict: change needed.** In -`nk-bind-executor-approval`, `consumer_namespace` is `sso`. That is the same -namespace where `deliver-secret` writes `sso/keycape-{client_id}-client` for -KeyCape. So the copy does exist beside this consumer. It is acceptable only -because ServiceAccount `sso/nk-bind-executor` has no Secret access: its Role -covers `deployments/authelia` and `externalsecrets/authelia-secrets` only. The -type relies on that fact but never checks it. Add: - -- verification `consumer-cannot-read-delivered-secret`: - `kubectl auth can-i get secrets/keycape-{client_id}-client -n sso --as system:serviceaccount:{consumer_namespace}:{consumer_service_account}` - must return `no`. Also check `list` and `watch` on secrets in `sso`. Set - `expect: deny` and `reads_secret_value: false`. -- in the README threat model, change "no Secret copy in the consumer's - namespace" to "the consumer has no Secret read anywhere; when - `consumer_namespace` is `sso`, the KeyCape copy is beside it and is protected - by RBAC alone". - -**R8 — `service-token-claims` is reported by the consumer. Verdict: question.** -"Claims only; the verifier reads no secret and no token" is the right boundary, -and ops-warden accepts consumer-reported claims as evidence. But the consumer is -also the party under test. Two questions: - -1. Can a second, consumer-independent check be added that reads the non-secret - KeyCape client view in the attended `register-client` session? It would - confirm `grant_types` = `client_credentials` only, no redirect URIs, and that - `serviceSubject`, `tenant`, `audience`, `scopes` and lifetime match the case. - The browser type does this with `mfa-pinned`. -2. Is `keycape verify-client` (key-cape `docs/native-authentication.md`) the - intended owner command for `service-token-claims` and `excess-scope-refused`? - It already reports a positive exchange and excess-scope denial without - printing tokens. If so, name it as the entry point, so the consumer's report - has a fixed shape. For the bounded executor, also say whether these test - exchanges need their own admission. Under NK-WP-0056 the executor acts only - under a consumed admission. - -## Ownership and forward path - -**R9 — Attended lane: `key-cape-oidc-login` or `keycape-attended-case-change`. -Verdict: question.** Since 2026-10-17 (OW-1.3-C1) the catalog has had a draft -`keycape-attended-case-change` lane, with `key-cape-oidc-login` as its -`session_lane`. It separates the change authority from the login. 0.1 correctly -follows the admitted browser 1.1. Please confirm that 0.1 moves to the new lane -in step with the browser type once that lane is promoted, and not on its own. - -**R10 — `hand-over` owner. Verdict: question.** -`sso-mfa/k8s/bounded-executor/rbac.yaml` says the executor's packaging and apply -belong to `rapp-bounded-executor` (ADR-0015, founder decision `6d1adce9`). The -first case sets `consumer_owner_repo: net-kingdom`. If the consumer's -configuration (issuer, client, audience, OpenBao role) ships in the rapp, then -`consumer_owner_repo` should be `rapp-bounded-executor`. It names both the -`hand-over` owner and the rollback owner. - -## Not in scope for ops-warden - -Not reviewed here: - -- `materialization` of the policies and roles: railiance-platform CCRs, as for - the browser type; -- the scope set `approval:create,approval:read,approval:consume`, and whether - one workload should both create and consume approvals: approval-engine and the - founder decide that; -- the `audience`-to-scope check named under "Known limits". - -The rollback order is sound: KeyCape before custody, roles before policies, and -the ExternalSecret before the path. diff --git a/intakes/intakes.md b/intakes/intakes.md deleted file mode 100644 index aaebce3..0000000 --- a/intakes/intakes.md +++ /dev/null @@ -1,164 +0,0 @@ -# Intake records - -## WARDEN-IN-0001 — Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule demarcation - -```yaml -id: WARDEN-IN-0001 -kind: intake -title: 'Assent requested: Staff layer, doctrine vs runbook, and the access lane/rule - demarcation' -status: closed -outcome: assented -origin: cross-repo -origin_ref: gate-house GH-DEC-2026-001 -priority: medium -owner: ops-warden -requested_by: gate-house -standard: net-kingdom/canon/standards/security-layer-model_v0.1.md -description: 'gate-house asks ops-warden to assent to three boundary items. (1) ops-warden - is Staff, bound by the rule that Staff acts only through Engine APIs and never touches - Tooling directly (standard section 5). (2) Doctrine versus runbook: the NetKingdom - Security Literacy section in ops-warden INTENT is evidence the security curriculum - had no owner; it now has one in gate-house. Proposal is that doctrine and curriculum - move to gate-house and that section becomes lane-specific runbooks referencing gate-house - doctrine rather than restating it. ops-warden keeps the lanes it stewards and everything - operational about them. (3) The access lane/rule demarcation, normative in standard - section 8: ops-warden and ops-mason own access lanes — how a worker reaches a host; - access-engine owns access rules — whether they may. This demarcation is the condition - attached to renaming flex-auth to access-engine, so ops-warden effectively holds - a veto on that name. Also requested: add gate-house to the Security Literacy and - routing tables — currently every plane is listed and gate-house appears nowhere - — routing doctrine and authority-model questions there while continuing to route - policy decisions to access-engine. If moving the curriculum out leaves ops-warden - unable to instruct its own workers, say so; the boundary is wrong if it does.' -notes: 'Assented to all three items in ADR-0010, with reasoning in - history/2026-08-28-security-layer-model-assent.md. (1) Staff accepted; the section 5 - binding rule exposed a real non-conformance — src/warden/vault.py is a direct - OpenBao client performing a write, as is warden desk''s bao kv put. Declared in - INTENT.md as an engine gap with intended owner secrets-engine and blocker "no engine - exposes an SSH-CA surface", not negotiated as an exemption; taint.py declared under - the read-only allowance; warden access proxies run under the caller''s identity. - An amendment is offered back to gate-house: a second sanctioned shape in section 5 for - a declared engine gap carrying intended owner, blocker and review date, machine-readable - so section 10 can tell a tracked gap from an undeclared violation. (2) Doctrine versus - runbook accepted; the literacy section is now a lane routing runbook referencing - gate-house doctrine. Answering gate-house''s test question: it does not leave ops-warden - unable to instruct its workers, because what instructs them is warden plan / warden route - and .claude/rules/credential-routing.md, which stays inline by design. (3) The lane/rule - demarcation assented unconditionally and the access-engine veto not exercised — ops-warden - already consumes decisions and renders none. One request on sequencing only: a deprecation - window in which both names resolve (598 references across 82 files here). gate-house added - to the routing tables in INTENT.md and SCOPE.md.' -created: '2026-08-28T19:30:28.087109Z' -updated: '2026-08-28T21:05:00Z' -state_hub_intake_id: "01a049ed-bbbc-7520-bc7c-6b0912ca534a" -``` - -## WARDEN-IN-0002 — Review requested: security layer model v0.3 — and does maturity-engine absorb warden route gaps? - -```yaml -id: WARDEN-IN-0002 -kind: intake -title: 'Review requested: security layer model v0.3 — and does maturity-engine absorb - warden route gaps?' -status: closed -outcome: answered-mirror -resolution: 'Routing and the delegation facts STAY; maturity-engine MIRRORS read-only via warden route gaps --json; assent to the SSH-CA gap being tracked there. Answered 2026-09-30.' -origin: cross-repo -origin_ref: net-kingdom security-layer-model_v0.3 -priority: medium -owner: ops-warden -requested_by: gate-house -description: 'v0.3 is proposed and changes sections 4, 9 and 13 only; the v0.2 assent - record stands. Two new engines: approval-engine (section 9.4) and maturity-engine - (section 9.5). THE QUESTION FOR YOU concerns section 5.3, which exists because you - offered the amendment. v0.3 gives declared gaps an owner: maturity-engine takes - the gap register with intended_owner, blocked_on and review dates, and section 13 - now says the register in the standard is interim and should not outlive that engine. - You offered warden route gaps and the 27 delegation catalog entries as reusable - prior art. So the question is whether that machinery should MOVE, be MIRRORED, or - STAY. Our tentative reading, which we want tested rather than accepted: routing - is yours and stays yours — warden route find answers where a credential need goes, - and that is lane knowledge, not maturity. What might move is the readiness half: - whether a declared gap is still within its review date, and whether an intended - owner has an engine surface yet. If splitting those creates two sources for one - fact, that is worse than either option and we would rather hear it now. Your SSH-CA - signing write would be tracked in maturity-engine as a declared gap with intended - owner secrets-engine and a review date — that is reporting your own non-conformance - to an engine, so we would rather you assent to it than discover it. Also note approval-engine - (section 9.4): it owns the approval object, not the approval workflow, so ops-warden - lanes needing approval consume a claim rather than implementing one. Assent, revision, - or rejection acceptable.' -created: '2026-08-28T20:40:24.957468Z' -updated: '2026-08-28T20:40:24.957468Z' -state_hub_intake_id: "01a04d97-94cd-7b49-8019-a91c7fce8adb" -``` - -## WARDEN-IN-0003 — FLEX-WP-0020 handoff: flex-auth → access-engine repository rename, credential-route surface - -```yaml -id: WARDEN-IN-0003 -kind: intake -title: 'FLEX-WP-0020 handoff: flex-auth to access-engine repository rename, credential-route - surface' -status: closed -outcome: verified-no-change -resolution: 'Rename landed 2026-09-30; warden route find flex-auth-policy-check resolves under both names, no catalog change needed (e6ef060); owner_repo stays flex-auth per FLEX-DEC-2026-013.' -origin: cross-repo -origin_ref: flex-auth FLEX-WP-0020 (message 0a1956c5) -priority: medium -owner: ops-warden -requested_by: flex-auth -surface: credential-route -description: 'flex-auth is preparing the repository-coordinate rename flex-auth -> - access-engine. The repository UUID fda8ad85-a7d7-4055-8f21-902a533e59df and Forge - ID 42 are unchanged; RUNTIME names stay flex-auth per FLEX-DEC-2026-013. The ask: - review registry/routing/catalog.yaml''s owner repository field so routing still - resolves after the rename, create a live record on this side, and reply with its - id. No secret requested, none returned. The message is explicitly not completion - evidence.' -notes: 'REVIEWED 2026-09-21; one change made, one deliberately not made. - - SCOPE. Exactly one catalog lane names flex-auth as owner_repo: flex-auth-policy-check - (authorization decision — may this actor perform this action), warden_executes false, - route-only, no credential flows. Two other lanes mention flex-auth without owning - anything by it: flex-auth-decision-envelope-signing-key is owned by railiance-platform, - and ten lanes carry a policy_ref string of the form "flex-auth check secret.read:". - - CHANGED. access-engine added to flex-auth-policy-check''s need_keywords, so the lane - resolves under both names from today rather than from the rename. That is the - deprecation window ops-warden asked for in WARDEN-IN-0001 and is the only change - routing needs: warden route find matches on keywords and lane id, neither of which is - a repository coordinate. - - NOT CHANGED, and this is the finding worth having. owner_repo is NOT purely a - repository coordinate in this catalog. src/warden/policy.py sends it verbatim as - resource.system, and again as context.owner_repo, on every flex-auth /v1/check. - FLEX-DEC-2026-013 keeps runtime names as flex-auth, so flipping owner_repo at rename - time would rename a policy resource the PDP matches on, under cover of a repository - rename — a runtime change wearing a coordinate change''s clothes, and one that would - fail at the check rather than at the edit. It flips when the runtime name flips, not - when the repository does. If access-engine intends resource.system to follow the - repository rather than the runtime, that is a policy question for flex-auth to answer - before the rename lands, not an inference for ops-warden to make from its own catalog. - - The lane id flex-auth-policy-check, the subsystem string, and the policy_ref strings - are runtime-facing for the same reason and are unchanged. - - Related and unresolved: WARDEN-WP-0039-T03 already waits on the credential proxy''s - admitted policy binding, which is the same resource.system field seen from the - delegated-read side. The rename must not be allowed to answer that question by - accident. - - - RENAME LANDED 2026-09-30 (flex-auth msg 2026-09-30; access-engine head b943d9a). - Verified after the landing: warden route find resolves flex-auth-policy-check under - both "access-engine" and "flex-auth" keywords; no catalog edit was required. The - open question above is answered by FLEX-DEC-2026-013 (runtime names, including the - policy resource.system string, stay flex-auth), so owner_repo/subsystem stay - flex-auth until the runtime name changes. If flex-auth intends otherwise it must - say so explicitly. Intake stays open for the owner to close.' -created: '2026-09-21T00:00:00Z' -updated: '2026-09-30T00:00:00Z' -state_hub_intake_id: "01a0c153-98e1-7705-bf82-a58c85df980f" -``` diff --git a/layer.yaml b/layer.yaml deleted file mode 100644 index a944997..0000000 --- a/layer.yaml +++ /dev/null @@ -1,156 +0,0 @@ -# ops-warden — DERIVED form of the NetKingdom security layer declaration -# -# THIS FILE DOES NOT GOVERN. The declaration is the `layer:` key in INTENT.md's -# frontmatter; this file is a derived artifact under §11's derived-artifact rule -# and must agree with it (GH-DEC-2026-017 §1, amendment A11). A disagreement -# between the two forms is a finding in its own right and is reported, never -# resolved away by precedence. -# -# Framework: net-kingdom/canon/standards/security-layer-model -# Assent: docs/adr/ADR-0010 (ops-warden's own voice, per §11 "who must declare") -# Validate: python3 scripts/check_layer_conformance.py -# -# §11 makes one check mechanical: "every direct Tooling client in a Staff -# repository maps to a declared §5.1, §5.2, or §5.3 entry". This file is that -# map. It is machine-readable because §5.3 requires it to be — ops-warden -# proposed that shape and is implementing it rather than declaring in prose. -# -# Conformance rule inherited from tenancy.yaml: accuracy, not altitude. A -# declared gap is TRACKED NON-CONFORMANCE (§11), never a claim of conformance. - -schema_version: "0.2" -framework: netkingdom-security-layer-model - -# §11 derived-artifact marking (GH-DEC-2026-017 §1 / A11). -derived: true -derived_from: INTENT.md - -# NO `standard_version` KEY, AND DO NOT ADD ONE BACK. -# GH-DEC-2026-017 §5 / amendment A12: a layer declaration MUST NOT carry a -# standard version. The declared layer is a standing property of the repository -# and does not change when the standard is revised; a version here makes every -# revision read as though it invalidated every declaration. Version-scoped state -# belongs in the derived conformance record, which already must carry the -# version or commit it was derived at. Keeping the field "for information" was -# declined explicitly: a field that is present will be branched on. Absence is -# enforced by tests/test_layer_conformance.py. - -repository: ops-warden - -# RULED, 2026-09-21 (GH-DEC-2026-017 §2): INTENT.md declares `Staff` and this -# file declares `staff`, and THAT IS NOT A DISAGREEMENT. §3's vocabulary is -# closed — {Taxonomy, Tooling, Engine, Staff} — and comparison is ASCII -# case-insensitive: a conformance run MUST fold case before comparing, and a -# lowercase declaration is conforming, not tolerated. No repository was asked to -# re-spell anything. Do not "fix" this line to match INTENT.md, or the reverse; -# folding case is the checker's job, and it does it. -layer: staff -declared_by: docs/adr/ADR-0010 -declared_at: "2026-08-29" - -# §6.4 — ops-warden is PEP-shaped (it causes a protected side effect: issuing a -# certificate). Its unreachable-engine stance map is published separately, and -# asserted equal to shipped behaviour by tests/test_layer_conformance.py. -pep_stance: pep-stance.yaml - -# Every direct contact with a Tooling-layer system (§4), one entry each. -tooling_contacts: - - - id: ssh-ca-signing-write - shape: "5.3" # declared engine gap - module: src/warden/vault.py - symbol: VaultCA.sign - tooling: OpenBao - operation: "HTTP POST /v1//sign/ with X-Vault-Token" - write: true - capability: "Sign a short-lived SSH certificate for an adm/agt/atm actor" - intended_owner: secrets-engine - blocked_on: >- - No engine exposes an SSH certificate signing surface. secrets-engine owns - credential abstraction, custody and lifecycle, which is the layer this - belongs in, but fronts no SSH-CA API today. - review: "2026-11-28" - note: >- - Production-verified and in daily use. This is the one lane ops-warden owns - permanently (§4). Signing continues while the gap is open: refusing would - remove production host access to close a documentation gap. - - - id: desk-paste-once-provision - shape: "5.3" - module: src/warden/desk.py - symbol: _provision_to_openbao - tooling: OpenBao - operation: "bao kv put =- (value on stdin, never argv)" - write: true - capability: "Founder paste-once provisioning of a secret straight into OpenBao" - intended_owner: secrets-engine - blocked_on: >- - No engine exposes an attended provisioning surface for a value the founder - holds and no automated path can produce. - review: "2026-11-28" - note: >- - Attended and founder-operated (WP-0029). The value reaches OpenBao without - passing through a terminal, an argv, or the audit log. - - - id: taint-metadata-read - shape: "5.1" # read-only diagnostic observation - module: src/warden/taint.py - symbol: fetch_taint_status - tooling: OpenBao - operation: "bao kv metadata get -format=json " - write: false - capability: "Report EXPOSED taint (custom_metadata) without reading secret data" - intended_owner: secrets-engine - blocked_on: >- - No engine exposes a disclosure-taint query. Metadata-only by construction — - reading the data would be the 2026-07-16 vector this exists to avoid. - review: "2026-11-28" - - - id: access-proxy-conduit - shape: "5.2" # conduit - module: src/warden/proxy.py - symbol: proxy_fetch, proxy_attended_login_exec - tooling: OpenBao, key-cape - operation: "Runs the catalog-declared owner fetch_command as a child process" - write: false - capability: "warden access --fetch/--exec/--out/--wrap for exec_capable lanes" - supplied_authority: none - evidence: - no_own_credential: src/warden/proxy.py::_caller_env - test: tests/test_proxy.py::test_conduit_supplies_no_authority_of_its_own - audit: "audit.jsonl records the caller, the lane, and the outcome; never a value" - note: >- - The §5.2 test is the supplied-authority property: ops-warden presents no - credential of its own, cannot widen what the caller could already do, and - the action reconstructs as the caller's. Governed by ADR-0002. - - - id: caller-identity-token - shape: "5.2" - module: src/warden/caller_identity.py - symbol: resolve_caller_token - tooling: OpenBao - operation: "Runs the operator-configured caller_auth command, or reads token env" - write: false - capability: "Establish the caller's own identity for the pre-sign policy gate" - supplied_authority: none - detection: voluntary # runs an operator-configured command, so no fixed - # argv shape to scan for; declared rather than omitted - note: >- - Obtains the CALLER's credential by the operator's configured means; adds no - authority. Never mints, and never persists what it resolves. - -# Contacts that are deliberately NOT Tooling contacts, recorded so the check is -# total rather than silently selective. -non_tooling_clients: - - - module: src/warden/policy.py - target: access-engine (flex-auth) - rationale: "Engine API — §5 permits it; this is the shape §5 prescribes." - - - module: src/warden/worker.py - target: state-hub, llm-connect - rationale: >- - Not catalogued in §4. The layer catalog scopes the security estate, and - neither the State Hub nor llm-connect appears in it, so no §5 shape applies - on the standard's own terms. Raised with gate-house 2026-08-29 as a scope - question rather than resolved unilaterally — see the assessment note. diff --git a/pep-stance.yaml b/pep-stance.yaml deleted file mode 100644 index f441f88..0000000 --- a/pep-stance.yaml +++ /dev/null @@ -1,160 +0,0 @@ -# ops-warden — PEP unreachable-engine stance map -# -# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3, §9.7 -# Rule of record: docs/adr/ADR-0009 -# Validate: pytest tests/test_layer_conformance.py -k stance -# -# §6.4 obligation 3 requires a declared unreachable-engine stance that is total, -# scoped per zone, carries no implicit default and no per-call discretion, and is -# "published rather than held in code comments". §6.4 further requires every -# PEP-shaped consumer to PUBLISH its map so the maps can be inventoried. This -# file is ops-warden's, published because a map that lives only in a dataclass -# default is not published — it is merely written down. -# -# The property that makes this worth reading: it is asserted equal to the shipped -# default in src/warden/config.py (PolicyConfig.failure_modes) by -# tests/test_layer_conformance.py. A published map that may drift from the code -# is worse than none, because it invites reliance it cannot support. - -schema_version: "0.1" -framework: netkingdom-security-layer-model -# v0.7 is the accepted standard and the one in force. v0.8 is `proposed`: its -# assent round closed 2026-09-09 (GH-DEC-2026-011) with nine corrections applied, -# but the version is not yet accepted, so this declaration stays pinned to what -# binds. The one cell v0.8 would change is marked inline below rather than -# silently pre-adopted. -standard_version: "0.7" -standard_version_reviewed: "0.8" # reviewed and assented; see history/2026-09-09-layer-model-v08-review.md -repository: ops-warden -pep_shape: true -declared_by: docs/adr/ADR-0009 - -protected_action: "SSH certificate issuance (warden sign / cert_command)" -decision_engine: access-engine # flex-auth until the governed rename -scope: security-zone # security-zones_v0.1 membership of the TARGET workload - -# Total by construction: every zone in security-zones_v0.1, plus the two -# non-zone outcomes. No implicit default — an unlisted value is a config error, -# not a permissive fallback. -stance: - z0-experimental: fail_open - z1-operational: fail_open - z2-protected: fail_open - z2-continuity: fail_open - z3-critical: fail_closed - # DECLARED GAP under security-layer-model v0.8 §6.4 obligation 3 (GH-DEC-2026-011, - # net-kingdom@64394e9): unknown is not a zone and MUST resolve to fail_closed. - # ops-warden ASSENTED to that rule — we went looking for the §5.1 read-only - # diagnostic its reversal clause predicts and do not have one, because this map - # governs `warden sign`, a credential-issuing side effect. - # - # The cell has not been flipped, and the reason is measured rather than argued: - # 0 of 3 signing targets resolve to a zone, so converting today would fail closed - # on essentially every certificate whenever the engine is unreachable — including - # the certificate an operator needs to reach the host and repair it. That is - # ADR-0006's rejected configuration reached from another direction. - # - # We asked for a coverage-gated transitional fail_open and were DECLINED: a - # sanctioned transitional fail_open is indistinguishable at runtime from the - # stance the rule forbids. Our second preference was adopted instead — §13.1 now - # carries a Coverage column, and this repo's figures are its first entries. - # - # So this is tracked non-conformance with a route, not an exemption. - # Route: WARDEN-WP-0040. Register row: §13.1, marked. - unknown: fail_open # non-conformant at v0.8; see above - not-applicable: fail_closed - -# What happens when the stance is applied. §6.4 obligation 1 requires a decision -# record for a protected side effect; where the engine is unreachable there is no -# decision to hold, so ops-warden records the APPLICATION OF THE STANCE instead. -# See the assessment note: obligation 1 as written admits no such case. -on_apply: - recorded_fields: - - policy_zone - - policy_failure_mode - - policy_decision_id # present only where a decision was actually rendered - - outcome - written_to: - - "signatures log (src/warden/ca.py)" - - "audit.jsonl (src/warden/audit.py)" - never_recorded: "any secret material, any certificate private key" - -# §6.4 obligation 2 — the verdict is never cached. Input claims (zone membership, -# compiled from the flex-auth registry snapshot) are cached under their own -# freshness rules; the answer is not. -# Classification coverage, published beside the stance because v0.8 §6.4 -# obligation 3 now requires it and because ops-warden asked for the column. -# Coverage is DISCLOSURE, never a transitional licence: it does not soften this -# map's stance, does not gate it, and never makes the marked cell conformant. -# Self-measured; regenerate with `python scripts/report_coverage.py`. -# 2026-09-21 re-measure: routing lanes grew from 30 to 38 catalogued entries -# since 2026-09-09, so unknown rose 18 -> 20 and not-applicable 12 -> 15 while -# RESOLVED STAYED AT 3. Classification did not regress; the denominator moved. -# Signing targets are unchanged. Reported to gate-house per §13.1 — a figure in -# a register that cannot detect its own staleness is only as good as the last -# re-measure, and this one was stale by eight lanes before the test caught it. -# 2026-10-05: inbound-library-imap-approle added as an applicable lane without a -# published workload declaration, so routing unknown rose 20 -> 21; resolved unchanged. -# 2026-10-08: platform-supplier-credentials added as a not-applicable path pattern, -# so routing not_applicable rose 16 -> 17. -# 2026-10-20: agt-custodian-ssh-cert added as a not-applicable ceremony lane -# (WARDEN-WP-0045), so routing not_applicable rose 22 -> 23; signing target -# agt-custodian names workload custodian-agents without a published resolution, -# so signing unknown rose 3 -> 4. -classification_coverage: - measured: "2026-10-20" - attribution: self-measured - signing_targets: - resolved: 0 - unknown: 4 - not_applicable: 1 - routing_lanes: - resolved: 4 - unknown: 22 - not_applicable: 23 - -verdict_caching: none -input_claim_caching: "registry/flex-auth/production_registry_snapshot.json, rebuilt by scripts/build_flex_auth_registry.py" - -# §6.4 obligation 4 — reconstructability, bounded by §9.6. ops-warden's audit -# emission on this lane is deliberately non-atomic and therefore ATTRIBUTIVE, not -# load-bearing: no control branches on the presence of a signing record -# (`warden activity` displays it; nothing gates on it). Registered in §13. -reconstructability: - bound: "§9.6 attributive — completeness is not claimed" - declared_at: wiki/AuditTrail.md - -# §9.7.2 — the certificate-verification PEP has no active revocation channel. -# A previously allowed certificate remains usable until its enforced TTL expires, -# so the visibility deadline is exactly the maximum issued lifetime per actor type. -revocation_visibility: - boundary: "SSH certificate verification at the target host" - mechanism: ttl_expiry - revocation_channel: none - deadline_hours: - adm: 48 - agt: 24 - atm: 8 - note: >- - No CRL or KRL distribution exists. Host-side authorization belongs to - railiance-infra; shortening or actively recalling this window is a joint - design question, not an implied capability of ops-warden. - -# §9.6 — signing evidence is attributive, not load-bearing. Measurement as of -# 2026-09-04 found three signature records across 79 calendar days, concentrated -# on only two active days (1 event on 2026-06-17; 2 within 40 minutes on -# 2026-08-22). That bursty operator-driven sample cannot support a useful rate -# threshold, so cadence is explicitly deferred instead of fabricated. -emission_cadence: - classification: attributive - status: deferred - measured_as_of: "2026-09-04" - observed_window: - first: "2026-06-17T23:18:33Z" - last: "2026-09-04T21:21:30Z" - signature_records: 3 - active_signature_days: 2 - daily_counts: - "2026-06-17": 1 - "2026-08-22": 2 - reason: "operator-driven issuance is too sparse and bursty for a meaningful rate threshold" diff --git a/registry/capabilities/capability.security.access-routing.md b/registry/capabilities/capability.security.access-routing.md deleted file mode 100644 index dbab2a0..0000000 --- a/registry/capabilities/capability.security.access-routing.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: capability.security.access-routing -owner: ops-warden -status: draft -domain: infotech -maturity: - discovery: - current: D3 - target: D5 - confidence: medium - rationale: Owner scope, neighboring responsibilities, concrete use cases and limitations - are documented; no exhaustive or generalized maturity claim. - availability: - current: A3 - target: A3 - confidence: medium - rationale: 'Source interface assessed 2026-10-14: warden route and access catalog; - exact lane conduit and secret-exec routing. Availability describes the delivery - form, not production acceptance. Routing grants no authority; interim delegates - and attended high-risk lanes persist.' -external_evidence: - completeness: - level: C1 - confidence: low - basis: Source artifacts establish a bounded capability; no independent consumer-completeness - assessment in this review. - reliability: - level: R0 - confidence: low - basis: No longitudinal consumer reliability measurement established by this review. -name: Credential routing and contained access -summary: warden route and access catalog; exact lane conduit and secret-exec routing. -tags: -- security -- netkingdom -- framework-reuse -discovery: - intent: warden route and access catalog; exact lane conduit and secret-exec routing - includes: - - warden route and access catalog; exact lane conduit and secret-exec routing - excludes: - - Routing grants no authority; interim delegates and attended high-risk lanes persist - use_cases: - - 'U04: Protected operation obtains and consumes exact human approval before its - side effect' - - 'U05: Operator provisions and delivers a scoped workload credential through the - approved owner lane' - - 'U06: Operator or automation obtains a bounded SSH certificate and maintains private - connectivity' -availability: - current_level: A3 - target_level: A3 - current_artifacts: - - SCOPE.md - - wiki/CredentialRouting.md - consumption_modes: - - library - - CLI -relations: - related_to: - - capability.security.credential-lifecycle - - capability.authorization.policy-evaluate - - capability.ops.tunnel-bridge - - capability.security.structure-construction -evidence: - documentation: - - SCOPE.md - - wiki/CredentialRouting.md -consumer_guidance: - recommended_for: - - Protected operation obtains and consumes exact human approval before its side - effect - - Operator provisions and delivers a scoped workload credential through the approved - owner lane - - Operator or automation obtains a bounded SSH certificate and maintains private - connectivity - not_recommended_for: - - Routing grants no authority; interim delegates and attended high-risk lanes persist - known_limitations: - - Routing grants no authority; interim delegates and attended high-risk lanes persist - - 2026-10-14 architecture review estimates operational maturity separately; a registry - entry does not authorize execution or establish deployment. ---- - -# Credential routing and contained access - -warden route and access catalog; exact lane conduit and secret-exec routing. - -## Reuse boundary - -Routing grants no authority; interim delegates and attended high-risk lanes persist. - -## Supported use cases - -- U04: Protected operation obtains and consumes exact human approval before its side effect -- U05: Operator provisions and delivers a scoped workload credential through the approved owner lane -- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity - -## Evidence and integration - -- [SCOPE.md](../../SCOPE.md) -- [wiki/CredentialRouting.md](../../wiki/CredentialRouting.md) - -Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists. diff --git a/registry/capabilities/capability.security.case-follow-through.md b/registry/capabilities/capability.security.case-follow-through.md deleted file mode 100644 index 1268c23..0000000 --- a/registry/capabilities/capability.security.case-follow-through.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -id: capability.security.case-follow-through -owner: ops-warden -status: draft -domain: infotech -maturity: - discovery: - current: D3 - target: D5 - confidence: medium - rationale: Owner scope, neighboring responsibilities, concrete use cases and limitations - are documented; no exhaustive or generalized maturity claim. - availability: - current: A2 - target: A2 - confidence: medium - rationale: 'Source interface assessed 2026-10-14: KnownSecurityCase route, plan - and operational receipts. Availability describes the delivery form, not production - acceptance. Execution requires accepted structure and exact owner contracts; - case closure is not automatic.' -external_evidence: - completeness: - level: C1 - confidence: low - basis: Source artifacts establish a bounded capability; no independent consumer-completeness - assessment in this review. - reliability: - level: R0 - confidence: low - basis: No longitudinal consumer reliability measurement established by this review. -name: Security case operational follow-through -summary: KnownSecurityCase route, plan and operational receipts. -tags: -- security -- netkingdom -- framework-reuse -discovery: - intent: KnownSecurityCase route, plan and operational receipts - includes: - - KnownSecurityCase route, plan and operational receipts - excludes: - - Execution requires accepted structure and exact owner contracts; case closure - is not automatic - use_cases: - - 'U01: Architect composes a security deployment from explicit owner contracts and - case types' - - 'U05: Operator provisions and delivers a scoped workload credential through the - approved owner lane' - - 'U06: Operator or automation obtains a bounded SSH certificate and maintains private - connectivity' -availability: - current_level: A2 - target_level: A2 - current_artifacts: - - workplans/WARDEN-WP-0044-security-case-follow-through.md - consumption_modes: - - CLI - - source -relations: - related_to: - - capability.security.credential-lifecycle - - capability.authorization.policy-evaluate - - capability.ops.tunnel-bridge - - capability.security.structure-construction -evidence: - documentation: - - workplans/WARDEN-WP-0044-security-case-follow-through.md -consumer_guidance: - recommended_for: - - Architect composes a security deployment from explicit owner contracts and case - types - - Operator provisions and delivers a scoped workload credential through the approved - owner lane - - Operator or automation obtains a bounded SSH certificate and maintains private - connectivity - not_recommended_for: - - Execution requires accepted structure and exact owner contracts; case closure - is not automatic - known_limitations: - - Execution requires accepted structure and exact owner contracts; case closure - is not automatic - - 2026-10-14 architecture review estimates operational maturity separately; a registry - entry does not authorize execution or establish deployment. ---- - -# Security case operational follow-through - -KnownSecurityCase route, plan and operational receipts. - -## Reuse boundary - -Execution requires accepted structure and exact owner contracts; case closure is not automatic. - -## Supported use cases - -- U01: Architect composes a security deployment from explicit owner contracts and case types -- U05: Operator provisions and delivers a scoped workload credential through the approved owner lane -- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity - -## Evidence and integration - -- [workplans/WARDEN-WP-0044-security-case-follow-through.md](../../workplans/WARDEN-WP-0044-security-case-follow-through.md) - -Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists. diff --git a/registry/capabilities/capability.security.ssh-certificate-issuance.md b/registry/capabilities/capability.security.ssh-certificate-issuance.md index d2e20d9..d62d348 100644 --- a/registry/capabilities/capability.security.ssh-certificate-issuance.md +++ b/registry/capabilities/capability.security.ssh-certificate-issuance.md @@ -1,38 +1,39 @@ --- id: capability.security.ssh-certificate-issuance -name: Decision-bound SSH certificates -summary: warden sign with local and OpenBao backends and adm/agt/atm actors. +name: SSH Certificate Issuance +summary: Issue short-lived CA-signed SSH certificates for adm, agt, and atm actors through a stable cert_command CLI interface; steward operational access routing across NetKingdom security lanes. owner: ops-warden status: draft domain: helix_forge tags: -- ssh -- certificate -- ca -- ops-warden -- openbao -- security -- netkingdom -- framework-reuse + - ssh + - certificate + - ca + - ops-warden + - openbao + - security + maturity: discovery: current: D4 target: D5 confidence: medium - rationale: 'SCOPE, AccessManagementDirective alignment, config runbooks, and cert_command - contract are documented; production OpenBao integration is documented but engine - deployment lives in railiance-platform. A machine-readable routing catalog (registry/routing/catalog.yaml) - and wiki/AccessRouting.md make the "issue SSH, route the rest" boundary discoverable. - - ' + rationale: > + SCOPE, AccessManagementDirective alignment, config runbooks, and cert_command + contract are documented; production OpenBao integration is documented but + engine deployment lives in railiance-platform. A machine-readable routing + catalog (registry/routing/catalog.yaml) and wiki/AccessRouting.md make the + "issue SSH, route the rest" boundary discoverable. availability: current: A3 target: A5 confidence: medium - rationale: 'Source interface assessed 2026-10-14: warden sign with local and OpenBao - backends and adm/agt/atm actors. Availability describes the delivery form, not - production acceptance. Direct Tooling contact is a declared gap; certificate - reachability is not target authorization.' + rationale: > + Installable `warden` CLI and `ops-ssh-wrapper` entry points; ops-bridge and + other callers integrate via cert_command without backend-specific branching. + A `warden route` lookup over the pointer catalog (WARDEN-WP-0011) will move + routing discovery from wiki prose to a structured surface for agents (A3 -> A4). + external_evidence: completeness: level: C3 @@ -40,100 +41,97 @@ external_evidence: confidence: medium basis: scope_vs_intent_and_consumer_expectations satisfied_expectations: - - local and OpenBao/Vault-compatible signing backends - - TTL policy enforcement per actor type - - principals inventory and cert-side scorecard - - signatures audit log and stale-cert cleanup - - cert_command stdout contract for ops-bridge - - per-actor extension allow-list and max_ttl_hours ceiling (WARDEN-WP-0045) + - local and OpenBao/Vault-compatible signing backends + - TTL policy enforcement per actor type + - principals inventory and cert-side scorecard + - signatures audit log and stale-cert cleanup + - cert_command stdout contract for ops-bridge broken_expectations: - - host-side principal deployment not owned here - - OpenBao SSH engine mount not deployed from this repo + - host-side principal deployment not owned here + - OpenBao SSH engine mount not deployed from this repo out_of_scope_expectations: - - long-lived API key custody - - tunnel lifecycle management - - Vault/OpenBao cluster operations + - long-lived API key custody + - tunnel lifecycle management + - Vault/OpenBao cluster operations reliability: level: R2 name: Tolerable confidence: medium basis: consumer_quality_signals known_reliability_risks: - - production signing depends on OpenBao availability and token policy - - local backend requires protected CA key handling by operators + - production signing depends on OpenBao availability and token policy + - local backend requires protected CA key handling by operators + discovery: - intent: warden sign with local and OpenBao backends and adm/agt/atm actors + intent: > + Give the ops fleet short-lived SSH credentials for humans, agents, and + automations without static keys, through a single cert_command surface that + callers can rely on regardless of CA backend; route non-SSH credential needs + to the correct NetKingdom subsystems (OpenBao, flex-auth, key-cape). includes: - - warden sign with local and OpenBao backends and adm/agt/atm actors + - certificate signing for adm, agt, and atm actors + - actor principals inventory and TTL policy + - cert_command interface (`warden sign`) + - cert-side compliance scorecard and signatures log + - ops-ssh-wrapper for automatic cert acquisition + - NetKingdom credential routing and alignment documentation + - machine-readable routing pointer catalog (registry/routing/catalog.yaml) excludes: - - Direct Tooling contact is a declared gap; certificate reachability is not target - authorization + - tunnel lifecycle + - host /etc/ssh/auth_principals deployment + - OpenBao or Vault cluster setup + - long-lived secret storage assumptions: - - callers supply actor public keys; humans self-issue admin keys - - production platform uses OpenBao with Vault-compatible SSH engine API + - callers supply actor public keys; humans self-issue admin keys + - production platform uses OpenBao with Vault-compatible SSH engine API use_cases: - - ops-bridge tunnel cert_command - - Inter-Hub bootstrap short-lived agent access - - 'U06: Operator or automation obtains a bounded SSH certificate and maintains private - connectivity' + - ops-bridge tunnel cert_command + - Inter-Hub bootstrap short-lived agent access research_memos: - - ops-warden/SCOPE.md - - ops-warden/wiki/CertCommandInterface.md - - ops-warden/wiki/OpsWardenConfig.md - - ops-warden/wiki/AccessRouting.md + - ops-warden/SCOPE.md + - ops-warden/wiki/CertCommandInterface.md + - ops-warden/wiki/OpsWardenConfig.md + - ops-warden/wiki/AccessRouting.md + availability: current_level: A3 target_level: A5 current_artifacts: - - SCOPE.md - - README.md + - ops-warden/src/warden/ + - ops-warden/wiki/CertCommandInterface.md + - ops-warden/wiki/OpsWardenConfig.md target_artifacts: - - packaged ops-warden release with documented OpenBao role bootstrap - - '`warden route` lookup CLI over the pointer catalog (WARDEN-WP-0011)' + - packaged ops-warden release with documented OpenBao role bootstrap + - "`warden route` lookup CLI over the pointer catalog (WARDEN-WP-0011)" consumption_modes: - - library - - CLI + - CLI + - cert_command subprocess + relations: depends_on: [] supports: [] - related_to: - - capability.security.credential-lifecycle - - capability.authorization.policy-evaluate - - capability.ops.tunnel-bridge - - capability.security.structure-construction + related_to: [] + consumer_guidance: recommended_for: - - Operator or automation obtains a bounded SSH certificate and maintains private - connectivity + - issuing short-lived SSH certs for ops-bridge tunnels + - agent or automation access with TTL-bound principals + - checking cert-side compliance before rotation windows + - orienting dev workers on which NetKingdom subsystem owns each credential type not_recommended_for: - - Direct Tooling contact is a declared gap; certificate reachability is not target - authorization + - storing OpenRouter or Inter-Hub API keys + - replacing OpenBao deployment or host SSH hardening playbooks + - static-key-only legacy access (use ops-bridge static key mode instead) known_limitations: - - Direct Tooling contact is a declared gap; certificate reachability is not target - authorization - - 2026-10-14 architecture review estimates operational maturity separately; a registry - entry does not authorize execution or establish deployment. -evidence: - documentation: - - SCOPE.md - - README.md + - "VaultCA backend config key remains backend: vault for API compatibility" + - host-side scorecard checks live in railiance-infra --- -# Decision-bound SSH certificates +# SSH Certificate Issuance -warden sign with local and OpenBao backends and adm/agt/atm actors. +ops-warden is the custodian-domain SSH CA tool. It signs short-lived certificates, +maintains the actor inventory, and exposes `warden sign` as the cert_command +contract for ops-bridge and other callers. -## Reuse boundary - -Direct Tooling contact is a declared gap; certificate reachability is not target authorization. - -## Supported use cases - -- U06: Operator or automation obtains a bounded SSH certificate and maintains private connectivity - -## Evidence and integration - -- [SCOPE.md](../../SCOPE.md) -- [README.md](../../README.md) - -Assessment: CUST-WP-0081, 2026-10-14. Relations identify interfaces to compose; they do not certify native integration. Existing external evidence levels are retained where present. New entries use conservative C1/R0 until consumer assessment exists. +Production environments point the vault-compatible backend at OpenBao; labs use +the local ssh-keygen CA backend without platform dependencies. \ No newline at end of file diff --git a/registry/flex-auth/production_registry_snapshot.json b/registry/flex-auth/production_registry_snapshot.json index d0d6b03..cba4291 100644 --- a/registry/flex-auth/production_registry_snapshot.json +++ b/registry/flex-auth/production_registry_snapshot.json @@ -108,31 +108,6 @@ "security_zone_reason": "workload_resolution_absent" } }, - { - "id": "ssh-cert:actor/agt-custodian", - "type": "ssh-certificate", - "labels": [ - "ssh-signing", - "agt" - ], - "owner": "team:platform-security", - "attributes": { - "actor_id": "agt-custodian", - "actor_type": "agt", - "allowed_subjects": [ - "agt-custodian", - "iam:agt-custodian" - ], - "allowed_principals": [ - "agt-custodian" - ], - "max_ttl_hours": 12, - "workload_id": "custodian-agents", - "security_zone": "unknown", - "security_zone_admission": "unknown", - "security_zone_reason": "workload_resolution_absent" - } - }, { "id": "ssh-cert:actor/agt-state-hub-bridge", "type": "ssh-certificate", @@ -179,10 +154,8 @@ "max_ttl_hours": 8, "workload_id": "backup-daily", "security_zone": "unknown", - "security_zone_declared": "z2-protected", - "security_zone_admission": "unsatisfied", - "security_zone_reason": "M1_below_M2_zone_floor", - "security_zone_revision": "sha256:4b937fce814a937a034f63598f4c7f4d8a76cf92e82cada752ae31dbe5f3dc8f" + "security_zone_admission": "unknown", + "security_zone_reason": "workload_resolution_absent" } } ], @@ -235,22 +208,6 @@ "actor_type": "agt" } }, - { - "id": "agt-custodian", - "type": "Agent", - "display_name": "Supervised interactive custodian agent (custodian-agt on Railiance01)", - "organization_relation": "ServiceProvider", - "roles": [ - "Operator" - ], - "groups": [ - "group:ops-warden-agents" - ], - "tenant": "tenant:platform", - "metadata": { - "actor_type": "agt" - } - }, { "id": "agt-state-hub-bridge", "type": "Agent", @@ -298,7 +255,6 @@ "display_name": "Ops Warden Agents", "members": [ "agt-codex-interhub-bootstrap", - "agt-custodian", "agt-state-hub-bridge" ], "tenant": "tenant:platform" @@ -407,53 +363,6 @@ "access_path": "mediated" } }, - { - "id": "rel:agt-custodian-sign-agt-custodian", - "system": "ops-warden", - "subject": "group:ops-warden-agents", - "relation": "signer", - "object": "ssh-cert:actor/agt-custodian", - "tenant": "tenant:platform", - "conditions": [ - "TimeLimited", - "Logged" - ], - "caring": { - "id": "descriptor:ops-warden-agt-signer", - "profile": "caring-0.4.0-rc2", - "subject_type": "Group", - "organization_relation": "ServiceProvider", - "canonical_role": "Operator", - "scope": { - "level": "Resource", - "id": "ssh-cert:actor/agt-custodian", - "tenant": "tenant:platform", - "resource": "ssh-cert:actor/agt-custodian" - }, - "planes": [ - "Identity", - "Secret", - "Audit" - ], - "capabilities": [ - "Use", - "Operate", - "Audit" - ], - "exposure_modes": [ - "Metadata" - ], - "conditions": [ - "TimeLimited", - "Logged" - ], - "restrictions": [ - "PrivilegeEscalationBlocked", - "SecretAccessBlocked" - ], - "access_path": "mediated" - } - }, { "id": "rel:agt-state-hub-bridge-sign-agt-state-hub-bridge", "system": "ops-warden", diff --git a/registry/flex-auth/zone-resolutions.json b/registry/flex-auth/zone-resolutions.json deleted file mode 100644 index cd79cd7..0000000 --- a/registry/flex-auth/zone-resolutions.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "changes": { - "added": [ - "backup-daily" - ], - "baseline": "initial", - "changed": [], - "removed": [] - }, - "errors": [], - "ok": true, - "profile_errors": [], - "records": [ - { - "admission": "unsatisfied", - "admission_reason": "M1_below_M2_zone_floor", - "control_profile": { - "id": "netkingdom-build-v0.1", - "version": "flex-auth@e521e7b+ops-warden@7ce58ae" - }, - "controls": [ - { - "failure_mode": "fail_open", - "id": "flex-auth/pre-sign", - "pep_owner": "ops-warden", - "policy_owner": "flex-auth", - "policy_ref": "flex-auth@e521e7b:examples/ops-warden/policy_package.md", - "stance": "advisory" - }, - { - "failure_mode": "fail_closed", - "id": "ops-warden/agent-high-risk-read", - "pep_owner": "ops-warden", - "policy_owner": "ops-warden", - "policy_ref": "ops-warden@7ce58ae:src/warden/routing/models.py", - "stance": "enforced" - }, - { - "failure_mode": "fail_closed", - "id": "ops-warden/plan-zone-rule", - "pep_owner": "ops-warden", - "policy_owner": "ops-warden", - "policy_ref": "security-zones_v0.1@104d5c6:section-5.1", - "stance": "enforced" - } - ], - "declared_zone": "z2-protected", - "effective_zone": "unknown", - "guarantees": [ - "authoritative-workload-identity", - "explicit-workload-reference", - "explicit-zone-membership", - "non-inferred-resolution", - "source-revision-bound-membership" - ], - "identity_bindings": [ - "kubernetes-service-account/railiance01/system:serviceaccount:activity-core:default", - "ssh-certificate/ops-warden/atm-backup-daily" - ], - "membership_revision": "sha256:4b937fce814a937a034f63598f4c7f4d8a76cf92e82cada752ae31dbe5f3dc8f", - "membership_revision_reason": "source_bound", - "source": "activity-core/declarations/backup-daily/tenancy.yaml", - "source_revision": "activity-core@fd84f2b6518ff17509930499099ec2a6692d088c", - "subject_id": "backup-daily", - "workload_id": "backup-daily", - "workload_ref": { - "applicability": "applicable", - "deployable": null, - "name": "backup-daily", - "rapp_id": null - } - } - ], - "standard": "security-zones_v0.1" -} diff --git a/registry/generated/high-risk-data-paths.yaml b/registry/generated/high-risk-data-paths.yaml index 94a49f6..c77940d 100644 --- a/registry/generated/high-risk-data-paths.yaml +++ b/registry/generated/high-risk-data-paths.yaml @@ -10,39 +10,26 @@ # declares it, and is null where the field set has not been established -- # null means unknown, never 'one field'. -generated_at: "2026-10-22T06:53:51Z" +generated_at: "2026-08-26T19:56:48Z" source: ops-warden/registry/routing/catalog.yaml -catalog_revision: "537d551ef28c87def9043e65e316ab4f64f5d9d6" -catalog_revision_date: "2026-10-22T08:53:33+02:00" +catalog_revision: "3a4333d850295d2bed39136bfe53f786564eec03" +catalog_revision_date: "2026-08-26T21:56:44+02:00" catalog_dirty: false -high_risk_lane_count: 40 -concrete_path_count: 17 +high_risk_lane_count: 24 +concrete_path_count: 14 # Graded high but not a single KV address -- a routing pattern, a broker # grant, or a non-KV lane. Nothing here for a policy to deny. no_concrete_path: - - apps-consumer-handover - - consumer-owner-deployment - database-dynamic-credentials - - flex-auth-decision-envelope-signing-key - - fluid-telegram-operator - - informed-decision-sitting-requester-login - inter-hub-bootstrap-ssh - - keycape-attended-case-change - - keycape-client-secret-generate - - kubernetes-railiance01-admin - net-kingdom-lldap-bind-credential - net-kingdom-privacyidea-admin-token - object-storage-sts - openbao-api-key - openbao-platform-admin-login - openbao-shamir-recovery-ceremony - - openbao-structure-survey - ops-warden-warden-sign-token - - platform-secret-delivery - - platform-supplier-credentials - - secrets-engine-approval-client-login - - secrets-engine-requester-login - state-hub-forge-derivation-read paths: @@ -71,11 +58,6 @@ paths: metadata_path: tenants/metadata/binky/qonto-api owner_repo: railiance-platform fields: null # field set not established -- unknown, not one - - id: core-hub-forgejo-source-read - data_path: platform/data/workloads/core-hub/forgejo-source-read - metadata_path: platform/metadata/workloads/core-hub/forgejo-source-read - owner_repo: railiance-platform - fields: [FORGEJO_SOURCE_TOKEN, API_USER, API_BASE_URL, TOKEN_SCOPES, GENERATED_AT] - id: email-connect-transactional data_path: platform/data/workloads/email-connect/transactional metadata_path: platform/metadata/workloads/email-connect/transactional @@ -86,11 +68,6 @@ paths: metadata_path: platform/metadata/workloads/forgejo/forgejo-admin owner_repo: railiance-platform fields: null # field set not established -- unknown, not one - - id: inbound-library-imap-approle - data_path: platform/data/workloads/inbound-library/imap - metadata_path: platform/metadata/workloads/inbound-library/imap - owner_repo: ops-mason - fields: null # field set not established -- unknown, not one - id: issue-core-ingestion-api-key data_path: platform/data/workloads/issue-core/issue-core/issue-core-runtime metadata_path: platform/metadata/workloads/issue-core/issue-core/issue-core-runtime @@ -101,11 +78,6 @@ paths: metadata_path: platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets owner_repo: railiance-platform fields: null # field set not established -- unknown, not one - - id: policy-nexus-forgejo-source-read - data_path: platform/data/workloads/policy-nexus/forgejo-source-read - metadata_path: platform/metadata/workloads/policy-nexus/forgejo-source-read - owner_repo: railiance-platform - fields: null # field set not established -- unknown, not one - id: railiance-backup-offsite-lane data_path: platform/data/workloads/railiance/backup/offsite-lane metadata_path: platform/metadata/workloads/railiance/backup/offsite-lane diff --git a/registry/indexes/capabilities.yaml b/registry/indexes/capabilities.yaml index 45ed79e..218a9ab 100644 --- a/registry/indexes/capabilities.yaml +++ b/registry/indexes/capabilities.yaml @@ -1,13 +1,16 @@ version: 1 -updated: '2026-10-21' +updated: '2026-06-17' domain: helix_forge capabilities: - id: capability.security.ssh-certificate-issuance - name: Decision-bound SSH certificates - summary: warden sign with local and OpenBao backends and adm/agt/atm actors. + name: SSH Certificate Issuance + summary: Issue short-lived CA-signed SSH certificates for adm, agt, and atm actors + through a stable cert_command CLI interface; steward NetKingdom operational access routing. + vector: D4 / A3 / C3 / R2 domain: helix_forge status: draft owner: ops-warden + path: registry/capabilities/capability.security.ssh-certificate-issuance.md tags: - ssh - certificate @@ -15,40 +18,6 @@ capabilities: - ops-warden - openbao - security - - netkingdom - - framework-reuse - vector: D4 / A3 / C3 / R2 - path: registry/capabilities/capability.security.ssh-certificate-issuance.md - consumption_modes: - - library - - CLI -- id: capability.security.access-routing - name: Credential routing and contained access - summary: warden route and access catalog; exact lane conduit and secret-exec routing. - domain: infotech - status: draft - owner: ops-warden - tags: - - security - - netkingdom - - framework-reuse - vector: D3 / A3 / C1 / R0 - path: registry/capabilities/capability.security.access-routing.md - consumption_modes: - - library - - CLI -- id: capability.security.case-follow-through - name: Security case operational follow-through - summary: KnownSecurityCase route, plan and operational receipts. - domain: infotech - status: draft - owner: ops-warden - tags: - - security - - netkingdom - - framework-reuse - vector: D3 / A2 / C1 / R0 - path: registry/capabilities/capability.security.case-follow-through.md consumption_modes: - CLI - - source + - cert_command subprocess \ No newline at end of file diff --git a/registry/routing/catalog.yaml b/registry/routing/catalog.yaml index f37cd6c..d3ca951 100644 --- a/registry/routing/catalog.yaml +++ b/registry/routing/catalog.yaml @@ -40,24 +40,6 @@ version: 1 entries: - - id: netkingdom-layer-declaration - title: NetKingdom layer declaration and PEP stance-map route - risk: standard - workload_ref: - applicability: not-applicable - reason: "Repository conformance guidance, not a workload credential or runtime action." - need_keywords: [how, declare, layer, declaration, netkingdom, security, companion, pep, pep-shaped, stance, conformance, tooling, staff, engine] - owner_repo: ops-warden - subsystem: NetKingdom layer-conformance routing - warden_executes: false - wiki_ref: wiki/playbooks/netkingdom-layer-declaration.md#worker-checklist - canon_ref: net-kingdom/canon/standards/security-layer-model_v0.8.md - reviewed: "2026-10-11" - status: active - delegation: - mode: permanent - reviewed: "2026-09-04" - - id: ssh-cert-host-access title: Short-lived SSH certificate for host / ops reachability # Emits a signed certificate — a public artifact. The private key never leaves the caller (WARDEN-WP-0032-T05). @@ -83,30 +65,6 @@ entries: - "Sign: `warden sign --pubkey ` — cert is written to stdout (the cert_command contract)." - "TTL is enforced per actor type: adm 48h / agt 24h / atm 8h. No long-lived keys." - - id: agt-custodian-ssh-cert - title: Supervisor-issued SSH certificate for the custodian agent account (agt-custodian) - # Attended issuance pointer (CUST-WP-0073-T02, WARDEN-WP-0045). The cert is a - # public artifact, but issuance must stay outside the agent account: the agent - # never runs warden sign for itself, so this is a ceremony, not an SSH exec lane. - risk: standard - workload_ref: - applicability: not-applicable - reason: "Supervisor act that issues a cert for the custodian-agt host account; no workload credential is retrieved." - need_keywords: [agt-custodian, custodian-agt, custodian-agents, custodian, agent, supervised, interactive, codex, claude, grok, ssh, certificate, cert, railiance01, renew, reissue] - owner_repo: ops-warden - subsystem: ops-warden (supervisor operator account) + railiance-infra principals - warden_executes: false - wiki_ref: wiki/ActorInventoryPatterns.md#supervised-interactive-agent-account-agt-custodian - canon_ref: the-custodian/workplans/CUST-WP-0073-agent-credential-separation.md - reviewed: "2026-10-20" - status: active - delegation: - mode: native - intended_owner: ops-warden - reviewed: "2026-10-20" - verified: unverified - lane: ceremony - - id: ops-warden-warden-sign-token title: Scoped OpenBao token for ops-warden SSH signing (warden-sign) # A scoped VAULT_TOKEN is a credential in its own right. Graded on what the value is, not on whether ops-warden currently proxies it (WARDEN-WP-0032-T05). @@ -214,114 +172,6 @@ entries: exec_capable: true lane: login - - id: kubernetes-railiance01-admin - title: Attended railiance01 cluster-admin kubeconfig - # Identity pointer for the admin kubeconfig file. The agent default path is - # the scoped ops-mason-builder identity (MASON-IN-0006 choice (b), decision - # 30d29060-8d14-4a8e-9cde-34aa826eb8d1). This lane does not read the file. - risk: high - workload_ref: - applicability: not-applicable - reason: "Attended operator identity for the railiance01 admin kubeconfig file. The agent default path is the scoped ops-mason-builder user." - need_keywords: [kubernetes, kubeconfig, railiance01, cluster-admin, k3s, admin-kubeconfig, ops-mason-builder] - owner_repo: railiance-platform - subsystem: attended kubeconfig file outside the agent default path - warden_executes: false - wiki_ref: wiki/playbooks/kubernetes-railiance01-admin.md#worker-checklist - canon_ref: railiance-platform/argocd/platform-addons/ops-mason-builder - reviewed: "2026-10-22" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-10-22" - verified: source-read - auth_method: "attended operator; set KUBECONFIG to /home/worsch/.kube/config-railiance01 for one command. That file is not the agent default path." - path_template: "/home/worsch/.kube/config-railiance01" - fetch_command: "test -f /home/worsch/.kube/config-railiance01" - exec_capable: true - lane: secret - # vends_secret is structural (active secret lane with a path). Renewal is - # re-establishing the file in place, never reading or copying it. - rotation: - method: re-establish - owner: railiance-platform - automatable: false - steps: - - "Re-establish the attended admin kubeconfig at /home/worsch/.kube/config-railiance01, outside the agent default path. Do not copy the admin client key into the builder kubeconfig and do not print the file." - - - id: secrets-engine-approval-client-login - title: Attended secrets-engine approval client reader login - risk: high - workload_ref: - applicability: not-applicable - reason: "Attended operator identity for the exact CCR-2026-0019 reader." - need_keywords: [secrets-engine, approval-client, reader, login, ccr-2026-0019] - owner_repo: railiance-platform - subsystem: OpenBao scoped operator OIDC via KeyCape - warden_executes: false - wiki_ref: wiki/playbooks/secrets-engine-approval-client-login.md - canon_ref: railiance-platform/credential-change-requests/CCR-2026-0019-secrets-engine-approval-client-read.yaml - reviewed: "2026-09-14" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-09-14" - verified: source-read - auth_method: "attended KeyCape OIDC, netkingdom role secrets-engine-approval-client-workload-kv-read" - fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=secrets-engine-approval-client-workload-kv-read" - exec_capable: true - lane: login - - - id: secrets-engine-requester-login - title: Attended T03 create-only requester reader login - risk: high - workload_ref: - applicability: not-applicable - reason: "Attended operator identity for the exact CCR-2026-0025 reader." - need_keywords: [secrets-engine, approval-requester, reader, login, ccr-2026-0025] - owner_repo: railiance-platform - subsystem: OpenBao scoped operator OIDC via KeyCape - warden_executes: false - wiki_ref: wiki/playbooks/secrets-engine-requester-login.md - canon_ref: railiance-platform/credential-change-requests/CCR-2026-0025-t03-requester-reader.yaml - reviewed: "2026-09-14" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-09-14" - verified: source-read - auth_method: "attended KeyCape OIDC, netkingdom role secrets-engine-requester-workload-kv-read" - fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=secrets-engine-requester-workload-kv-read" - exec_capable: true - lane: login - - - id: informed-decision-sitting-requester-login - title: Attended Informed Decision sitting-requester reader login - risk: high - workload_ref: - applicability: not-applicable - reason: "Attended operator identity for the exact CCR-2026-0027 reader." - need_keywords: [informed-decision, sitting-requester, reader, login, ccr-2026-0027] - owner_repo: railiance-platform - subsystem: OpenBao scoped operator OIDC via KeyCape - warden_executes: false - wiki_ref: wiki/playbooks/informed-decision-sitting-requester-login.md - canon_ref: railiance-platform/credential-change-requests/CCR-2026-0027-informed-decision-sitting-requester-reader.yaml - reviewed: "2026-09-15" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-09-15" - verified: source-read - auth_method: "attended KeyCape OIDC, netkingdom role informed-decision-sitting-requester-workload-kv-read" - fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=informed-decision-sitting-requester-workload-kv-read" - exec_capable: true - lane: login - - id: openbao-shamir-recovery-ceremony title: Attended OpenBao Shamir seal and unseal recovery ceremony # A ceremony pointer, not a credential-value lane. Approval coordinates @@ -346,19 +196,19 @@ entries: lane: ceremony - id: whynot-design-npm-publish - title: whynot-design npm publish token (@whynot/design → coulomb Forgejo registry) + title: whynot-design npm publish token (@whynot/design → coulomb Gitea registry) # Publish rights to the package registry — a leaked token is a supply-chain write, not a read (WARDEN-WP-0032-T05). risk: high workload_ref: applicability: applicable unknown_reason: "whynot-design has not published an authoritative workload identity declaration." - need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, forgejo, registry, coulomb, package] + need_keywords: [whynot-design, whynot, npm, publish, npm_auth_token, gitea, registry, coulomb, package] owner_repo: railiance-platform subsystem: OpenBao warden_executes: false wiki_ref: wiki/playbooks/whynot-design-npm-publish.md#worker-checklist canon_ref: net-kingdom/docs/platform-identity-security-architecture.md - reviewed: "2026-09-04" + reviewed: "2026-06-29" status: active delegation: mode: native @@ -373,28 +223,6 @@ entries: # form is superseded; do not reintroduce it. auth_method: "bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read" path_template: "platform/workloads/coulomb/whynot-design/npm-publish" - # Field REVERTED to NPM_AUTH_TOKEN on 2026-09-10, and the path CONFIRMED, by the - # custodian of the path (railiance-platform msgs c6841bdb, 79f3e047). An attended, - # read-only, no-value founder session enumerated the field names actually present - # at this path: exactly one, NPM_AUTH_TOKEN. Receipt: railiance-platform - # docs/evidence/2026-09-10-npm-lane-field-resolution.json (commit 9d24086); prior - # basis CCR-2026-0001's two dated receipts (2026-06-28) naming the same field. - # - # The 2026-09-09 change to `npm_token` was adopted from secrets-engine's statement - # (msg 15f0c0ca) and is wrong FOR THIS PATH. That is ADR-0001's failure mode in its - # proper form: a correction taken from a coordination message is still an unverified - # mutation, and this one would have named a field that does not exist — failing at - # the rotation (WARDEN-WP-0037-T03) rather than on a cheap day. Reverting is a - # correction back to the evidenced value, not a new lane change. - # - # OPEN, and not ops-warden's to settle: a legacy location - # secret/coulomb/whynot-design/npm/publish exists (metadata only, v1 created - # 2026-07-03, outside this lane's exact-path policy and outside any CCR). - # railiance-platform's hypothesis is that secrets-engine's lowercase field belongs - # to THAT path and that the native exec front door below may be reading the - # ungoverned duplicate. Tracked as railiance-platform RPF-WP-0035-T07; ops-warden - # has asked secrets-engine which location `secrets-engine exec` reads. Until that - # is answered, this catalog names the governed path and the field evidenced at it. fetch_command: "bao kv get -field=NPM_AUTH_TOKEN platform/workloads/coulomb/whynot-design/npm-publish" policy_ref: "flex-auth check secret.read:whynot-design" exec_capable: true @@ -410,84 +238,9 @@ entries: owner: railiance-platform automatable: false steps: - - "In the coulomb Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity." - - "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file). NPM_AUTH_TOKEN is the KV field name at this path, evidenced by an attended enumeration on 2026-09-10; it is also the env var the publication-scope policy injects, which is what made the two readings easy to confuse." - - "Verify capabilities-safe, then publish a fresh version and confirm it with Forgejo-supported `npm view @` through the governed execution lane (value used, not printed)." - - - id: policy-nexus-forgejo-source-read - title: Policy Nexus Forgejo private-source repository read token - # Read-only at Forgejo, but disclosure grants estate-wide private source access. - # Agent callers therefore remain inside Warden's sanctioned transport boundary. - risk: high - workload_ref: - applicability: applicable - unknown_reason: "policy-nexus-actions has not published an authoritative workload identity declaration." - need_keywords: [policy-nexus, policy, nexus, forgejo, private, source, repository, read, token, actions, FORGEJO_SOURCE_TOKEN] - owner_repo: railiance-platform - subsystem: OpenBao + Forgejo Actions - warden_executes: false - wiki_ref: wiki/playbooks/policy-nexus-forgejo-source-read.md#worker-checklist - canon_ref: railiance-platform/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml - reviewed: "2026-09-01" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-09-01" - verified: source-read - auth_method: "bao login -method=oidc -path=netkingdom role=policy-nexus-forgejo-source-workload-kv-read" - path_template: "platform/workloads/policy-nexus/forgejo-source-read" - fetch_command: "bao kv get -field=FORGEJO_SOURCE_TOKEN platform/workloads/policy-nexus/forgejo-source-read" - policy_ref: "flex-auth check secret.read:policy-nexus" - exec_capable: true - lane: secret - rotation: - method: rotate - owner: railiance-platform - automatable: false - steps: - - "Mint a replacement PAT for the restricted policy-nexus-source identity with scope exactly read:repository; retain the predecessor until verification passes." - - "Use the attended railiance-platform bootstrap to update OpenBao and the coulomb/policy-nexus FORGEJO_SOURCE_TOKEN Actions secret without exposing the value." - - "Pass one exact-commit candidate workflow, then revoke the predecessor PAT and record bounded non-secret evidence in CCR-2026-0014." - - - id: core-hub-forgejo-source-read - title: Core Hub repo-manager publisher Forgejo private-source repository read token - # Read-only at Forgejo, but disclosure grants estate-wide private source read for - # org coulomb. ESO is the only consumer (CCR-2026-0031): no operator fetch lane, - # so exec_capable stays false and Warden never proxies a read of this value. - risk: high - workload_ref: - applicability: applicable - rapp_id: rapp-core-hub - name: core-hub - deployable: repository-classification-publisher - fields: [FORGEJO_SOURCE_TOKEN, API_USER, API_BASE_URL, TOKEN_SCOPES, GENERATED_AT] # CCR-2026-0031 - need_keywords: [core-hub, core, hub, repo-manager, publisher, forgejo, private, source, repository, read, token, eso, external-secrets, FORGEJO_SOURCE_TOKEN] - owner_repo: railiance-platform - subsystem: OpenBao + External Secrets + Forgejo - warden_executes: false - wiki_ref: wiki/playbooks/core-hub-forgejo-source-read.md#worker-checklist - canon_ref: railiance-platform/credential-change-requests/CCR-2026-0031-core-hub-forgejo-source-read.yaml - reviewed: "2026-10-03" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-10-03" - verified: source-read - auth_method: "Kubernetes auth (mount kubernetes) role=external-secrets-core-hub-forgejo-source, bound to external-secrets/external-secrets; ESO delivery only (ClusterSecretStore openbao-core-hub-forgejo-source → ExternalSecret core-hub/repo-manager-forgejo-source)" - path_template: "platform/workloads/core-hub/forgejo-source-read" - policy_ref: "workload-kv-read-core-hub-forgejo-source" - exec_capable: false - lane: secret - rotation: - method: rotate - owner: railiance-platform - automatable: false - steps: - - "Every 180 days or on owner change: mint a replacement PAT for the restricted core-hub-source identity with scope exactly read:repository; retain the predecessor until verification passes." - - "Write the replacement to platform/workloads/core-hub/forgejo-source-read from stdin through the attended railiance-platform bootstrap, then let ESO refresh core-hub/repo-manager-forgejo-source (5m)." - - "Pass make verify-publisher in rapp-core-hub, then revoke the predecessor PAT and record bounded non-secret evidence in CCR-2026-0031." + - "In the coulomb Gitea/Forgejo registry, revoke the current @whynot/design publish token and generate a new one (scope: package read/write) for the whynot-design publish identity." + - "Write it back: `bao kv put platform/workloads/coulomb/whynot-design/npm-publish NPM_AUTH_TOKEN=@file` (value from a mode-0600 file)." + - "Verify capabilities-safe, then confirm publish works via `secrets-engine exec --catalog whynot-design-npm-publish -- npm whoami` (value used, not printed)." - id: flex-auth-policy-check title: Authorization decision — may this actor perform this action @@ -496,19 +249,7 @@ entries: workload_ref: applicability: not-applicable reason: "Generic authorization action; the governed resource supplies workload identity." - need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, access-engine, topaz, pdp, decision] - # FLEX-WP-0020 rename review, 2026-09-21 (flex-auth msg 0a1956c5; WARDEN-IN-0003). - # `access-engine` added to the keywords so this lane resolves under BOTH names from - # today — the deprecation window ops-warden asked for in WARDEN-IN-0001 and the only - # change the rename needs here to keep routing working. - # - # `owner_repo` is deliberately NOT flipped, and the reason is not inertia. In this - # catalog `owner_repo` is not purely a repository coordinate: src/warden/policy.py - # sends it verbatim as `resource.system` (and as `context.owner_repo`) on every - # /v1/check. FLEX-DEC-2026-013 keeps runtime names as `flex-auth`, so flipping this - # string would rename a policy resource the PDP matches on, under cover of a - # repository rename — a runtime change wearing a coordinate change's clothes. It - # flips when the runtime name does, not when the repository does. + need_keywords: [authorization, policy, permission, allow, deny, may, flex-auth, topaz, pdp, decision] owner_repo: flex-auth subsystem: flex-auth warden_executes: false @@ -540,16 +281,16 @@ entries: delegation: mode: interim intended_owner: key-cape - blocked_on: "ACCEPTED by key-cape 2026-09-08 (msg 12f1bdfa): key-cape owns interactive login, OIDC identity-token issuance and MFA for this lane, confirming the 2026-08-28 correction of intended_owner from secrets-engine (whose 2026-08-21 refusal was right). Lane stays interim because the accepted ownership covers only the identity half: the fetch_command yields an OPENBAO token, and the netkingdom auth mount, the role= mapping, the token store and its enforcement are OpenBao's, with role/policy definition railiance-platform's. keycape login is not a drop-in — it yields a key-cape issuer JWT written to a private file, a different credential type with a different verifier (KEY-WP-0014-T04). Retires when a key-cape-native front door yields the credential this route's consumers expect, or when the route is deliberately re-contracted to a JWT. Verified against key-cape docs/native-authentication.md, docs/openbao-service-auth-contract.md, config/service-clients.example.yaml, src/internal/server/oidc/discovery.go." - reviewed: "2026-09-08" - verified: owner-confirmed + blocked_on: "REFUSED by secrets-engine 2026-08-21: login, MFA and identity-token issuance are key-cape/Keycloak's; secrets-engine may consume OIDC for OpenBao auth but does not own the login capability. ops-warden agrees — intended_owner corrected from secrets-engine to key-cape. Not yet asked of key-cape." + reviewed: "2026-08-21" + verified: asked-and-waiting # Login lane (WP-0014 T4) — interactive auth bootstrap, not a secret read. No # secret-read gate (you have no identity yet) and no caller-auth precheck (the # point is to obtain one). warden runs it interactively as the caller and never # captures the resulting token — the owner tool writes it to the caller's store. lane: login - auth_method: "browser OIDC via key-cape / Keycloak at auth/netkingdom" - fetch_command: "bao login -no-print -method=oidc -path=netkingdom role=" + auth_method: "browser OIDC via key-cape / Keycloak" + fetch_command: "bao login -method=oidc role=" exec_capable: true - id: ops-bridge-tunnel @@ -723,12 +464,7 @@ entries: # reuse/reuse-surface-runtime SecretSynced to reuse-surface-env on Railiance01; # positive + negative access verified. Production consumer is ESO; warden access # proxies reads as the caller and never holds the value. - # Dedicated caller: CCR-2026-0044 applied. Group reuse-surface-hub-writers, - # assurance aal2, policy workload-kv-read-reuse-surface-runtime only. - # Member and non-member receipts passed 2026-10-14 (ops-mason). The proxy - # still uses the ambient token after this login. platform-admin must not - # receive the scoped policy. ESO stays on reuse-surface-runtime-eso. - auth_method: "bao login -method=oidc -path=netkingdom role=reuse-surface-hub-writer-workload-kv-read" + auth_method: "caller's own OpenBao token (operator OIDC via key-cape, or a token carrying workload-kv-read-reuse-surface-runtime)" path_template: "platform/workloads/reuse/reuse-surface/runtime-secrets" fetch_command: "bao kv get -field=REUSE_SURFACE_TOKEN platform/workloads/reuse/reuse-surface/runtime-secrets" policy_ref: "flex-auth check secret.read:reuse" @@ -838,48 +574,32 @@ entries: - "Verify capabilities-safe on the data path (`bao token capabilities`); run a restore drill against a re-encrypted artifact." - "After rotation, clear EXPOSED taint: remove custom_metadata exposed_at/exposed_version (see `warden taint railiance-backup-offsite-lane`)." - - id: flex-auth-decision-envelope-signing-key - title: Ed25519 key that signs flex-auth decision envelopes - risk: high - workload_ref: - applicability: applicable - unknown_reason: "flex-auth has not published an authoritative workload identity declaration for envelope signing." - need_keywords: [flex-auth, decision, envelope, signing, signature, ed25519, responder, authenticity, FLEX-WP-0024] - owner_repo: railiance-platform - subsystem: OpenBao + flex-auth - warden_executes: false - wiki_ref: wiki/playbooks/flex-auth-decision-envelope-signing-key.md#worker-checklist - canon_ref: flex-auth/docs/decision-envelope-signature.md - reviewed: "2026-09-14" - status: draft - delegation: - mode: interim - intended_owner: railiance-platform - blocked_on: "Shape is chosen in flex-auth (FLEX-WP-0024-T02). OpenBao KV path, Kubernetes auth role, and paste_once_provision of the private seed are not built. Testdata key in flex-auth is not this lane." - reviewed: "2026-09-14" - verified: unverified - auth_method: "Kubernetes auth from the flex-auth pin (to be bound); not a static secret in git" - path_template: "platform/workloads/flex-auth/envelope-signing (fields ED25519_PRIVATE_KEY, ED25519_PUBLIC_KEY, kid)" - - id: state-hub-forge-derivation-read title: Forgejo read-only token for State Hub projection derivation + # Organisation-wide repository read: if the value leaks, every private + # repository is readable. Read-only, no write, no admin, no persistence — + # but the breadth is the grade (approved knowingly 2026-08-26). risk: high workload_ref: applicability: applicable - unknown_reason: "state-hub has not published an authoritative workload identity declaration; Kubernetes auth role state-hub-forge-derivation binds ServiceAccount state-hub/state-hub." + unknown_reason: "The state-hub deployment has no authoritative workload identity declaration yet; the AppRole is the identity until it does." need_keywords: [state-hub, statehub, forge, forgejo, derivation, derive, projection, private, repository, clone, FORGE_READ_TOKEN, ADR-012, work-records, unreadable, kubernetes-auth] owner_repo: ops-mason subsystem: OpenBao + Forgejo warden_executes: false wiki_ref: wiki/playbooks/state-hub-forge-derivation-read.md#worker-checklist canon_ref: ops-mason/plans/state-hub-forge-derivation-read.md - reviewed: "2026-09-14" - status: active + reviewed: "2026-08-26" + status: draft delegation: mode: native intended_owner: ops-mason - reviewed: "2026-09-14" - verified: owner-confirmed + blocked_on: "Policy and Kubernetes auth role built and capability-verified 2026-08-27 (MASON-WP-0003-T02). Still draft: the KV path holds no token until paste_once_provision delivers one, and ServiceAccount state-hub/state-hub does not exist yet (STATE-WP-0084-T02). Promote to active once both land." + reviewed: "2026-08-26" + verified: unverified + # ops-warden routes this and executes nothing: it issues SSH certificates + # only and owns no secret store. ops-mason builds the structure; the forge + # owner mints the value; paste_once_provision delivers it. auth_method: "Kubernetes auth (role state-hub-forge-derivation, ServiceAccount state-hub/state-hub) from the in-cluster state-hub workload" path_template: "platform/workloads/state-hub/forge-derivation (field FORGE_READ_TOKEN)" rotation: @@ -1017,7 +737,7 @@ entries: - id: rapp-qonto-keycape-client title: rapp-qonto KeyCape workload client - need_keywords: [rapp-qonto, qonto, keycape, oidc, client-credentials, service-token, workload-identity, binky, client secret, secret, successor, rotate, rotation] + need_keywords: [rapp-qonto, qonto, keycape, oidc, client-credentials, service-token, workload-identity, binky] owner_repo: key-cape subsystem: KeyCape + OpenBao warden_executes: false @@ -1028,9 +748,9 @@ entries: delegation: mode: interim intended_owner: key-cape - blocked_on: "CHARACTER CHANGED 2026-09-10 (key-cape msgs 10cf8fca, 546f76e4), substance unchanged. This is no longer blocked on an OPEN QUESTION: the authority is named, the transport is named, and the rotation CCR is available on request. It is blocked on a DECISION TAKEN — key-cape put the question to the operator rather than answering for him, and the answer is NOT YET, with the offer standing open. ops-warden routed the ask and deliberately did not make it on key-cape's behalf (ADR-0003): a routing errand that implies an owner request manufactures a prepared CCR nobody decided to want. This schema carries no field distinguishing 'no authority exists' from 'authority exists, unexercised by owner decision' — the second is what is true here, and it is recorded in prose because inventing a delegation field to hold it would be a catalog change made to describe one lane. NOT A DEADLINE, and not remediation: key-cape checked and there is no incident behind it. KEY-WP-0011 recovered a real exposure on 2026-08-23, but the rapp-qonto client secret was not in that payload — it is an env: secretRef from a separate Kubernetes Secret, never inline in config.yaml, enforced by config validation. So this is lifecycle hygiene, which may wait for a chosen window; if anyone is carrying it as leftover incident work, it is not. WHAT REOPENS IT (events, never elapsed time): an actual or suspected exposure; the secret's age becoming a stated concern; a consumer requiring proof of rotation; or a decision to prove rotation step 4 before relying on it. Prior state, unchanged in substance: Narrowed again 2026-09-09: the AUTHORITY is answered, the ARTIFACT is not. railiance-platform (msg 7c7228ac) confirmed steps 1-2 are theirs: executed by the platform operator attended, never by ops-warden, secrets-engine autonomously, or any unattended agent; transport is the governed openbao-platform-admin-login lane invoked only through `warden access openbao-platform-admin-login --exec -- ` with a unique metadata receipt path (RPF-WP-0017 output containment); authority is founder_required attended OIDC via netkingdom role=platform-admin. What is still missing is a reviewed rotation CCR — a two-custodian CAS rotation with a service restart is a distinct version-guarded operation, not an implementation detail of this lane, and it must name the CAS precondition and expected version on both custodians, sibling-field preservation, the restart window, bounded predecessor retention, and the reconcile-to-same-version failure step. That CCR is railiance-platform to write under RPF-WP-0035 once an owner asks for the rotation; ops-warden has asked key-cape whether to schedule it. Executable precedent for the same two-custodian shape: railiance-platform scripts/keycape_approval_custody.py, with its dated receipt under docs/evidence/ and review packet under docs/credential-lane-designs/. Prior (2026-09-08) narrowing to rotation steps 1-2 only: successor generation and the CAS write to both custodians (platform/workloads/rapp-qonto/keycape-client field client_secret, and sso/keycape-rapp-qonto-client key client-secret) remain custody/deployment acts. The key-cape-native exchange now exists (keycape service-token, 2026-09-05, KEY-WP-0014-T03) and step 3 verification exists as one command (keycape verify-client, 2026-09-08, including predecessor rejection and identical-secret detection); both are documented in key-cape/docs/native-authentication.md. The prior blocker recorded both as absent, which was accurate on 2026-08-28 and is not accurate now — corrected by key-cape (msg 08d42f47). Re-checked against key-cape source 2026-09-08." - reviewed: "2026-09-21" - verified: owner-confirmed + blocked_on: "client_secret_basic exchange is a key-cape protocol procedure, not a KV read; KEY-WP-0008 finished 2026-08-15 without a key-cape-native exchange/rotation command, and the workplan that would add one is not yet opened (key-cape acked interim 2026-08-15)" + reviewed: "2026-08-17" + verified: unverified risk: high workload_ref: applicability: applicable @@ -1045,15 +765,11 @@ entries: rotation: method: rotate owner: key-cape - # Not drivable end to end: steps 1-2 are custody/deployment acts with no - # admitted execution transport, so an executable driver (WARDEN-WP-0027 - # Strand B) must not treat this lane as automatable. Step 3 alone is one - # owner command today — the precision lives in the steps, not the flag. - automatable: false + automatable: true steps: - - "Generate a fresh newline-free secret through an approved execution transport; never print it. NOT automatable — no admitted execution and rollback contract (see delegation.blocked_on)." - - "CAS-write the same value to platform/workloads/rapp-qonto/keycape-client and sso/keycape-rapp-qonto-client preserving siblings, then restart KeyCape in the agreed window. NOT automatable — same blocker; on failure reconcile both custodians to the same version before declaring rollback complete." - - "Verify positive qonto:read exchange plus predecessor-secret and excessive-scope denial without printing tokens: run the owner command keycape verify-client (key-cape/docs/native-authentication.md)." + - "Generate a fresh newline-free secret through an approved execution transport; never print it." + - "Write the same value to OpenBao and sso/keycape-rapp-qonto-client, then restart KeyCape." + - "Verify positive qonto:read exchange plus wrong-secret and excessive-scope denial without printing tokens." - id: net-kingdom-lldap-bind-credential title: NetKingdom LLDAP bind credential for identity and privacyIDEA resolver @@ -1290,46 +1006,6 @@ entries: - "Optionally re-mint the OpenRouter key itself if it rotated independently." - "Verify AppRole login + field presence (length only); confirm default/other policies still deny sibling paths." - - id: inbound-library-imap-approle - title: inbound-library AppRole for non-interactive library@coulomb.social IMAP read - need_keywords: [inbound-library, approle, library@coulomb.social, mail, imap, unattended] - owner_repo: ops-mason - subsystem: OpenBao AppRole + inbound-library - warden_executes: false - wiki_ref: wiki/playbooks/inbound-library-imap-approle.md#worker-checklist - canon_ref: ops-mason/plans/inbound-library-imap-approle.md - reviewed: '2026-10-05' - status: active - delegation: - mode: interim - intended_owner: ops-mason - blocked_on: AppRole is a host-standing credential; no owner-fronted exec for the - library mailbox read - reviewed: '2026-10-05' - verified: unverified - risk: high - workload_ref: - applicability: applicable - unknown_reason: inbound-library has not published an authoritative workload identity - declaration. - auth_method: AppRole login role=inbound-library (role_id+secret_id in ~/.local/inbound-library/approle, - config/library.json credential.approle_dir) - path_template: platform/workloads/inbound-library/imap - fetch_command: 'bao write -field=token auth/approle/login role_id=$ROLE_ID secret_id=$SECRET_ID # - then bao kv get -field=IMAP_PASSWORD platform/workloads/inbound-library/imap' - exec_capable: false - lane: secret - rotation: - method: rotate - owner: ops-mason - automatable: false - steps: - - bao write -f auth/approle/role/inbound-library/secret-id -> new secret_id; deliver - mode-0600 to ~/.local/inbound-library/approle; shred old secret_id file. - - Optionally re-mint the IMAP password at IONOS and add it as a new version of - platform/workloads/inbound-library/imap (founder, attended). - - Verify via one inbound-library run-due (AppRole login + read-only scan); confirm - a default-policy token still gets deny on the data path. - id: coulomb-social-runtime-env title: coulomb.social runtime env Secret (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET) need_keywords: @@ -1385,53 +1061,29 @@ entries: - id: audit-core-senders title: audit-core sender registry (write and operator-read tokens) # Vends write and operator-read tokens (WARDEN-WP-0032-T05). - # Activated 2026-10-10 as a pointer on ops-mason's custody check (message - # 5a516911, ops-mason-survey grant, metadata and policy text only): - # - KV v2 at platform/workloads/audit-core/senders, v11, with all five - # custody fields present. - # - Only external-secrets-audit-core grants a data read. - # - agent-high-risk-boundary denies the data path, and the tenant-engine - # projection policy does not name it. - # ESO reads it by token auth; the parent token renewal is due 2026-10-25, - # under railiance-platform RPF-WP-0046. A live-token capability check is - # owner-run and not yet done. risk: high workload_ref: applicability: applicable unknown_reason: "audit-core has not published an authoritative workload identity declaration." - need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS, tenant-engine audit-core sender] + need_keywords: [audit-core, senders, sender registry, ingest token, AUDIT_CORE_SENDERS] owner_repo: ops-mason subsystem: OpenBao + audit-core warden_executes: false wiki_ref: wiki/playbooks/audit-core-senders.md#worker-checklist canon_ref: audit-core/docs/operator-runbook.md - reviewed: "2026-10-10" - status: active + reviewed: "2026-08-13" + status: draft delegation: mode: native intended_owner: ops-mason - reviewed: "2026-10-10" - verified: source-read - # Per-sender projections. Each is verified on its own and none activates the - # registry lane. tenant-engine: KV platform/workloads/tenant-engine/audit-core-sender - # (v1), read policy workload-kv-read-tenant-engine-audit-core-sender-eso (exact - # read, no registry path), ESO-delivered; verified by ops-mason 2026-10-09 - # (MASON-WP-0007-T06, message e7e866bf). - consumers: [tenant-engine] - auth_method: "OpenBao KV v2 custody; audit-core reads via ESO (policy external-secrets-audit-core, token auth). Operators and agents never read the registry; a sender gets its own exact-path projection." + reviewed: "2026-08-15" + verified: unverified + auth_method: "in-cluster generated Secret audit-core-senders; later OpenBao KV via Mason wrap-migrate" path_template: "platform/workloads/audit-core/senders" policy_ref: "external-secrets-audit-core" exec_capable: false resolvable: false lane: secret - rotation: - method: rotate - owner: audit-core - automatable: false - steps: - - "Overlap-first per audit-core/docs/operator-runbook.md#sender-credential-rotation: add the replacement token to the sender's tokens list, move the sender to it, then drop the predecessor." - - "Write the updated registry back to platform/workloads/audit-core/senders from a mode-0600 file (never a founder paste, never argv); force the ExternalSecret sync or wait for the 1h refresh, then restart audit-core." - - "Senders with an exact-path projection (tenant-engine) are re-seeded through their own projection lane; verify by metadata and capabilities only." - id: email-connect-transactional title: email-connect transactional SMTP and caller ingest token @@ -1514,245 +1166,3 @@ entries: - "In Scaleway IAM, revoke the bootstrap API key after the scoped bucket key exists." - "Delete or overwrite platform/workloads/railiance/scaleway/bootstrap; do not copy values into chat." - "Confirm metadata gone or version bumped; capabilities-safe only." - - - id: openbao-structure-survey - title: OpenBao existing-structure survey for a construction plan (section 2) - # Pointer only (WARDEN-WP-0042). Founder decision: ops-warden routes, - # ops-mason surveys. No survey code and no token live in ops-warden; the - # survey runs under ops-mason's read-only ops-mason-survey grant (MASON-IN-0004). - risk: high - workload_ref: - applicability: not-applicable - reason: "Attended read-only survey under a founder-minted grant; reads OpenBao structure metadata, not a workload credential." - need_keywords: [survey, structure survey, openbao structure, existing structure, existing policies, existing lane, auth roles, approle roles, policy list, auth list, secrets list, construction plan, construction plan section 2, plan-survey, ops-mason-survey] - owner_repo: ops-mason - subsystem: OpenBao structure metadata via read-only ops-mason-survey grant - warden_executes: false - wiki_ref: wiki/playbooks/openbao-structure-survey.md#worker-checklist - canon_ref: ops-mason/docs/construction-plan-format.md - reviewed: "2026-10-08" - status: active - delegation: - mode: native - intended_owner: ops-mason - reviewed: "2026-10-08" - verified: source-read - auth_method: "founder-minted scoped grant: ops-mason scripts/bao-session.sh grant (policy ops-mason-survey, 45m, display_name claude-)" - exec_owner: ops-mason - exec_command: "ops-mason/scripts/plan-survey.py --kv-path --policy --approle --consumer --keyword " - exec_capable: false - lane: ceremony - - - id: platform-supplier-credentials - title: Supplier account credentials under platform/suppliers// - # Pointer only (WARDEN-WP-0043-T01). Founder decision 2026-10-08: credentials - # we hold with a supplier live on the platform mount under - # suppliers//. railiance-platform owns the KV write and the - # credential-change allowlist; ops-warden routes the shape and holds nothing. - # Workload and rein paths with a live reader stay where they are until each - # reader is retargeted (see the playbook table). - risk: high - workload_ref: - applicability: not-applicable - reason: "Generic supplier-account path pattern; concrete consumers keep their own workload lanes." - need_keywords: [supplier, suppliers, supplier account, supplier credential, vendor, vendor account, platform/suppliers, account credential] - owner_repo: railiance-platform - subsystem: OpenBao platform mount (suppliers/) - warden_executes: false - wiki_ref: wiki/playbooks/platform-supplier-credentials.md#worker-checklist - canon_ref: railiance-platform/scripts/credential-change.py - reviewed: "2026-10-08" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-10-08" - verified: source-read - auth_method: "railiance-platform delegated credential change (CCR); founder-attended provision from a mode-0600 file" - path_template: "platform/suppliers//" - exec_capable: false - rotation: - method: rotate - owner: railiance-platform - automatable: false - steps: - - "Re-mint the credential at the supplier, written to a mode-0600 file and never printed." - - "Write it through a railiance-platform delegated credential change to platform/suppliers// (value from the file, never on argv); revoke the predecessor at the supplier." - - "Verify with capabilities and metadata only (`bao token capabilities`, `bao kv metadata get`), then tell readers to re-fetch." - - - id: fluid-telegram-operator - title: fluid-telegram Hall of Helix operator KV lane (attended OIDC) - # Pointer only (MASON-WP-0005-T06, proposal 856bd6ba, 2026-10-09). Built by - # ops-mason as CCR-2026-0039; ops-mason verified the policy text and role: - # exact four-path matrix, role bound to fluid-telegram-operators at aal2, and - # 8 agent denies present. ops-mason proposed `applied-pending-verify`; the - # catalog has only draft/active, so it stays draft until the owner-run checks - # pass: member capabilities and non-member refusal - # (ops-mason scripts/verify-fluid-telegram-lane.sh), plus the salt - # create-once probe (FT-WP-0002). Then ops-warden activates it. The adapter - # runtime lane is separate (MASON-IN-0003): bot-token and redaction-salt only. - risk: high - workload_ref: - applicability: applicable - unknown_reason: "fluid-telegram has not published an authoritative workload identity declaration." - need_keywords: [fluid-telegram, hall-of-helix, telegram operator, telegram bot, mtproto, operator-session, bot-token, redaction-salt, fluid-telegram-operators] - owner_repo: ops-mason - subsystem: OpenBao netkingdom OIDC + platform KV (fluid-telegram campaign) - warden_executes: false - wiki_ref: wiki/playbooks/fluid-telegram-operator.md#worker-checklist - canon_ref: ops-mason/plans/fluid-telegram-operator-credential-lane.md - reviewed: "2026-10-09" - status: draft - delegation: - mode: native - intended_owner: ops-mason - reviewed: "2026-10-09" - verified: owner-confirmed - auth_method: "bao login -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv (members of fluid-telegram-operators, aal2, 15m token)" - path_template: "platform/workloads/coulomb/fluid-telegram/hall-of-helix/{operator-app,operator-session,bot-token,redaction-salt}" - exec_capable: false - resolvable: false - lane: login - - - id: keycape-client-secret-generate - title: SecurityCase mint-secret, first KeyCape client secret generated in custody - # Pointer only, draft (WARDEN-WP-0044; railiance-platform RPF-WP-0054, - # message d951386f, 2026-10-09). Lane named by SecurityCaseType - # oidc-browser-relying-party 1.0, step mint-secret - # (value_handling: generated_in_custody). The broker generates the value - # in its own process, writes once with CAS 0, verifies by metadata only and - # returns a receipt with no value. Grant applied (CCR-2026-0040). Not live - # until: the issuer policy is bound to the operator OIDC role (founder), - # key-cape confirms the secret format, and the RPF-WP-0054-T05 disposable - # probe passes. Then railiance-platform confirms and ops-warden activates. - # 2026-10-10: issuer role bound (CCR-2026-0043) and T05 probe passed - # (railiance-platform 21b8af3: generated v1, second run exit 3, grant token - # revoked, value_reads 0). Activated 2026-10-10 on railiance-platform's - # go-ahead (message 20973a9c) after key-cape confirmed the secret format - # (message 4e0213b3); RPF-WP-0054 T01, T03, T05 done. - # How warden case run executes it is WARDEN-WP-0044-T03. - risk: high - workload_ref: - applicability: not-applicable - reason: "Per-case generation lane; each case's consuming service gets its own {consuming_service}-keycape-client entry." - # Case-specific terms only. Broad phrases such as "keycape client secret" - # outranked named per-client lanes (rapp-qonto-keycape-client) on rotation - # needs, and this lane refuses an existing path (first generation only). - need_keywords: [mint-secret, client-secret-generate, keycape/client-secret-generate, generated in custody, generated_in_custody, security case, securitycase, oidc-browser-relying-party] - owner_repo: railiance-platform - subsystem: railiance-platform credential broker + OpenBao platform/workloads/keycape/ - warden_executes: false - wiki_ref: wiki/playbooks/keycape-client-secret-generate.md#worker-checklist - canon_ref: railiance-platform/workplans/RPF-WP-0054-keycape-client-secret-generate-lane.md - reviewed: "2026-10-10" - status: active - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-10-10" - verified: owner-confirmed - auth_method: "bao login -method=oidc -path=netkingdom role=credential-broker-keycape-client-secret-generate (net-kingdom-admins, aal2, 15m; token holds only the issuer policy, CCR-2026-0043); broker then mints a 5m create-only grant token (keycape/client-secret-generate). Agents and root refused." - path_template: "platform/workloads/keycape/" - exec_owner: railiance-platform - exec_command: "scripts/credential.py generate --grant keycape/client-secret-generate --client-id --case-file --case-ref @sha256: [--receipt ] [--dry-run]" - exec_capable: false - resolvable: false - lane: secret - rotation: - method: rotate - owner: railiance-platform - automatable: false - steps: - - "This lane covers first generation only and refuses an existing path (exit 3). Rotation is a separate railiance-platform task that does not exist yet; route rotation needs to railiance-platform." - - # CATALOG-1 (nk48 1.3 owner review, 2026-10-17): four lanes for the 1.3 - # SecurityCaseType oidc-browser-relying-party. Pointer only, draft. Each - # stays draft until its owner confirms the pointer - # (wiki/playbooks/catalog-lane-promotion.md); the kernel refuses draft lanes. - - id: keycape-attended-case-change - title: SecurityCase attended KeyCape change (ensure-groups, bind-env, register-client) - # OW-1.3-C1: dedicated attended change authority replacing key-cape-oidc-login - # for kernel-dispatched KeyCape registry, group and Deployment env changes. - # The session may be established by a key-cape OIDC login; that login lane - # stays the identity step, not the change authority. - lane: attended-change - session_lane: key-cape-oidc-login - risk: high - workload_ref: - applicability: not-applicable - reason: "Per-case attended change lane; the consuming service is a case parameter." - need_keywords: [keycape-attended-case-change, ensure-groups, bind-env, register-client, attended keycape change] - owner_repo: key-cape/net-kingdom - subsystem: key-cape registry and Deployment env, net-kingdom groups - warden_executes: false - wiki_ref: wiki/playbooks/catalog-lane-promotion.md - canon_ref: net-kingdom/security-case-types/oidc-browser-relying-party/type.yaml - reviewed: "2026-10-17" - status: draft - delegation: - mode: interim - intended_owner: key-cape - blocked_on: "key-cape and net-kingdom confirm the pointer and the re-frozen 1.3 descriptors (OW-1.3-C1)." - reviewed: "2026-10-17" - verified: source-read - - id: platform-secret-delivery - title: SecurityCase deliver-secret, KeyCape client secret delivered by Platform - risk: high - workload_ref: - applicability: not-applicable - reason: "Per-case delivery lane; each case's consuming service gets its own client entry." - need_keywords: [platform-secret-delivery, deliver-secret, keycape client secret delivery] - owner_repo: railiance-platform - subsystem: railiance-platform External Secrets delivery - warden_executes: false - wiki_ref: wiki/playbooks/catalog-lane-promotion.md - canon_ref: railiance-platform/AGENTS.md - reviewed: "2026-10-17" - status: draft - delegation: - mode: native - intended_owner: railiance-platform - reviewed: "2026-10-17" - - id: consumer-owner-deployment - title: SecurityCase deliver-consumer-secret, consumer deployment by its owner - # Descriptor actor is agent: ADR-0004 read boundary applies. Owner-side - # delivery only; no raw value reaches the agent session. If that cannot be - # shown, the actor changes to human and the railiance-apps digest changes. - lane: attended-change - session_lane: pending-owner-confirmation - risk: high - workload_ref: - applicability: not-applicable - reason: "Per-case consumer deployment; the consumer is a case parameter." - need_keywords: [consumer-owner-deployment, deliver-consumer-secret] - owner_repo: railiance-apps - subsystem: railiance-apps consumer deployment - warden_executes: false - wiki_ref: wiki/playbooks/catalog-lane-promotion.md - canon_ref: railiance-apps/AGENTS.md - reviewed: "2026-10-17" - status: draft - delegation: - mode: native - intended_owner: railiance-apps - reviewed: "2026-10-17" - - id: apps-consumer-handover - title: SecurityCase hand-over to the consuming service owner - lane: attended-change - session_lane: pending-owner-confirmation - risk: high - workload_ref: - applicability: not-applicable - reason: "Per-case hand-over; the consumer owner is a case parameter." - need_keywords: [apps-consumer-handover, hand-over, consumer handover] - owner_repo: railiance-apps - subsystem: railiance-apps consumer hand-over - warden_executes: false - wiki_ref: wiki/playbooks/catalog-lane-promotion.md - canon_ref: railiance-apps/AGENTS.md - reviewed: "2026-10-17" - status: draft - delegation: - mode: native - intended_owner: railiance-apps - reviewed: "2026-10-17" diff --git a/scripts/check_agent_read_boundary.py b/scripts/check_agent_read_boundary.py index fc8fd99..18bbf01 100755 --- a/scripts/check_agent_read_boundary.py +++ b/scripts/check_agent_read_boundary.py @@ -79,15 +79,8 @@ def denied_data_paths(policy_text: str) -> set[str]: def to_data_path(path_template: str) -> str | None: - """Catalog path -> KV v2 data path. `/rest` -> `/data/rest`. - - An absolute filesystem path is not a KV mount. - """ - if ( - PLACEHOLDER.search(path_template) - or " " in path_template - or path_template.startswith("/") - ): + """Catalog path -> KV v2 data path. `/rest` -> `/data/rest`.""" + if PLACEHOLDER.search(path_template) or " " in path_template: return None mount, _, rest = path_template.partition("/") return f"{mount}/data/{rest}" if rest else None diff --git a/scripts/check_flex_auth_manifest_coverage.py b/scripts/check_flex_auth_manifest_coverage.py deleted file mode 100644 index 755ba02..0000000 --- a/scripts/check_flex_auth_manifest_coverage.py +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env python3 -"""Assert every ssh-certificate resource declares its ceilings in the manifest. - -Read-only. Reads `inventory.yaml` and the flex-auth registry snapshot; touches -no network, no OpenBao, and no secret material. - -Why this exists (FLEX-DEC-2026-012, 2026-09-07). flex-auth's enrichment used to -overlay registry facts additive-if-absent, so a caller-supplied value for a key -won and the registry's ceiling never applied. That is fixed on their side: -registry facts now win. But they win only where the registry HAS a value — - - "A resource whose manifest omits max_ttl_hours hands that ceiling back to - the caller, and registering the resource is not sufficient; the specific - key must be present." - -So there are two ways to hand a ceiling back, and this check covers both: - - 1. An actor with no manifest resource at all. `warden sign` names - `ssh-cert:actor/` for any actor in inventory, whether or not the - snapshot was regenerated. Adding an actor and forgetting to rebuild is an - honour-system step in `SCOPE.md`, and honour-system steps are what - `ADR-0004`'s WARDEN_AGENT_ID marker taught us to stop relying on. - 2. A manifest resource missing one of the ceiling or allowlist keys. The - builder emits all of them today; this asserts it stays true, including for - resources added by hand or by a future code path. - -ops-warden sends no `resource.attributes` on a CheckRequest (`src/warden/policy.py`), -which is why FLEX-DEC-2026-012 was defence-in-depth rather than a live hole for -this repo. That property is asserted separately in `tests/test_policy.py`; this -check covers the half that survives it — if anything ever does reach a request -field, the ceiling it would have to beat must actually exist. - -Usage: - python scripts/check_flex_auth_manifest_coverage.py \ - --inventory examples/inventory.seed.yaml \ - --registry registry/flex-auth/production_registry_snapshot.json [--json] - -Exit: 0 covered, 2 coverage gap, 1 usage/IO error. -""" -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - -import yaml - -#: Every key ops-warden's shipped policy package branches on as a ceiling or an -#: allowlist. Named by flex-auth in FLEX-DEC-2026-012; kept here rather than -#: derived from the package so a package edit that drops a branch does not -#: silently shrink what this check requires. -REQUIRED_CEILING_KEYS = ( - "actor_id", - "actor_type", - "allowed_principals", - "allowed_subjects", - "max_ttl_hours", - "security_zone", - "security_zone_admission", -) - -RESOURCE_TYPE = "ssh-certificate" - - -def _resources(registry: dict[str, Any]) -> list[dict[str, Any]]: - out: list[dict[str, Any]] = [] - for manifest in registry.get("resource_manifests") or []: - out.extend(manifest.get("resources") or []) - return out - - -def audit(inventory: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]: - actors = sorted((inventory.get("actors") or {}).keys()) - resources = _resources(registry) - by_id = {str(r.get("id")): r for r in resources} - - unregistered: list[str] = [] - missing_keys: list[dict[str, Any]] = [] - - for name in actors: - if f"ssh-cert:actor/{name}" not in by_id: - unregistered.append(name) - - # Check every ssh-certificate resource in the manifest, not only the ones an - # inventory actor maps to: a resource flex-auth can be asked about is one it - # holds, regardless of where it came from. - for resource in resources: - if str(resource.get("type")) != RESOURCE_TYPE: - continue - attributes = resource.get("attributes") or {} - absent = [ - key for key in REQUIRED_CEILING_KEYS - # A null is not a declaration: flex-auth overlays a registry value - # only where one exists, so `key: null` hands the ceiling back - # exactly as an absent key does. - if attributes.get(key) is None - ] - if absent: - missing_keys.append({"resource": str(resource.get("id")), "missing": absent}) - - # Not a security defect — a stale resource has ceilings, it just has no - # actor. Reported so drift is visible rather than accumulating silently. - orphaned = sorted( - rid for rid, r in by_id.items() - if str(r.get("type")) == RESOURCE_TYPE - and rid.removeprefix("ssh-cert:actor/") not in actors - ) - - return { - "actors": len(actors), - "resources": sum(1 for r in resources if str(r.get("type")) == RESOURCE_TYPE), - "required_keys": list(REQUIRED_CEILING_KEYS), - "unregistered_actors": unregistered, - "resources_missing_keys": missing_keys, - "orphaned_resources": orphaned, - "covered": not unregistered and not missing_keys, - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--inventory", default="examples/inventory.seed.yaml", type=Path) - parser.add_argument( - "--registry", - default="registry/flex-auth/production_registry_snapshot.json", - type=Path, - ) - parser.add_argument("--json", action="store_true", dest="as_json") - args = parser.parse_args(argv) - - try: - inventory = yaml.safe_load(args.inventory.read_text()) or {} - registry = json.loads(args.registry.read_text()) - except (OSError, ValueError) as exc: - print(f"error: {exc}", file=sys.stderr) - return 1 - - report = audit(inventory, registry) - report["inventory"] = str(args.inventory) - report["registry"] = str(args.registry) - - if args.as_json: - print(json.dumps(report, indent=2)) - else: - print(f"inventory : {args.inventory} ({report['actors']} actors)") - print(f"registry : {args.registry} ({report['resources']} ssh-certificate resources)") - for name in report["unregistered_actors"]: - print(f" MISSING {name} — no manifest resource; every ceiling is caller-supplied") - for row in report["resources_missing_keys"]: - print(f" UNDECLARED {row['resource']} — {', '.join(row['missing'])}") - for rid in report["orphaned_resources"]: - print(f" orphaned {rid} — manifest resource with no inventory actor") - print("covered" if report["covered"] else "NOT COVERED") - - return 0 if report["covered"] else 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py deleted file mode 100644 index d05f234..0000000 --- a/scripts/check_layer_conformance.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 -"""Check ops-warden against the NetKingdom security layer model (§5, §11). - -Read-only. Two declaration forms are read, and the precedence between them is -`GH-DEC-2026-017` §1 (amendment A11): **INTENT.md's frontmatter governs** and -`layer.yaml` is a derived artifact that must be marked derived, must name -INTENT.md as its source, and must agree with it. The sidecar is read anyway, -because a disagreement between the two is itself a finding and is reported -rather than resolved away by precedence. - -Comparison against §3's closed four-token vocabulary ASCII-folds case before -comparing (`GH-DEC-2026-017` §2, amendment A9): two spellings of a token do not -describe two boundaries, and a check that reports findings about capital letters -buries the one real disagreement it exists to find. - -Neither form carries a version of the standard or of its companion, in any key -or value (`GH-DEC-2026-017` §5, amendment A12 as refined by A12 r2 / -`GH-DEC-2026-020` §1-§2). The rule reaches content, not a key name: a -`standard_version` key, a `companion_version` key, and a version-bearing path -such as `standard: .../security-layer-model_v0.7.md` are the same pin. Comments -and a file's own `schema_version` are not reached. Stance, claims and -evidence-classification maps (`pep-stance.yaml`) are NOT declarations and this -check does not read them (`GH-DEC-2026-020` §3). - -The version belongs to the run (`GH-DEC-2026-020` §4): every run prints -VALIDATED_AGAINST and SCOPE below, including the PASS line, following -kings-guard's pattern. A retained copy of this output is a derived artifact -whose version is owed by whoever retains it. - -Makes §11's second mechanical check real: - - every direct Tooling client in a Staff repository maps to a declared - §5.1, §5.2, or §5.3 entry - -The failure this catches is a *new* direct OpenBao contact appearing in -src/warden/ without an entry in layer.yaml — an undeclared violation (§11), -which is a finding rather than a tracked gap. It deliberately does NOT check -the review dates: a date-triggered failure breaks the build on a calendar day -with no code change (the reasoning recorded in WARDEN-WP-0033-T05), so -staleness is reported and left to `--report`, never to CI. - -Exit 0 clean, 1 undeclared contact found, 2 declaration malformed. -""" -from __future__ import annotations - -import argparse -import re -import sys -from datetime import date -from pathlib import Path - -import yaml - -ROOT = Path(__file__).resolve().parents[1] -SRC = ROOT / "src" / "warden" -INTENT = ROOT / "INTENT.md" -DECL = ROOT / "layer.yaml" - -VALID_SHAPES = {"5.1", "5.2", "5.3"} - -# Accepted v0.8 publication; version provenance belongs to this run, never -# the standing layer declaration (§11). Exact publisher artifact is retained -# in net-kingdom/docs/security-layer-v0.8-acceptance.md. -VALIDATED_AGAINST = ( - "net-kingdom/canon/standards/security-layer-model_v0.8.md " - "(accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b)" -) - -SCOPE = ("single repository ops-warden (catalogued in §4); " - "INTENT.md frontmatter, layer.yaml, src/warden/**/*.py") - -# A12 r2: a version of the standard or companion in any key or value of the -# declaration. Keys: anything naming a standard/companion version. Values: a -# versioned file name or path (`_v0.7`, `-v0.8.md`) or a bare version string on a -# version-named key. `schema_version` is the file's own schema, not reached. -# GH-DEC-2026-021 §1/§3 (A12 r3): a version is reached only as a pin. Any version -# token (`v?N.N`) in the value of an identity-bearing key (`standard:`, -# `companion:`) is a pin; a revision cited in other prose is provenance and is not -# reached. Keys such as `intent_version` name neither and are not flagged. -VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)", re.I) -VERSION_IN_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.I) -NOT_REACHED_KEYS = {"schema_version"} -IDENTITY_KEYS = {"standard", "companion"} -IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I) - - -def find_version_pins(node, where: str = "", identity: bool = False) -> list[str]: - """Every place in a parsed declaration that carries a standard/companion version. - - Walks every key and value (comments are gone after parsing, which is the - A12 r2 exclusion). Returns human-readable locations; empty means clean. - """ - pins: list[str] = [] - if isinstance(node, dict): - for k, v in node.items(): - here = f"{where}.{k}" if where else str(k) - if str(k) in NOT_REACHED_KEYS: - continue - if VERSION_KEY.search(str(k)): - pins.append(f"{here} (key names a standard/companion version)") - continue - pins.extend(find_version_pins(v, here, str(k).lower() in IDENTITY_KEYS)) - elif isinstance(node, list): - for i, v in enumerate(node): - pins.extend(find_version_pins(v, f"{where}[{i}]", identity)) - elif isinstance(node, str) and VERSION_IN_VALUE.search(node): - pins.append(f"{where} = {node!r} (value carries a version)") - elif isinstance(node, str) and identity and IDENTITY_VERSION.search(node): - pins.append(f"{where} = {node!r} (identity-bearing value carries a version)") - return pins - - -def _reject_version_pins(label: str, node) -> None: - pins = find_version_pins(node) - if pins: - print( - f"MALFORMED: {label} carries a standard/companion version — a layer " - "declaration MUST NOT, in any key or value (§11 as amended by A12 r2, " - "GH-DEC-2026-020 §1-§2):" - ) - for p in pins: - print(f" {p}") - raise SystemExit(2) - -# §3's vocabulary, closed, four tokens (GH-DEC-2026-017 §3, amendment A9). The -# canonical spellings are §4's catalog-column forms; comparison is ASCII -# case-insensitive, so the fold is what is stored and `Taxonomy` is in the set — -# omitting it is the defect A9 records against the estate's other validator. -LAYER_VOCABULARY = {"taxonomy", "tooling", "engine", "staff"} - - -def _fold(value: str) -> str: - """ASCII case-fold, per §3 as amended: two spellings of a token are one token.""" - return str(value).strip().translate(str.maketrans( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")) - - -def load_governing_layer() -> str: - """The declaration is INTENT.md's frontmatter `layer:` key (§11, GH-DEC-2026-017 §1). - - layer.yaml is derived and does not govern. Read without a YAML frontmatter - dependency: the file is read by humans first and a parser second. - """ - if not INTENT.exists(): - print(f"MISSING: {INTENT} — §11's governing declaration form") - raise SystemExit(2) - lines = INTENT.read_text().splitlines() - if not lines or lines[0].strip() != "---": - print("MALFORMED: INTENT.md has no frontmatter to carry the declaration (§11)") - raise SystemExit(2) - end = next((i for i, ln in enumerate(lines[1:], 1) if ln.strip() == "---"), None) - if end is None: - print("MALFORMED: INTENT.md frontmatter is not terminated") - raise SystemExit(2) - front = yaml.safe_load("\n".join(lines[1:end])) or {} - if "layer" not in front: - print("MALFORMED: INTENT.md frontmatter has no 'layer' key — §11's declaration") - raise SystemExit(2) - _reject_version_pins("INTENT.md frontmatter", front) - layer = front["layer"] - if _fold(layer) not in LAYER_VOCABULARY: - print( - f"MALFORMED: INTENT.md declares layer {layer!r}, outside §3's closed " - f"vocabulary {sorted(LAYER_VOCABULARY)} (case-insensitive)" - ) - raise SystemExit(2) - return layer - -# A direct Tooling contact is an *invocation*, not a mention. Matching the word -# "bao" caught help text, a docstring, and the dev-tier doubles library that -# simulates bao rather than calling it — three false positives on first run. -# So match the two shapes that actually execute: -# 1. an HTTP request built against the OpenBao address -# 2. an argv list whose first element is the bao binary -TOOLING_PATTERNS = ( - # httpx call whose URL is built from the configured OpenBao/Vault address - re.compile(r"""\bhttpx\.\w+\(|url\s*=\s*f?["'].*\{self\._cfg\.addr\}"""), - # argv construction: [bao_bin, ...] / ["bao", ...] / [bao_binary, ...] - re.compile(r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b)\s*,"""), -) - -# httpx alone is not a Tooling contact — policy.py calls an Engine and worker.py -# calls the State Hub. A module matching only the httpx pattern counts as a -# contact only if it also references the OpenBao address configuration. -ADDR_HINT = re.compile(r"""_cfg\.addr|VAULT_ADDR|BAO_ADDR""") - -# Modules that talk to an Engine or to something outside the §4 catalog. Listed -# in layer.yaml under non_tooling_clients and excluded from the scan with it. -def _excluded(decl: dict) -> set[str]: - return {e["module"].split("/")[-1] for e in decl.get("non_tooling_clients", [])} - - -def load_declaration() -> dict: - if not DECL.exists(): - print(f"MISSING: {DECL} — ops-warden must declare in its own voice (§11)") - raise SystemExit(2) - decl = yaml.safe_load(DECL.read_text()) - for key in ("layer", "repository", "derived", "derived_from", "tooling_contacts"): - if key not in decl: - print(f"MALFORMED: layer.yaml has no {key!r}") - raise SystemExit(2) - # §11's derived-artifact rule: marked as derived, naming what it derives from. - if decl["derived"] is not True: - print("MALFORMED: layer.yaml must be marked 'derived: true' (§11, GH-DEC-2026-017 §1)") - raise SystemExit(2) - if decl["derived_from"] != "INTENT.md": - print( - f"MALFORMED: layer.yaml derives from {decl['derived_from']!r}; §11 names " - "INTENT.md as the governing declaration" - ) - raise SystemExit(2) - # A12: the version has no home in a declaration, governing or derived. - _reject_version_pins("layer.yaml", decl) - if _fold(decl["layer"]) not in LAYER_VOCABULARY: - print( - f"MALFORMED: layer.yaml declares layer {decl['layer']!r}, outside §3's " - f"closed vocabulary {sorted(LAYER_VOCABULARY)} (case-insensitive)" - ) - raise SystemExit(2) - for c in decl["tooling_contacts"]: - if c.get("shape") not in VALID_SHAPES: - print(f"MALFORMED: {c.get('id')} has shape {c.get('shape')!r}, not one of {sorted(VALID_SHAPES)}") - raise SystemExit(2) - # §5.3 carries four fields, machine-readably. That is the whole point of - # the shape; a gap missing them is prose wearing a schema. - if c["shape"] == "5.3": - for field in ("capability", "intended_owner", "blocked_on", "review"): - if not c.get(field): - print(f"MALFORMED: §5.3 entry {c['id']!r} is missing {field!r}") - raise SystemExit(2) - # §5.2's test is the supplied-authority property. - if c["shape"] == "5.2" and c.get("supplied_authority") != "none": - print(f"MALFORMED: §5.2 conduit {c['id']!r} must declare supplied_authority: none") - raise SystemExit(2) - return decl - - -def scan_modules() -> dict[str, list[int]]: - """Return {module_name: [line numbers]} for direct Tooling contacts.""" - found: dict[str, list[int]] = {} - for path in sorted(SRC.rglob("*.py")): - if path.name.startswith("test_"): - continue - text = path.read_text() - hits: list[int] = [] - for n, line in enumerate(text.splitlines(), 1): - stripped = line.strip() - if stripped.startswith("#") or stripped.startswith('"'): - continue - if any(p.search(line) for p in TOOLING_PATTERNS): - hits.append(n) - if hits: - # An httpx-only match needs the OpenBao address to be a Tooling - # contact; otherwise it is an Engine or non-catalogued call. - argv_shape = any(TOOLING_PATTERNS[1].search(ln) for ln in text.splitlines()) - if argv_shape or ADDR_HINT.search(text): - found[path.name] = hits - return found - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--report", action="store_true", help="also print the declaration and gap review dates") - args = ap.parse_args() - - # Printed before anything can fail, so even a MALFORMED run states what it - # checked against and over what (GH-DEC-2026-020 §4). - print(f"validated against: {VALIDATED_AGAINST}") - print(f"scope: {SCOPE}") - governing = load_governing_layer() - decl = load_declaration() - declared = {c["module"].split("/")[-1] for c in decl["tooling_contacts"]} - excluded = _excluded(decl) - found = scan_modules() - - undeclared = {m: lines for m, lines in found.items() if m not in declared and m not in excluded} - # A voluntary declaration has no fixed argv shape to detect (an - # operator-configured command). Over-declaring is safe; not reporting it as - # stale keeps the signal meaningful. - voluntary = { - c["module"].split("/")[-1] - for c in decl["tooling_contacts"] - if c.get("detection") == "voluntary" - } - stale_decls = declared - set(found) - voluntary - - if args.report: - print(f"{decl['repository']} — layer: {governing} (declared in INTENT.md; §11 governing form)") - print(f" layer.yaml: derived from {decl['derived_from']}, layer: {decl['layer']}") - print(f"declared by {decl['declared_by']}\n") - for c in decl["tooling_contacts"]: - line = f" §{c['shape']} {c['id']:<28} {c['module']}" - if c["shape"] == "5.3": - overdue = str(c["review"]) < date.today().isoformat() - line += f" -> {c['intended_owner']} review {c['review']}" - if overdue: - line += " [REVIEW OVERDUE]" - print(line) - gaps = [c for c in decl["tooling_contacts"] if c["shape"] == "5.3"] - print(f"\n{len(gaps)} declared gap(s) — tracked non-conformance, not conformance (§11).") - - ok = True - - # §11 as amended (A11): the derived form MUST agree with the governing one, - # and a disagreement is a finding in its own right — reported, not resolved - # away by precedence. Comparison folds case (A9): `Staff` and `staff` agree. - if _fold(decl["layer"]) != _fold(governing): - ok = False - print( - "\nDECLARATION DISAGREEMENT — a finding under §11, not a precedence question:\n" - f" INTENT.md (governs): layer: {governing}\n" - f" layer.yaml (derived): layer: {decl['layer']}\n" - "Precedence says which value is ops-warden's answer. It does not say the\n" - "disagreement did not happen. Case is already folded, so this is a real\n" - "disagreement about a layer, not about orthography." - ) - - if undeclared: - ok = False - print("\nUNDECLARED TOOLING CONTACT — a finding under §11, not a tracked gap:") - for m, lines in sorted(undeclared.items()): - print(f" src/warden/{m}: line(s) {', '.join(map(str, lines[:6]))}") - print("\nAdd a §5.1/§5.2/§5.3 entry to layer.yaml, or route it through an engine.") - - if stale_decls: - print("\nNote: declared but no contact found (module removed or refactored?):") - for m in sorted(stale_decls): - print(f" {m}") - - if ok and not args.report: - print( - f"PASS — {len(found)} module(s) with Tooling contact, all declared; " - f"validated against {VALIDATED_AGAINST}" - ) - elif ok: - print( - "\nPASS — every direct Tooling contact maps to a declared shape; " - f"validated against {VALIDATED_AGAINST}" - ) - return 0 if ok else 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/emit_high_risk_paths.py b/scripts/emit_high_risk_paths.py index ac0e464..f8d533d 100755 --- a/scripts/emit_high_risk_paths.py +++ b/scripts/emit_high_risk_paths.py @@ -118,14 +118,7 @@ def build() -> tuple[str, int]: def _to_data_path(template: str) -> str | None: import re - # A leading slash is a filesystem path, not an OpenBao mount. partition() - # would otherwise invent an empty mount and a fake `/data/...` address. - if ( - re.search(r"[<>{}*]", template) - or " " in template - or template.startswith("k8s:") - or template.startswith("/") - ): + if re.search(r"[<>{}*]", template) or " " in template or template.startswith("k8s:"): return None mount, _, rest = template.partition("/") return f"{mount}/data/{rest}" if rest else None diff --git a/scripts/policy_gate_production_smoke.sh b/scripts/policy_gate_production_smoke.sh index 3aad4a4..68646a1 100755 --- a/scripts/policy_gate_production_smoke.sh +++ b/scripts/policy_gate_production_smoke.sh @@ -19,7 +19,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" INVENTORY="${INVENTORY:-$HOME/.config/warden/inventory.yaml}" REGISTRY="$ROOT/registry/flex-auth/production_registry_snapshot.json" -POLICY="${FLEX_AUTH_POLICY:-$HOME/access-engine/examples/ops-warden/policy_package.md}" +POLICY="${FLEX_AUTH_POLICY:-$HOME/flex-auth/examples/ops-warden/policy_package.md}" FLEX_AUTH_BIN="${FLEX_AUTH_BIN:-/tmp/flex-auth}" ADDR="${FLEX_AUTH_ADDR:-127.0.0.1:18090}" PUBKEY="${PUBKEY:-$HOME/.ssh/agt-state-hub-bridge_ed25519.pub}" diff --git a/scripts/report_coverage.py b/scripts/report_coverage.py deleted file mode 100644 index 11955ea..0000000 --- a/scripts/report_coverage.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env python3 -"""Measure the classification coverage published in `pep-stance.yaml`. - -Read-only. No network, no OpenBao, no secret material. - -Why this exists. security-layer-model v0.8 §6.4 obligation 3 requires a dated -classification-coverage figure beside each stance, and §13.1 carries a Coverage -column whose first entries are ops-warden's — because ops-warden asked for the -column after measuring that 0 of 3 signing targets resolve to a zone. - -Having asked for it, we own the figure's accuracy. A hand-counted number in a -published register decays silently: the register explicitly does not compute -anyone's coverage, and a stale figure beside a marked cell is worse than a blank, -which at least reads as "not reported". So the figure is measured from the same -two artifacts the runtime uses, and `tests/test_layer_conformance.py` asserts the -published block equals what this reports — the same property that makes the stance -map worth publishing (`pep-stance.yaml` equals `PolicyConfig.failure_modes` by -test), applied one level up. - -Two populations, deliberately not summed. They answer different questions and -share no denominator: - - signing targets — actor resources in the flex-auth registry snapshot. This is - the population the stance map actually governs: `warden sign` resolves a zone - per actor resource, and an unresolved one takes the `unknown` cell. - routing lanes — catalog entries with an explicit workload reference. Wider than - the stance map's reach, and the figure that shows *why* coverage is low: an - unknown lane is almost always another repository's undeclared workload - identity, which `ADR-0009` rule 3 forbids closing by inference here. - -Usage: - python scripts/report_coverage.py [--json] - -Exit: 0 always. This reports; it does not gate. Coverage is disclosure, never a -transitional licence, and a script that failed on low coverage would be arguing -the case v0.8 declined. -""" -from __future__ import annotations - -import argparse -import json -import sys -from pathlib import Path -from typing import Any - -REPO = Path(__file__).resolve().parent.parent -sys.path.insert(0, str(REPO / "src")) - -SNAPSHOT = REPO / "registry" / "flex-auth" / "production_registry_snapshot.json" -NOT_APPLICABLE = "not-applicable" - - -def signing_target_coverage(snapshot_path: Path = SNAPSHOT) -> dict[str, int]: - """Zone resolution across the actor resources `warden sign` can name.""" - registry = json.loads(snapshot_path.read_text()) - resolved = unknown = not_applicable = 0 - for manifest in registry.get("resource_manifests") or []: - for resource in manifest.get("resources") or []: - if str(resource.get("type")) != "ssh-certificate": - continue - attributes = resource.get("attributes") or {} - admission = str(attributes.get("security_zone_admission") or "unknown") - zone = str(attributes.get("security_zone") or "unknown") - if admission == NOT_APPLICABLE: - not_applicable += 1 - elif zone == "unknown": - unknown += 1 - else: - resolved += 1 - return {"resolved": resolved, "unknown": unknown, "not_applicable": not_applicable} - - -def routing_lane_coverage(estate_root: Path | None = None) -> dict[str, int]: - """Workload resolution across catalog lanes. - - Delegates to `report_workload_join.build` rather than re-deriving the join: two - implementations of "is this lane resolved" would drift, and the published figure - should be the one the join report shows. - """ - import importlib.util - - spec = importlib.util.spec_from_file_location( - "report_workload_join", Path(__file__).resolve().parent / "report_workload_join.py" - ) - join = importlib.util.module_from_spec(spec) - spec.loader.exec_module(join) - - report = join.build( - REPO / "registry" / "routing" / "catalog.yaml", - estate_root if estate_root is not None else Path.home(), - ) - return { - "resolved": len(report["resolved"]), - "unknown": len(report["unknown"]), - "not_applicable": len(report["not_applicable"]), - } - - -def measure(estate_root: Path | None = None) -> dict[str, Any]: - return { - "signing_targets": signing_target_coverage(), - "routing_lanes": routing_lane_coverage(estate_root), - } - - -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--json", action="store_true", dest="as_json") - args = parser.parse_args(argv) - - report = measure() - if args.as_json: - print(json.dumps(report, indent=2)) - return 0 - - for name, counts in report.items(): - total = sum(counts.values()) - scoped = counts["resolved"] + counts["unknown"] - print(f"{name.replace('_', ' ')}: {counts['resolved']}/{scoped} resolved " - f"({counts['unknown']} unknown, {counts['not_applicable']} not-applicable, " - f"{total} total)") - print() - print("Coverage is disclosure, not a transitional licence (v0.8 §6.4 obligation 3).") - print("Publish in pep-stance.yaml; §13.1 does not compute it for you.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/run_unit_tests.py b/scripts/run_unit_tests.py deleted file mode 100644 index 6c812b2..0000000 --- a/scripts/run_unit_tests.py +++ /dev/null @@ -1,283 +0,0 @@ -"""Run trusted unit tests with no inherited credentials or external I/O. - -This is an accident guard for this suite, not an OS sandbox for hostile Python. -Direct pytest bypasses it. Use make test (already installed dependencies only). -""" -from __future__ import annotations - -import hashlib -import json -import os -from pathlib import Path -import runpy -import socket -import subprocess -import sys -import tempfile - -ROOT = Path(__file__).resolve().parents[1] -SELF = Path(__file__).resolve() -SHELL_FIXTURES = { - "printf 'the-secret-value'", "printf 'sekret'", "printf 'new'", - "printf '%s' '{\"wrap_info\":{\"token\":\"hvs.WRAP\"}}'", - "printf 'not-json'", -} -JOURNAL_CODE_SHA256 = '69aa18f302fd698104cb2253b4af0c3a077e72a0a427183e3dce1444c4926df9' - - -class IsolationDenied(PermissionError, subprocess.SubprocessError): - """A unit test attempted an unreviewed external operation.""" - - -def clean_environment(home: Path, temporary: Path) -> dict[str, str]: - # Construct, never copy/inventory the caller's environment. - return {'HOME': str(home), 'TMPDIR': str(temporary), 'PATH': '/usr/bin:/bin', - 'LANG': 'C.UTF-8', 'PYTHONDONTWRITEBYTECODE': '1', - 'PYTEST_DISABLE_PLUGIN_AUTOLOAD': '1'} - - -def inside(path: Path, roots: tuple[Path, ...]) -> bool: - return any(path == root or root in path.parents for root in roots) - - -class Guard: - def __init__(self, temporary: Path, read_roots: list[Path], child: bool = False, git_metadata: bool = False): - self.temporary = temporary.resolve() - self.read_roots = tuple(p.resolve() for p in read_roots) - self.child = child - self.git_metadata = git_metadata - self.loopback_test = False - self.endpoints: set[tuple[str, int]] = set() - self.launch = None - self.original_popen = subprocess.Popen - - def fixture_path(self, value: str) -> Path: - path = Path(value).resolve() - if not inside(path, (self.temporary,)): - raise IsolationDenied('unit isolation: non-fixture path') - return path - - def audit(self, event, args): - if event == 'open' and not isinstance(args[0], int): - path = Path(os.fsdecode(args[0])).resolve() - mode, flags = args[1:3] - write = ((isinstance(mode, str) and any(c in mode for c in 'wax+')) - or (isinstance(flags, int) and flags & (os.O_WRONLY | os.O_RDWR | os.O_CREAT | os.O_TRUNC | os.O_APPEND))) - roots = (self.temporary,) if write else self.read_roots - if path != Path('/dev/null') and not inside(path, roots): - raise IsolationDenied('unit isolation: file outside reviewed roots') - if event == 'subprocess.Popen': - if self.launch is None or tuple(args[1]) != self.launch: - raise IsolationDenied('unit isolation: unexpected process') - if event in {'os.system', 'os.exec', 'os.posix_spawn', 'os.fork', 'os.forkpty'}: - raise IsolationDenied('unit isolation: native execution disabled') - if event.startswith('socket.'): - if not self.loopback_test or self.child: - raise IsolationDenied('unit isolation: sockets disabled') - if event == 'socket.__new__': - if args[1] != socket.AF_INET or args[2] & 15 != socket.SOCK_STREAM: - raise IsolationDenied('unit isolation: socket family/type denied') - elif event == 'socket.bind': - if args[1] != ('127.0.0.1', 0): - raise IsolationDenied('unit isolation: only ephemeral fixture bind') - elif event == 'socket.connect': - if args[1] not in self.endpoints: - raise IsolationDenied('unit isolation: only this fixture endpoint') - elif event not in {'socket.gethostname'}: - raise IsolationDenied('unit isolation: DNS/socket operation denied') - - def python_payload(self, argv): - """Closed subprocess inventory; every allowed Python child installs guard.""" - from warden import case_inputs - if (len(argv) == 5 and argv[:3] == [sys.executable, '-I', '-c'] - and argv[3] == case_inputs._VALIDATE): - directory = self.fixture_path(argv[4]) - if not directory.name.startswith('warden-case-validator-'): - raise IsolationDenied('unit isolation: validator fixture directory differs') - # Reverify copied modules immediately before launch, independently of caller. - for relative, digest in case_inputs.SOURCE_PINS.items(): - if hashlib.sha256((directory / relative).read_bytes()).hexdigest() != digest: - raise IsolationDenied('unit isolation: validator source pin differs') - return {'code': argv[3], 'args': [str(directory)]} - if len(argv) == 3 and argv[:2] == [sys.executable, '-c'] and argv[2] == 'print("SYNTHETIC-SECRET")': - return {'code': argv[2], 'args': []} - if (len(argv) == 6 and argv[:3] == [sys.executable, '-B', '-c'] - and hashlib.sha256(argv[3].encode()).hexdigest() == JOURNAL_CODE_SHA256): - self.fixture_path(argv[4]) - if len(argv[5]) > 192: - raise IsolationDenied('unit isolation: journal source bound') - return {'code': argv[3], 'args': argv[4:]} - if len(argv) >= 2 and argv[0] in (sys.executable, 'python3'): - script = Path(argv[1]).resolve() - args = argv[2:] - if script == ROOT / 'tests/unit_isolation_probe.py' and len(args) == 1 and args[0] in {'state', 'socket', 'provider', 'home', 'nested'}: - return {'script': str(script), 'args': args} - if script == ROOT / 'scripts/check_layer_conformance.py' and not args: - return {'script': str(script), 'args': args} - if script == ROOT / 'scripts/emit_high_risk_paths.py' and args == ['--check']: - return {'script': str(script), 'args': args, 'git_metadata': True} - if script == ROOT / 'scripts/check_principals_drift.py' and len(args) == 4 and args[::2] == ['--inventory', '--infra']: - for value in args[1::2]: - self.fixture_path(value) - return {'script': str(script), 'args': args} - if script == ROOT / 'scripts/build_flex_auth_registry.py': - if (len(args) in (3, 5) and Path(args[0]).resolve() == ROOT / 'examples/inventory.seed.yaml' - and args[-2] == '-o' and (len(args) == 3 or args[1] == '--zone-resolutions')): - self.fixture_path(args[-1]) - if len(args) == 5: - self.fixture_path(args[2]) - return {'script': str(script), 'args': args} - raise IsolationDenied('unit isolation: command not in reviewed subprocess inventory') - - def popen(self, argv, *pos, **kw): - if pos or kw.get('executable') or kw.get('preexec_fn') or kw.get('pass_fds') or kw.get('close_fds') is False: - raise IsolationDenied('unit isolation: unexpected process options') - if kw.pop('shell', False): - if self.child or not isinstance(argv, str) or argv not in SHELL_FIXTURES: - raise IsolationDenied('unit isolation: unreviewed shell command') - return self.spawn(['/bin/bash', '--noprofile', '--norc', '-c', argv], kw) - if not isinstance(argv, (list, tuple)): - raise IsolationDenied('unit isolation: argv list required') - argv = [os.fspath(x) for x in argv] - # Never run a real provider: the only shell fixture is an exact-byte double. - from warden.doubles import _DOUBLES - metadata_commands = (['git', 'log', '-1', '--format=%H %cI', '--', str(ROOT / 'registry/routing/catalog.yaml')], - ['git', 'status', '--porcelain', '--', str(ROOT / 'registry/routing/catalog.yaml')]) - if self.child and not (self.git_metadata and argv in metadata_commands): - raise IsolationDenied('unit isolation: nested process disabled') - candidate = Path(argv[0]) - if not candidate.is_absolute(): - import shutil - resolved = shutil.which(argv[0], path=kw.get('env', os.environ).get('PATH', '')) - candidate = Path(resolved) if resolved else candidate - if self.child and self.git_metadata: - if Path(kw.get('cwd', '.')).resolve() != ROOT: - raise IsolationDenied('unit isolation: metadata cwd differs') - command = ['/usr/bin/git', '-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '--no-pager', *argv[1:]] - elif candidate.is_absolute() and inside(candidate.resolve(), (self.temporary,)) and candidate.name in _DOUBLES: - if candidate.read_text() != _DOUBLES[candidate.name].script: - raise IsolationDenied('unit isolation: double bytes differ') - tail = argv[1:] - if (candidate.name == 'bao' and tail not in ( - ['kv', 'get', '-field=NPM_AUTH_TOKEN', 'platform/x/y'], - ['kv', 'get', '-field=API_KEY', 'platform/x/y/z'], - ['login', '-method=oidc'], ['write', 'secret/x'])) or (candidate.name == 'key-cape' and tail != ['login']): - raise IsolationDenied('unit isolation: double arguments differ') - command = ['/bin/bash', '--noprofile', '--norc', str(candidate), *tail] - else: - payload = self.python_payload(argv) - payload.update(temporary=str(self.temporary), read_roots=[str(x) for x in self.read_roots]) - command = [sys.executable, '-I', str(SELF), '--child', json.dumps(payload)] - return self.spawn(command, kw) - - def spawn(self, command, kw): - cwd = Path(kw.get('cwd') or ROOT).resolve() - if not inside(cwd, (ROOT, self.temporary)): - raise IsolationDenied('unit isolation: non-fixture working directory') - kw['cwd'] = str(cwd) - # env supplied by tests is never propagated: even synthetic token mutations - # remain in the parent. All approved children have only this fresh environment. - kw['env'] = clean_environment(self.temporary / 'home', self.temporary) - kw['env'].update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_OPTIONAL_LOCKS='0') - self.launch = tuple(command) - try: - return self.original_popen(command, **kw) - finally: - self.launch = None - - def install(self): - sys.addaudithook(self.audit) - subprocess.Popen = self.popen - guard = self - original_socket = socket.socket - class FixtureSocket(original_socket): - def bind(self, address): - super().bind(address) - guard.endpoints.add(self.getsockname()) - socket.socket = FixtureSocket - def numeric_addresses(host, port, family=0, type=0, proto=0, flags=0): - if not guard.loopback_test or (host, port) not in guard.endpoints: - raise IsolationDenied('unit isolation: DNS/external endpoint denied') - return [(socket.AF_INET, socket.SOCK_STREAM, 6, '', (host, port))] - socket.getaddrinfo = numeric_addresses - def local_name(host=''): - if not guard.loopback_test or host != '127.0.0.1': - raise IsolationDenied('unit isolation: hostname lookup denied') - return 'localhost' - socket.getfqdn = local_name - - def pytest_configure(self, config): - config._unit_guard = self - - # Exact single synthetic HTTP test only; no other test gets a loopback lease. - def pytest_runtest_setup(self, item): - self.loopback_test = item.nodeid == 'tests/test_desk.py::test_approve_flow_http_dry' - self.endpoints.clear() - - def pytest_runtest_teardown(self, item): - self.loopback_test = False - self.endpoints.clear() - - -def worker(temporary: Path, arguments: list[str], netkingdom: Path | None, phase: Path | None): - sys.dont_write_bytecode = True - tempfile.tempdir = str(temporary) - paths = [ROOT / 'src'] - if phase: - paths.append(phase / 'src') - # Explicit trusted source roots only, never ambient PYTHONPATH/plugins. - sys.path[:0] = [str(p) for p in paths] - roots = [ROOT, *paths, Path(sys.prefix), Path(sys.base_prefix), temporary] - if phase: - roots.append(phase / 'tests/fixtures/ops-warden-coordination-profile.json') - if netkingdom: - roots.append(netkingdom) - os.environ['WARDEN_TEST_NETKINGDOM_ROOT'] = str(netkingdom) - guard = Guard(temporary, roots) - guard.install() - # Pytest itself and collection run only after installation. - import pytest - raise SystemExit(pytest.main(['-p', 'no:cacheprovider', '--basetemp', str(temporary / 'pytest'), *arguments], plugins=[guard])) - - -def main(): - if sys.argv[1:2] == ['--child']: - payload = json.loads(sys.argv[2]) - temporary = Path(payload['temporary']) - sys.dont_write_bytecode = True - tempfile.tempdir = str(temporary) - sys.path.insert(0, str(ROOT / 'src')) - guard = Guard(temporary, [Path(p) for p in payload['read_roots']], child=True, - git_metadata=payload.get('git_metadata', False)) - guard.install() - sys.argv = [payload.get('script', '-c'), *payload['args']] - if 'script' in payload: - runpy.run_path(payload['script'], run_name='__main__') - else: - exec(compile(payload['code'], '', 'exec'), {'__name__': '__main__'}) - return - if sys.argv[1:2] == ['--worker']: - temporary = Path(sys.argv[2]) - netkingdom = Path(sys.argv[3]) if sys.argv[3] else None - phase = Path(sys.argv[4]) if sys.argv[4] else None - worker(temporary, sys.argv[5:], netkingdom, phase) - return - import argparse - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--netkingdom-root', type=Path, default=ROOT.parent / 'net-kingdom') - parser.add_argument('--phase-memory-root', type=Path, default=ROOT.parent / 'phase-memory') - options, arguments = parser.parse_known_args() - netkingdom = options.netkingdom_root.resolve() if options.netkingdom_root.is_dir() else None - phase = options.phase_memory_root.resolve() if (options.phase_memory_root / 'src').is_dir() else None - with tempfile.TemporaryDirectory(prefix='warden-unit-') as directory: - temporary = Path(directory) - home = temporary / 'home' - home.mkdir() - result = subprocess.run([sys.executable, '-I', str(SELF), '--worker', str(temporary), - str(netkingdom) if netkingdom else '', str(phase) if phase else '', *arguments], - cwd=ROOT, env=clean_environment(home, temporary)) - raise SystemExit(result.returncode) - - -if __name__ == '__main__': - main() diff --git a/src/warden/access.py b/src/warden/access.py index 1f217d9..ad3af27 100644 --- a/src/warden/access.py +++ b/src/warden/access.py @@ -12,21 +12,13 @@ reuses `expand_handoff` to build the command it runs as the caller. """ from __future__ import annotations -import os from dataclasses import dataclass -from typing import Mapping, Optional +from typing import Optional from warden.config import ConfigError, load_config from warden.routing.models import RouteEntry -# KeyCape KEY-WP-0009-T03 is the issued coding-agent identity available today. -# This is intentionally an exact subject allowlist, not a guess based on every -# ``service:*`` identity. OpenBao validates and enforces the credential; this -# module only recognizes the already-issued subject for the advisory CLI guard. -ISSUED_AGENT_SUBJECTS = frozenset({"service:codex:railiance-platform"}) - - @dataclass class ExpandedHandoff: """Handoff templates with `` substituted when a domain is supplied. @@ -42,25 +34,6 @@ class ExpandedHandoff: exec_capable: bool -def agent_read_boundary_identity( - environ: Mapping[str, str] | None = None, -) -> str | None: - """Return the issued/fallback agent marker used by the advisory read guard. - - ``WARDEN_POLICY_SUBJECT`` carries the principal identity used by the policy - request. When it names an issued coding-agent subject, prefer it over the - self-declared legacy marker. This function does not validate a token or - render an authorization decision; OpenBao's agent policy is the enforced - boundary. ``WARDEN_AGENT_ID`` remains a fail-toward-safety fallback. - """ - env = os.environ if environ is None else environ - issued_subject = str(env.get("WARDEN_POLICY_SUBJECT") or "").strip() - if issued_subject in ISSUED_AGENT_SUBJECTS: - return issued_subject - fallback = str(env.get("WARDEN_AGENT_ID") or "").strip() - return fallback or None - - def _sub_domain(value: Optional[str], domain: Optional[str]) -> Optional[str]: if value and domain: return value.replace("", domain) @@ -98,10 +71,6 @@ def policy_gate_status() -> str: cfg = load_config() except ConfigError: return "advisory — no warden.yaml (caller identity; gate not enforced)" - fetch_url = cfg.policy.fetch_flex_auth_url - if fetch_url: - return f"zone-aware — fetches gated by flex-auth-ops-warden-fetch at {fetch_url}" - return ( - "fail-closed — secret-read evaluator unconfigured " - "(policy.fetch_flex_auth_url unset); fetches are refused" - ) + if cfg.policy.flex_auth_url: + return f"zone-aware — flex-auth at {cfg.policy.flex_auth_url}" + return "zone-aware — evaluator unconfigured; unknown-zone fail_open applies" diff --git a/src/warden/audit.py b/src/warden/audit.py index 1132c74..ec81685 100644 --- a/src/warden/audit.py +++ b/src/warden/audit.py @@ -4,8 +4,6 @@ Every ops-warden action appends a JSONL event. Secret values are rejected at wri """ from __future__ import annotations -from warden.clock_attribution import recording_time - import json import os import re @@ -87,7 +85,6 @@ def record_event( if value is None: continue event[key] = value - event.update(recording_time()) _assert_metadata_safe(json.dumps(event, default=str)) state_dir.mkdir(parents=True, exist_ok=True) path = _audit_path(state_dir) diff --git a/src/warden/ca.py b/src/warden/ca.py index b3aaf02..68aefd8 100644 --- a/src/warden/ca.py +++ b/src/warden/ca.py @@ -33,25 +33,6 @@ def _enforce_ttl(spec: CertSpec) -> None: f"TTL {spec.ttl_hours}h exceeds maximum {max_h}h for actor type " f"{spec.actor_type.value!r} (AccessManagementDirective §2)" ) - if spec.max_ttl_hours is not None and spec.ttl_hours > spec.max_ttl_hours: - raise CAError( - f"TTL {spec.ttl_hours}h exceeds actor {spec.actor_name!r} profile " - f"maximum {spec.max_ttl_hours}h" - ) - - -def extension_options(spec: CertSpec) -> List[str]: - """ssh-keygen -O arguments for the actor's extension allow-list. - - ``None`` keeps ssh-keygen defaults (all permit-* extensions). A list clears - every default extension and permits only the listed ones. - """ - if spec.extensions is None: - return [] - opts = ["-O", "clear"] - for ext in spec.extensions: - opts += ["-O", ext] - return opts def _evict_cert(actor_name: str, state_dir: Path) -> None: @@ -205,7 +186,6 @@ class LocalCA(CABackend): "-I", spec.identity, "-n", principals_str, "-V", f"+{spec.ttl_hours}h", - *extension_options(spec), str(pubkey_copy), ] result = subprocess.run(cmd, capture_output=True, text=True) diff --git a/src/warden/case_inputs.py b/src/warden/case_inputs.py deleted file mode 100644 index 9c876d7..0000000 --- a/src/warden/case_inputs.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Reviewed browser metadata projection; no native trust or command dispatch. - -The producer validator and all reviewed producer modules are pinned below. -Their verified bytes run in a disposable tree, without caller environment or -ambient Python imports. Type parameters must match the exact reviewed version. -Machine descriptor extensions still require Executor's independent admission. -""" - -import hashlib -import json -from pathlib import Path -import subprocess -import sys -import tempfile - -from .case_receipt_state import Refused - -# Reviewed producer: net-kingdom 015694ee414c802dc2ef1fa0120dfed610086e69. -# Review: docs/reviews/2026-10-16-browser-producer-pin-refresh.md -SOURCE_PINS = { - "tools/security-case-types/security_case_evidence.py": "da8efa59c52ad73283397ebe639ae259d0cb9c6348b5cb829135c24230dcdc75", - "tools/security-case-types/security_case_validator.py": "1bcc62e9bc374c711363295549f3cd5bb0b32952671ed4b08fddc7f74742959b", - "tools/security-scenario-composer/security_scenario_composer.py": "9f0cc69930aa8ee2cc3fb92236e6a99728df17b8c0fe36e1250241c38fc01c4f", - "tools/playbook-capability-contract/playbook_contract_validator.py": "2e1e0c809d4bc9d64c2d974cc23fecd1c5636785154c2554edd9191852fd926d", -} -PARAMETER_PINS = { - "1.1": "9ec2d6113bf70dbd35cfc247412363ff187e792bc82e24159a6f78e090aa3e4c", - "1.2": "c04385d176f5f70257fb0fa6fef7e48ab26ed7cf26f6b1c7f14b16237b0ebea6", -} - -_VALIDATE = """ -import json, sys -from pathlib import Path -sys.path.insert(0, str(Path(sys.argv[1]) / 'tools/security-case-types')) -import security_case_validator as v -case, typ = json.load(sys.stdin) -issues = v.validate_type(typ) + v.validate_case(case, typ, require_implemented=False) -print('valid' if not v.has_errors(issues) else 'invalid') -""" - - -class BrowserInputs: - """Explicit owner configuration, never inferred from a case-supplied path.""" - - def __init__(self, netkingdom_root: Path): - self.root = Path(netkingdom_root) - - def target(self, case: dict, typ: dict, target_parameter: str) -> str: - try: - spec = typ["spec"] - parameters = spec["parameters"] - fingerprint = hashlib.sha256( - json.dumps(parameters, sort_keys=True, separators=(",", ":")).encode() - ).hexdigest() - if ( - spec["id"] != "oidc-browser-relying-party" - or PARAMETER_PINS.get(spec["version"]) != fingerprint - ): - raise ValueError - declared = {item["name"]: item for item in parameters} - definition = declared.get(target_parameter, {}) - if ( - definition.get("sensitivity") not in ("public", "operational") - or definition.get("case_settable") is not True - ): - raise ValueError - # No secret references or newly classified metadata are grandfathered. - for name in case["spec"]["parameters"]: - item = declared.get(name, {}) - if item.get("case_settable") is not True or item.get( - "sensitivity" - ) not in ("public", "operational", "security_sensitive"): - raise ValueError - with tempfile.TemporaryDirectory( - prefix="warden-case-validator-" - ) as directory: - for relative, expected in SOURCE_PINS.items(): - source = (self.root / relative).read_bytes() - if hashlib.sha256(source).hexdigest() != expected: - raise ValueError - dest = Path(directory) / relative - dest.parent.mkdir(parents=True, exist_ok=True) - dest.write_bytes(source) - result = subprocess.run( - [sys.executable, "-I", "-c", _VALIDATE, directory], - input=json.dumps([case, typ]), - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - env={}, - timeout=10, - check=True, - ) - if result.stdout != "valid\n": - raise ValueError - return case["spec"]["parameters"][target_parameter] - except (OSError, ValueError, TypeError, KeyError, subprocess.SubprocessError): - raise Refused("browser metadata failed exact-version validation") from None diff --git a/src/warden/case_plan.py b/src/warden/case_plan.py deleted file mode 100644 index 15cb14b..0000000 --- a/src/warden/case_plan.py +++ /dev/null @@ -1,226 +0,0 @@ -"""Plan a KnownSecurityCase run (WARDEN-WP-0044-T02). - -``warden case plan `` checks that a case is accepted and materialized -(canon ``security-case-types_v0.1``: ``proposed -> accepted -> materialized -> -live``), checks its type with :mod:`warden.case_type`, resolves each -follow-through step against the case's parameters, and prints the run: which -steps ops-warden executes as the caller's proxy, which run in an attended -session, which the owner runs, and what verification follows. - -Read-only: it parses YAML and the catalog, executes nothing, and never handles a -secret value. Parameters are rendered into entry points and checks, so a -parameter whose type declares ``sensitivity: secret`` is refused outright. -""" -from __future__ import annotations - -import hashlib -import re -import string -from dataclasses import dataclass, field -from pathlib import Path -from typing import Dict, List, Optional - -import yaml - -from warden.case_type import Finding, check_follow_through -from warden.routing.catalog import Catalog - -PLANNABLE_CASE_STATUS = "materialized" -PLANNABLE_TYPE_STATUSES = ("acknowledged", "implemented") -_WHEN = re.compile(r"^\s*([a-z_][a-z0-9_]*)\s*==\s*(\S+)\s*$") - - -class CaseError(ValueError): - """The case cannot be planned; the message says why.""" - - -@dataclass(frozen=True) -class PlannedStep: - id: str - owner: str - mode: str # "warden" | "attended" | "owner" - entry_point: str - auth: str - lane: Optional[str] - value_handling: str - ready_check: Optional[str] = None - - def as_dict(self) -> dict: - return {k: v for k, v in self.__dict__.items() if v is not None} - - -@dataclass -class CasePlan: - case_id: str - type_ref: str - steps: List[PlannedStep] = field(default_factory=list) - skipped: List[str] = field(default_factory=list) - attended_sessions: List[str] = field(default_factory=list) - verification: List[dict] = field(default_factory=list) - case_ref: Optional[str] = None - - def as_dict(self) -> dict: - return { - "case": self.case_id, - "case_ref": self.case_ref, - "type": self.type_ref, - "steps": [s.as_dict() for s in self.steps], - "skipped": self.skipped, - "attended_sessions": self.attended_sessions, - "verification": self.verification, - } - - -def load_case(path: Path) -> dict: - with open(path) as fh: - doc = yaml.safe_load(fh) - if not isinstance(doc, dict) or doc.get("kind") != "SecurityCase": - raise CaseError(f"{path}: not a SecurityCase document") - return doc - - -def case_ref(case_id: str, case_bytes: bytes) -> str: - """``@sha256:`` over the case file's exact bytes. - - The reference an owner entry point records for a run (RPF-WP-0054-T01). The - owner re-derives it from the file rather than trusting the caller. - """ - return f"{case_id}@sha256:{hashlib.sha256(case_bytes).hexdigest()}" - - -def default_type_path(case_path: Path, type_ref: str) -> Path: - """net-kingdom layout: security-cases/.yaml beside security-case-types//type.yaml.""" - type_id = type_ref.split("@", 1)[0] - return case_path.resolve().parent.parent / "security-case-types" / type_id / "type.yaml" - - -def _applies(when: Optional[str], params: Dict[str, str]) -> bool: - if not when: - return True - match = _WHEN.match(when) - if not match: - raise CaseError(f"unsupported condition {when!r}; only ' == ' is planned") - name, expected = match.groups() - if name not in params: - raise CaseError(f"condition {when!r} names unknown parameter '{name}'") - return str(params[name]).lower() == expected.lower() - - -#: Format specs a template may apply, matching net-kingdom's validator -#: (``FORMAT_SPECS``): ``{client_id:env}`` is upper case with ``-`` as ``_``. -FORMAT_SPECS = ("", "env") - - -class _Param(str): - def __format__(self, spec: str) -> str: - if spec == "env": - return self.upper().replace("-", "_") - return str.__format__(self, spec) - - -def _render(text: Optional[str], params: Dict[str, str], where: str) -> Optional[str]: - if text is None: - return None - fields = [(f, spec or "", conv) for _, f, spec, conv in string.Formatter().parse(text) if f] - bad = sorted({f"{f}:{spec}" for f, spec, _ in fields if spec not in FORMAT_SPECS}) - if bad: - raise CaseError(f"{where}: unknown format spec in {bad}; only {{param:env}} is supported") - if any(conv for _, _, conv in fields): - raise CaseError(f"{where}: conversions such as !r are not supported in templates") - missing = sorted({f for f, _, _ in fields} - set(params)) - if missing: - raise CaseError(f"{where}: parameters {missing} are not set by the case") - return text.format_map({k: _Param(v) for k, v in params.items()}) - - -def plan_case(case_doc: dict, type_doc: dict, catalog: Optional[Catalog]) -> CasePlan: - case = case_doc.get("spec") or {} - spec = type_doc.get("spec") or {} - case_id = str(case.get("id", "")) - type_ref = str(case.get("type", "")) - - status = case.get("status") - if status != PLANNABLE_CASE_STATUS: - hint = {"proposed": "it is not accepted", "accepted": "ops-mason has not materialized it", - "live": "it is already live", "retired": "it is retired"}.get(status, "unknown status") - raise CaseError(f"case {case_id} is '{status}' ({hint}); planning needs '{PLANNABLE_CASE_STATUS}'") - if not case.get("acceptance"): - raise CaseError(f"case {case_id} carries no acceptance record") - - want = f"{spec.get('id')}@{spec.get('version')}" - if type_ref != want: - raise CaseError(f"case names type {type_ref}, type file is {want}") - if spec.get("status") not in PLANNABLE_TYPE_STATUSES: - raise CaseError(f"type {want} is '{spec.get('status')}', not acknowledged") - - report = check_follow_through(type_doc, catalog) - if report.findings: - raise CaseError("type is not plannable:\n" + "\n".join(_line(f) for f in report.findings)) - - declared = {p["name"]: p for p in spec.get("parameters") or [] if isinstance(p, dict) and "name" in p} - params = {k: str(v) for k, v in (case.get("parameters") or {}).items()} - unknown = sorted(set(params) - set(declared)) - if unknown: - raise CaseError(f"case sets parameters the type does not declare: {unknown}") - fixed = sorted(k for k in params if declared[k].get("case_settable") is False) - if fixed: - raise CaseError(f"case sets parameters the type fixes: {fixed}") - secret = sorted(k for k in params if declared[k].get("sensitivity") == "secret") - if secret: - raise CaseError(f"case carries secret-sensitivity parameters {secret}; values never enter a plan") - for name, decl in declared.items(): - if name not in params and "default" in decl: - params[name] = str(decl["default"]) - - plan = CasePlan(case_id=case_id, type_ref=type_ref) - for step in spec.get("follow_through") or []: - sid = step["id"] - if not _applies(step.get("when"), params): - plan.skipped.append(sid) - continue - mode = "warden" if step["warden_executes"] else "attended" if step["attended"] else "owner" - where = f"follow_through[{sid}]" - plan.steps.append(PlannedStep( - id=sid, owner=_render(str(step["owner"]), params, where), mode=mode, - entry_point=_render(step["entry_point"], params, where), - auth=_render(step["auth"], params, where), - lane=step.get("lane"), value_handling=step["value_handling"], - ready_check=_render(step.get("ready_check"), params, where), - )) - if mode == "attended" and step["lane"] not in plan.attended_sessions: - plan.attended_sessions.append(step["lane"]) - - for item in spec.get("verification") or []: - if not _applies(item.get("when"), params): - continue - plan.verification.append({ - "id": item.get("id"), - "check": _render(item.get("check"), params, f"verification[{item.get('id')}]"), - "expect": item.get("expect"), - }) - return plan - - -def _line(f: Finding) -> str: - return f"[{f.kind}] {f.code} {f.where}: {f.message}" - - -def format_plan(plan: CasePlan) -> str: - labels = {"warden": "warden (caller proxy)", "attended": "ATTENDED", "owner": "owner"} - lines = [f"case {plan.case_id} ({plan.type_ref}): plan only, nothing executed"] - if plan.case_ref: - lines.append(f"case-ref: {plan.case_ref}") - lines.append("") - for n, s in enumerate(plan.steps, 1): - lane = f" lane={s.lane}" if s.lane else "" - lines.append(f"{n}. {s.id} [{labels[s.mode]}] owner={s.owner}{lane} values={s.value_handling}") - lines.append(f" entry: {s.entry_point}") - lines.append(f" auth: {s.auth}") - if s.ready_check: - lines.append(f" ready: {s.ready_check}") - if plan.skipped: - lines.append(f"skipped (condition false): {', '.join(plan.skipped)}") - lines.append(f"attended sessions: {', '.join(plan.attended_sessions) or '-'}") - lines.append("verification:") - lines.extend(f" - {v['id']}: {v['check']} (expect {v['expect']})" for v in plan.verification) - return "\n".join(lines) diff --git a/src/warden/case_receipt_state.py b/src/warden/case_receipt_state.py deleted file mode 100644 index 03a53e9..0000000 --- a/src/warden/case_receipt_state.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Proposed, pure case receipt reducer for WARDEN-WP-0044-T03. - -This prototype executes nothing and implements no accepted owner adapter API. -Callers MUST durably persist a returned running receipt before a side effect. -Only metadata identifiers/digests enter this model; no command output is accepted. -""" -from __future__ import annotations - -from dataclasses import dataclass, replace -from enum import Enum -import re - -_ID = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,127}\Z") -_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") - - -class Refused(ValueError): - """A transition requires an independent owner reconciliation or new review.""" - - -class Outcome(str, Enum): - RUNNING = "running" - SUCCESS = "verified-success" - NO_CHANGE = "verified-no-change" - FAILED_BEFORE = "failed-before-change" - FAILED_AFTER = "failed-after-change" - UNCERTAIN = "uncertain" - ROLLBACK_RUNNING = "rollback-running" - ROLLED_BACK = "verified-rolled-back" - ROLLBACK_UNCERTAIN = "rollback-uncertain" - - -@dataclass(frozen=True) -class Pins: - """Exact reviewed case/type/adapter/catalog/acceptance/run-sheet digests.""" - - case: str - case_type: str - adapter: str - catalog: str - acceptance: str - run_sheet: str - - def __post_init__(self) -> None: - if not all(_DIGEST.fullmatch(value) for value in vars(self).values()): - raise Refused("pins require sha256 digests") - - -@dataclass(frozen=True) -class Receipt: - attempt: int - outcome: Outcome - created: tuple[str, ...] = () - - -@dataclass(frozen=True) -class StepState: - step_id: str - owner: str - pins: Pins - allowed_created: tuple[str, ...] = () - retry_safe: bool = False - receipts: tuple[Receipt, ...] = () - - def __post_init__(self) -> None: - if not all(_ID.fullmatch(v) for v in (self.step_id, self.owner, *self.allowed_created)): - raise Refused("only metadata identifiers are supported") - if len(set(self.allowed_created)) != len(self.allowed_created): - raise Refused("duplicate reviewed object identifier") - - @property - def last(self) -> Receipt | None: - return self.receipts[-1] if self.receipts else None - - -def _same_pins(state: StepState, pins: Pins) -> None: - if state.pins != pins: - raise Refused("pins changed; a newly reviewed sheet is required") - - -def _append(state: StepState, outcome: Outcome, created: tuple[str, ...] = ()) -> StepState: - receipt = Receipt(len(state.receipts) + 1, outcome, created) - return replace(state, receipts=(*state.receipts, receipt)) - - -def begin(state: StepState, pins: Pins, *, retry: bool = False) -> StepState: - """Return intent to dispatch. Persist before calling an owner adapter.""" - _same_pins(state, pins) - last = state.last - if last is None: - if retry: - raise Refused("no failed attempt to retry") - elif not (last.outcome is Outcome.FAILED_BEFORE and state.retry_safe and retry): - raise Refused("step cannot dispatch or replay without owner reconciliation") - return _append(state, Outcome.RUNNING) - - -def finish(state: StepState, pins: Pins, outcome: Outcome, - *, created: tuple[str, ...] = ()) -> StepState: - """Accept only an adapter's verified, closed metadata result, never output.""" - _same_pins(state, pins) - if not state.last or state.last.outcome is not Outcome.RUNNING: - raise Refused("no running side effect") - if not isinstance(outcome, Outcome) or outcome not in (Outcome.SUCCESS, Outcome.NO_CHANGE, Outcome.FAILED_BEFORE, - Outcome.FAILED_AFTER, Outcome.UNCERTAIN): - raise Refused("unsupported adapter result") - if len(set(created)) != len(created) or not set(created) <= set(state.allowed_created): - raise Refused("created objects are outside the reviewed case") - if created and outcome not in (Outcome.SUCCESS, Outcome.FAILED_AFTER): - raise Refused("this result cannot claim created objects") - return _append(state, outcome, created) - - -def recover(state: StepState) -> StepState: - """Lost result after persisted dispatch is uncertain, never safe to retry.""" - if state.last and state.last.outcome is Outcome.RUNNING: - return _append(state, Outcome.UNCERTAIN) - if state.last and state.last.outcome is Outcome.ROLLBACK_RUNNING: - return _append(state, Outcome.ROLLBACK_UNCERTAIN, state.last.created) - return state - - -def rollback_objects(state: StepState, pins: Pins) -> tuple[str, ...]: - """Reviewed identifiers only, reverse created order; no shared-object undo.""" - _same_pins(state, pins) - last = state.last - if last is None or last.outcome is Outcome.NO_CHANGE: - return () - if last.outcome is not Outcome.SUCCESS: - raise Refused("rollback needs a verified successful creation receipt") - return tuple(reversed(last.created)) - - -def begin_rollback(state: StepState, pins: Pins) -> StepState: - objects = rollback_objects(state, pins) - if not objects: - raise Refused("no case-created objects to undo") - return _append(state, Outcome.ROLLBACK_RUNNING, objects) - - -def finish_rollback(state: StepState, pins: Pins, *, verified: bool) -> StepState: - _same_pins(state, pins) - if not state.last or state.last.outcome is not Outcome.ROLLBACK_RUNNING: - raise Refused("no running rollback") - outcome = Outcome.ROLLED_BACK if verified else Outcome.ROLLBACK_UNCERTAIN - return _append(state, outcome, state.last.created) diff --git a/src/warden/case_run.py b/src/warden/case_run.py deleted file mode 100644 index 7d84bfb..0000000 --- a/src/warden/case_run.py +++ /dev/null @@ -1,1092 +0,0 @@ -"""Closed local execution kernel v1; no installed adapters or live CLI dispatch. - -An owner must acknowledge each descriptor and provide an independent provenance -verifier. Descriptive case types remain plan-only. Nothing executes shell prose. -""" - -from __future__ import annotations - -from contextlib import contextmanager, redirect_stderr, redirect_stdout -from dataclasses import asdict, dataclass, replace -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import sqlite3 -import threading -import uuid -from typing import Callable - -import yaml -from .case_inputs import BrowserInputs -from .case_receipt_state import ( - Outcome, - Pins, - Receipt, - Refused, - StepState, - begin, - finish, - recover, - begin_rollback, - finish_rollback, -) - -IDENTIFIER = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9_.:/@-]{0,191}\Z") -MAX_SOURCE_BYTES = 262144 -MAX_HISTORY = 128 -OUTPUT_LOCK = threading.RLock() - - -def bounded_source(value: bytes): - if type(value) is not bytes or len(value) > MAX_SOURCE_BYTES: - raise Refused("source exceeds bounded input contract") - - -@contextmanager -def contained_output(): - """Discard Python and inherited OS stdout/stderr; serialize process fd changes.""" - with OUTPUT_LOCK: - saved = [] - null = os.open(os.devnull, os.O_WRONLY) - try: - for target in (1, 2): - saved.append((target, os.dup(target))) - os.dup2(null, target) - with redirect_stdout(_Discard()), redirect_stderr(_Discard()): - try: - yield - except Exception: - raise Refused("owner callback failed") from None - except BaseException: - raise OwnerInterrupted() from None - finally: - for target, original in saved: - os.dup2(original, target) - os.close(original) - os.close(null) - - -DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z") - - -def digest(value: bytes) -> str: - return "sha256:" + hashlib.sha256(value).hexdigest() - - -@dataclass(frozen=True, slots=True) -class Descriptor: - version: str - step: str - owner: str - adapter: str - adapter_source: str - actor: str - lane: str - target_parameter: str - effect: str - allowed_created: tuple[str, ...] - retry_safe: bool - revocation_scopes: tuple[str, ...] - - @classmethod - def decode(cls, source: bytes) -> Descriptor: - bounded_source(source) - try: - obj = json.loads(source) - if set(obj) != set(cls.__dataclass_fields__): - raise ValueError - if not isinstance(obj["allowed_created"], list): - raise ValueError - obj["allowed_created"] = tuple(obj["allowed_created"]) - if not isinstance(obj["revocation_scopes"], list): - raise ValueError - obj["revocation_scopes"] = tuple(obj["revocation_scopes"]) - result = cls(**obj) - identifiers = ( - result.step, - result.owner, - result.adapter, - result.lane, - result.target_parameter, - *result.allowed_created, - ) - if result.version != "warden.case.execution.v1" or not all( - isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in identifiers - ): - raise ValueError - if not DIGEST.fullmatch(result.adapter_source) or result.actor not in ( - "human", - "agent", - ): - raise ValueError - if ( - result.effect not in ("read", "change", "mint") - or type(result.retry_safe) is not bool - ): - raise ValueError - scopes = set(result.revocation_scopes) - if ( - len(scopes) != len(result.revocation_scopes) - or not scopes <= {"parent-session", "broker-grant"} - or "parent-session" not in scopes - ): - raise ValueError - if result.effect == "mint" and "broker-grant" not in scopes: - raise ValueError - if len(result.allowed_created) > 32: - raise ValueError - if len(set(result.allowed_created)) != len(result.allowed_created): - raise ValueError - return result - except (ValueError, TypeError, KeyError): - raise Refused("invalid closed execution descriptor") from None - - -@dataclass(frozen=True, slots=True) -class OwnerResult: - outcome: Outcome - evidence: str - created: tuple[str, ...] = () - revoked: bool = False - session_revocation_evidence: str | None = None - grant_revocation_evidence: str | None = None - attempt_id: str = "" - operation: str = "" - - -@dataclass(frozen=True, slots=True) -class Invocation: - step: str - owner: str - target: str - actor: str - principal: str - lane: str - pins: Pins - attempt_id: str = "" - operation: str = "admission" - tenant: str = "" - realm: str = "" - - -INTENT_SCHEMA = "warden.case.original-intent.v1" - - -@dataclass(frozen=True, slots=True) -class OriginalIntent: - """Durable record of the original mutating dispatch, written before dispatch. - - Binds the original Invocation (attempt, step, owner, target, actor, principal, - lane), all pins and the admitted adapter source. Current admit leaves tenant - and realm empty; their reserved fields are not admitted provenance. This is - local dispatch correspondence, not owner proof of creation. Owners still - authenticate original created-only provenance independently. - """ - - binding: str - invocation: Invocation - source: str - - def validate(self) -> None: - """Fixed bounded metadata only; tenant/realm are not authority claims. - - Current admit leaves tenant/realm empty. Nonempty future coordinates need - separate owner provenance; serializability alone must never supply it. - """ - if ( - type(self.invocation) is not Invocation - or type(self.invocation.pins) is not Pins - ): - raise Refused("exact original invocation required") - i = self.invocation - fields = (i.step, i.owner, i.target, i.principal, i.lane, i.attempt_id) - if ( - not all( - type(value) is str and IDENTIFIER.fullmatch(value) for value in fields - ) - or type(i.actor) is not str - or i.actor not in ("human", "agent") - or type(i.operation) is not str - or i.operation not in ("run", "rollback") - or not all( - type(value) is str and (not value or IDENTIFIER.fullmatch(value)) - for value in (i.tenant, i.realm) - ) - or type(self.source) is not str - or not DIGEST.fullmatch(self.source) - or type(self.binding) is not str - or self.binding != f"{i.step}:{i.target}" - ): - raise Refused("invalid original intent metadata") - if set(Invocation.__dataclass_fields__) != { - "step", - "owner", - "target", - "actor", - "principal", - "lane", - "pins", - "attempt_id", - "operation", - "tenant", - "realm", - }: - raise Refused("original invocation field contract changed") - if not all( - type(v) is str and DIGEST.fullmatch(v) - for v in ( - i.pins.case, - i.pins.case_type, - i.pins.adapter, - i.pins.catalog, - i.pins.acceptance, - i.pins.run_sheet, - ) - ): - raise Refused("invalid original intent pins") - - def payload(self) -> str: - self.validate() - return json.dumps( - {"schema": INTENT_SCHEMA, "binding": self.binding, "source": self.source} - | asdict(self.invocation), - separators=(",", ":"), - sort_keys=True, - ) - - @classmethod - def decode(cls, raw: str) -> OriginalIntent: - def unique(pairs): - result = {} - for key, value in pairs: - if key in result: - raise Refused("duplicate original intent field") - result[key] = value - return result - - try: - if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: - raise Refused("original intent input exceeds bound") - obj = json.loads(raw, object_pairs_hook=unique) - fields = { - "step", - "owner", - "target", - "actor", - "principal", - "lane", - "pins", - "attempt_id", - "operation", - "tenant", - "realm", - } - if ( - type(obj) is not dict - or set(obj) != fields | {"schema", "binding", "source"} - or obj["schema"] != INTENT_SCHEMA - ): - raise Refused("unknown original intent schema") - inv = {k: obj[k] for k in fields} - inv["pins"] = Pins(**inv["pins"]) - result = cls(obj["binding"], Invocation(**inv), obj["source"]) - result.validate() - return result - except (TypeError, ValueError, KeyError): - raise Refused("invalid original intent record") from None - - -@dataclass(frozen=True, slots=True) -class ReconcileContext: - """Original intent and the new reconciliation attempt, kept separate.""" - - original: OriginalIntent - attempt: Invocation - - -@dataclass(frozen=True, slots=True) -class Adapter: - reference: str - source: str - invoke: Callable[[Invocation], OwnerResult] - verify: Callable[[Invocation, OwnerResult], bool] - rollback: Callable[[Invocation, tuple[str, ...]], OwnerResult] | None = None - reconcile: Callable[[Invocation], OwnerResult] | None = None - reconcile_original: Callable[[ReconcileContext], OwnerResult] | None = None - - -class Journal: - """Private SQLite journal plus process lock; transaction commits before dispatch.""" - - def __init__(self, directory: Path): - directory.mkdir(mode=0o700, parents=True, exist_ok=True) - if ( - directory.is_symlink() - or directory.stat().st_uid != os.getuid() - or directory.stat().st_mode & 0o077 - ): - raise Refused("journal directory is not private") - self.directory = directory - for name in ("journal.sqlite", "execution.lock"): - path = directory / name - fd = os.open(path, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) - try: - if os.fstat(fd).st_uid != os.getuid() or os.fstat(fd).st_mode & 0o077: - raise Refused("journal file is not private") - finally: - os.close(fd) - self.db = sqlite3.connect(directory / "journal.sqlite", isolation_level=None) - self.db.execute("PRAGMA synchronous=FULL") - self.db.execute( - "CREATE TABLE IF NOT EXISTS receipts (seq INTEGER PRIMARY KEY, binding TEXT NOT NULL, payload TEXT NOT NULL)" - ) - self.db.execute( - "CREATE TABLE IF NOT EXISTS intents (seq INTEGER PRIMARY KEY, attempt_id TEXT NOT NULL UNIQUE, binding TEXT NOT NULL, payload TEXT NOT NULL)" - ) - - def original_intent( - self, binding: str, current: Invocation, source: str - ) -> OriginalIntent: - """Latest original run intent for binding, refused unless it matches exactly. - - Missing or mismatched records refuse; the step stays uncertain. - """ - row = self.db.execute( - "SELECT attempt_id,payload FROM intents WHERE binding=? ORDER BY seq DESC LIMIT 1", - (binding,), - ).fetchone() - if row is None: - raise Refused("original intent record missing; step stays uncertain") - intent = OriginalIntent.decode(row[1]) - o = intent.invocation - if row[0] != o.attempt_id: - raise Refused("original intent attempt identity differs") - if intent.binding != binding or o.operation != "run": - raise Refused("original intent binding mismatch") - if o.pins != current.pins: - raise Refused("original intent pins mismatch") - if intent.source != source: - raise Refused("original intent source mismatch") - for name in ( - "step", - "owner", - "target", - "actor", - "principal", - "lane", - "tenant", - "realm", - ): - if getattr(o, name) != getattr(current, name): - raise Refused(f"original intent {name} mismatch") - # Match the persisted original running receipt. An orphan context or a - # payload from another attempt cannot become the current lookup key. - receipts = self.db.execute( - "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT ?", - (binding, MAX_HISTORY + 1), - ).fetchall() - if len(receipts) > MAX_HISTORY: - raise Refused("original intent receipt history exceeds bound") - matched = False - try: - for (raw,) in receipts: - if type(raw) is not str or len(raw.encode()) > MAX_SOURCE_BYTES: - raise Refused("original receipt input exceeds bound") - payload = json.loads(raw) - if ( - payload.get("attempt_id") != o.attempt_id - or payload.get("operation") != "run" - ): - continue - last = payload.get("receipts", [])[-1] - if last.get("outcome") != "running": - continue - if ( - payload.get("journal_schema") != "warden.case.step-journal.v1" - or payload.get("step_id") != o.step - or payload.get("owner") != o.owner - or payload.get("pins") != asdict(o.pins) - ): - raise Refused("original running receipt binding differs") - matched = True - break - except (TypeError, ValueError, KeyError, IndexError, AttributeError): - raise Refused("invalid original running receipt") from None - if not matched: - raise Refused("original running receipt missing; no backfill") - return intent - - @contextmanager - def locked(self): - with (self.directory / "execution.lock").open("rb") as fh: - try: - fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB) - except BlockingIOError: - raise Refused("case execution already active") from None - try: - yield - finally: - fcntl.flock(fh, fcntl.LOCK_UN) - - def load(self, binding: str, initial: StepState) -> StepState: - row = self.db.execute( - "SELECT payload FROM receipts WHERE binding=? ORDER BY seq DESC LIMIT 1", - (binding,), - ).fetchone() - if row is None: - return initial - obj = json.loads(row[0]) - expected = { - "step_id", - "owner", - "pins", - "allowed_created", - "retry_safe", - "receipts", - "journal_schema", - "evidence_digest", - "session_revoked", - "session_revocation_digest", - "grant_revocation_digest", - "attempt_id", - "operation", - } - if ( - set(obj) != expected - or obj["journal_schema"] != "warden.case.step-journal.v1" - ): - raise Refused("unknown journal receipt schema") - state = StepState( - obj["step_id"], - obj["owner"], - Pins(**obj["pins"]), - tuple(obj["allowed_created"]), - obj["retry_safe"], - tuple( - Receipt(r["attempt"], Outcome(r["outcome"]), tuple(r["created"])) - for r in obj["receipts"] - ), - ) - if ( - state.step_id, - state.owner, - state.pins, - state.allowed_created, - state.retry_safe, - ) != ( - initial.step_id, - initial.owner, - initial.pins, - initial.allowed_created, - initial.retry_safe, - ): - raise Refused("journal pins changed") - return state - - def save( - self, - binding: str, - state: StepState, - evidence: OwnerResult | None = None, - invocation: Invocation | None = None, - intent: OriginalIntent | None = None, - ): - if intent is not None: - if type(intent) is not OriginalIntent: - raise Refused("exact original intent required") - intent.validate() - expected = ( - Outcome.RUNNING - if intent.invocation.operation == "run" - else Outcome.ROLLBACK_RUNNING - ) - if ( - intent.binding != binding - or intent.invocation != invocation - or intent.invocation.pins != state.pins - or not state.last - or state.last.outcome is not expected - ): - raise Refused("original intent and running receipt differ") - self.db.execute("BEGIN IMMEDIATE") - try: - if intent is not None: - self.db.execute( - "INSERT INTO intents(attempt_id,binding,payload) VALUES (?,?,?)", - (intent.invocation.attempt_id, binding, intent.payload()), - ) - self.db.execute( - "INSERT INTO receipts(binding,payload) VALUES (?,?)", - ( - binding, - json.dumps( - asdict(state) - | { - "journal_schema": "warden.case.step-journal.v1", - "attempt_id": invocation.attempt_id if invocation else None, - "operation": invocation.operation if invocation else None, - "evidence_digest": digest(evidence.evidence.encode()) - if evidence - else None, - "session_revoked": evidence.revoked if evidence else None, - "session_revocation_digest": digest( - evidence.session_revocation_evidence.encode() - ) - if evidence and evidence.session_revocation_evidence - else None, - "grant_revocation_digest": digest( - evidence.grant_revocation_evidence.encode() - ) - if evidence and evidence.grant_revocation_evidence - else None, - }, - separators=(",", ":"), - ), - ), - ) - self.db.execute("COMMIT") - except BaseException: - self.db.execute("ROLLBACK") - raise - - -def _catalog_lane(catalog_source: bytes, descriptor: Descriptor): - """KERNEL-2: the descriptor's lane must be an active, fitting catalog entry. - - catalog-routing-acceptance still covers the whole catalog digest; this - ties the one lane the descriptor names to an entry inside that digest. - """ - from .routing.catalog import CatalogError, _parse_entry - - try: - raw = yaml.safe_load(catalog_source) - items = raw["entries"] - if not isinstance(items, list): - raise TypeError - entries = {} - for index, item in enumerate(items): - parsed = _parse_entry(item, index) - if parsed.id in entries: - raise ValueError - entries[parsed.id] = parsed - except (yaml.YAMLError, CatalogError, KeyError, TypeError, ValueError): - raise Refused("invalid catalog source shape") from None - entry = entries.get(descriptor.lane) - if entry is None: - raise Refused("descriptor lane is not in the catalog") - if not entry.is_active: - raise Refused("descriptor lane is not active in the catalog") - if entry.delegation is None: - raise Refused("descriptor lane has no catalog delegation record") - if not entry.is_graded: - raise Refused("descriptor lane carries no risk grade") - if descriptor.owner not in entry.owner_repo.split("/"): - raise Refused("descriptor owner does not fit catalog lane owner_repo") - if descriptor.effect in ("mint", "change") and entry.risk != "high": - raise Refused("catalog lane risk does not cover the descriptor effect") - return entry, entries - - -def _attended_admission(descriptor, actor, entry, entries): - from .routing.models import attended_lane_issue - - if (actor != "human" or "WARDEN_AGENT_ID" in os.environ - or "parent-session" not in descriptor.revocation_scopes): - raise Refused("attended step requires a human parent session") - issue = attended_lane_issue(entry, entries.get) - if issue: - raise Refused(issue[1]) - - -def _step_mode(case_type: dict, case: dict, descriptor: Descriptor) -> str: - """KERNEL-3: warden, attended or owner, from the type step's own text.""" - steps = [ - step - for step in case_type.get("follow_through") or [] - if isinstance(step, dict) and step.get("id") == descriptor.step - ] - step = steps[0] if len(steps) == 1 else {} - executes, attended = step.get("warden_executes"), step.get("attended") - if ( - type(executes) is not bool - or type(attended) is not bool - or (executes and attended) - or not isinstance(step.get("lane"), str) - ): - raise Refused("type step does not declare how the kernel dispatches it") - owner = step.get("owner") - if isinstance(owner, str) and re.fullmatch(r"\{[a-z_]+\}", owner): - owner = (case.get("parameters") or {}).get(owner[1:-1]) - if step["lane"] != descriptor.lane or owner != descriptor.owner: - raise Refused("type step lane or owner disagrees with descriptor") - return "warden" if executes else "attended" if attended else "owner" - - -class Executor: - """Injected fixed owner adapters and provenance verifier; none ship by default. - - evidence verifier must authenticate owner custody/provenance or explicitly - attest attended manual evidence; a bare JSON reference is never sufficient. - It receives purpose, exact source digest, and named evidence reference. - """ - - def __init__( - self, - journal: Journal, - adapters: dict[str, Adapter], - evidence_verifier: Callable[[str, str, str], bool], - *, - browser_inputs: BrowserInputs | None = None, - ): - self.journal, self.adapters, self.evidence_verifier = ( - journal, - dict(adapters), - evidence_verifier, - ) - self._admitted = {} - self.browser_inputs = browser_inputs - - def admit( - self, - case_source: bytes, - type_source: bytes, - descriptor_source: bytes, - catalog_source: bytes, - *, - principal: str, - actor: str, - lane: str, - acceptance_evidence: str, - materialization_evidence: str, - descriptor_evidence: str, - session_evidence: str, - original_session_reference: str | None = None, - ) -> tuple[Descriptor, Invocation, StepState]: - # Current authentication is distinct from an immutable original receipt - # reference. The latter can only resume an exact existing journal intent; - # it is never passed to the current-session verifier as authority. - receipt_session = session_evidence - if original_session_reference is not None: - if ( - type(original_session_reference) is not str - or not IDENTIFIER.fullmatch(original_session_reference) - or original_session_reference == session_evidence - ): - raise Refused("distinct original session reference required") - receipt_session = original_session_reference - for source in (case_source, type_source, descriptor_source, catalog_source): - bounded_source(source) - descriptor = Descriptor.decode(descriptor_source) - try: - case = yaml.safe_load(case_source)["spec"] - case_type = yaml.safe_load(type_source)["spec"] - if not isinstance(case, dict) or not isinstance(case_type, dict): - raise ValueError - except (yaml.YAMLError, KeyError, TypeError, ValueError): - raise Refused("invalid case or type source shape") from None - if case.get("status") != "materialized" or not case.get("acceptance"): - raise Refused("case is not accepted and structurally materialized") - if case_type.get("status") not in ("acknowledged", "implemented"): - raise Refused("type is not acknowledged") - if case.get("type") != f"{case_type['id']}@{case_type['version']}": - raise Refused("case type pin mismatch") - if case_type.get("execution_descriptors", {}).get(descriptor.step) != digest( - descriptor_source - ): - raise Refused( - "type has no exact acknowledged machine descriptor; plan only" - ) - for purpose, source, evidence in ( - ("case-acceptance", digest(case_source), acceptance_evidence), - ( - "ops-mason-materialization", - digest(case_source), - materialization_evidence, - ), - ( - "owner-descriptor-acceptance:" + descriptor.owner, - digest(descriptor_source), - descriptor_evidence, - ), - ("type-machine-seam-acceptance", digest(type_source), descriptor_evidence), - ("catalog-routing-acceptance", digest(catalog_source), descriptor_evidence), - ( - f"caller-session:{actor}:{principal}:{lane}", - digest(case_source), - session_evidence, - ), - ): - if ( - not isinstance(evidence, str) - or not IDENTIFIER.fullmatch(evidence) - or self._verify_evidence(purpose, source, evidence) is not True - ): - raise Refused("independent owner evidence not verified") - if ( - not IDENTIFIER.fullmatch(principal) - or actor != descriptor.actor - or lane != descriptor.lane - ): - raise Refused("principal actor or lane mismatch") - if descriptor.effect == "mint" and ( - actor != "human" or os.getenv("WARDEN_AGENT_ID") - ): - raise Refused("agent mint is forbidden") - entry, entries = _catalog_lane(catalog_source, descriptor) - mode = _step_mode(case_type, case, descriptor) - if mode == "attended": - _attended_admission(descriptor, actor, entry, entries) - if mode == "owner" and ( - entry.effective_delegation.mode != "native" - or entry.owner_repo != descriptor.owner - or entry.warden_executes - or descriptor.effect == "mint" - ): - raise Refused("owner step lane is not the owner's native path") - parameters = case.get("parameters", {}) - declared = { - item["name"]: item - for item in case_type.get("parameters", []) - if isinstance(item, dict) and isinstance(item.get("name"), str) - } - if ( - not isinstance(parameters, dict) - or descriptor.target_parameter not in declared - ): - raise Refused("target parameter is not declared by acknowledged type") - if case_type.get("id") == "oidc-browser-relying-party": - if type(self.browser_inputs) is not BrowserInputs: - raise Refused("browser metadata validator is not configured") - target = self.browser_inputs.target( - yaml.safe_load(case_source), - yaml.safe_load(type_source), - descriptor.target_parameter, - ) - else: - for name in parameters: - definition = declared.get(name) - if ( - definition is None - or definition.get("sensitivity") not in ("public", "operational") - or definition.get("case_settable") is False - ): - raise Refused("unknown, fixed or secret-sensitivity case parameter") - target = parameters.get(descriptor.target_parameter) - if not isinstance(target, str) or not IDENTIFIER.fullmatch(target): - raise Refused("target must be exact case metadata") - adapter = self.adapters.get(descriptor.adapter) - if adapter is None or (adapter.reference, adapter.source) != ( - descriptor.adapter, - descriptor.adapter_source, - ): - raise Refused("unknown adapter or owner source pin") - sheet = json.dumps( - [ - descriptor.step, - descriptor.owner, - target, - actor, - principal, - lane, - acceptance_evidence, - materialization_evidence, - descriptor_evidence, - receipt_session, - ], - separators=(",", ":"), - ).encode() - pins = Pins( - digest(case_source), - digest(type_source), - digest(descriptor_source), - digest(catalog_source), - digest(acceptance_evidence.encode()), - digest(sheet), - ) - invocation = Invocation( - descriptor.step, descriptor.owner, target, actor, principal, lane, pins - ) - initial = StepState( - descriptor.step, - descriptor.owner, - pins, - descriptor.allowed_created, - descriptor.retry_safe, - ) - if original_session_reference is not None: - with self.journal.locked(): - binding = f"{invocation.step}:{invocation.target}" - existing = self.journal.load(binding, initial) - if not existing.last: - raise Refused("original dispatch journal required; no backfill") - self.journal.original_intent(binding, invocation, adapter.source) - # Keep the mode and exact authenticated catalog bytes with this admission; - # dispatch must not substitute a later ambient catalog or lose its mode. - self._admitted[invocation] = ( - descriptor, initial, session_evidence, mode, catalog_source - ) - return descriptor, invocation, initial - - def _result( - self, adapter: Adapter, invocation: Invocation, result: OwnerResult - ) -> OwnerResult: - if type(result) is not OwnerResult or not isinstance(result.outcome, Outcome): - raise Refused("unknown owner result shape") - if type(result.created) is not tuple or not all( - isinstance(v, str) and IDENTIFIER.fullmatch(v) for v in result.created - ): - raise Refused("owner created-object result invalid") - if not isinstance(result.evidence, str) or not IDENTIFIER.fullmatch( - result.evidence - ): - raise Refused("owner evidence identifier invalid") - if adapter.verify(invocation, result) is not True or result.revoked is not True: - raise Refused("owner result or session revocation unverified") - if (result.attempt_id, result.operation) != ( - invocation.attempt_id, - invocation.operation, - ): - raise Refused("owner receipt belongs to another attempt") - descriptor = self._admitted[ - replace(invocation, attempt_id="", operation="admission") - ][0] - proof_fields = { - "parent-session": ( - f"session-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", - result.session_revocation_evidence, - ), - "broker-grant": ( - f"grant-revocation:{invocation.lane}:{invocation.operation}:{invocation.attempt_id}", - result.grant_revocation_evidence, - ), - } - proofs = [proof_fields[scope] for scope in descriptor.revocation_scopes] - for purpose, evidence in proofs: - if ( - not isinstance(evidence, str) - or not IDENTIFIER.fullmatch(evidence) - or self._verify_evidence(purpose, invocation.pins.case, evidence) - is not True - ): - raise Refused("independent grant or parent-session revocation absent") - return result - - def _verify_evidence(self, purpose, source, evidence): - try: - with contained_output(): - return self.evidence_verifier(purpose, source, evidence) is True - except Exception: - raise Refused("independent evidence verification failed") from None - - def _admission( - self, descriptor: Descriptor, invocation: Invocation, initial: StepState - ): - if self._admitted.get(invocation, ())[:2] != (descriptor, initial): - raise Refused("execution requires verified admission on this executor") - adapter = self.adapters.get(descriptor.adapter) - if adapter is None or (adapter.reference, adapter.source) != ( - descriptor.adapter, - descriptor.adapter_source, - ): - raise Refused("adapter changed after admission") - _, _, session, mode, catalog_source = self._admitted[invocation] - if ( - self._verify_evidence( - f"caller-session:{invocation.actor}:{invocation.principal}:{invocation.lane}", - invocation.pins.case, - session, - ) - is not True - ): - raise Refused("caller session no longer verified") - if descriptor.effect == "mint" and os.getenv("WARDEN_AGENT_ID"): - raise Refused("agent mint is forbidden") - if mode == "attended": - entry, entries = _catalog_lane(catalog_source, descriptor) - _attended_admission(descriptor, invocation.actor, entry, entries) - return adapter - - def _attempt(self, invocation: Invocation, state: StepState, operation: str): - identity = json.dumps( - [asdict(invocation.pins), operation, state.last.attempt, uuid.uuid4().hex], - separators=(",", ":"), - ).encode() - return replace( - invocation, - attempt_id="attempt-" + hashlib.sha256(identity).hexdigest(), - operation=operation, - ) - - def run( - self, - descriptor: Descriptor, - invocation: Invocation, - initial: StepState, - *, - retry=False, - ) -> StepState: - adapter = self._admission(descriptor, invocation, initial) - with self.journal.locked(): - binding = f"{invocation.step}:{invocation.target}" - state = self.journal.load(binding, initial) - recovered = recover(state) - if recovered != state: - self.journal.save(binding, recovered) - state = recovered - if len(state.receipts) > MAX_HISTORY - 4: - raise Refused("bounded journal history requires owner archival") - running = begin(state, invocation.pins, retry=retry) - dispatched = self._attempt(invocation, running, "run") - self.journal.save( - binding, - running, - invocation=dispatched, - intent=OriginalIntent(binding, dispatched, adapter.source), - ) - evidence = None - try: - with contained_output(): - result = self._result( - adapter, dispatched, adapter.invoke(dispatched) - ) - completed = finish( - running, invocation.pins, result.outcome, created=result.created - ) - evidence = result - except Exception: - completed = finish(running, invocation.pins, Outcome.UNCERTAIN) - self.journal.save(binding, completed, evidence, dispatched) - return completed - - def rollback( - self, descriptor: Descriptor, invocation: Invocation, initial: StepState - ) -> StepState: - adapter = self._admission(descriptor, invocation, initial) - with self.journal.locked(): - binding = f"{invocation.step}:{invocation.target}" - state = self.journal.load(binding, initial) - if recover(state) != state: - self.journal.save(binding, recover(state)) - raise Refused("lost rollback result requires owner reconciliation") - if adapter.rollback is None: - raise Refused("owner has no reviewed undo adapter") - if len(state.receipts) > MAX_HISTORY - 4: - raise Refused("bounded journal history requires owner archival") - running = begin_rollback(state, invocation.pins) - dispatched = self._attempt(invocation, running, "rollback") - self.journal.save( - binding, - running, - invocation=dispatched, - intent=OriginalIntent(binding, dispatched, adapter.source), - ) - evidence = None - try: - with contained_output(): - result = self._result( - adapter, - dispatched, - adapter.rollback(dispatched, running.last.created), - ) - verified = result.outcome is Outcome.ROLLED_BACK and not result.created - evidence = result if verified else None - except Exception: - verified = False - completed = finish_rollback(running, invocation.pins, verified=verified) - self.journal.save(binding, completed, evidence, dispatched) - return completed - - def reconcile( - self, descriptor: Descriptor, invocation: Invocation, initial: StepState - ) -> StepState: - """Explicit owner-native state inspection; never redispatches the mutation.""" - adapter = self._admission(descriptor, invocation, initial) - if adapter.reconcile is None and adapter.reconcile_original is None: - raise Refused("owner reconciliation adapter absent") - with self.journal.locked(): - binding = f"{invocation.step}:{invocation.target}" - state = recover(self.journal.load(binding, initial)) - if not state.last or state.last.outcome not in ( - Outcome.UNCERTAIN, - Outcome.FAILED_AFTER, - ): - raise Refused("no uncertain mutation to reconcile") - if len(state.receipts) > MAX_HISTORY - 4: - raise Refused("bounded journal history requires owner archival") - state = replace( - state, - receipts=( - *state.receipts, - Receipt(len(state.receipts) + 1, Outcome.UNCERTAIN), - ), - ) - original = ( - self.journal.original_intent(binding, invocation, adapter.source) - if adapter.reconcile_original is not None - else None - ) - dispatched = self._attempt(invocation, state, "reconcile") - self.journal.save(binding, state, invocation=dispatched) - with contained_output(): - result = self._result( - adapter, - dispatched, - adapter.reconcile_original(ReconcileContext(original, dispatched)) - if original is not None - else adapter.reconcile(dispatched), - ) - if result.outcome not in ( - Outcome.NO_CHANGE, - Outcome.SUCCESS, - Outcome.FAILED_BEFORE, - ): - raise Refused("owner did not reconcile target state") - # Validate created-object scope using the same closed result reducer. - finish( - begin( - StepState( - state.step_id, - state.owner, - state.pins, - state.allowed_created, - state.retry_safe, - ), - state.pins, - ), - state.pins, - result.outcome, - created=result.created, - ) - reconciled = StepState( - state.step_id, - state.owner, - state.pins, - state.allowed_created, - state.retry_safe, - ( - *state.receipts, - Receipt(len(state.receipts) + 1, result.outcome, result.created), - ), - ) - self.journal.save(binding, reconciled, result, dispatched) - return reconciled - - -class OwnerInterrupted(KeyboardInterrupt): - """Sanitized interruption preserves persisted running/crash recovery semantics.""" - - def __init__(self): - super().__init__("owner execution interrupted") - - -class _Discard: - """Never retain or forward adapter output, including unbounded malicious text.""" - - def write(self, text): - return len(text) - - def flush(self): - pass diff --git a/src/warden/case_type.py b/src/warden/case_type.py deleted file mode 100644 index 1454cd3..0000000 --- a/src/warden/case_type.py +++ /dev/null @@ -1,173 +0,0 @@ -"""SecurityCaseType follow-through check (WARDEN-WP-0044-T01). - -Checks the ``follow_through`` section of a NetKingdom SecurityCaseType against -the rules ops-warden needs before it can plan a run (canon -``security-case-types_v0.1`` section on follow_through; ops-warden review -``history/2026-10-08-oidc-rp-follow-through-review.md``, R1-R3, R5-R7). - -Two kinds of finding: - -* ``schema`` — the type itself is wrong; net-kingdom fixes the type. -* ``catalog`` — the type is right but a lane it names is not yet a reviewed, - active catalog entry; the lane's owner supplies it. ``warden case plan`` - (T02) refuses to plan while any catalog finding remains. - -Read-only: it parses YAML and the catalog, executes nothing, and never handles a -secret value. -""" -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import Iterable, List, Optional - -import yaml - -from warden.routing.catalog import Catalog -from warden.routing.models import attended_lane_issue - -VALUE_HANDLING = ("none", "generated_in_custody") -REQUIRED_STEP_FIELDS = ("id", "owner", "entry_point", "auth", "warden_executes", "attended", "value_handling") - - -@dataclass(frozen=True) -class Finding: - kind: str # "schema" | "catalog" - code: str - where: str - message: str - - def as_dict(self) -> dict: - return {"kind": self.kind, "code": self.code, "where": self.where, "message": self.message} - - -@dataclass(frozen=True) -class FollowThroughReport: - type_id: str - version: str - findings: List[Finding] - attended_lanes: List[str] # one attended session per lane, in step order - warden_steps: List[str] - - @property - def schema_ok(self) -> bool: - return not any(f.kind == "schema" for f in self.findings) - - @property - def plannable(self) -> bool: - return not self.findings - - def as_dict(self) -> dict: - return { - "type": self.type_id, - "version": self.version, - "schema_ok": self.schema_ok, - "plannable": self.plannable, - "warden_steps": self.warden_steps, - "attended_sessions": self.attended_lanes, - "findings": [f.as_dict() for f in self.findings], - } - - -def load_type(path: Path) -> dict: - with open(path) as fh: - doc = yaml.safe_load(fh) - if not isinstance(doc, dict) or doc.get("kind") != "SecurityCaseType": - raise ValueError(f"{path}: not a SecurityCaseType document") - return doc - - -def _bool(step: dict, key: str) -> Optional[bool]: - value = step.get(key) - return value if isinstance(value, bool) else None - - -def check_follow_through(doc: dict, catalog: Optional[Catalog] = None) -> FollowThroughReport: - spec = doc.get("spec") or {} - steps = spec.get("follow_through") or [] - findings: List[Finding] = [] - attended_lanes: List[str] = [] - warden_steps: List[str] = [] - - def schema(code: str, where: str, message: str) -> None: - findings.append(Finding("schema", code, where, message)) - - if not steps: - schema("FT-EMPTY", "follow_through", "type declares no follow-through steps") - - seen: set = set() - for index, step in enumerate(steps): - sid = step.get("id") if isinstance(step, dict) else None - where = f"follow_through[{sid or index}]" - if not isinstance(step, dict): - schema("FT-SHAPE", where, "step is not a mapping") - continue - for key in REQUIRED_STEP_FIELDS: - if key not in step or step[key] in (None, ""): - schema("FT-FIELD", where, f"missing required field '{key}'") - if sid in seen: - schema("FT-DUP", where, "duplicate step id") - seen.add(sid) - - executes = _bool(step, "warden_executes") - attended = _bool(step, "attended") - for key, value in (("warden_executes", executes), ("attended", attended)): - if key in step and value is None: - schema("FT-BOOL", where, f"'{key}' must be a boolean") - - # R3: values never pass through the run. - handling = step.get("value_handling") - if handling is not None and handling not in VALUE_HANDLING: - schema("FT-VALUE", where, f"value_handling '{handling}' not in {list(VALUE_HANDLING)}") - if handling == "generated_in_custody" and attended: - schema("FT-VALUE", where, "a generated-in-custody step may not run in an attended session") - - # Attended steps: justified, and never executed by warden itself. - if attended: - if not step.get("attended_justification"): - schema("FT-ATTEND", where, "attended step carries no attended_justification") - if executes: - schema("FT-ATTEND", where, "a step is either warden_executes or attended, not both") - - # R1: a lane for every step warden executes or groups into a session. - lane = step.get("lane") - if (executes or attended) and not lane: - schema("FT-LANE", where, "warden_executes or attended step names no catalog lane") - if lane and catalog is not None: - entry = catalog.get(lane) - if entry is None: - findings.append(Finding("catalog", "LANE-MISSING", where, f"lane '{lane}' is not in the catalog")) - elif not entry.is_active or not entry.reviewed: - findings.append(Finding("catalog", "LANE-UNREVIEWED", where, f"lane '{lane}' is not active and reviewed")) - elif attended: - issue = attended_lane_issue(entry, catalog.get) - if issue: - findings.append(Finding("catalog", issue[0], where, issue[1])) - - if executes: - warden_steps.append(sid) - if attended and lane and lane not in attended_lanes: - attended_lanes.append(lane) - - # R7: verification never reads a value. - for index, item in enumerate(spec.get("verification") or []): - if not isinstance(item, dict) or item.get("reads_secret_value") is not False: - vid = item.get("id", index) if isinstance(item, dict) else index - schema("VER-VALUE", f"verification[{vid}]", "reads_secret_value must be explicitly false") - - # Every step must be reversible (or explicitly the consumer's to undo). - reversed_ids = {r.get("reverses") for r in spec.get("rollback") or [] if isinstance(r, dict)} - for sid in sorted(s for s in seen if s and s not in reversed_ids): - schema("RB-MISSING", f"follow_through[{sid}]", "no rollback entry reverses this step") - - return FollowThroughReport( - type_id=str(spec.get("id", "")), - version=str(spec.get("version", "")), - findings=findings, - attended_lanes=attended_lanes, - warden_steps=warden_steps, - ) - - -def summarize(findings: Iterable[Finding]) -> str: - return "\n".join(f"[{f.kind}] {f.code} {f.where}: {f.message}" for f in findings) diff --git a/src/warden/cli.py b/src/warden/cli.py index 6987f0c..b28d6da 100644 --- a/src/warden/cli.py +++ b/src/warden/cli.py @@ -2,12 +2,12 @@ from __future__ import annotations import json +import os from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Annotated, List, Optional import typer -import yaml from rich.console import Console from rich.table import Table @@ -51,11 +51,6 @@ memory_app = typer.Typer( no_args_is_help=True, ) app.add_typer(memory_app, name="memory") -case_app = typer.Typer( - help="SecurityCaseType follow-through (WARDEN-WP-0044; read-only check, no execution)", - no_args_is_help=True, -) -app.add_typer(case_app, name="case") console = Console() err = Console(stderr=True) @@ -165,8 +160,6 @@ def sign( ttl_hours=ttl or entry.ttl_hours, principals=entry.principals, identity=actor_name, - extensions=entry.extensions, - max_ttl_hours=entry.max_ttl_hours, ) ca = _get_ca(cfg) @@ -227,8 +220,6 @@ def issue( ttl_hours=ttl or entry.ttl_hours, principals=entry.principals, identity=actor_name, - extensions=entry.extensions, - max_ttl_hours=entry.max_ttl_hours, ) try: _apply_policy_gate(cfg, spec) @@ -712,26 +703,6 @@ def _print_entry_table( console.print(table) -@route_app.command("scaffold") -def route_scaffold( - input_path: Annotated[Path, typer.Option("--input", help="Non-secret draft request YAML")], - output_dir: Annotated[Path, typer.Option("--output-dir", help="New staging directory")], -) -> None: - """Generate validated draft catalog/playbook pointers offline.""" - from warden.routing.scaffold import ScaffoldError, scaffold - from warden.routing.catalog import CatalogError - - try: - paths = scaffold(input_path, output_dir) - except ScaffoldError as exc: - err.print(f"[red]Scaffold refused:[/red] {exc}") - raise typer.Exit(1) - except CatalogError: - err.print("[red]Scaffold refused:[/red] existing routing catalog is invalid") - raise typer.Exit(1) - console.print("Draft staging created: " + ", ".join(path.name for path in paths)) - - @route_app.command("list") def route_list( output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, @@ -1176,41 +1147,6 @@ def route_find( # warden access — operator front door (advisory; proxy lands in T3) # --------------------------------------------------------------------------- -def _openbao_caller_view(entry) -> Optional[dict]: - """Sanitized data-plane identity. Absent for lanes that do not use it.""" - if entry.warden_executes or entry.lane == "login" or not entry.exec_capable: - return None - from warden.openbao_caller import caller_boundary_note, describe_ambient_openbao_caller - - described = describe_ambient_openbao_caller() - note = caller_boundary_note(described, high_risk=bool(entry.is_high_risk)) - if note: - described = {**described, "boundary_note": note} - return described - - -def _print_openbao_caller(entry) -> None: - described = _openbao_caller_view(entry) - if described is None: - return - if described.get("error"): - console.print( - f" openbao : [yellow]{described['source']}[/yellow] — {described['error']}" - ) - return - policies = ", ".join(described.get("policies") or []) or "(none)" - role = described.get("oidc_role") or "(none)" - console.print( - f" openbao : {described['source']} entity {described.get('entity_id') or '(none)'}" - ) - console.print(f" role : {role} policies: {policies}") - if described.get("boundary_note"): - console.print(f" caller : [yellow]{described['boundary_note']}[/yellow]") - console.print( - " gate id : [dim]flex-auth uses policy.caller_auth, not this OpenBao entity[/dim]" - ) - - def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict: """Stable, secret-free JSON shape for agentic operators. WP-0014 T2.""" payload = _entry_summary(entry) @@ -1223,9 +1159,6 @@ def _access_json(entry, expanded, gate: str, domain: Optional[str]) -> dict: "policy_ref": expanded.policy_ref, "exec_capable": expanded.exec_capable, } - caller = _openbao_caller_view(entry) - if caller is not None: - payload["openbao_caller"] = caller if entry.warden_executes: payload["next_action"] = "ops-warden issues this directly — see cert_command" payload["cert_command"] = entry.cert_command @@ -1280,7 +1213,6 @@ def _access_proxy( goes to stderr so --fetch stdout carries only the secret. """ from warden.proxy import ( - AttendedLoginError, ProxyError, build_wrapped_fetch, caller_auth_present, @@ -1377,9 +1309,7 @@ def _access_proxy( # T04 — agent identity on a high-risk lane: never stream raw secret data. # Agents may use sanctioned transports (--out / --exec / --wrap / --fingerprint). - from warden.access import agent_read_boundary_identity - - agent_id = agent_read_boundary_identity() + agent_id = os.environ.get("WARDEN_AGENT_ID", "").strip() raw_value_stream = ( not is_login and not do_exec and not wrap and not out_path and not fingerprint ) @@ -1414,32 +1344,11 @@ def _access_proxy( ) raise typer.Exit(6) - # After the refusals above, so a denied gate does not look like a fetch. - # The lookup uses the OpenBao CLI and must not run on those paths. - if not is_login: - caller = _openbao_caller_view(entry) - if caller is not None: - if caller.get("error"): - err.print( - f"[yellow]OpenBao caller:[/yellow] {caller['source']} — {caller['error']}" - ) - else: - err.print( - "[dim]OpenBao caller[/dim] " - f"{caller['source']} entity {caller.get('entity_id') or '(none)'} " - f"role {caller.get('oidc_role') or '(none)'} " - f"policies {', '.join(caller.get('policies') or []) or '(none)'}" - ) - if caller.get("boundary_note"): - err.print(f"[yellow]{caller['boundary_note']}[/yellow]") - action = "login" if is_login else ("exec" if do_exec else "fetch") err.print( f"[dim]proxy {action}: {entry.id} → {entry.owner_repo} " f"(caller identity; value not persisted)[/dim]" ) - rc = 10 if is_login else 5 - outcome = "login_failed" if is_login else "error" try: if is_login: rc = proxy_attended_login_exec(resolved, child_argv=child_argv) @@ -1471,12 +1380,9 @@ def _access_proxy( rc = 0 else: rc = proxy_fetch(resolved) - outcome = "ok" if rc == 0 else "error" except ProxyError as e: - if isinstance(e, AttendedLoginError): - rc, outcome = e.exit_code, e.outcome err.print(f"[red]{e}[/red]") - raise typer.Exit(rc) + raise typer.Exit(5) finally: try: write_audit( @@ -1486,8 +1392,6 @@ def _access_proxy( domain=domain, action=action, decision_id=decision_id, - exit_code=rc, - outcome=outcome, ) except OSError as e: err.print(f"[yellow]audit write failed:[/yellow] {e}") @@ -1627,7 +1531,6 @@ def access( if expanded.auth_method: console.print(f" auth : {expanded.auth_method}") - _print_openbao_caller(entry) if expanded.path_template: console.print(f" path : {expanded.path_template}") if expanded.fetch_command and entry.lane != "login": @@ -1639,8 +1542,6 @@ def access( ) if expanded.policy_ref: console.print(f" policy : {expanded.policy_ref} [dim]({gate})[/dim]") - elif entry.exec_capable: - console.print(f" gate : [dim]{gate}[/dim]") console.print(f" wiki : {entry.wiki_ref}") console.print(f" canon : {entry.canon_ref}") @@ -1662,10 +1563,6 @@ def access( f" {label:<8} : [dim]{proxy} --exec -- [/dim] " "[yellow](contained login + command; output suppressed)[/yellow]" ) - console.print( - " contract : reviewed command must be silent; any stdout/stderr " - "fails closed. Redirect both streams inside the reviewed command." - ) else: console.print( f" {label:<8} : [dim]{proxy} --fetch[/dim] " @@ -2280,58 +2177,3 @@ def memory_activate( print(json.dumps(payload, indent=2)) return console.print(warden_memory.format_activation_summary(payload)) - - -@case_app.command("check-type") -def case_check_type( - type_file: Annotated[Path, typer.Argument(help="SecurityCaseType type.yaml")], - output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, -) -> None: - """Check a type's follow_through against ops-warden's planning rules (exit 1 if not plannable).""" - from warden.case_type import check_follow_through, load_type, summarize - - try: - doc = load_type(type_file) - except (OSError, ValueError, yaml.YAMLError) as exc: - err.print(f"[red]{exc}[/red]") - raise typer.Exit(2) - report = check_follow_through(doc, _load_catalog()) - if output_json: - print(json.dumps(report.as_dict(), indent=2)) - else: - console.print(f"{report.type_id} {report.version}: schema_ok={report.schema_ok} plannable={report.plannable}") - console.print(f"warden executes: {', '.join(report.warden_steps) or '-'}") - console.print(f"attended sessions: {', '.join(report.attended_lanes) or '-'}") - if report.findings: - console.print(summarize(report.findings), markup=False) - raise typer.Exit(0 if report.plannable else 1) - - -@case_app.command("plan") -def case_plan( - case_file: Annotated[Path, typer.Argument(help="SecurityCase yaml (accepted and materialized)")], - type_file: Annotated[ - Optional[Path], - typer.Option("--type", help="type.yaml (default: ../security-case-types//type.yaml beside the case)"), - ] = None, - output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False, -) -> None: - """Print a case's run (warden, attended, owner steps and verification); executes nothing (exit 1 if refused).""" - from warden.case_plan import CaseError, case_ref, default_type_path, format_plan, load_case, plan_case - from warden.case_type import load_type - - try: - case_doc = load_case(case_file) - type_path = type_file or default_type_path(case_file, str((case_doc.get("spec") or {}).get("type", ""))) - plan = plan_case(case_doc, load_type(type_path), _load_catalog()) - plan.case_ref = case_ref(plan.case_id, case_file.read_bytes()) - except CaseError as exc: - err.print(str(exc), markup=False) - raise typer.Exit(1) - except (OSError, ValueError, yaml.YAMLError) as exc: - err.print(f"[red]{exc}[/red]") - raise typer.Exit(2) - if output_json: - print(json.dumps(plan.as_dict(), indent=2)) - else: - console.print(format_plan(plan), markup=False, soft_wrap=True) diff --git a/src/warden/clock_attribution.py b/src/warden/clock_attribution.py deleted file mode 100644 index 6f7c04a..0000000 --- a/src/warden/clock_attribution.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Optional recording-time evidence; original local timestamps remain intact.""" -import os - -def recording_time(): - if not os.environ.get('WARDEN_CLOCK_TRUST_FILE'): - return {} - try: - from railiance_clock.attribution import from_environment - result = from_environment('WARDEN_CLOCK_TRUST_FILE') - # These are time observations, not raw credentials. Keep audit metadata - # within the existing detector's restrictions without weakening it. - if result.get('railiance_time', {}).get('status') == 'usable': - result['railiance_time'].pop('epoch', None) - result['railiance_time'].pop('sample_id', None) - return result - except (ImportError, OSError, ValueError): - return {'railiance_time': {'status': 'unavailable', 'kind': 'recording_time'}} diff --git a/src/warden/config.py b/src/warden/config.py index d0a612a..8cf83f3 100644 --- a/src/warden/config.py +++ b/src/warden/config.py @@ -45,11 +45,7 @@ class CallerAuthConfig: @dataclass class PolicyConfig: - # SSH signing pin (flex-auth-ops-warden). Admits only resource.system ops-warden. flex_auth_url: Optional[str] = None - # Secret-read pin (flex-auth-ops-warden-fetch, CUST-WP-0075). Used only by - # check_fetch_policy; there is no fallback to flex_auth_url. - fetch_flex_auth_url: Optional[str] = None zone_registry_path: Optional[Path] = None failure_modes: Dict[str, str] = field( default_factory=lambda: { @@ -215,12 +211,8 @@ def load_config(path: Optional[Path] = None) -> WardenConfig: ) zone_registry_path = policy_raw.get("zone_registry_path") flex_auth_url = str(policy_raw.get("flex_auth_url", "")).strip() or None - fetch_flex_auth_url = ( - str(policy_raw.get("fetch_flex_auth_url", "") or "").strip() or None - ) policy_cfg = PolicyConfig( flex_auth_url=flex_auth_url, - fetch_flex_auth_url=fetch_flex_auth_url, zone_registry_path=( Path(os.path.expanduser(str(zone_registry_path))) if zone_registry_path diff --git a/src/warden/inventory.py b/src/warden/inventory.py index 40904bd..ef60ba3 100644 --- a/src/warden/inventory.py +++ b/src/warden/inventory.py @@ -3,17 +3,11 @@ from __future__ import annotations from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List import yaml -from warden.models import ( - ActorType, - DEFAULT_TTL_HOURS, - MAX_TTL_HOURS, - SSH_USER_EXTENSIONS, - validate_actor_name, -) +from warden.models import ActorType, DEFAULT_TTL_HOURS, validate_actor_name class InventoryError(Exception): @@ -27,9 +21,6 @@ class ActorEntry: principals: List[str] ttl_hours: int description: str = "" - # Optional issuance profile: certificate extension allow-list and TTL ceiling. - extensions: Optional[List[str]] = None - max_ttl_hours: Optional[int] = None @dataclass @@ -44,37 +35,6 @@ class PrincipalsInventory: hosts: Dict[str, HostEntry] = field(default_factory=dict) -def _parse_extensions(name: str, raw: object) -> Optional[List[str]]: - if raw is None: - return None - if not isinstance(raw, list): - raise InventoryError(f"Actor {name!r} extensions must be a list") - extensions = [str(e) for e in raw] - unknown = sorted(set(extensions) - SSH_USER_EXTENSIONS) - if unknown: - raise InventoryError( - f"Actor {name!r} has unknown certificate extension(s): {unknown}" - ) - return extensions - - -def _parse_max_ttl( - name: str, actor_type: ActorType, ttl: int, raw: object -) -> Optional[int]: - if raw is None: - return None - max_ttl = int(raw) # type: ignore[arg-type] - if max_ttl <= 0 or max_ttl > MAX_TTL_HOURS[actor_type]: - raise InventoryError( - f"Actor {name!r} max_ttl_hours {max_ttl} must be 1..{MAX_TTL_HOURS[actor_type]}" - ) - if ttl > max_ttl: - raise InventoryError( - f"Actor {name!r} ttl_hours {ttl} exceeds its max_ttl_hours {max_ttl}" - ) - return max_ttl - - def load_inventory(path: Path) -> PrincipalsInventory: """Load inventory.yaml. Returns empty inventory if path does not exist.""" if not path.exists(): @@ -105,16 +65,12 @@ def load_inventory(path: Path) -> PrincipalsInventory: ttl = int(data.get("ttl_hours", DEFAULT_TTL_HOURS[actor_type])) principals = list(data.get("principals") or [name]) - extensions = _parse_extensions(name, data.get("extensions")) - max_ttl = _parse_max_ttl(name, actor_type, ttl, data.get("max_ttl_hours")) actors[name] = ActorEntry( name=name, actor_type=actor_type, principals=principals, ttl_hours=ttl, description=str(data.get("description", "")), - extensions=extensions, - max_ttl_hours=max_ttl, ) hosts: Dict[str, HostEntry] = {} @@ -139,12 +95,6 @@ def save_inventory(inventory: PrincipalsInventory, path: Path) -> None: "principals": e.principals, "ttl_hours": e.ttl_hours, **({"description": e.description} if e.description else {}), - **({"extensions": e.extensions} if e.extensions is not None else {}), - **( - {"max_ttl_hours": e.max_ttl_hours} - if e.max_ttl_hours is not None - else {} - ), } for name, e in inventory.actors.items() }, diff --git a/src/warden/models.py b/src/warden/models.py index d1878fe..fcc0aec 100644 --- a/src/warden/models.py +++ b/src/warden/models.py @@ -32,18 +32,6 @@ ACTOR_PREFIX: dict[ActorType, str] = { } -# OpenSSH user-certificate extensions (PROTOCOL.certkeys). An actor profile may -# restrict a certificate to a subset; ``None`` keeps the backend default. -SSH_USER_EXTENSIONS: frozenset[str] = frozenset({ - "no-touch-required", - "permit-X11-forwarding", - "permit-agent-forwarding", - "permit-port-forwarding", - "permit-pty", - "permit-user-rc", -}) - - def validate_actor_name(name: str, actor_type: ActorType) -> None: """Raise ValueError if name does not carry the required prefix for actor_type.""" prefix = ACTOR_PREFIX[actor_type] @@ -68,10 +56,6 @@ class CertSpec: policy_zone: Optional[str] = None policy_failure_mode: Optional[str] = None policy_outcome: Optional[str] = None - # Per-actor issuance profile (CUST-WP-0073-T02): explicit extension allow-list - # (None = backend default) and a TTL ceiling tighter than the type maximum. - extensions: Optional[List[str]] = None - max_ttl_hours: Optional[int] = None def __post_init__(self) -> None: if not self.identity: diff --git a/src/warden/openbao_caller.py b/src/warden/openbao_caller.py deleted file mode 100644 index 548c151..0000000 --- a/src/warden/openbao_caller.py +++ /dev/null @@ -1,137 +0,0 @@ -"""Sanitized view of the OpenBao identity a proxied fetch will use. - -``warden access`` does not select a caller. The data-plane command inherits the -process environment, and the OpenBao CLI then resolves a token in this order: -``BAO_TOKEN``, ``VAULT_TOKEN``, then ``~/.vault-token``. That identity is -independent of the flex-auth caller (``policy.caller_auth``). - -The lookup response contains the token itself. This module keeps only the -fields an operator needs to see which principal will be evaluated. -""" - -from __future__ import annotations - -import json -import os -import subprocess -from pathlib import Path -from typing import Mapping, Optional, Sequence - -_TOKEN_KEYS = ("BAO_TOKEN", "VAULT_TOKEN") -_DROPPED = {"id", "accessor", "client_token", "token", "wrap_info"} - - -def ambient_token_source(env: Optional[Mapping[str, str]] = None) -> str: - """Which credential the OpenBao CLI will use. Never reads the value.""" - current = os.environ if env is None else env - if current.get("BAO_TOKEN", "").strip(): - return "BAO_TOKEN" - if current.get("VAULT_TOKEN", "").strip(): - return "VAULT_TOKEN" - if (Path.home() / ".vault-token").is_file(): - return "vault-token-file" - return "none" - - -def describe_ambient_openbao_caller( - env: Optional[Mapping[str, str]] = None, - *, - command: Sequence[str] = ("bao", "token", "lookup", "-format=json"), - timeout: float = 10.0, -) -> dict: - """Return entity, policies, and auth path for the ambient token. - - ``id`` and ``accessor`` from the lookup payload are dropped. On failure the - result carries ``error`` and no raw CLI output. - """ - current = os.environ if env is None else env - source = ambient_token_source(current) - described = { - "source": source, - "entity_id": None, - "display_name": None, - "auth_path": None, - "policies": [], - "oidc_role": None, - "ttl": None, - "error": None, - } - if source == "none": - described["error"] = "no ambient OpenBao token" - return described - - try: - completed = subprocess.run( # noqa: S603 — fixed argv, caller environment - list(command), - capture_output=True, - text=True, - timeout=timeout, - env=dict(current), - check=False, - ) - except FileNotFoundError: - described["error"] = "bao client not found" - return described - except subprocess.TimeoutExpired: - described["error"] = "token lookup timed out" - return described - except OSError: - described["error"] = "token lookup could not start" - return described - - if completed.returncode != 0: - described["error"] = "token lookup failed" - return described - - try: - payload = json.loads(completed.stdout or "") - except json.JSONDecodeError: - described["error"] = "token lookup returned non-JSON" - return described - - data = payload.get("data") if isinstance(payload, dict) else None - if not isinstance(data, dict): - described["error"] = "token lookup returned no data" - return described - - meta = data.get("meta") if isinstance(data.get("meta"), dict) else {} - role = meta.get("role") - policies = data.get("policies") - described.update( - { - "entity_id": _text(data.get("entity_id")), - "display_name": _text(data.get("display_name")), - "auth_path": _text(data.get("path")), - "policies": [str(item) for item in policies] if isinstance(policies, list) else [], - "oidc_role": role if isinstance(role, str) and role else None, - "ttl": data.get("ttl") if isinstance(data.get("ttl"), int) else None, - "error": None, - } - ) - # Guard the contract even if a future field is copied by mistake. - for key in _DROPPED: - described.pop(key, None) - return described - - -def caller_boundary_note(described: Mapping[str, object], *, high_risk: bool) -> Optional[str]: - """One line when this token cannot be the lane's scoped reader.""" - policies = described.get("policies") - names = set(policies) if isinstance(policies, list) else set() - if high_risk and "agent-high-risk-boundary" in names: - return ( - "this token carries agent-high-risk-boundary, which denies data read " - "on high-risk lanes; a platform-admin capabilities check is a different caller" - ) - if "platform-admin" in names: - return ( - "this token is the broad platform-admin role; do not attach the lane's " - "scoped read policy to this entity" - ) - return None - - -def _text(value: object) -> Optional[str]: - if isinstance(value, str) and value.strip(): - return value - return None diff --git a/src/warden/plan.py b/src/warden/plan.py index 1b56831..730a43a 100644 --- a/src/warden/plan.py +++ b/src/warden/plan.py @@ -98,48 +98,6 @@ def _candidate_row(entry: RouteEntry, score: int) -> dict: } -#: Verbs that make a need a request to CHANGE custody rather than to read it -#: (WARDEN-WP-0038). Reported independently by key-cape (2026-09-08) and -#: railiance-platform (2026-09-09): a need saying "generate a successor secret and -#: CAS-write it to two custodians" scored well against the lane that READS that -#: path and inherited its `autonomous` verdict, answered with three read -#: transports. -#: -#: Deliberately not including "issue" or "sign". SSH certificate issuance is a -#: mutating act ops-warden owns outright, and the ownership test below is what -#: separates it from someone else's custody — not the absence of the verb. -MUTATE_TOKENS = frozenset({ - "rotate", "rotating", "rotation", "rerotate", - "generate", "regenerate", "mint", "reissue", - "write", "rewrite", "put", "patch", "cas", - "provision", "reprovision", "create", "install", - "revoke", "revoking", "disable", "delete", "remove", - "replace", "successor", "reset", "restart", "update", -}) - - -def _need_intent(need: str) -> str: - """``mutate`` when the need asks to change a credential, else ``read``. - - Token match, not substring: "update" must not fire on "updated docs" any - more than it already would, but "no-update" style hyphenation is normalised - the same way the scorer normalises it. - """ - tokens = {t.strip(".,;:()") for t in need.lower().replace("-", " ").split()} - return "mutate" if tokens & MUTATE_TOKENS else "read" - - -def _owns_write_authority(entry: RouteEntry) -> bool: - """Whether ops-warden may itself perform a mutating act on this lane. - - True only where ops-warden is the designed owner of the front door — the SSH - signing lane, `delegation.mode: permanent`. Everywhere else ops-warden is a - pointer or a caller-identity proxy (`ADR-0002`, `ADR-0005`), so a write is - another component's custody act and no plan verdict here can authorise it. - """ - return entry.effective_delegation.mode == "permanent" - - def _score_for(catalog: Catalog, entry: RouteEntry, need: str) -> int: if entry.id == need.strip(): return 100 @@ -289,8 +247,7 @@ def _ccr_stub(need: str) -> dict: ], "commands": [ "warden route list --all", - "warden route scaffold --input --output-dir ", - "# platform drafts: use railiance-platform's documented scaffold-lane command", + "# after CCR: edit registry/routing/catalog.yaml + playbook", ], } @@ -337,7 +294,6 @@ def build_plan( ) entry, score = matches[0] - intent = _need_intent(need) # Draft-only top match without active alternatives → unroutable if entry.status == "draft" and not include_draft: @@ -422,80 +378,6 @@ def build_plan( domain=domain, ) - # --- mutating need on a lane ops-warden does not own (WARDEN-WP-0038) ------ - # - # Must run before any branch that can return `autonomous`. A write is a - # custody act belonging to the lane owner, and `autonomous` is documented as - # the signal to proceed without the founder — so returning it here would let - # this front door authorise a mutation on someone else's custody. It would - # also answer a write with `--out`/`--exec`/`--wrap`, which are reads. - if intent == "mutate" and not _owns_write_authority(entry): - rotation = entry.rotation - owner = (rotation.owner if rotation else None) or entry.owner_repo - reasons = [ - "need asks to change a credential, not read one", - f"write authority on this lane belongs to {owner}, not ops-warden", - ] - if rotation is not None: - act = FounderAct( - kind="approve", - summary=( - f"Attended owner act required to change {entry.id} — " - f"{owner} holds write authority" - ), - details={ - "lane_id": entry.id, - "write_owner": owner, - "rotation_method": rotation.method, - "rotation_automatable": rotation.automatable, - "wiki_ref": entry.wiki_ref, - "guidance_command": f"warden rotate-guide {entry.id}", - "desk_hint": f"warden desk --act approve --lane {entry.id}", - }, - ) - if not rotation.automatable: - reasons.append( - "lane records the rotation as not automatable — an executable " - "driver must not attempt it" - ) - return AccessPlan( - need=need, - verdict="founder_required", - organization_posture=org, - policy_gate=gate, - lane_id=entry.id, - lane_title=entry.title, - match_score=score, - # Deliberately empty: every command this lane offers is a READ, - # and offering one against a write need is the reported defect. - commands=[f"warden rotate-guide {entry.id} # guidance, not execution"], - founder_act=act, - catalog=freshness, - candidates=candidates, - reasons=reasons, - actor=actor, - domain=domain, - ) - stub = _ccr_stub(need) - stub["owner_hint"] = f"{owner} (write authority for {entry.id})" - reasons.append("lane records no rotation route — the act has no admitted transport") - return AccessPlan( - need=need, - verdict="unroutable", - organization_posture=org, - policy_gate=gate, - lane_id=entry.id, - lane_title=entry.title, - match_score=score, - commands=[], - ccr_stub=stub, - catalog=freshness, - candidates=candidates, - reasons=reasons, - actor=actor, - domain=domain, - ) - if _lane_is_autonomous(entry): return AccessPlan( need=need, diff --git a/src/warden/policy.py b/src/warden/policy.py index 3476259..140c8c4 100644 --- a/src/warden/policy.py +++ b/src/warden/policy.py @@ -131,12 +131,6 @@ def check_sign_policy(cfg: PolicyConfig, spec: CertSpec) -> str | None: response = httpx.post(url, json=request, headers=headers, timeout=10.0) response.raise_for_status() except httpx.HTTPStatusError as e: - if e.response.status_code in {401, 403}: - spec.policy_outcome = "deny" - raise CAError( - f"flex-auth refused sign policy check (HTTP {e.response.status_code}); " - "caller authentication or system binding was rejected" - ) from e _evaluator_failure( f"flex-auth rejected sign policy check (HTTP {e.response.status_code}) " f"for security zone {zone!r}", @@ -191,21 +185,15 @@ def check_fetch_policy( The action is ``read`` on a ``secret`` resource owned by another subsystem — ops-warden is the conduit, not the owner. Unresolved target workload identity selects the explicit ``unknown`` profile; no secret value enters the request. - - The check goes only to ``cfg.fetch_flex_auth_url`` — the secret-read pin - (flex-auth-ops-warden-fetch, CUST-WP-0075). It never falls back to the SSH - signing pin (``cfg.flex_auth_url``), whose caller binding admits only the - ``ops-warden`` system. An unset or unreachable fetch evaluator always fails - closed, whatever the zone failure mode. """ zone = "unknown" fail_closed = _is_fail_closed(cfg, zone) - if cfg.fetch_flex_auth_url is None: - raise CAError( - "secret-read evaluator not configured: set policy.fetch_flex_auth_url " - "(flex-auth-ops-warden-fetch pin); fetch checks never use the SSH " - "signing pin policy.flex_auth_url" + if cfg.flex_auth_url is None: + _evaluator_failure( + "flex-auth URL is not configured for security zone 'unknown'", + fail_closed=fail_closed, ) + return None subject_id = os.environ.get(cfg.subject_env, "").strip() or "operator" request = { "subject": {"id": subject_id, "type": "operator", "tenant": cfg.tenant}, @@ -219,17 +207,12 @@ def check_fetch_policy( "context": {"need_id": need_id, "owner_repo": owner_repo, "domain": domain}, } - url = cfg.fetch_flex_auth_url.rstrip("/") + "/v1/check" - headers = _caller_headers(cfg, fail_closed=True) + url = cfg.flex_auth_url.rstrip("/") + "/v1/check" + headers = _caller_headers(cfg, fail_closed=fail_closed) try: response = httpx.post(url, json=request, headers=headers, timeout=10.0) response.raise_for_status() except httpx.HTTPStatusError as e: - if e.response.status_code in {401, 403}: - raise CAError( - f"flex-auth refused fetch policy check (HTTP {e.response.status_code}); " - "caller authentication or system binding was rejected" - ) from e _evaluator_failure( f"flex-auth rejected fetch policy check (HTTP {e.response.status_code})", fail_closed=fail_closed, @@ -237,10 +220,12 @@ def check_fetch_policy( ) return None except httpx.RequestError as e: - raise CAError( - "secret-read evaluator (flex-auth-ops-warden-fetch) unreachable at " - f"{cfg.fetch_flex_auth_url!r}; fetch refused (no fallback to the signing pin)" - ) from e + _evaluator_failure( + f"flex-auth unreachable at {cfg.flex_auth_url!r} for security zone 'unknown'", + fail_closed=fail_closed, + cause=e, + ) + return None try: decision = response.json() diff --git a/src/warden/proxy.py b/src/warden/proxy.py index b4eca41..0acd379 100644 --- a/src/warden/proxy.py +++ b/src/warden/proxy.py @@ -12,10 +12,8 @@ intact. Three guardrails are enforced here in code: the tool with inherited stdout/stderr so the value never enters warden's memory; sanctioned exec/file transports hold it only for their bounded handoff. The high-risk attended-login lane is stricter: it captures every client byte inside - an isolated helper session, never returns that output, fails closed on any - non-allowlisted login output, requires successful persistence to a private token - helper, self-revokes, and cleans up. Audit records - are metadata only. + an isolated helper session, permits no output, self-revokes, and cleans up. Audit + records are metadata only. * **G3 — policy gate before fetch.** The CLI runs ``check_fetch_policy`` before calling anything here; this module refuses to run an unresolved command template. @@ -23,8 +21,6 @@ This module shells out but never *interprets* secret bytes in the ``--fetch`` pa """ from __future__ import annotations -from warden.clock_attribution import recording_time - import json import os import re @@ -41,51 +37,10 @@ from typing import List, Optional from warden.routing.models import RouteEntry _PLACEHOLDER = re.compile(r"<[^>]+>") -_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr|s)\.[A-Za-z0-9_-]{8,}\b") +_OPENBAO_TOKEN = re.compile(rb"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b") _ATTENDED_LOGIN_ROOT = ".warden-attended-login" _TOKEN_HELPER_NAME = ".vault-token" -# Attended-login output allowlist (RPF-WP-0017 criterion 4). Structure only: the -# values are never logged, returned, stored, or included in an exception. -# -# stdout: empty, or exactly one JSON object shaped like a ``bao login -format=json`` -# response (api.Secret with an ``auth`` block). ``wrap_info`` must be null or -# absent (a wrapping token would survive ``token revoke -self``), ``data`` null, -# absent or empty, and ``auth.client_token`` a non-empty string. -_LOGIN_JSON_TOP_TYPES: dict = { - "request_id": (str,), - "lease_id": (str,), - "lease_duration": (int,), - "renewable": (bool,), - "data": (dict, type(None)), - "warnings": (list, type(None)), - "wrap_info": (type(None),), - "mount_type": (str,), - "auth": (dict,), -} -_LOGIN_JSON_AUTH_TYPES: dict = { - "client_token": (str,), - "accessor": (str,), - "policies": (list, type(None)), - "token_policies": (list, type(None)), - "identity_policies": (list, type(None)), - "metadata": (dict, type(None)), - "orphan": (bool,), - "entity_id": (str,), - "lease_duration": (int,), - "renewable": (bool,), - "mfa_requirement": (dict, type(None)), - "num_uses": (int,), -} -# stderr: empty, or exactly the OpenBao OIDC CLI prompt (browser-launch or -# open-link variant, one https URL, optional wait line). Any other byte fails closed. -_LOGIN_OIDC_STDERR = re.compile( - r"Complete the login via your OIDC provider\. " - r"(?:Launching browser to:|Open the following link in your browser:)" - r"[ \t]*\n\s*https://[^\s]+[ \t]*\n" - r"\s*(?:Waiting for OIDC authentication to complete\.\.\.[ \t]*\n?\s*)?" -) - @dataclass(frozen=True) class ResolvedFetch: @@ -103,15 +58,6 @@ class ProxyError(Exception): """Raised when a proxy fetch cannot be performed safely.""" -class AttendedLoginError(ProxyError): - """Value-free phase result for the attended command envelope.""" - - def __init__(self, message: str, *, exit_code: int, outcome: str): - super().__init__(message) - self.exit_code = exit_code - self.outcome = outcome - - def _has_shell_pipe(cmd: str) -> bool: """True when ``cmd`` contains an unquoted shell pipe operator.""" in_single = in_double = False @@ -195,7 +141,6 @@ def write_audit( action: str, decision_id: Optional[str], exit_code: Optional[int] = None, - outcome: Optional[str] = None, ) -> Path: """Append a metadata-only audit record. Never contains a secret value (G2).""" state_dir.mkdir(parents=True, exist_ok=True) @@ -209,9 +154,7 @@ def write_audit( "subject": os.environ.get("WARDEN_POLICY_SUBJECT", "").strip() or "operator", "policy_decision_id": decision_id, "exit_code": exit_code, - "outcome": outcome or ("ok" if exit_code in (None, 0) else "error"), } - record.update(recording_time()) with log_path.open("a") as f: f.write(json.dumps(record) + "\n") try: @@ -224,11 +167,10 @@ def write_audit( subject=record["subject"], target=need_id, decision_id=decision_id, - outcome=record["outcome"], + outcome="ok" if exit_code in (None, 0) else "error", source="access", owner_repo=owner_repo, domain=domain, - exit_code=exit_code, ) except Exception: pass @@ -391,71 +333,6 @@ def _output_bytes(completed: subprocess.CompletedProcess) -> bytes: return stdout + b"\n" + stderr -def _stream_bytes(value: object) -> bytes: - if isinstance(value, str): - return value.encode("utf-8", errors="replace") - return value if isinstance(value, bytes) else b"" - - -def _reject_duplicate_keys(pairs: list) -> dict: - keys = [key for key, _ in pairs] - if len(keys) != len(set(keys)): - raise ValueError("duplicate key") - return dict(pairs) - - -def _typed_keys_allowed(obj: dict, allowed: dict) -> bool: - for key, value in obj.items(): - types = allowed.get(key) - if types is None: - return False - # bool is an int subclass; never accept it where only int is expected. - if isinstance(value, bool) and bool not in types: - return False - if not isinstance(value, types): - return False - return True - - -def _login_stdout_allowed(raw: bytes) -> bool: - if not raw.strip(): - return True - try: - parsed = json.loads( - raw.decode("utf-8"), object_pairs_hook=_reject_duplicate_keys - ) - except (UnicodeDecodeError, ValueError, RecursionError): - return False - if not isinstance(parsed, dict) or "auth" not in parsed: - return False - if not _typed_keys_allowed(parsed, _LOGIN_JSON_TOP_TYPES): - return False - if parsed.get("data"): - return False - auth = parsed["auth"] - if not _typed_keys_allowed(auth, _LOGIN_JSON_AUTH_TYPES): - return False - token = auth.get("client_token") - return isinstance(token, str) and token != "" - - -def _login_stderr_allowed(raw: bytes) -> bool: - if not raw: - return True - try: - text = raw.decode("utf-8") - except UnicodeDecodeError: - return False - return _LOGIN_OIDC_STDERR.fullmatch(text) is not None - - -def _login_output_allowed(completed: subprocess.CompletedProcess) -> bool: - """True only for allowlisted login output; checks structure, never values.""" - return _login_stdout_allowed(_stream_bytes(completed.stdout)) and ( - _login_stderr_allowed(_stream_bytes(completed.stderr)) - ) - - def _revoke_contained( bao_binary: str, *, @@ -503,13 +380,9 @@ def proxy_attended_login_exec( """Run an attended login and one silent child inside a private helper home. The default home is proven writable before the OIDC client starts. Login, - child, and revocation output are captured and discarded. A successful login - proceeds only when the private helper is populated and its captured output is - empty or allowlisted (one ``-format=json`` login object on stdout, the OIDC - prompt on stderr); anything else, persistence defects, and non-zero results - fail closed with self-revocation. The reviewed child must - remain silent. Any possibly issued token is revoked before the isolated helper - directory is removed. + child, and revocation output are captured and discarded. Any non-empty output, + persistence defect, or non-zero result fails closed; any possibly issued token + is revoked before the isolated helper directory is removed. """ if not child_argv: raise ProxyError( @@ -527,10 +400,6 @@ def proxy_attended_login_exec( root, session, root_created = _prepare_attended_login_home() helper = session / _TOKEN_HELPER_NAME env = _caller_env() - if not env.get("WARDEN_CONFIG"): - caller_config = Path.home() / ".config" / "warden" / "warden.yaml" - if caller_config.is_file(): - env["WARDEN_CONFIG"] = str(caller_config) env["HOME"] = str(session) env.pop("BAO_TOKEN", None) env.pop("VAULT_TOKEN", None) @@ -544,7 +413,7 @@ def proxy_attended_login_exec( login_argv = list(resolved.argv) if not any(arg == "-format" or arg.startswith("-format=") for arg in login_argv): - login_argv.insert(2, "-format=json") + login_argv.append("-format=json") try: try: @@ -559,38 +428,14 @@ def proxy_attended_login_exec( except (OSError, ProxyError): helper_valid = False - output_allowed = _login_output_allowed(login) - if login.returncode != 0 or not helper_valid or not output_allowed: + if login.returncode != 0 or login_output.strip() or not helper_valid: revoked = _revoke_contained( resolved.argv[0], env=env, possible_output=login_output ) status = "revoked" if revoked else "revocation could not be confirmed" - reason = ( - "" if output_allowed - else " (login client produced non-allowlisted output)" - ) - # Fixed categories and booleans only; captured bytes stay discarded. - if login.returncode != 0: - if b"timed out waiting for response from provider" in login_output: - category = "callback_timeout" - elif b"Error handling callback" in login_output: - category = "callback_error" - elif b"Error authenticating:" in login_output: - category = "authentication_error" - else: - category = "unknown_client_failure" - elif not helper_valid: - category = "helper_persistence_missing" - else: - category = "output_shape_rejected" - raise AttendedLoginError( - "attended login failed closed before command handoff" - f"{reason}; category={category}, client_exit={login.returncode}, " - f"helper_present={helper_valid}, " - f"stdout_allowed={_login_stdout_allowed(_stream_bytes(login.stdout))}, " - f"stderr_allowed={_login_stderr_allowed(_stream_bytes(login.stderr))}; " - f"any possible issued session was contained and {status}", - exit_code=10, outcome="login_failed", + raise ProxyError( + "attended login failed closed before command handoff; any possible " + f"issued session was contained and {status}" ) try: @@ -600,33 +445,23 @@ def proxy_attended_login_exec( resolved.argv[0], env=env, possible_output=b"" ) status = "revoked" if revoked else "revocation could not be confirmed" - raise AttendedLoginError( - "attended command could not start; the login session was " + status, - exit_code=11, outcome="child_failed", + raise ProxyError( + "attended command could not start; the login session was " + status ) from exc child_output = _output_bytes(child) revoked = _revoke_contained( resolved.argv[0], env=env, possible_output=child_output ) - # Even whitespace is output. The separator added by _output_bytes is not. - if child.stdout or child.stderr: + if child.returncode != 0 or child_output.strip(): status = "revoked" if revoked else "revocation could not be confirmed" - raise AttendedLoginError( - "attended command produced output; reviewed commands must remain " - f"silent (redirect both streams); the login session was {status}", - exit_code=12, outcome="child_output", - ) - if child.returncode != 0: - status = "revoked" if revoked else "revocation could not be confirmed" - raise AttendedLoginError( - f"attended command exited non-zero; the login session was {status}", - exit_code=11, outcome="child_failed", + raise ProxyError( + "attended command failed closed because it returned a failure or " + f"unexpected output; the login session was {status}" ) if not revoked: - raise AttendedLoginError( - "attended command completed but session revocation could not be confirmed", - exit_code=13, outcome="revoke_unconfirmed", + raise ProxyError( + "attended command completed but session revocation could not be confirmed" ) return 0 finally: @@ -635,11 +470,7 @@ def proxy_attended_login_exec( if root_created: root.rmdir() except OSError as exc: - raise AttendedLoginError( - "attended login private storage cleanup failed; inspect the prior phase " - "before retrying the reviewed command", - exit_code=14, outcome="cleanup_failed", - ) from exc + raise ProxyError("attended login private storage cleanup failed") from exc def _capture_value(resolved: ResolvedFetch) -> str: diff --git a/src/warden/routing/catalog.py b/src/warden/routing/catalog.py index b776d09..c04f7d7 100644 --- a/src/warden/routing/catalog.py +++ b/src/warden/routing/catalog.py @@ -69,7 +69,7 @@ _REQUIRED_FIELDS = ( "workload_ref", ) _VALID_STATUS = ("active", "draft") -_VALID_LANES = ("secret", "login", "ceremony", "attended-change") +_VALID_LANES = ("secret", "login", "ceremony") _VALID_ROTATION_METHODS = ("rotate", "re-establish") # Default review cadence for a catalog pointer — "is this still the right owner @@ -199,36 +199,6 @@ class CatalogFreshness: } -def _openbao_configuration_need(query: str) -> bool: - """Separate explicit control-plane configuration from metadata discovery. - - This is a routing distinction, never approval. Exact lane IDs are handled - first; reads and surveys continue through ordinary catalog ranking. - """ - text = query.lower().replace("-", " ") - # The attended administration front door is itself explicit intent. Its - # reviewed operation may concern signer custody or KV metadata rather than - # one of the control nouns below; keyword ranking must not turn that into a - # metadata-survey approval. This selects identity, never grants authority. - if (re.search(r"\battended\b", text) - and re.search(r"\bplatform\s+administration\b", text) - and re.search(r"\bopenbao\b", text)): - return True - control = re.search(r"\b(?:polic(?:y|ies)|auth\s+roles?|token\s+roles?|" - r"database\s*(?:/| )\s*config|secrets?\s+engine)\b", text) - change = re.search(r"\b(?:configure|configuring|create|apply|install|update|" - r"administration|administer)\b", text) - return bool(re.search(r"\bopenbao\b", text) and control and change) - - -def _openbao_provision_need(query: str) -> bool: - """Value provisioning belongs to custody, never the structure survey.""" - text = query.lower().replace("-", " ") - return bool(re.search(r"\bopenbao\b", text) - and re.search(r"\b(?:provision|provisioning|seed|seeding)\b", text) - and re.search(r"\b(?:keys?|credentials?|secrets?|tokens?|values?)\b", text)) - - @dataclass class Catalog: path: Path @@ -262,19 +232,6 @@ class Catalog: scored = [(e.match_score(tokens), e) for e in pool] scored = [(s, e) for s, e in scored if s > 0] scored.sort(key=lambda pair: (-pair[0], pair[1].id)) - if _openbao_provision_need(query): - custody = self.get("openbao-api-key") - if custody is None or not custody.is_active or custody.lane != "secret": - return [] - return ([custody] + [entry for _, entry in scored if entry.id != custody.id])[:limit] - if _openbao_configuration_need(query): - admin = self.get("openbao-platform-admin-login") - # A missing/inactive administration front door must not fall back to - # a survey ceremony or a secret-read transport. - if admin is None or not admin.is_active or admin.lane != "login": - return [] - ranked = [admin] + [entry for _, entry in scored if entry.id != admin.id] - return ranked[:limit] return [e for _, e in scored[:limit]] def stale( @@ -697,9 +654,6 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry: raise CatalogError( f"entry {entry_id!r} has invalid lane {lane!r} (expected one of {_VALID_LANES})" ) - session_lane = str(raw["session_lane"]).strip() if raw.get("session_lane") else None - if lane == "attended-change" and not session_lane: - raise CatalogError(f"entry {entry_id!r} is an attended-change lane and must name session_lane") risk_value = raw.get("risk") risk = str(risk_value).strip() if risk_value is not None else "ungraded" @@ -731,7 +685,6 @@ def _parse_entry(raw: dict, index: int) -> RouteEntry: exec_capable=exec_capable, policy_ref=handoff["policy_ref"], lane=lane, - session_lane=session_lane, exec_owner=str(raw["exec_owner"]) if raw.get("exec_owner") else None, exec_command=handoff["exec_command"], pointer_command=handoff["pointer_command"], diff --git a/src/warden/routing/models.py b/src/warden/routing/models.py index c691b33..2485c69 100644 --- a/src/warden/routing/models.py +++ b/src/warden/routing/models.py @@ -8,7 +8,7 @@ pointers only (the no-double-source rule, enforced in `catalog.py`). from __future__ import annotations from dataclasses import dataclass, field -from typing import Callable, List, Optional +from typing import List, Optional # Risk grade vocabulary (ADR-0007). Grades outside LOW_RISK_GRADES — including @@ -177,7 +177,6 @@ class RouteEntry: # "ceremony" — attended owner operation such as Shamir seal/unseal. It is a # pointer plus approval boundary, never an executable access lane. lane: str = "secret" - session_lane: Optional[str] = None # attended-change: login lane whose session establishes it # Owner-native exec front door (WP-0019). When `exec_owner` is set, that subsystem # (e.g. secrets-engine) provides the PRIMARY way to run a secret-backed command; the # catalog routes to it and keeps ops-warden's own --fetch/--exec proxy as a transparent @@ -324,20 +323,3 @@ class RouteEntry: elif any(t in h or h in t for h in haystack): score += 1 return score - - -def attended_lane_issue( - entry: RouteEntry, get_entry: Callable[[str], Optional[RouteEntry]] -) -> Optional[tuple[str, str]]: - """Shared planner/executor boundary, using one parsed catalog snapshot.""" - if not entry.is_active or not entry.reviewed: - return ("LANE-UNREVIEWED", f"lane '{entry.id}' is not active and reviewed") - if entry.lane == "attended-change": - session = get_entry(entry.session_lane or "") - if (session is None or not session.is_active or not session.reviewed - or session.lane != "login"): - return ("LANE-KIND", f"attended-change lane '{entry.id}' session_lane " - f"'{entry.session_lane}' does not resolve to an active, reviewed login lane") - elif entry.lane != "login": - return ("LANE-KIND", f"attended step's lane '{entry.id}' is not a login or attended-change lane") - return None diff --git a/src/warden/routing/scaffold.py b/src/warden/routing/scaffold.py deleted file mode 100644 index c82ec38..0000000 --- a/src/warden/routing/scaffold.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Offline, draft-only catalog scaffolding (WARDEN-WP-0041).""" - -from __future__ import annotations - -from copy import deepcopy -from datetime import date -from pathlib import Path -import re -import shutil - -import yaml - -from warden.routing.catalog import ( - Catalog, - CatalogError, - _assert_no_secret_material, - _parse_entry, - load_catalog, -) - - -class ScaffoldError(ValueError): - """Safe diagnostic: field names only, never supplied values.""" - - -FIELDS = frozenset( - { - "schema_version", - "lane_id", - "title", - "need_keywords", - "consumer_repo", - "owner_repo", - "workload_ref", - "risk", - "wiki_ref", - "canon_ref", - "platform_request_ref", - "reviewed", - } -) -WORKLOAD_FIELDS = frozenset( - { - "applicability", - "rapp_id", - "name", - "deployable", - "declaration_ref", - "reason", - "unknown_reason", - } -) -SLUG = re.compile(r"[a-z][a-z0-9]*(?:-[a-z0-9]+)*\Z") - - -def _screen(value: object, field: str) -> None: - if isinstance(value, dict): - for key, item in value.items(): - _screen(item, field) - elif isinstance(value, list): - for item in value: - _screen(item, field) - elif isinstance(value, str): - try: - _assert_no_secret_material("draft", field, value, prose=False) - except CatalogError: - raise ScaffoldError(f"{field}: suspected secret material") from None - - -def render(request: object, catalog: Catalog) -> dict[str, str]: - """Validate before any write; preserve explicit owner metadata.""" - if not isinstance(request, dict) or set(request) != FIELDS: - raise ScaffoldError( - "request: required fields missing or unknown fields present" - ) - if type(request["schema_version"]) is not int or request["schema_version"] != 1: - raise ScaffoldError("schema_version: expected 1") - for field in FIELDS - {"schema_version", "need_keywords", "workload_ref"}: - value = request[field] - if ( - not isinstance(value, str) - or not value.strip() - or any(c in value for c in "\r\n\x00") - ): - raise ScaffoldError(f"{field}: expected nonempty single-line text") - _screen(value, field) - for field in ("lane_id", "consumer_repo", "owner_repo"): - if not SLUG.fullmatch(request[field]): - raise ScaffoldError(f"{field}: expected repository/lane slug") - lane = request["lane_id"] - if catalog.get(lane) is not None: - raise ScaffoldError("lane_id: already exists in routing catalog") - if request["owner_repo"] != "railiance-platform": - raise ScaffoldError( - "owner_repo: this scaffold supports railiance-platform KV read pointers" - ) - if request["risk"] not in ("standard", "high", "ungraded"): - raise ScaffoldError("risk: expected explicit standard, high or ungraded") - if not re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", request["reviewed"]): - raise ScaffoldError("reviewed: expected ISO date") - try: - date.fromisoformat(request["reviewed"]) - except ValueError: - raise ScaffoldError("reviewed: expected ISO date") from None - keywords = request["need_keywords"] - if ( - not isinstance(keywords, list) - or not keywords - or any( - not isinstance(k, str) or not k.strip() or any(c in k for c in "\r\n\x00") - for k in keywords - ) - ): - raise ScaffoldError("need_keywords: expected nonempty text list") - _screen(keywords, "need_keywords") - workload = request["workload_ref"] - if not isinstance(workload, dict) or not set(workload) <= WORKLOAD_FIELDS: - raise ScaffoldError("workload_ref: expected known catalog fields") - if any( - not isinstance(v, str) or not v.strip() or any(c in v for c in "\r\n\x00") - for v in workload.values() - ): - raise ScaffoldError("workload_ref: expected nonempty text values") - _screen(workload, "workload_ref") - entry = { - "id": lane, - "title": request["title"], - "need_keywords": deepcopy(keywords), - "owner_repo": request["owner_repo"], - "subsystem": "openbao", - "warden_executes": False, - "wiki_ref": request["wiki_ref"], - "canon_ref": request["canon_ref"], - "reviewed": request["reviewed"], - "status": "draft", - "workload_ref": deepcopy(workload), - "risk": request["risk"], - "lane": "secret", - "exec_capable": False, - } - try: - _parse_entry(entry, 0) - except CatalogError: - raise ScaffoldError( - "workload_ref: invalid catalog workload declaration" - ) from None - playbook = f"""# {request["title"]} - -Draft lane: `{lane}`. Consumer: `{request["consumer_repo"]}`. -Owner: `{request["owner_repo"]}`. -Platform request: `{request["platform_request_ref"]}`. -Authoritative reference: `{request["canon_ref"]}`. - -## Worker checklist - -- Obtain platform review and confirmed workload/auth bindings. -- Review delegated frontdoor and whole-path rotation requirements with owners. -- Use the platform owner's CCR lifecycle and evidence procedure. -- Record approved metadata, attended values and deployment receipts separately. -- Obtain positive/negative capability evidence without reading secret data. -- Review catalog-lane-promotion criteria before any activation. - -This draft supplies no executable fetch, approval, grant or runtime readiness. -""" - return { - "catalog.yaml": yaml.safe_dump({"entries": [entry]}, sort_keys=False), - "playbook.md": playbook, - "request.yaml": yaml.safe_dump(deepcopy(request), sort_keys=True), - } - - -def scaffold( - input_path: Path, output_dir: Path, *, catalog: Catalog | None = None -) -> list[Path]: - """Publish a new staging directory; never replace existing destinations.""" - try: - request = yaml.safe_load(input_path.read_text()) - except (OSError, yaml.YAMLError): - raise ScaffoldError("input: unreadable or invalid YAML") from None - outputs = render(request, catalog if catalog is not None else load_catalog()) - if output_dir.exists() or output_dir.is_symlink(): - raise ScaffoldError("output_dir: destination already exists") - if not output_dir.parent.is_dir() or output_dir.parent.is_symlink(): - raise ScaffoldError( - "output_dir: parent must be an existing nonsymlink directory" - ) - try: - output_dir.mkdir() # exclusive creation; collision races fail before writes - except OSError: - raise ScaffoldError("output_dir: cannot create new destination") from None - try: - for name, content in outputs.items(): - with (output_dir / name).open("x") as stream: - stream.write(content) - except OSError: - shutil.rmtree(output_dir) - raise ScaffoldError("output_dir: publication failed") from None - return [output_dir / name for name in outputs] diff --git a/src/warden/vault.py b/src/warden/vault.py index e8cc00e..2668688 100644 --- a/src/warden/vault.py +++ b/src/warden/vault.py @@ -43,20 +43,16 @@ class VaultCA(CABackend): ) url = f"{self._cfg.addr}/v1/{self._cfg.mount}/sign/{role}" - payload = { - "public_key": pubkey_text, - "valid_principals": ",".join(spec.principals), - "ttl": f"{spec.ttl_hours}h", - "cert_type": "user", - "key_id": spec.identity, - } - if spec.extensions is not None: - # Explicit allow-list replaces the role's default_extensions. - payload["extensions"] = {ext: "" for ext in spec.extensions} try: response = httpx.post( url, - json=payload, + json={ + "public_key": pubkey_text, + "valid_principals": ",".join(spec.principals), + "ttl": f"{spec.ttl_hours}h", + "cert_type": "user", + "key_id": spec.identity, + }, headers={"X-Vault-Token": self._token()}, timeout=10.0, ) diff --git a/tests/fixtures/workload-coverage/provenance.json b/tests/fixtures/workload-coverage/provenance.json deleted file mode 100644 index 6340fb1..0000000 --- a/tests/fixtures/workload-coverage/provenance.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "repo": "rapp-core-hub", - "source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation", - "source_sha256": "98e81fb1116aa0ac9dc1a3e7aef9dd87c6d2110d98d46fbfca2f76eec8f3a77b", - "projection_sha256": "9ce902da0c77809f1094a634244652835b162f59682e9fa6923d8c8aa4ed4a3c" - }, - { - "repo": "rapp-issue-core", - "source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation", - "source_sha256": "0425c9fe84e26b359b2a35fc37ef8fcb781de1aaa6229c59fcd246ee523514a5", - "projection_sha256": "a1956deecf2f0fbca44549e5ea5e8f1c4b74ebf91a9482c64c982491af0d923b" - }, - { - "repo": "rapp-qonto", - "source": "retained public snapshot from warden-1.3-kernel-fixes-r1 test preparation", - "source_sha256": "6279dc2f3ab09519dffa1ceeedb2095f1182960e0106109a87aa5c0052446883", - "projection_sha256": "15aa9809e8d6a120c516be5ce9adfb72c00382140c171e791083ad117f503b3f" - } -] diff --git a/tests/fixtures/workload-coverage/rapp-core-hub/declarations/rapp.yaml b/tests/fixtures/workload-coverage/rapp-core-hub/declarations/rapp.yaml deleted file mode 100644 index 4f7bf9c..0000000 --- a/tests/fixtures/workload-coverage/rapp-core-hub/declarations/rapp.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Public declaration projection for the published coverage snapshot; not live evidence. -rapp_id: rapp-core-hub -workload_identity: - name: core-hub -composition: - member_repos: - - deployables: - - core-hub-api - - core-hub-migrate - - hub-core-candidate - - hub-core-candidate-migrate - - repository-classification-publisher -data_classification: confidential -criticality: high -readiness_state: verified diff --git a/tests/fixtures/workload-coverage/rapp-issue-core/declarations/rapp.yaml b/tests/fixtures/workload-coverage/rapp-issue-core/declarations/rapp.yaml deleted file mode 100644 index 0fda745..0000000 --- a/tests/fixtures/workload-coverage/rapp-issue-core/declarations/rapp.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Public declaration projection for the published coverage snapshot; not live evidence. -rapp_id: rapp-issue-core -workload_identity: - name: issue-core -composition: - member_repos: - - deployables: - - issue-core -data_classification: confidential -criticality: high -readiness_state: verified diff --git a/tests/fixtures/workload-coverage/rapp-qonto/declarations/rapp.yaml b/tests/fixtures/workload-coverage/rapp-qonto/declarations/rapp.yaml deleted file mode 100644 index 7392f75..0000000 --- a/tests/fixtures/workload-coverage/rapp-qonto/declarations/rapp.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Public declaration projection for the published coverage snapshot; not live evidence. -rapp_id: rapp-qonto -workload_identity: - name: qonto -composition: - member_repos: - - deployables: - - rapp-qonto -data_classification: restricted -criticality: critical -readiness_state: verified diff --git a/tests/test_access.py b/tests/test_access.py index 1736a45..f5765d9 100644 --- a/tests/test_access.py +++ b/tests/test_access.py @@ -4,45 +4,15 @@ from __future__ import annotations import json from pathlib import Path -import pytest - from typer.testing import CliRunner -from warden.access import agent_read_boundary_identity, expand_handoff, policy_gate_status +from warden.access import expand_handoff, policy_gate_status from warden.cli import app from warden.routing.models import RouteEntry runner = CliRunner() -@pytest.fixture(autouse=True) -def synthetic_openbao_advisory_identity(monkeypatch): - """Advisory CLI tests must not inspect or use workstation credentials. - - Identity-display tests override this stub explicitly. The real helper has - separate subprocess-mocked unit coverage in test_openbao_caller.py. - """ - monkeypatch.setattr( - "warden.openbao_caller.describe_ambient_openbao_caller", - lambda *args, **kwargs: { - "source": "none", "entity_id": None, "display_name": None, - "auth_path": None, "policies": [], "oidc_role": None, "ttl": None, - "error": "synthetic test: no ambient OpenBao token", - }, - ) - - -def test_advisory_does_not_invoke_ambient_lookup(monkeypatch): - monkeypatch.setenv("BAO_TOKEN", "fixture-only-never-a-real-token") - def forbidden(*args, **kwargs): - raise AssertionError("advisory unit tests must not launch a token lookup") - monkeypatch.setattr("warden.openbao_caller.subprocess.run", forbidden) - result = runner.invoke(app, ["access", "npm token", "--json"]) - assert result.exit_code == 0 - assert json.loads(result.stdout)["openbao_caller"]["source"] == "none" - - - def _repo_catalog() -> Path: return Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml" @@ -94,60 +64,8 @@ def test_policy_gate_status_no_config(monkeypatch, tmp_path): assert "advisory" in policy_gate_status() -def test_issued_coding_agent_subject_drives_read_boundary_without_legacy_marker(): - env = {"WARDEN_POLICY_SUBJECT": "service:codex:railiance-platform"} - assert agent_read_boundary_identity(env) == "service:codex:railiance-platform" - - -def test_legacy_agent_marker_remains_fail_toward_safety_fallback(): - env = {"WARDEN_AGENT_ID": "grok"} - assert agent_read_boundary_identity(env) == "grok" - - -def test_non_agent_policy_subject_does_not_manufacture_agent_identity(): - env = {"WARDEN_POLICY_SUBJECT": "iam:bernd"} - assert agent_read_boundary_identity(env) is None - - -def test_explicit_empty_environment_does_not_inherit_process_identity(monkeypatch): - monkeypatch.setenv("WARDEN_AGENT_ID", "codex") - monkeypatch.setenv("WARDEN_POLICY_SUBJECT", "service:codex:railiance-platform") - assert agent_read_boundary_identity({}) is None - assert agent_read_boundary_identity() == "service:codex:railiance-platform" - - -def test_operator_subject_does_not_override_legacy_agent_marker(): - env = {"WARDEN_POLICY_SUBJECT": "iam:bernd", "WARDEN_AGENT_ID": "codex"} - assert agent_read_boundary_identity(env) == "codex" - - # --- CLI ------------------------------------------------------------------- -def test_access_advisory_shows_ambient_openbao_caller(monkeypatch): - monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog())) - - def described(env=None, **kwargs): - return { - "source": "BAO_TOKEN", - "entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b", - "display_name": "platform-root", - "auth_path": "auth/netkingdom/oidc/callback", - "policies": ["agent-high-risk-boundary"], - "oidc_role": "coding-agent", - "ttl": 900, - "error": None, - } - - monkeypatch.setattr("warden.openbao_caller.describe_ambient_openbao_caller", described) - r = runner.invoke(app, ["access", "reuse-surface hub write token", "--json"]) - assert r.exit_code == 0 - payload = json.loads(r.stdout) - caller = payload["openbao_caller"] - assert caller["entity_id"] == "4a1f663c-0621-0483-edd0-f852274f4b6b" - assert "agent-high-risk-boundary" in caller["boundary_note"] - assert "id" not in caller - - def test_access_advisory_output(monkeypatch): monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(_repo_catalog())) r = runner.invoke(app, ["access", "npm token", "--domain", "coulomb_social"]) diff --git a/tests/test_agent_read_boundary_check.py b/tests/test_agent_read_boundary_check.py index abb0112..ec697d1 100644 --- a/tests/test_agent_read_boundary_check.py +++ b/tests/test_agent_read_boundary_check.py @@ -81,10 +81,6 @@ class TestToDataPath: """`ops-warden-warden-sign-token` is a broker grant, not a KV path.""" assert mod.to_data_path("credential-grants/catalog.yaml grant ops-warden/warden-sign") is None - def test_absolute_filesystem_path_has_no_kv_address(self): - """A kubeconfig path must not become an empty-mount `/data/...` address.""" - assert mod.to_data_path("/home/worsch/.kube/config-railiance01") is None - class TestAgainstTheRealCatalog: def test_every_high_risk_lane_resolves_or_is_explicitly_pattern(self): diff --git a/tests/test_agt_custodian_profile.py b/tests/test_agt_custodian_profile.py deleted file mode 100644 index 2f822af..0000000 --- a/tests/test_agt_custodian_profile.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Issuance profile for agt-custodian (CUST-WP-0073-T02, WARDEN-WP-0045). - -No real signing: ssh-keygen and Vault are mocked; no CA key is read. -""" -from pathlib import Path -from unittest.mock import MagicMock, patch - -import pytest -import yaml - -from warden.ca import CAError, LocalCA, _enforce_ttl, extension_options -from warden.inventory import InventoryError, load_inventory -from warden.models import ActorType, CertSpec, MAX_TTL_HOURS -from warden.routing.catalog import load_catalog -from warden.vault import VaultCA -from warden.config import VaultConfig - -ROOT = Path(__file__).resolve().parents[1] -SEED = ROOT / "examples" / "inventory.seed.yaml" -CATALOG = ROOT / "registry" / "routing" / "catalog.yaml" - - -@pytest.fixture -def entry(): - return load_inventory(SEED).actors["agt-custodian"] - - -def _spec(entry, pubkey: Path, ttl=None) -> CertSpec: - return CertSpec( - actor_name=entry.name, - actor_type=entry.actor_type, - pubkey_path=pubkey, - ttl_hours=ttl or entry.ttl_hours, - principals=entry.principals, - extensions=entry.extensions, - max_ttl_hours=entry.max_ttl_hours, - ) - - -def test_profile_principal_and_type(entry): - assert entry.actor_type is ActorType.AGT - assert entry.principals == ["agt-custodian"] - - -def test_profile_ttl_bound(entry): - assert entry.ttl_hours == 12 - assert entry.max_ttl_hours == 12 - assert entry.max_ttl_hours < MAX_TTL_HOURS[ActorType.AGT] - - -def test_profile_extensions_pty_only(entry): - assert entry.extensions == ["permit-pty"] - for forbidden in ( - "permit-agent-forwarding", - "permit-port-forwarding", - "permit-X11-forwarding", - "permit-user-rc", - ): - assert forbidden not in entry.extensions - - -def test_ttl_override_cannot_exceed_profile(entry, tmp_path): - _enforce_ttl(_spec(entry, tmp_path / "k.pub")) - with pytest.raises(CAError, match="profile maximum 12h"): - _enforce_ttl(_spec(entry, tmp_path / "k.pub", ttl=13)) - with pytest.raises(CAError): - _enforce_ttl(_spec(entry, tmp_path / "k.pub", ttl=24)) - - -def test_local_ca_clears_default_extensions(entry, tmp_path): - pubkey = tmp_path / "agent.pub" - pubkey.write_text("ssh-ed25519 AAAA agent\n") - ca_key = tmp_path / "ca" # placeholder path; ssh-keygen is mocked - ca_key.write_text("not-a-key") - captured = {} - - def fake_run(cmd, **kwargs): - if "-s" in cmd: - captured["cmd"] = cmd - Path(cmd[-1]).with_name("key-cert.pub").write_text("cert\n") - return MagicMock(returncode=0, stdout="", stderr="") - return MagicMock( - returncode=0, - stdout=( - 'Key ID: "agt-custodian"\n' - "Valid: from 2026-10-20T08:00:00 to 2026-10-20T20:00:00\n" - "Principals:\n agt-custodian\n" - "Critical Options: (none)\n" - ), - stderr="", - ) - - with patch("warden.ca.subprocess.run", side_effect=fake_run): - record = LocalCA(ca_key, tmp_path / "state").sign(_spec(entry, pubkey)) - - cmd = captured["cmd"] - assert cmd[cmd.index("-n") + 1] == "agt-custodian" - assert cmd[cmd.index("-V") + 1] == "+12h" - opts = [cmd[i + 1] for i, a in enumerate(cmd) if a == "-O"] - assert opts == ["clear", "permit-pty"] - assert record.principals == ["agt-custodian"] - - -def test_vault_payload_carries_explicit_extensions(entry, tmp_path): - pubkey = tmp_path / "agent.pub" - pubkey.write_text("ssh-ed25519 AAAA agent\n") - ca = VaultCA( - VaultConfig(addr="http://vault.invalid", role_map={"agt": "agt-role"}), - tmp_path / "state", - ) - with patch.dict("os.environ", {"VAULT_TOKEN": "test"}), patch( - "warden.vault.httpx.post", side_effect=RuntimeError("stop") - ) as post: - with pytest.raises(RuntimeError): - ca.sign(_spec(entry, pubkey)) - payload = post.call_args.kwargs["json"] - assert payload["valid_principals"] == "agt-custodian" - assert payload["ttl"] == "12h" - assert payload["extensions"] == {"permit-pty": ""} - - -def test_default_actor_keeps_backend_extensions(tmp_path): - spec = CertSpec( - actor_name="agt-state-hub-bridge", - actor_type=ActorType.AGT, - pubkey_path=tmp_path / "k.pub", - ttl_hours=24, - principals=["agt-task-bridge"], - ) - assert extension_options(spec) == [] - - -def test_inventory_rejects_unknown_extension(tmp_path): - inv = tmp_path / "inv.yaml" - inv.write_text(yaml.dump({"actors": {"agt-x": { - "type": "agt", "extensions": ["permit-everything"]}}})) - with pytest.raises(InventoryError, match="unknown certificate extension"): - load_inventory(inv) - - -def test_inventory_rejects_ttl_above_profile_max(tmp_path): - inv = tmp_path / "inv.yaml" - inv.write_text(yaml.dump({"actors": {"agt-x": { - "type": "agt", "ttl_hours": 20, "max_ttl_hours": 12}}})) - with pytest.raises(InventoryError, match="exceeds its max_ttl_hours"): - load_inventory(inv) - - -def test_catalog_lane_is_attended_not_executable(): - lane = load_catalog(CATALOG).get("agt-custodian-ssh-cert") - assert lane is not None - assert lane.lane == "ceremony" - assert lane.warden_executes is False diff --git a/tests/test_case_1_3_integration.py b/tests/test_case_1_3_integration.py deleted file mode 100644 index bb13c09..0000000 --- a/tests/test_case_1_3_integration.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Integration: the real net-kingdom 1.3 candidate against this kernel and catalog. - -Reads the actual 1.3 type, its forgejo example and the committed composition -artifact from net-kingdom, plus this branch's real routing catalog. The descriptor -map and metadata come from those committed files. All authority is fixture-only: -we change the proposed type to acknowledged, change the example case to -materialized and replace its acceptance record, promote draft lanes in memory, -and temporarily install the 1.3 parameter pin. Stub adapters replace native -adapters; an unconditional evidence verifier simulates every acceptance and caller -session. WARDEN_AGENT_ID is removed to simulate a human runtime. No adapter is -dispatched. This proves conditional source admission, not authenticated owner -acceptance, native execution or readiness; unresolved Apps sessions stay refused. -""" -from dataclasses import asdict -import hashlib -import json -import os -from pathlib import Path -from unittest import mock - -import pytest -import yaml - -from warden import case_inputs as inputs -from warden.case_receipt_state import Refused -from warden.case_run import Adapter, Descriptor, Executor, Journal, digest -from warden.case_type import check_follow_through -from warden.routing.catalog import load_catalog - -NK = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT", - Path(__file__).resolve().parents[2] / "net-kingdom")) -TYPE_DIR = NK / "security-case-type-revisions/1.3/oidc-browser-relying-party" -ARTIFACT = NK / "sso-mfa/k8s/keycape/descriptor-composition-1.3.candidate.json" -CATALOG = Path(__file__).resolve().parents[1] / "registry/routing/catalog.yaml" - -pytestmark = pytest.mark.skipif(not (TYPE_DIR / "type.yaml").is_file() or not ARTIFACT.is_file(), - reason="net-kingdom 1.3 candidate not available") - -DRAFT_LANES = {"keycape-attended-case-change", "platform-secret-delivery", - "consumer-owner-deployment", "apps-consumer-handover"} - - -def encode(desc): # net-kingdom compose-1.2-descriptors.py encode() - return json.dumps(asdict(desc), sort_keys=True, separators=(",", ":")).encode() - - -def type_doc(status="acknowledged"): - doc = yaml.safe_load((TYPE_DIR / "type.yaml").read_bytes()) - doc["spec"]["status"] = status - return doc - - -def catalog_bytes(promote): - raw = yaml.safe_load(CATALOG.read_bytes()) - if promote: - for entry in raw["entries"]: - if entry["id"] in DRAFT_LANES and entry.get("status") == "draft": - entry["status"] = "active" - return yaml.safe_dump(raw).encode() - - -def descriptors(): - art = json.loads(ARTIFACT.read_text()) - out = {} - for row in art["steps"]: - d = dict(row["descriptor"]) - for key in ("allowed_created", "revocation_scopes"): - d[key] = tuple(d[key]) - out[row["step"]] = Descriptor(**d) - return out - - -def pin_13(): - params = type_doc()["spec"]["parameters"] - fp = hashlib.sha256(json.dumps(params, sort_keys=True, separators=(",", ":")).encode()).hexdigest() - return mock.patch.dict(inputs.PARAMETER_PINS, {"1.3": fp}) - - -def unused(*_): - raise AssertionError("admission must not dispatch") - - -def admit(tmp_path, desc, *, promote=True, pinned=True, status="acknowledged"): - case = yaml.safe_load((TYPE_DIR / "examples/forgejo.yaml").read_bytes()) - case["spec"].update(status="materialized", acceptance={ - "at": "2026-10-17", "record": "fixture-only acceptance, not an owner record", - "by": [{"name": "fixture", "role": "owner"}, {"name": "fixture", "role": "founder"}]}) - adapter = Adapter(desc.adapter, desc.adapter_source, unused, unused) - executor = Executor(Journal(tmp_path / desc.step), {adapter.reference: adapter}, - lambda purpose, source, evidence: True, - browser_inputs=inputs.BrowserInputs(NK)) - with mock.patch.dict(os.environ), (pin_13() if pinned else mock.patch.dict({})): - os.environ.pop("WARDEN_AGENT_ID", None) - return executor.admit(yaml.safe_dump(case).encode(), yaml.safe_dump(type_doc(status)).encode(), - encode(desc), catalog_bytes(promote), principal="founder", - actor=desc.actor, lane=desc.lane, - acceptance_evidence="e-acceptance", materialization_evidence="e-mason", - descriptor_evidence="e-descriptor", session_evidence="e-session") - - -def test_artifact_descriptors_are_the_type_map(): - descs = descriptors() - assert len(descs) == 7 - assert {s: digest(encode(d)) for s, d in descs.items()} == type_doc()["spec"]["execution_descriptors"] - - -def test_conditional_admission_keeps_unresolved_apps_sessions_refused(tmp_path): - for step, desc in descriptors().items(): - if step in ("deliver-consumer-secret", "hand-over"): - with pytest.raises(Refused, match="does not resolve to an active, reviewed login lane"): - admit(tmp_path, desc) - else: - admitted, invocation, _ = admit(tmp_path, desc) - assert admitted == desc - assert (invocation.step, invocation.actor, invocation.target) == (step, "human", "forgejo") - -def test_draft_lane_steps_refused_until_promoted(tmp_path): - for step, desc in descriptors().items(): - if desc.lane in DRAFT_LANES: - with pytest.raises(Refused, match="descriptor lane is not active in the catalog"): - admit(tmp_path, desc, promote=False) - else: - admit(tmp_path, desc, promote=False) # mint-secret's lane is already active - - -def test_unpinned_1_3_refused_by_browser_validation(tmp_path): - with pytest.raises(Refused, match="failed exact-version validation"): - admit(tmp_path, descriptors()["mint-secret"], pinned=False) - - -def test_proposed_type_refused(tmp_path): - with pytest.raises(Refused, match="type is not acknowledged"): - admit(tmp_path, descriptors()["mint-secret"], status="proposed") - - -def test_type_check_findings_against_the_real_catalog(tmp_path): - def findings(promote): - path = tmp_path / f"catalog-{promote}.yaml" - path.write_bytes(catalog_bytes(promote)) - report = check_follow_through(type_doc("proposed"), load_catalog(path)) - return {(f.code, f.where) for f in report.findings} - - draft = findings(False) - assert {code for code, _ in draft} == {"LANE-UNREVIEWED"} - assert len(draft) == 6 # six steps use the four draft lanes; mint-secret's lane is active - - promoted = findings(True) - # Only the two railiance-apps lanes remain: their session_lane awaits owner confirmation. - assert {code for code, _ in promoted} == {"LANE-KIND"} - assert len(promoted) == 2 - assert all("deliver-consumer-secret" in where or "hand-over" in where for _, where in promoted) diff --git a/tests/test_case_admission_modes.py b/tests/test_case_admission_modes.py deleted file mode 100644 index 5069491..0000000 --- a/tests/test_case_admission_modes.py +++ /dev/null @@ -1,136 +0,0 @@ -"""KERNEL-2 lane resolution and KERNEL-3 mode-aware admission (fixture only).""" -from dataclasses import asdict, replace -import json - -import pytest -import yaml - -from warden.case_receipt_state import Refused -from warden.case_run import Descriptor, digest -from tests.test_case_run import MINT_STEP, fixture_catalog, lane_entry, make_rig - - -@pytest.fixture -def rig(tmp_path): - return make_rig(tmp_path) - -EVIDENCE = {"acceptance", "materialization", "descriptor", "session"} - - -def admit(rig, *, step=None, entries=None, catalog=None, desc=None, **over): - executor, _, _, sources, kwargs = rig - case, typ, descriptor, _ = sources - if desc is not None: - base = Descriptor.decode(descriptor) - descriptor = json.dumps(asdict(replace(base, **desc))).encode() - doc = yaml.safe_load(typ) - doc["spec"]["execution_descriptors"] = {"mint-secret": digest(descriptor)} - if step is not None: - doc["spec"]["follow_through"] = step - typ = yaml.safe_dump(doc).encode() - if catalog is None: - catalog = fixture_catalog(*(entries or [lane_entry()])) - executor.evidence_verifier = lambda purpose, source, evidence: evidence in EVIDENCE - return executor.admit(case, typ, descriptor, catalog, **{**kwargs, **over}) - - -def mode_step(executes, attended): - return [dict(MINT_STEP, warden_executes=executes, attended=attended)] - - -def refused(message, rig, **kw): - with pytest.raises(Refused, match=message): - admit(rig, **kw) - - -# Positive modes -def test_warden_mode_admits(rig): - assert admit(rig)[1].lane == "issuer-lane" - - -def test_attended_mode_admits_human_parent_session(rig, monkeypatch): - monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) - admitted = admit( - rig, - step=mode_step(False, True), - entries=[dict(lane_entry(), lane="login")], - desc=dict(effect="change", revocation_scopes=("parent-session",)), - ) - assert admitted[1].actor == "human" - - -def test_owner_mode_admits_native_owner_lane(rig): - admitted = admit(rig, step=mode_step(False, False), desc=dict(effect="change")) - assert admitted[1].owner == "owner" - - -# KERNEL-2 -@pytest.mark.parametrize( - "entries, catalog, message", - [ - (None, b"not: [a catalog", "invalid catalog source shape"), - ([lane_entry("other-lane")], None, "descriptor lane is not in the catalog"), - ([lane_entry(status="draft")], None, "descriptor lane is not active in the catalog"), - ([{k: v for k, v in lane_entry().items() if k != "delegation"}], None, - "descriptor lane has no catalog delegation record"), - ([{k: v for k, v in lane_entry().items() if k != "risk"}], None, - "descriptor lane carries no risk grade"), - ([lane_entry(owner="someone-else")], None, - "descriptor owner does not fit catalog lane owner_repo"), - ([lane_entry(risk="standard")], None, - "catalog lane risk does not cover the descriptor effect"), - ], -) -def test_catalog_lane_refusals(rig, entries, catalog, message): - refused(message, rig, entries=entries, catalog=catalog) - - -# KERNEL-3: the step must declare dispatch and agree with the descriptor -@pytest.mark.parametrize( - "step, message", - [ - ([], "type step does not declare how the kernel dispatches it"), - (mode_step(True, True), "type step does not declare how the kernel dispatches it"), - ([{k: v for k, v in MINT_STEP.items() if k != "lane"}], - "type step does not declare how the kernel dispatches it"), - ([dict(MINT_STEP, warden_executes="yes")], - "type step does not declare how the kernel dispatches it"), - ([dict(MINT_STEP, lane="other-lane")], "type step lane or owner disagrees with descriptor"), - ([dict(MINT_STEP, owner="other")], "type step lane or owner disagrees with descriptor"), - ], -) -def test_step_declaration_refusals(rig, step, message): - refused(message, rig, step=step) - - -def test_attended_refuses_agent_session(rig, monkeypatch): - monkeypatch.setenv("WARDEN_AGENT_ID", "agent-1") - refused("attended step requires a human parent session", rig, - step=mode_step(False, True), desc=dict(effect="change")) - - -def test_attended_refuses_agent_actor(rig): - refused("attended step requires a human parent session", rig, - step=mode_step(False, True), desc=dict(effect="change", actor="agent"), - actor="agent") - - -def test_attended_refuses_caller_lane_other_than_step_lane(rig): - refused("principal actor or lane mismatch", rig, - step=mode_step(False, True), desc=dict(effect="change"), lane="login-lane") - - -@pytest.mark.parametrize( - "entry, desc", - [ - (lane_entry(delegation={"mode": "interim", "intended_owner": "owner", - "blocked_on": "fixture", "reviewed": "2026-10-17"}), - dict(effect="change")), - (lane_entry(owner="owner/other"), dict(effect="change")), - (lane_entry(warden_executes=True), dict(effect="change")), - (lane_entry(), {}), # owner step may not be a Warden-brokered mint - ], -) -def test_owner_mode_refusals(rig, entry, desc): - refused("owner step lane is not the owner's native path", rig, - step=mode_step(False, False), entries=[entry], desc=desc) diff --git a/tests/test_case_attended_boundary.py b/tests/test_case_attended_boundary.py deleted file mode 100644 index ba07b33..0000000 --- a/tests/test_case_attended_boundary.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Synthetic attended admission/dispatch regressions; no owner/native actions.""" -from dataclasses import replace - -import pytest - -from tests.test_case_admission_modes import admit, mode_step -from tests.test_case_run import fixture_catalog, lane_entry, make_rig -from warden.case_receipt_state import Outcome, Refused -from warden.case_type import check_follow_through -from warden.routing.catalog import CatalogError, _parse_entry, load_catalog - - -def attended(rig, entries): - return admit(rig, step=mode_step(False, True), entries=entries, - desc=dict(effect="change", revocation_scopes=("parent-session",))) - - -@pytest.mark.parametrize("operation", ["run", "rollback", "reconcile"]) -@pytest.mark.parametrize("marker", ["agent-1", "", None]) -def test_identity_change_refuses_before_any_write_or_callback(tmp_path, monkeypatch, operation, marker): - monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) - rig = make_rig(tmp_path) - executor = rig[0] - admitted = attended(rig, [dict(lane_entry(), lane="login")]) - executor.evidence_verifier = lambda *_: True - if operation == "rollback": - executor.run(*admitted) - elif operation == "reconcile": - old = executor.adapters[admitted[0].adapter] - def lost(_): - raise RuntimeError("synthetic lost reply") - executor.adapters[old.reference] = replace(old, invoke=lost) - executor.run(*admitted) - calls = [] - old = executor.adapters[admitted[0].adapter] - callback = getattr(old, "invoke" if operation == "run" else operation) - def observed(*args): - calls.append(args) - return callback(*args) - executor.adapters[old.reference] = replace(old, **{ - "invoke" if operation == "run" else operation: observed, - }) - def rows(): - return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] - for table in ("intents", "receipts")) - before = rows() - if marker is None: - result = getattr(executor, operation)(*admitted) - assert result.last.outcome is {"run": Outcome.SUCCESS, - "rollback": Outcome.ROLLED_BACK, - "reconcile": Outcome.NO_CHANGE}[operation] - assert len(calls) == 1 - assert rows() != before - return - monkeypatch.setenv("WARDEN_AGENT_ID", marker) - with pytest.raises(Refused, match="attended step requires a human parent session"): - getattr(executor, operation)(*admitted) - assert rows() == before - assert not calls - - -def entries_for(case): - change = dict(lane_entry(), lane="attended-change", session_lane="login-lane") - login = dict(lane_entry("login-lane"), lane="login") - if case == "missing": - return [change] - if case == "draft-login": - login["status"] = "draft" - elif case == "unreviewed-login": - login["reviewed"] = "" - elif case == "wrong-kind-login": - login["lane"] = "secret" - elif case == "draft-change": - change["status"] = "draft" - elif case == "unreviewed-change": - change["reviewed"] = "" - elif case == "missing-session-field": - del change["session_lane"] - elif case in ("secret", "ceremony"): - change["lane"] = case - del change["session_lane"] - return [change, login] - - -@pytest.mark.parametrize("case, code", [ - ("valid", None), ("missing", "LANE-KIND"), ("draft-login", "LANE-KIND"), - ("unreviewed-login", "LANE-KIND"), ("wrong-kind-login", "LANE-KIND"), - ("draft-change", "LANE-UNREVIEWED"), ("unreviewed-change", "LANE-UNREVIEWED"), - ("secret", "LANE-KIND"), ("ceremony", "LANE-KIND"), -]) -def test_attended_change_planner_and_executor_agree(tmp_path, monkeypatch, case, code): - monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) - entries = entries_for(case) - path = tmp_path / "catalog.yaml" - path.write_bytes(fixture_catalog(*entries)) - catalog = load_catalog(path) - step = dict(mode_step(False, True)[0], attended_justification="synthetic test", - entry_point="owner/test", auth="synthetic session", value_handling="none") - doc = {"spec": {"follow_through": [step], "rollback": [{"reverses": step["id"]}]}} - report = check_follow_through(doc, catalog) - assert [f.code for f in report.findings] == ([] if code is None else [code]) - rig = make_rig(tmp_path) - if code: - with pytest.raises(Refused): - attended(rig, entries) - assert rig[0].journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone()[0] == 0 - else: - assert attended(rig, entries)[1].lane == "issuer-lane" - - -def test_attended_change_missing_session_is_parser_rejection(): - with pytest.raises(CatalogError, match="must name session_lane"): - _parse_entry(entries_for("missing-session-field")[0], 0) diff --git a/tests/test_case_current_session_reference.py b/tests/test_case_current_session_reference.py deleted file mode 100644 index 8a474a3..0000000 --- a/tests/test_case_current_session_reference.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Current caller authentication never replaces historical dispatch provenance.""" -from dataclasses import replace - -import pytest - -from tests.test_case_run import make_rig -from warden.case_receipt_state import Outcome, Refused -from warden.case_run import Executor, Journal - - -def fresh_verifier(executor, *, allow=True): - original = executor.evidence_verifier - def verify(purpose, source, evidence): - if purpose.startswith("caller-session:") and evidence == "fresh-session": - return allow - return original(purpose, source, evidence) - executor.evidence_verifier = verify - return verify - - -def rows(executor): - return tuple(executor.journal.db.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0] - for table in ("receipts", "intents")) - - -def readmit(executor, sources, kwargs, reference="session"): - return executor.admit(*sources, **{**kwargs, "session_evidence": "fresh-session", - "original_session_reference": reference}) - - -def test_normal_new_session_cannot_rewrite_original_journal_pins(tmp_path): - executor, admitted, _, sources, kwargs = make_rig(tmp_path) - executor.run(*admitted) - fresh_verifier(executor) - before = rows(executor) - changed = executor.admit(*sources, **{**kwargs,"session_evidence":"fresh-session"}) - assert changed[1].pins != admitted[1].pins - with pytest.raises(Refused,match="journal pins changed"): - executor.rollback(*changed) - assert rows(executor) == before - - -def test_fresh_process_inverse_retains_original_pins_without_replay(tmp_path): - executor, admitted, calls, sources, kwargs = make_rig(tmp_path) - assert executor.run(*admitted).last.outcome is Outcome.SUCCESS - before = rows(executor) - fresh = Executor(Journal(executor.journal.directory), executor.adapters, - fresh_verifier(executor)) - renewed = readmit(fresh, sources, kwargs) - assert renewed[1].pins == admitted[1].pins - assert rows(fresh) == before - with pytest.raises(Refused): - fresh.run(*renewed) - assert len(calls) == 1 - assert fresh.rollback(*renewed).last.outcome is Outcome.ROLLED_BACK - - -@pytest.mark.parametrize("kind", ["missing-intent", "orphan-intent", "wrong-principal", "wrong-reference", "same-as-current", "invalid-reference", "inactive-current"]) -def test_original_reference_is_never_authentication_or_new_intent(tmp_path, kind): - executor, admitted, calls, sources, kwargs = make_rig(tmp_path) - if kind != "missing-intent": - executor.run(*admitted) - if kind == "orphan-intent": - executor.journal.db.execute("DELETE FROM intents") - if kind == "wrong-principal": - kwargs = {**kwargs,"principal":"another-operator"} - fresh_verifier(executor, allow=kind != "inactive-current") - before = rows(executor) - reference = {"wrong-reference": "unrelated-session", "same-as-current": "fresh-session", - "invalid-reference": "not valid"}.get(kind, "session") - with pytest.raises(Refused): - readmit(executor, sources, kwargs, reference) - assert rows(executor) == before - - -def test_unknown_effect_stays_unknown_after_current_readmission(tmp_path): - executor, admitted, calls, sources, kwargs = make_rig(tmp_path) - old = executor.adapters[admitted[0].adapter] - def interrupted(inv): - calls.append(inv) - raise KeyboardInterrupt - executor.adapters[old.reference] = replace(old, invoke=interrupted) - with pytest.raises(KeyboardInterrupt): - executor.run(*admitted) - fresh_verifier(executor) - before = rows(executor) - renewed = readmit(executor, sources, kwargs) - assert rows(executor) == before - with pytest.raises(Refused): - executor.run(*renewed, retry=True) - state = executor.journal.load("mint-secret:fixture-client", renewed[2]) - assert state.last.outcome is Outcome.UNCERTAIN - assert len(calls) == 1 - - -def test_current_session_revalidated_after_readmission_before_journal_write(tmp_path): - executor, admitted, _, sources, kwargs = make_rig(tmp_path) - executor.run(*admitted) - fresh_verifier(executor) - renewed = readmit(executor, sources, kwargs) - executor.evidence_verifier = lambda *_: False - before = rows(executor) - with pytest.raises(Refused, match="caller session no longer verified"): - executor.rollback(*renewed) - assert rows(executor) == before diff --git a/tests/test_case_inputs.py b/tests/test_case_inputs.py deleted file mode 100644 index c8d1b70..0000000 --- a/tests/test_case_inputs.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Actual producer validation and Executor; fake owner evidence, no native claims.""" - -import os -from pathlib import Path - -import pytest -import yaml - -from warden.case_inputs import BrowserInputs, SOURCE_PINS -from warden.case_run import digest -from warden.case_receipt_state import Outcome, Refused -from tests.test_case_run import rig as base_rig - - -@pytest.fixture -def rig(tmp_path): - return base_rig.__wrapped__(tmp_path) - - -NK = Path( - os.environ.get( - "WARDEN_TEST_NETKINGDOM_ROOT", - Path(__file__).resolve().parents[2] / "net-kingdom", - ) -) - - -def browser(rig, version="1.1", mutate=None): - executor, _, calls, sources, kwargs = rig - case = yaml.safe_load( - (NK / "security-cases/forgejo-netkingdom-oidc.yaml").read_bytes() - ) - relative = ( - "security-case-types" - if version == "1.1" - else "security-case-type-revisions/1.2" - ) - typ = yaml.safe_load( - (NK / relative / "oidc-browser-relying-party/type.yaml").read_bytes() - ) - case["spec"]["type"] = "oidc-browser-relying-party@" + version - case["spec"]["status"] = "materialized" - if version == "1.2": - case["spec"]["parameters"]["consumer_namespace"] = "forgejo" - # This extension and all native provenance are TEST ONLY, never source assent. - typ["spec"]["execution_descriptors"] = {"mint-secret": digest(sources[2])} - for step in typ["spec"]["follow_through"]: - if step["id"] == "mint-secret": # TEST ONLY: route to the fixture lane - step.update(owner="owner", lane="issuer-lane") - if mutate: - mutate(case, typ) - changed = ( - yaml.safe_dump(case).encode(), - yaml.safe_dump(typ).encode(), - *sources[2:], - ) - old = executor.evidence_verifier - - def verified(purpose, source, evidence): - return old( - purpose, - source.replace(digest(changed[0]), digest(sources[0])).replace( - digest(changed[1]), digest(sources[1]) - ), - evidence, - ) - - executor.evidence_verifier = verified - executor.browser_inputs = BrowserInputs(NK) - return executor, calls, changed, kwargs - - -@pytest.mark.parametrize("version", ["1.1", "1.2"]) -def test_actual_browser_executor_and_owner_projection(rig, version): - executor, calls, sources, kwargs = browser(rig, version) - args = executor.admit(*sources, **kwargs) - assert args[1].target == "forgejo" - assert not calls - assert executor.run(*args).last.outcome is Outcome.SUCCESS - assert calls[0].target == "forgejo" - assert not hasattr(calls[0], "parameters") - # Owner callbacks/journal get target and fingerprints, no full metadata. - persisted = (executor.journal.directory / "journal.sqlite").read_bytes() - assert b"https://forgejo.coulomb.social" not in persisted - assert b"forge-admins" not in persisted - - -@pytest.mark.parametrize( - "field,value", - [ - ("redirect_uris", "https://*.example.org/callback"), - ("redirect_uris", "http://example.org/callback"), - ("allowed_scopes", "openid,admin"), - ("client_type", "arbitrary"), - ("mfa_required", False), - ("unknown", "value"), - ("issuer", "https://other.example"), - ("display_name", "hvs." + "A" * 30), - ], -) -def test_case_refusals_even_with_test_evidence(rig, field, value): - executor, calls, sources, kwargs = browser( - rig, mutate=lambda c, t: c["spec"]["parameters"].update({field: value}) - ) - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - assert not calls - - -@pytest.mark.parametrize( - "mutation", - [ - lambda c, t: t["spec"].update(version="1.3"), - lambda c, t: t["spec"]["parameters"][2].update(sensitivity="public"), - lambda c, t: t["spec"]["parameters"][2].update(constraints={}), - lambda c, t: t["spec"]["parameters"].append( - { - "name": "secret", - "type": "string", - "case_settable": True, - "sensitivity": "secret_reference", - } - ), - ], -) -def test_type_revision_cannot_widen_reviewed_parameters(rig, mutation): - def change(case, typ): - mutation(case, typ) - case["spec"]["type"] = typ["spec"]["id"] + "@" + typ["spec"]["version"] - - executor, calls, sources, kwargs = browser(rig, mutate=change) - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - assert not calls - - -def test_namespace_and_missing_required_are_validated(rig): - executor, calls, sources, kwargs = browser( - rig, "1.2", lambda c, t: c["spec"]["parameters"].update(consumer_namespace="*") - ) - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - assert not calls - - -@pytest.mark.parametrize("changed_dependency", list(SOURCE_PINS)) -def test_missing_producer_and_changed_transitive_source_refuse( - rig, tmp_path, changed_dependency -): - executor, calls, sources, kwargs = browser(rig) - executor.browser_inputs = BrowserInputs(tmp_path) - for relative in SOURCE_PINS: - path = tmp_path / relative - path.parent.mkdir(parents=True, exist_ok=True) - if relative != changed_dependency: - path.write_bytes((NK / relative).read_bytes()) - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - (tmp_path / changed_dependency).write_text('raise RuntimeError("must not execute")') - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - assert not calls - - -def test_browser_without_validator_still_refuses(rig): - executor, calls, sources, kwargs = browser(rig) - executor.browser_inputs = None - with pytest.raises(Refused, match="not configured"): - executor.admit(*sources, **kwargs) - assert not calls - - -def test_real_unextended_type_stays_plan_only(rig): - executor, calls, sources, kwargs = browser(rig) - typ = yaml.safe_load(sources[1]) - del typ["spec"]["execution_descriptors"] - with pytest.raises(Refused, match="plan only"): - executor.admit(sources[0], yaml.safe_dump(typ).encode(), *sources[2:], **kwargs) - assert not calls - - -def test_required_browser_parameter_is_not_defaulted_away(rig): - executor, calls, sources, kwargs = browser( - rig, mutate=lambda c, t: c["spec"]["parameters"].pop("redirect_uris") - ) - with pytest.raises(Refused, match="exact-version"): - executor.admit(*sources, **kwargs) - assert not calls - - -@pytest.mark.parametrize("version", ["1.1", "1.2"]) -@pytest.mark.parametrize( - "pins", - [ - None, - [], - {"mint-secret": "sha256:" + "A" * 64}, - {"mint-secret": "sha256:" + "a" * 64 + "\n"}, - {"undeclared-step": "sha256:" + "a" * 64}, - {"mint-secret": ["sha256:" + "a" * 64]}, - {"step-" + str(n): "sha256:" + "a" * 64 for n in range(129)}, - ], -) -def test_actual_producer_refuses_malformed_descriptor_pins(rig, version, pins): - executor, calls, sources, _ = browser(rig, version) - case, typ = (yaml.safe_load(source) for source in sources[:2]) - typ["spec"]["execution_descriptors"] = pins - with pytest.raises(Refused, match="exact-version"): - executor.browser_inputs.target(case, typ, "client_id") - assert not calls diff --git a/tests/test_case_original_context_consistency.py b/tests/test_case_original_context_consistency.py deleted file mode 100644 index ddd429d..0000000 --- a/tests/test_case_original_context_consistency.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Original-intent consistency checks with generated metadata only. - -Reuse the existing synthetic owner rig. These checks do not provide a native -holder or independently authenticate original creation from local SQL/JSON. -""" - -from dataclasses import asdict, replace -import json -from pathlib import Path -import sqlite3 -import subprocess -import sys -import pytest -from tests.test_case_run import rig as rig -from warden.case_receipt_state import Outcome, Refused -from warden.case_run import OriginalIntent - - -def interrupted(rig): - executor, args, calls, sources, kwargs = rig - old = executor.adapters[args[0].adapter] - seen = [] - - def lost(inv): - calls.append(inv) - raise TimeoutError("generated lost response") - - def original(ctx): - seen.append(ctx) - return old.reconcile(ctx.attempt) - - executor.adapters[old.reference] = replace( - old, invoke=lost, reconcile=None, reconcile_original=original - ) - assert executor.run(*args).last.outcome is Outcome.UNCERTAIN - return executor, args, calls, old, seen - - -def alter_payload(executor, **changes): - row = executor.journal.db.execute( - "SELECT seq,payload FROM intents ORDER BY seq DESC LIMIT 1" - ).fetchone() - obj = json.loads(row[1]) - obj.update(changes) - executor.journal.db.execute( - "UPDATE intents SET payload=? WHERE seq=?", (json.dumps(obj), row[0]) - ) - - -def test_original_actor_mismatch_is_refused(rig): - executor, args, calls, old, _ = interrupted(rig) - alter_payload(executor, actor="agent") - with pytest.raises(Refused): - executor.journal.original_intent( - "mint-secret:fixture-client", args[1], old.source - ) - - -def test_payload_attempt_id_must_match_sql_column_and_original_receipt(rig): - executor, args, calls, old, _ = interrupted(rig) - actual = calls[0].attempt_id - alter_payload(executor, attempt_id="attempt-unrelated") - with pytest.raises(Refused): - executor.journal.original_intent( - "mint-secret:fixture-client", args[1], old.source - ) - assert ( - executor.journal.db.execute("SELECT attempt_id FROM intents").fetchone()[0] - == actual - ) - - -def test_orphan_intent_without_original_running_receipt_is_refused(rig): - executor, args, calls, old, _ = interrupted(rig) - executor.journal.db.execute( - "DELETE FROM receipts WHERE seq=(SELECT MIN(seq) FROM receipts)" - ) - assert ( - executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome - is Outcome.UNCERTAIN - ) - with pytest.raises(Refused): - executor.journal.original_intent( - "mint-secret:fixture-client", args[1], old.source - ) - - -def test_serializer_refuses_arbitrary_values_outside_normal_admit(rig): - _, args, _, _, _ = rig - inv = replace( - args[1], - target={"unexpected": "synthetic-owner-payload"}, - operation="run", - attempt_id="attempt-fixture", - ) - record = OriginalIntent("mint-secret:fixture-client", inv, "sha256:" + "1" * 64) - with pytest.raises(Refused): - OriginalIntent.decode(record.payload()) - # This is a direct serialization API gap, not an observed normal-admit leak. - - # Owner trust is the existing synthetic fixture; this proves call shape only. - - -@pytest.mark.parametrize( - "field,value", - [ - ("principal", "another-person"), - ("tenant", "another-tenant"), - ("realm", "another-realm"), - ], -) -def test_existing_lookup_does_refuse_stored_principal_tenant_realm_mismatch( - rig, field, value -): - executor, args, _, old, _ = interrupted(rig) - alter_payload(executor, **{field: value}) - with pytest.raises(Refused): - executor.journal.original_intent( - "mint-secret:fixture-client", args[1], old.source - ) - - -def test_existing_atomic_insert_failure_prevents_dispatch_and_both_records(rig): - executor, args, calls, _, _ = rig - executor.journal.db.execute( - "CREATE TRIGGER fail_receipt BEFORE INSERT ON receipts BEGIN SELECT RAISE(ABORT,'fixture'); END" - ) - with pytest.raises(sqlite3.IntegrityError): - executor.run(*args) - assert not calls - assert executor.journal.db.execute("SELECT COUNT(*) FROM intents").fetchone() == ( - 0, - ) - assert executor.journal.db.execute("SELECT COUNT(*) FROM receipts").fetchone() == ( - 0, - ) - - -def test_existing_missing_original_refuses_before_callback(rig): - executor, args, _, _, seen = interrupted(rig) - executor.journal.db.execute("DELETE FROM intents") - with pytest.raises(Refused): - executor.reconcile(*args) - assert not seen - assert ( - executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome - is Outcome.UNCERTAIN - ) - - -def test_existing_current_parent_revocation_remains_mandatory(rig): - executor, args, _, _, _ = interrupted(rig) - old = executor.evidence_verifier - - def refuse_session(purpose, source, evidence): - return ( - False - if purpose.startswith("session-revocation:") - else old(purpose, source, evidence) - ) - - executor.evidence_verifier = refuse_session - with pytest.raises(Refused): - executor.reconcile(*args) - assert ( - executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome - is Outcome.UNCERTAIN - ) - - -def test_existing_intent_survives_actual_distinct_interpreter(rig): - executor, args, calls, old, _ = interrupted(rig) - code = """import json,sys -from pathlib import Path -from warden.case_run import Journal,Invocation -from warden.case_receipt_state import Pins -obj=json.load(sys.stdin);obj['pins']=Pins(**obj['pins']);j=Journal(Path(sys.argv[1])) -with j.locked(): - i=j.original_intent('mint-secret:fixture-client',Invocation(**obj),sys.argv[2]) - print(json.dumps({'attempt':i.invocation.attempt_id,'operation':i.invocation.operation})) -""" - result = subprocess.run( - [sys.executable, "-B", "-c", code, str(executor.journal.directory), old.source], - input=json.dumps(asdict(args[1])), - text=True, - capture_output=True, - timeout=15, - env={ - "PATH": "/usr/bin:/bin", - "PYTHONPATH": str(Path(__file__).parents[1] / "src"), - "PYTHONDONTWRITEBYTECODE": "1", - }, - check=True, - ) - assert json.loads(result.stdout) == { - "attempt": calls[0].attempt_id, - "operation": "run", - } diff --git a/tests/test_case_plan.py b/tests/test_case_plan.py deleted file mode 100644 index 7de67e4..0000000 --- a/tests/test_case_plan.py +++ /dev/null @@ -1,205 +0,0 @@ -"""WARDEN-WP-0044-T02: plan a KnownSecurityCase run.""" -from __future__ import annotations - -import copy -import os -from pathlib import Path - -import pytest -import yaml -from typer.testing import CliRunner - -from warden.case_plan import CaseError, plan_case -from warden.cli import app -from warden.routing.catalog import load_catalog - -LIVE_CASE = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT", - Path(__file__).resolve().parents[2] / "net-kingdom")) / "security-cases/forgejo-netkingdom-oidc.yaml" - -TYPE = { - "apiVersion": "netkingdom.io/security-case-type/v0.1", - "kind": "SecurityCaseType", - "spec": { - "id": "t", - "version": "1.0", - "status": "acknowledged", - "parameters": [ - {"name": "client_id", "case_settable": True, "sensitivity": "operational"}, - {"name": "client_type", "case_settable": True, "sensitivity": "operational"}, - {"name": "consumer", "case_settable": True, "sensitivity": "operational"}, - {"name": "issuer", "case_settable": False, "default": "https://kc.example"}, - ], - "follow_through": [ - {"id": "proxy", "owner": "o", "entry_point": "ensure {client_id}", "auth": "caller", - "lane": "key-cape-oidc-login", "warden_executes": True, "attended": False, - "value_handling": "none"}, - {"id": "mint", "owner": "o", "entry_point": "mint {client_id}", "auth": "caller", - "lane": "key-cape-oidc-login", "warden_executes": True, "attended": False, - "value_handling": "generated_in_custody", "when": "client_type == confidential"}, - {"id": "login", "owner": "o", "entry_point": "apply {client_id} env KEYCAPE_{client_id:env}_X", - "auth": "operator", - "lane": "key-cape-oidc-login", "warden_executes": False, "attended": True, - "value_handling": "none", "attended_justification": "live registry is a Secret"}, - {"id": "handover", "owner": "{consumer}", "entry_point": "use {issuer}", "auth": "a", - "warden_executes": False, "attended": False, "value_handling": "none", - "ready_check": "{consumer} ready"}, - ], - "verification": [ - {"id": "v", "check": "GET {issuer} for {client_id}", "expect": "present", - "reads_secret_value": False}, - {"id": "vc", "check": "custody {client_id}", "expect": "deny", - "reads_secret_value": False, "when": "client_type == confidential"}, - ], - "rollback": [{"reverses": s} for s in ("proxy", "mint", "login", "handover")], - }, -} - -CASE = { - "apiVersion": "netkingdom.io/security-case/v0.1", - "kind": "SecurityCase", - "spec": { - "id": "c", - "type": "t@1.0", - "status": "materialized", - "acceptance": {"at": "2026-10-09", "record": "r"}, - "parameters": {"client_id": "forgejo", "client_type": "public", "consumer": "railiance-apps"}, - }, -} - - -@pytest.fixture(scope="module") -def catalog(): - return load_catalog() - - -def _case(**spec): - doc = copy.deepcopy(CASE) - doc["spec"].update(spec) - return doc - - -def _type(): - return copy.deepcopy(TYPE) - - -def test_plan_resolves_modes_params_and_defaults(catalog): - plan = plan_case(_case(), _type(), catalog) - modes = {s.id: s.mode for s in plan.steps} - assert modes == {"proxy": "warden", "login": "attended", "handover": "owner"} - assert plan.skipped == ["mint"] - assert plan.attended_sessions == ["key-cape-oidc-login"] - handover = next(s for s in plan.steps if s.id == "handover") - assert handover.owner == "railiance-apps" - assert handover.entry_point == "use https://kc.example" - assert handover.ready_check == "railiance-apps ready" - assert [v["id"] for v in plan.verification] == ["v"] - assert plan.verification[0]["check"] == "GET https://kc.example for forgejo" - - -def test_condition_true_includes_step_and_check(catalog): - params = dict(CASE["spec"]["parameters"], client_type="confidential") - plan = plan_case(_case(parameters=params), _type(), catalog) - assert "mint" in [s.id for s in plan.steps] and not plan.skipped - assert [v["id"] for v in plan.verification] == ["v", "vc"] - - -@pytest.mark.parametrize("status,needle", [ - ("proposed", "not accepted"), - ("accepted", "not materialized"), - ("live", "already live"), -]) -def test_refuse_case_not_materialized(catalog, status, needle): - with pytest.raises(CaseError, match=needle): - plan_case(_case(status=status), _type(), catalog) - - -def test_refuse_case_without_acceptance(catalog): - with pytest.raises(CaseError, match="acceptance"): - plan_case(_case(acceptance=None), _type(), catalog) - - -def test_refuse_type_mismatch_and_unacknowledged_type(catalog): - with pytest.raises(CaseError, match="type file is"): - plan_case(_case(type="t@2.0"), _type(), catalog) - doc = _type() - doc["spec"]["status"] = "proposed" - with pytest.raises(CaseError, match="not acknowledged"): - plan_case(_case(), doc, catalog) - - -def test_refuse_missing_catalog_lane(catalog): - doc = _type() - doc["spec"]["follow_through"][0]["lane"] = "no-such-lane" - with pytest.raises(CaseError, match="LANE-MISSING"): - plan_case(_case(), doc, catalog) - - -def test_refuse_unreviewed_catalog_lane(catalog): - draft = next((e.id for e in catalog.entries if not (e.is_active and e.reviewed)), None) - if draft is None: - pytest.skip("catalog has no inactive or unreviewed lane") - doc = _type() - doc["spec"]["follow_through"][0]["lane"] = draft - with pytest.raises(CaseError, match="LANE-UNREVIEWED"): - plan_case(_case(), doc, catalog) - - -def test_refuse_bad_parameters(catalog): - with pytest.raises(CaseError, match="does not declare"): - plan_case(_case(parameters=dict(CASE["spec"]["parameters"], extra="x")), _type(), catalog) - with pytest.raises(CaseError, match="fixes"): - plan_case(_case(parameters=dict(CASE["spec"]["parameters"], issuer="https://evil")), _type(), catalog) - params = {k: v for k, v in CASE["spec"]["parameters"].items() if k != "consumer"} - with pytest.raises(CaseError, match="not set by the case"): - plan_case(_case(parameters=params), _type(), catalog) - - -def test_refuse_secret_parameter(catalog): - doc = _type() - doc["spec"]["parameters"].append({"name": "pw", "case_settable": True, "sensitivity": "secret"}) - with pytest.raises(CaseError, match="secret-sensitivity"): - plan_case(_case(parameters=dict(CASE["spec"]["parameters"], pw="hunter2")), doc, catalog) - - -def test_env_format_spec_renders_like_keycape_client(catalog): - params = dict(CASE["spec"]["parameters"], client_id="my-app") - plan = plan_case(_case(parameters=params), _type(), catalog) - login = next(s for s in plan.steps if s.id == "login") - assert login.entry_point == "apply my-app env KEYCAPE_MY_APP_X" - - -@pytest.mark.parametrize("template", ["{client_id:upper}", "{client_id:>9}", "{client_id!r}"]) -def test_refuse_unknown_format_spec_or_conversion(catalog, template): - doc = _type() - doc["spec"]["follow_through"][0]["entry_point"] = template - with pytest.raises(CaseError, match="format spec|conversions"): - plan_case(_case(), doc, catalog) - - -def test_refuse_unsupported_condition(catalog): - doc = _type() - doc["spec"]["follow_through"][0]["when"] = "client_type in (a, b)" - with pytest.raises(CaseError, match="unsupported condition"): - plan_case(_case(), doc, catalog) - - -def test_cli_plan_shows_attended_and_exit_codes(tmp_path): - case_file, type_file = tmp_path / "case.yaml", tmp_path / "type.yaml" - case_file.write_text(yaml.safe_dump(CASE)) - type_file.write_text(yaml.safe_dump(TYPE)) - runner = CliRunner() - ok = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)]) - assert ok.exit_code == 0, ok.output - assert "ATTENDED" in ok.output and "nothing executed" in ok.output - import hashlib - digest = hashlib.sha256(case_file.read_bytes()).hexdigest() - assert f"case-ref: c@sha256:{digest}" in ok.output - case_file.write_text(yaml.safe_dump(_case(status="accepted"))) - refused = runner.invoke(app, ["case", "plan", str(case_file), "--type", str(type_file)]) - assert refused.exit_code == 1 - - -@pytest.mark.skipif(not LIVE_CASE.exists(), reason="net-kingdom checkout absent") -def test_live_forgejo_case_is_refused_until_materialized(): - result = CliRunner().invoke(app, ["case", "plan", str(LIVE_CASE)]) - assert result.exit_code == 1 diff --git a/tests/test_case_receipt_state.py b/tests/test_case_receipt_state.py deleted file mode 100644 index 74ee9ea..0000000 --- a/tests/test_case_receipt_state.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Offline prototype regression cases; no owner adapter, credential or live use.""" -from dataclasses import FrozenInstanceError, replace -import pytest -from warden.case_receipt_state import ( - Outcome, Pins, Refused, StepState, begin, begin_rollback, finish, - finish_rollback, recover, rollback_objects, -) - - -@pytest.fixture -def pins(): - return Pins(*["sha256:" + character * 64 for character in "abcdef"]) - - -@pytest.fixture -def state(pins): - return StepState("mint-secret", "railiance-platform", pins, ("case-policy", "case-role")) - - -def test_records_running_before_dispatch_without_mutating_history(state, pins): - running = begin(state, pins) - assert state.receipts == () - assert running.last.outcome is Outcome.RUNNING - with pytest.raises(FrozenInstanceError): - running.last.attempt = 8 - - -def test_changed_exact_revision_refuses_dispatch(state, pins): - with pytest.raises(Refused): - begin(state, replace(pins, adapter="sha256:" + "0" * 64)) - - -def test_lost_receipt_becomes_uncertain_and_cannot_retry(state, pins): - resumed = recover(begin(state, pins)) - assert resumed.last.outcome is Outcome.UNCERTAIN - assert len(resumed.receipts) == 2 - with pytest.raises(Refused): - begin(replace(resumed, retry_safe=True), pins, retry=True) - - -@pytest.mark.parametrize("outcome", [Outcome.SUCCESS, Outcome.NO_CHANGE]) -def test_verified_success_or_no_change_never_replayed(state, pins, outcome): - done = finish(begin(state, pins), pins, outcome) - with pytest.raises(Refused): - begin(done, pins, retry=True) - - -def test_retry_requires_verified_no_change_failure_and_safe_adapter(state, pins): - failed = finish(begin(state, pins), pins, Outcome.FAILED_BEFORE) - with pytest.raises(Refused): - begin(failed, pins, retry=True) - safe = replace(failed, retry_safe=True) - with pytest.raises(Refused): - begin(safe, pins) - retry = begin(safe, pins, retry=True) - assert retry.last.outcome is Outcome.RUNNING - assert len(retry.receipts) == 3 - - -def test_failure_after_change_does_not_become_safe_retry_or_rollback(state, pins): - failed = finish(begin(state, pins), pins, Outcome.FAILED_AFTER, created=("case-policy",)) - with pytest.raises(Refused): - begin(replace(failed, retry_safe=True), pins, retry=True) - with pytest.raises(Refused): - rollback_objects(failed, pins) - - -def test_rollback_only_created_not_all_reviewed_objects(state, pins): - done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",)) - assert rollback_objects(done, pins) == ("case-role",) - assert "case-policy" not in rollback_objects(done, pins) - undo = begin_rollback(done, pins) - verified = finish_rollback(undo, pins, verified=True) - assert verified.last.outcome is Outcome.ROLLED_BACK - with pytest.raises(Refused): - begin_rollback(verified, pins) - - -def test_rollback_reverse_order_and_uncertain_undo_blocks_replay(state, pins): - done = finish(begin(state, pins), pins, Outcome.SUCCESS, - created=("case-policy", "case-role")) - assert rollback_objects(done, pins) == ("case-role", "case-policy") - uncertain = recover(begin_rollback(done, pins)) - assert uncertain.last.outcome is Outcome.ROLLBACK_UNCERTAIN - with pytest.raises(Refused): - begin_rollback(uncertain, pins) - - -def test_unverified_rollback_cannot_claim_success(state, pins): - done = finish(begin(state, pins), pins, Outcome.SUCCESS, created=("case-role",)) - failed = finish_rollback(begin_rollback(done, pins), pins, verified=False) - assert failed.last.outcome is Outcome.ROLLBACK_UNCERTAIN - - -def test_refuses_shared_objects_or_false_no_change_creation(state, pins): - running = begin(state, pins) - for outcome, created in [(Outcome.SUCCESS, ("shared-group",)), - (Outcome.NO_CHANGE, ("case-policy",)), - (Outcome.FAILED_BEFORE, ("case-policy",)), - (Outcome.SUCCESS, ("case-policy", "case-policy"))]: - with pytest.raises(Refused): - finish(running, pins, outcome, created=created) - - -def test_no_raw_payload_or_output_receipt_interface(state, pins): - with pytest.raises(TypeError): - finish(begin(state, pins), pins, Outcome.SUCCESS, output="sensitive output") - with pytest.raises(Refused): - StepState("step", "owner", pins, ("path with spaces or output",)) - with pytest.raises(Refused): - Pins("not-a-digest", pins.case_type, pins.adapter, pins.catalog, pins.acceptance, pins.run_sheet) - - -def test_out_of_order_result_and_noop_recovery(state, pins): - assert recover(state) is state - with pytest.raises(Refused): - finish(state, pins, Outcome.SUCCESS) - with pytest.raises(Refused): - finish_rollback(state, pins, verified=True) - - -def test_result_must_be_closed_enum_not_string(state, pins): - with pytest.raises(Refused): - finish(begin(state, pins), pins, "verified-success") diff --git a/tests/test_case_run.py b/tests/test_case_run.py deleted file mode 100644 index b7664a5..0000000 --- a/tests/test_case_run.py +++ /dev/null @@ -1,745 +0,0 @@ -"""Injected owner harness; these receipts are not native owner attestations.""" - -from dataclasses import asdict, replace -import json -import os -import subprocess -import sys -import sqlite3 -import pytest -import yaml -from warden.case_receipt_state import Outcome, Refused -from warden.case_run import Adapter, Descriptor, Executor, Journal, OwnerResult, digest - - -def lane_entry(lane="issuer-lane", owner="owner", **over): - """Fixture catalog entry (TEST ONLY; never a real routing pointer).""" - entry = { - "id": lane, - "title": "fixture lane", - "need_keywords": [lane], - "owner_repo": owner, - "subsystem": "fixture", - "warden_executes": False, - "wiki_ref": "wiki/fixture.md", - "canon_ref": "fixture/canon.md", - "reviewed": "2026-10-17", - "status": "active", - "risk": "high", - "workload_ref": {"applicability": "not-applicable", "reason": "fixture"}, - "delegation": { - "mode": "native", - "intended_owner": owner, - "reviewed": "2026-10-17", - }, - } - entry.update(over) - return entry - - -def fixture_catalog(*entries): - return yaml.safe_dump({"entries": list(entries) or [lane_entry()]}).encode() - - -CATALOG = fixture_catalog() - -MINT_STEP = { - "id": "mint-secret", - "owner": "owner", - "lane": "issuer-lane", - "warden_executes": True, - "attended": False, -} - - -def make_rig(tmp_path): - desc = Descriptor( - "warden.case.execution.v1", - "mint-secret", - "owner", - "owner/mint@v1", - "sha256:" + "1" * 64, - "human", - "issuer-lane", - "client_id", - "mint", - ("case-policy", "case-role"), - True, - ("parent-session", "broker-grant"), - ) - descriptor = json.dumps(asdict(desc)).encode() - typ = yaml.safe_dump( - { - "spec": { - "id": "test", - "version": "1", - "status": "acknowledged", - "parameters": [ - { - "name": "client_id", - "sensitivity": "operational", - "case_settable": True, - } - ], - "execution_descriptors": {"mint-secret": digest(descriptor)}, - "follow_through": [dict(MINT_STEP)], - } - } - ).encode() - case = yaml.safe_dump( - { - "spec": { - "id": "case-test", - "type": "test@1", - "status": "materialized", - "acceptance": "acceptance", - "parameters": {"client_id": "fixture-client"}, - } - } - ).encode() - calls = [] - - def invoke(inv): - calls.append(inv) - return bound( - inv, - OwnerResult( - Outcome.SUCCESS, - "owner-receipt", - ("case-policy", "case-role"), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - ) - - adapter = Adapter( - "owner/mint@v1", - desc.adapter_source, - invoke, - lambda inv, result: result.evidence == "owner-receipt", - lambda inv, objects: bound( - inv, - OwnerResult( - Outcome.ROLLED_BACK, - "owner-receipt", - (), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - ), - lambda inv: bound( - inv, - OwnerResult( - Outcome.NO_CHANGE, - "owner-receipt", - (), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - ), - ) - verified = { - ("case-acceptance", digest(case), "acceptance"), - ("ops-mason-materialization", digest(case), "materialization"), - ("owner-descriptor-acceptance:owner", digest(descriptor), "descriptor"), - ("type-machine-seam-acceptance", digest(typ), "descriptor"), - ("catalog-routing-acceptance", digest(CATALOG), "descriptor"), - ("caller-session:human:operator:issuer-lane", digest(case), "session"), - ("session-revocation:issuer-lane", digest(case), "session-revoked"), - ("grant-revocation:issuer-lane", digest(case), "grant-revoked"), - } - - def verifier(purpose, source, evidence): - if purpose.startswith(("session-revocation:", "grant-revocation:")): - scope, lane, operation, attempt = purpose.split(":") - return ( - source == digest(case) - and lane == "issuer-lane" - and operation in ("run", "rollback", "reconcile") - and evidence - == ("session-" if scope == "session-revocation" else "grant-") + attempt - ) - return (purpose, source, evidence) in verified - - executor = Executor( - Journal(tmp_path / "private"), {"owner/mint@v1": adapter}, verifier - ) - kwargs = dict( - principal="operator", - actor="human", - lane="issuer-lane", - acceptance_evidence="acceptance", - materialization_evidence="materialization", - descriptor_evidence="descriptor", - session_evidence="session", - ) - admitted = executor.admit(case, typ, descriptor, CATALOG, **kwargs) - return (executor, admitted, calls, (case, typ, descriptor, CATALOG), kwargs) - - -@pytest.fixture -def rig(tmp_path): - return make_rig(tmp_path) - - -def test_commit_running_is_visible_from_separate_connection_before_dispatch(rig): - executor, args, calls, _, _ = rig - old = executor.adapters[args[0].adapter] - - def inspect(inv): - other = sqlite3.connect(executor.journal.directory / "journal.sqlite") - payload = json.loads( - other.execute( - "SELECT payload FROM receipts ORDER BY seq DESC LIMIT 1" - ).fetchone()[0] - ) - assert payload["receipts"][-1]["outcome"] == "running" - return old.invoke(inv) - - executor.adapters[old.reference] = replace(old, invoke=inspect) - assert executor.run(*args).last.outcome is Outcome.SUCCESS - with pytest.raises(Refused): - executor.run(*args) - assert len(calls) == 1 - - -def test_crash_and_fresh_process_resume_never_duplicates_effect(rig): - executor, args, calls, sources, kwargs = rig - old = executor.adapters[args[0].adapter] - - def crash(inv): - calls.append(inv) - raise KeyboardInterrupt - - executor.adapters[old.reference] = replace(old, invoke=crash) - with pytest.raises(KeyboardInterrupt): - executor.run(*args) - fresh = Executor( - Journal(executor.journal.directory), - {old.reference: old}, - executor.evidence_verifier, - ) - admitted = fresh.admit(*sources, **kwargs) - with pytest.raises(Refused): - fresh.run(*admitted, retry=True) - state = fresh.journal.load("mint-secret:fixture-client", admitted[2]) - assert state.last.outcome is Outcome.UNCERTAIN - assert len(calls) == 1 - - -def test_lost_result_preserved_and_cas_reconciliation_does_not_mint(rig): - executor, args, calls, _, _ = rig - old = executor.adapters[args[0].adapter] - - def lost(inv): - calls.append(inv) - raise TimeoutError("SECRET-HIDDEN") - - executor.adapters[old.reference] = replace(old, invoke=lost) - assert executor.run(*args).last.outcome is Outcome.UNCERTAIN - assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE - with pytest.raises(Refused): - executor.run(*args, retry=True) - assert len(calls) == 1 - assert ( - b"SECRET-HIDDEN" - not in (executor.journal.directory / "journal.sqlite").read_bytes() - ) - - -def test_failed_before_change_requires_explicit_retry_and_safe_owner(rig): - executor, args, calls, _, _ = rig - old = executor.adapters[args[0].adapter] - - def no_change(inv): - calls.append(inv) - return bound( - inv, - OwnerResult( - Outcome.FAILED_BEFORE, - "owner-receipt", - (), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - ) - - executor.adapters[old.reference] = replace(old, invoke=no_change) - assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE - with pytest.raises(Refused): - executor.run(*args) - assert executor.run(*args, retry=True).last.outcome is Outcome.FAILED_BEFORE - assert len(calls) == 2 - - -@pytest.mark.parametrize( - "result", - [ - OwnerResult( - Outcome.SUCCESS, - "owner-receipt", - ("shared-policy",), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - OwnerResult( - Outcome.SUCCESS, - "owner-receipt", - (), - False, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - OwnerResult( - Outcome.SUCCESS, - "unknown-receipt", - (), - True, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - {"outcome": "verified-success", "stdout": "SECRET-HIDDEN"}, - ], -) -def test_unknown_secret_payload_created_scope_or_missing_revocation_never_succeeds( - rig, result, capsys -): - executor, args, _, _, _ = rig - old = executor.adapters[args[0].adapter] - - def malicious(inv): - print("SECRET-HIDDEN" * 1000) - return result - - executor.adapters[old.reference] = replace(old, invoke=malicious) - assert executor.run(*args).last.outcome is Outcome.UNCERTAIN - assert capsys.readouterr().out == "" - assert ( - b"SECRET-HIDDEN" - not in (executor.journal.directory / "journal.sqlite").read_bytes() - ) - - -def test_rollback_reverses_only_verified_case_created_objects(rig): - executor, args, _, _, _ = rig - old = executor.adapters[args[0].adapter] - undos = [] - - def undo(inv, objects): - undos.append(objects) - return old.rollback(inv, objects) - - executor.adapters[old.reference] = replace(old, rollback=undo) - executor.run(*args) - assert executor.rollback(*args).last.outcome is Outcome.ROLLED_BACK - assert undos == [("case-role", "case-policy")] - assert "shared-policy" not in undos[0] - - -def test_uncertain_rollback_cannot_replay(rig): - executor, args, _, _, _ = rig - old = executor.adapters[args[0].adapter] - executor.run(*args) - executor.adapters[old.reference] = replace( - old, - rollback=lambda inv, objs: bound( - inv, - OwnerResult( - Outcome.ROLLED_BACK, - "owner-receipt", - (), - False, - session_revocation_evidence="session-revoked", - grant_revocation_evidence="grant-revoked", - ), - ), - ) - assert executor.rollback(*args).last.outcome is Outcome.ROLLBACK_UNCERTAIN - with pytest.raises(Refused): - executor.rollback(*args) - - -@pytest.mark.parametrize( - "change", ["materialization", "type-prose", "actor", "lane", "source", "target"] -) -def test_exact_admission_requires_owner_evidence_and_closed_source_pins(rig, change): - executor, _, _, sources, kwargs = rig - case, typ, descriptor, catalog = sources - kwargs = dict(kwargs) - if change == "materialization": - kwargs["materialization_evidence"] = "bare-json" - if change == "type-prose": - obj = yaml.safe_load(typ) - obj["spec"].pop("execution_descriptors") - typ = yaml.safe_dump(obj).encode() - if change == "actor": - kwargs["actor"] = "agent" - if change == "lane": - kwargs["lane"] = "other-lane" - if change == "source": - obj = json.loads(descriptor) - obj["adapter_source"] = "sha256:" + "2" * 64 - descriptor = json.dumps(obj).encode() - if change == "target": - obj = yaml.safe_load(case) - obj["spec"]["parameters"]["client_id"] = "other-target" - case = yaml.safe_dump(obj).encode() - with pytest.raises(Refused): - executor.admit(case, typ, descriptor, catalog, **kwargs) - - -def test_agent_env_cannot_dispatch_after_human_admission(rig, monkeypatch): - executor, args, calls, _, _ = rig - monkeypatch.setenv("WARDEN_AGENT_ID", "test-agent") - with pytest.raises(Refused): - executor.run(*args) - assert calls == [] - - -def test_forged_invocation_cannot_bypass_admission(rig): - executor, args, calls, _, _ = rig - with pytest.raises(Refused): - executor.run(args[0], replace(args[1], target="other-target"), args[2]) - assert calls == [] - - -def test_journal_rejects_unsafe_directory_and_symlink(tmp_path): - unsafe = tmp_path / "unsafe" - unsafe.mkdir(mode=493) - with pytest.raises(Refused): - Journal(unsafe) - private = tmp_path / "private" - private.mkdir(mode=448) - (private / "journal.sqlite").symlink_to(tmp_path / "outside") - with pytest.raises(OSError): - Journal(private) - - -def test_process_lock_blocks_parallel_dispatch(rig): - executor, args, calls, _, _ = rig - with executor.journal.locked(): - with pytest.raises(Refused): - executor.run(*args) - assert calls == [] - - -def test_changed_catalog_pins_cannot_inherit_old_success(rig): - executor, args, calls, sources, kwargs = rig - executor.run(*args) - with pytest.raises(Refused): - executor.admit(*sources[:3], fixture_catalog(lane_entry(title="changed")), **kwargs) - assert len(calls) == 1 - - -def test_private_journal_does_not_retain_owner_payload_or_accept_unknown_schema(rig): - executor, args, _, _, _ = rig - executor.run(*args) - row = executor.journal.db.execute( - "SELECT seq,payload FROM receipts ORDER BY seq DESC LIMIT 1" - ).fetchone() - payload = json.loads(row[1]) - assert payload["journal_schema"] == "warden.case.step-journal.v1" - assert payload["evidence_digest"] == digest(b"owner-receipt") - assert payload["session_revoked"] is True - assert "owner-receipt" not in row[1] - payload["stdout"] = "UNTRUSTED-OUTPUT" - executor.journal.db.execute( - "UPDATE receipts SET payload=? WHERE seq=?", (json.dumps(payload), row[0]) - ) - with pytest.raises(Refused): - executor.run(*args) - - -def test_expired_caller_session_cannot_use_cached_admission(rig): - executor, args, calls, _, _ = rig - old = executor.evidence_verifier - executor.evidence_verifier = lambda purpose, source, evidence: ( - False - if purpose.startswith("caller-session:") - else old(purpose, source, evidence) - ) - with pytest.raises(Refused): - executor.run(*args) - assert calls == [] - - -@pytest.mark.parametrize("index", [0, 1, 2, 3]) -def test_source_sizes_are_bounded_before_parsing(rig, index): - executor, _, _, sources, kwargs = rig - oversized = list(sources) - oversized[index] = b"x" * 262145 - with pytest.raises(Refused, match="bounded"): - executor.admit(*oversized, **kwargs) - - -def test_owner_fd_and_inherited_subprocess_output_is_contained(rig, capfd): - executor, args, _, _, _ = rig - old = executor.adapters[args[0].adapter] - - def leaking(inv): - os.write(1, b"SYNTHETIC-SECRET") - os.write(2, b"SYNTHETIC-SECRET") - subprocess.run([sys.executable, "-c", 'print("SYNTHETIC-SECRET")'], check=True) - return old.invoke(inv) - - executor.adapters[old.reference] = replace(old, invoke=leaking) - assert executor.run(*args).last.outcome is Outcome.SUCCESS - observed = capfd.readouterr() - assert "SYNTHETIC-SECRET" not in observed.out + observed.err - - -def test_evidence_provider_output_and_error_never_escape(rig, capfd): - executor, _, calls, sources, kwargs = rig - - def broken(*args): - print("SYNTHETIC-SECRET") - os.write(2, b"SYNTHETIC-SECRET") - raise RuntimeError("SYNTHETIC-SECRET") - - executor.evidence_verifier = broken - with pytest.raises(Refused) as caught: - executor.admit(*sources, **kwargs) - assert "SYNTHETIC-SECRET" not in str(caught.value) - observed = capfd.readouterr() - assert "SYNTHETIC-SECRET" not in observed.out + observed.err - assert calls == [] - - -@pytest.mark.parametrize("missing", ["session", "grant"]) -def test_broker_grant_and_parent_session_have_separate_required_evidence(rig, missing): - executor, args, _, _, _ = rig - old = executor.adapters[args[0].adapter] - result = old.invoke(args[1]) - if missing == "session": - result = replace(result, session_revocation_evidence=None) - else: - result = replace(result, grant_revocation_evidence=None) - executor.adapters[old.reference] = replace(old, invoke=lambda inv: result) - assert executor.run(*args).last.outcome is Outcome.UNCERTAIN - - -def test_mint_descriptor_cannot_omit_broker_grant_scope(rig): - descriptor = json.loads(rig[3][2]) - descriptor["revocation_scopes"] = ["parent-session"] - with pytest.raises(Refused): - Descriptor.decode(json.dumps(descriptor).encode()) - - -def test_unsafe_owner_step_cannot_retry_even_after_verified_failed_before( - rig, tmp_path -): - executor, args, _, sources, kwargs = rig - descriptor = replace(args[0], retry_safe=False) - descriptor_source = json.dumps(asdict(descriptor)).encode() - type_doc = yaml.safe_load(sources[1]) - type_doc["spec"]["execution_descriptors"][descriptor.step] = digest( - descriptor_source - ) - type_source = yaml.safe_dump(type_doc).encode() - old_verifier = executor.evidence_verifier - - def verifier(purpose, source, evidence): - if purpose == "owner-descriptor-acceptance:owner": - return source == digest(descriptor_source) and evidence == "descriptor" - if purpose == "type-machine-seam-acceptance": - return source == digest(type_source) and evidence == "descriptor" - return old_verifier(purpose, source, evidence) - - adapter = executor.adapters[descriptor.adapter] - - def failed(inv): - return bound( - inv, - OwnerResult( - Outcome.FAILED_BEFORE, - "owner-receipt", - (), - True, - "session-revoked", - "grant-revoked", - ), - ) - - fresh = Executor( - Journal(tmp_path / "unsafe-retry"), - {adapter.reference: replace(adapter, invoke=failed)}, - verifier, - ) - admitted = fresh.admit( - sources[0], type_source, descriptor_source, sources[3], **kwargs - ) - assert fresh.run(*admitted).last.outcome is Outcome.FAILED_BEFORE - with pytest.raises(Refused): - fresh.run(*admitted, retry=True) - - -def bound(inv, result): - return replace( - result, - attempt_id=inv.attempt_id, - operation=inv.operation, - session_revocation_evidence=("session-" + inv.attempt_id) - if result.session_revocation_evidence - else None, - grant_revocation_evidence=("grant-" + inv.attempt_id) - if result.grant_revocation_evidence - else None, - ) - - -def test_old_grant_and_session_proofs_cannot_verify_a_new_retry(rig): - executor, args, _, _, _ = rig - adapter = executor.adapters[args[0].adapter] - retained = [] - - def attempts(inv): - if not retained: - result = bound( - inv, - OwnerResult( - Outcome.FAILED_BEFORE, - "owner-receipt", - (), - True, - "session-revoked", - "grant-revoked", - ), - ) - retained.append(result) - return result - # Even a caller rewriting labels cannot authenticate prior revocation - # evidence for the newly persisted attempt identity. - return replace(retained[0], attempt_id=inv.attempt_id, operation=inv.operation) - - executor.adapters[adapter.reference] = replace(adapter, invoke=attempts) - assert executor.run(*args).last.outcome is Outcome.FAILED_BEFORE - assert executor.run(*args, retry=True).last.outcome is Outcome.UNCERTAIN - - -def test_owner_interrupt_payload_is_sanitized_with_running_intent_retained(rig, capfd): - executor, args, _, _, _ = rig - adapter = executor.adapters[args[0].adapter] - - def interrupted(inv): - os.write(2, b"SYNTHETIC-SECRET") - raise KeyboardInterrupt("SYNTHETIC-SECRET") - - executor.adapters[adapter.reference] = replace(adapter, invoke=interrupted) - with pytest.raises(KeyboardInterrupt) as caught: - executor.run(*args) - assert "SYNTHETIC-SECRET" not in str(caught.value) - assert "SYNTHETIC-SECRET" not in "".join(capfd.readouterr()) - assert ( - executor.journal.load("mint-secret:fixture-client", args[2]).last.outcome - is Outcome.RUNNING - ) - - -def test_attempt_identity_changes_with_exact_catalog_revision_and_new_journal( - rig, tmp_path -): - executor, args, calls, sources, kwargs = rig - executor.run(*args) - original_attempt = calls[-1].attempt_id - new_catalog = fixture_catalog(lane_entry(title="independently reviewed v2")) - old_verifier = executor.evidence_verifier - - def verifier(purpose, source, evidence): - if purpose == "catalog-routing-acceptance": - return source == digest(new_catalog) and evidence == "descriptor" - return old_verifier(purpose, source, evidence) - - fresh = Executor(Journal(tmp_path / "new-revision"), executor.adapters, verifier) - revised = fresh.admit(*sources[:3], new_catalog, **kwargs) - assert fresh.run(*revised).last.outcome is Outcome.SUCCESS - assert calls[-1].attempt_id != original_attempt - assert calls[-1].pins.catalog != calls[0].pins.catalog - - -def test_secret_sensitivity_parameter_cannot_reach_admission_or_journal(rig): - executor, _, _, sources, kwargs = rig - type_doc = yaml.safe_load(sources[1]) - type_doc["spec"]["parameters"][0]["sensitivity"] = "secret" - source = yaml.safe_dump(type_doc).encode() - old = executor.evidence_verifier - executor.evidence_verifier = lambda purpose, digest_value, evidence: ( - True - if purpose == "type-machine-seam-acceptance" - else old(purpose, digest_value, evidence) - ) - with pytest.raises(Refused, match="secret-sensitivity"): - executor.admit(sources[0], source, sources[2], sources[3], **kwargs) - - -def _lose_result(executor, args, calls): - old = executor.adapters[args[0].adapter] - - def lost(inv): - calls.append(inv) - raise TimeoutError("lost") - - seen = [] - - def by_original(ctx): - seen.append(ctx) - return old.reconcile(ctx.attempt) - - executor.adapters[old.reference] = replace( - old, invoke=lost, reconcile=None, reconcile_original=by_original - ) - assert executor.run(*args).last.outcome is Outcome.UNCERTAIN - return seen - - -def test_original_intent_survives_restart_separate_from_reconcile_attempt(rig): - executor, args, calls, _, _ = rig - seen = _lose_result(executor, args, calls) - executor.journal = Journal(executor.journal.directory) # restart: fresh handle - assert executor.reconcile(*args).last.outcome is Outcome.NO_CHANGE - (ctx,) = seen - original = ctx.original.invocation - assert original.attempt_id == calls[0].attempt_id - assert original.operation == "run" and ctx.attempt.operation == "reconcile" - assert ctx.attempt.attempt_id != original.attempt_id - assert original.pins == args[1].pins - assert (original.principal, original.tenant, original.realm) == ( - args[1].principal, - args[1].tenant, - args[1].realm, - ) - assert ctx.original.source == executor.adapters[args[0].adapter].source - - -def test_missing_original_intent_refuses_and_stays_uncertain(rig): - executor, args, calls, _, _ = rig - seen = _lose_result(executor, args, calls) - executor.journal.db.execute("DELETE FROM intents") - with pytest.raises(Refused, match="original intent record missing"): - executor.reconcile(*args) - assert seen == [] - binding = f"{args[1].step}:{args[1].target}" - assert executor.journal.load(binding, args[2]).last.outcome is Outcome.UNCERTAIN - - -@pytest.mark.parametrize( - "change", [{"tenant": "other-tenant"}, {"realm": "other-realm"}, {"lane": "x"}] -) -def test_original_intent_mismatch_refuses(rig, change): - executor, args, calls, _, _ = rig - seen = _lose_result(executor, args, calls) - with pytest.raises(Refused): - executor.reconcile(args[0], replace(args[1], **change), args[2]) - assert seen == [] - - -def test_original_intent_source_mismatch_refuses(rig): - executor, args, calls, _, _ = rig - _lose_result(executor, args, calls) - binding = f"{args[1].step}:{args[1].target}" - with pytest.raises(Refused, match="source mismatch"): - executor.journal.original_intent(binding, calls[0], "other-source") diff --git a/tests/test_case_type.py b/tests/test_case_type.py deleted file mode 100644 index 2ca08b8..0000000 --- a/tests/test_case_type.py +++ /dev/null @@ -1,130 +0,0 @@ -"""WARDEN-WP-0044-T01: SecurityCaseType follow-through check.""" -from __future__ import annotations - -import copy -import os -from pathlib import Path - -import pytest -import yaml -from typer.testing import CliRunner - -from warden.case_type import check_follow_through, load_type -from warden.cli import app -from warden.routing.catalog import load_catalog - -LIVE_TYPE = Path(os.environ.get("WARDEN_TEST_NETKINGDOM_ROOT", - Path(__file__).resolve().parents[2] / "net-kingdom")) / "security-case-types/oidc-browser-relying-party/type.yaml" - -BASE = { - "apiVersion": "netkingdom.io/security-case-type/v0.1", - "kind": "SecurityCaseType", - "spec": { - "id": "t", - "version": "1.0", - "follow_through": [ - {"id": "run", "owner": "o", "entry_point": "e", "auth": "a", "lane": "key-cape-oidc-login", - "warden_executes": True, "attended": False, "value_handling": "none"}, - {"id": "login", "owner": "o", "entry_point": "e", "auth": "a", "lane": "key-cape-oidc-login", - "warden_executes": False, "attended": True, "value_handling": "none", - "attended_justification": "live registry is a Secret"}, - {"id": "handover", "owner": "o", "entry_point": "e", "auth": "a", - "warden_executes": False, "attended": False, "value_handling": "none"}, - ], - "verification": [{"id": "v", "check": "c", "expect": "present", "reads_secret_value": False}], - "rollback": [{"reverses": "run"}, {"reverses": "login"}, {"reverses": "handover"}], - }, -} - - -@pytest.fixture(scope="module") -def catalog(): - return load_catalog() - - -def _doc(**step_overrides): - doc = copy.deepcopy(BASE) - doc["spec"]["follow_through"][0].update(step_overrides) - return doc - - -def _codes(report): - return {f.code for f in report.findings} - - -def test_conforming_type_is_plannable(catalog): - report = check_follow_through(_doc(), catalog) - assert report.plannable, report.findings - assert report.warden_steps == ["run"] - assert report.attended_lanes == ["key-cape-oidc-login"] - - -def test_executed_step_without_lane_refused(catalog): - doc = _doc() - del doc["spec"]["follow_through"][0]["lane"] - assert "FT-LANE" in _codes(check_follow_through(doc, catalog)) - - -def test_unknown_lane_is_catalog_finding_not_schema(catalog): - report = check_follow_through(_doc(lane="no-such-lane"), catalog) - assert report.schema_ok and not report.plannable - assert _codes(report) == {"LANE-MISSING"} - - -def test_value_handling_required_and_enumerated(catalog): - assert "FT-VALUE" in _codes(check_follow_through(_doc(value_handling="returned"), catalog)) - doc = _doc() - del doc["spec"]["follow_through"][0]["value_handling"] - assert "FT-FIELD" in _codes(check_follow_through(doc, catalog)) - - -def test_attended_needs_justification_and_excludes_execution(catalog): - doc = copy.deepcopy(BASE) - del doc["spec"]["follow_through"][1]["attended_justification"] - doc["spec"]["follow_through"][1]["warden_executes"] = True - msgs = [f.message for f in check_follow_through(doc, catalog).findings if f.code == "FT-ATTEND"] - assert len(msgs) == 2 - - -def test_attended_step_needs_login_lane(catalog): - doc = copy.deepcopy(BASE) - secret_lane = next(e.id for e in catalog.entries if e.is_active and e.reviewed and e.lane == "secret") - doc["spec"]["follow_through"][1]["lane"] = secret_lane - assert "LANE-KIND" in _codes(check_follow_through(doc, catalog)) - - -def test_generated_value_never_in_attended_session(catalog): - doc = copy.deepcopy(BASE) - doc["spec"]["follow_through"][1]["value_handling"] = "generated_in_custody" - assert "FT-VALUE" in _codes(check_follow_through(doc, catalog)) - - -def test_verification_must_not_read_values(catalog): - doc = _doc() - doc["spec"]["verification"][0]["reads_secret_value"] = True - assert "VER-VALUE" in _codes(check_follow_through(doc, catalog)) - - -def test_every_step_reversible(catalog): - doc = _doc() - doc["spec"]["rollback"] = doc["spec"]["rollback"][1:] - assert "RB-MISSING" in _codes(check_follow_through(doc, catalog)) - - -def test_cli_exit_codes(tmp_path): - good, bad = tmp_path / "good.yaml", tmp_path / "bad.yaml" - good.write_text(yaml.safe_dump(_doc())) - bad.write_text(yaml.safe_dump(_doc(lane="no-such-lane"))) - runner = CliRunner() - assert runner.invoke(app, ["case", "check-type", str(good), "--json"]).exit_code == 0 - assert runner.invoke(app, ["case", "check-type", str(bad), "--json"]).exit_code == 1 - - -@pytest.mark.skipif(not LIVE_TYPE.exists(), reason="net-kingdom checkout not present") -def test_live_oidc_rp_type_schema_conforms(catalog): - """The acknowledged 1.0 type is plannable: every lane it names is active (2026-10-10).""" - report = check_follow_through(load_type(LIVE_TYPE), catalog) - assert report.schema_ok, report.findings - assert report.plannable, report.findings - assert report.attended_lanes == ["key-cape-oidc-login"] - assert report.warden_steps == ["mint-secret"] diff --git a/tests/test_flex_auth_registry.py b/tests/test_flex_auth_registry.py index 70dd26f..c329d27 100644 --- a/tests/test_flex_auth_registry.py +++ b/tests/test_flex_auth_registry.py @@ -81,86 +81,3 @@ def test_compiler_joins_explicit_workload_reference_to_resolved_zone(tmp_path): assert attrs["security_zone"] == "z2-continuity" assert attrs["security_zone_admission"] == "satisfied" assert attrs["security_zone_revision"] == "sha256:zone-revision" - - -# --- manifest ceiling coverage (FLEX-DEC-2026-012) ---------------------------- -# -# flex-auth fixed enrichment so registry facts beat caller-supplied ones, then -# asked every consumer to confirm the ceiling keys are actually declared: the -# fix wins only where the registry HAS a value. These assert the two ways a -# ceiling gets handed back to the caller. - -import importlib.util # noqa: E402 - -import pytest # noqa: E402 - -_REPO = ROOT -_spec = importlib.util.spec_from_file_location( - "check_flex_auth_manifest_coverage", - _REPO / "scripts" / "check_flex_auth_manifest_coverage.py", -) -_coverage = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(_coverage) -REQUIRED_CEILING_KEYS = _coverage.REQUIRED_CEILING_KEYS -audit = _coverage.audit - -_build_spec = importlib.util.spec_from_file_location("build_flex_auth_registry", SCRIPT) -_builder = importlib.util.module_from_spec(_build_spec) -_build_spec.loader.exec_module(_builder) -build_registry = _builder.build_registry - - -def _repo_inventory_and_registry(): - inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text()) - registry = json.loads( - (_REPO / "registry" / "flex-auth" / "production_registry_snapshot.json").read_text() - ) - return inventory, registry - - -def test_shipped_snapshot_declares_every_ceiling_key(): - """The confirmation flex-auth asked for, as a test rather than a reading.""" - report = audit(*_repo_inventory_and_registry()) - assert report["unregistered_actors"] == [] - assert report["resources_missing_keys"] == [] - assert report["covered"] is True - - -def test_actor_with_no_manifest_resource_is_a_coverage_gap(): - """`warden sign` names ssh-cert:actor/ whether or not the snapshot was rebuilt.""" - inventory, registry = _repo_inventory_and_registry() - inventory["actors"]["adm-added-after-the-last-build"] = { - "type": "adm", "principals": ["adm-full"], "ttl_hours": 48, - } - report = audit(inventory, registry) - assert report["unregistered_actors"] == ["adm-added-after-the-last-build"] - assert report["covered"] is False - - -@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS) -def test_dropping_any_single_ceiling_key_is_a_coverage_gap(key): - inventory, registry = _repo_inventory_and_registry() - resource = registry["resource_manifests"][0]["resources"][0] - resource["attributes"].pop(key) - report = audit(inventory, registry) - assert report["resources_missing_keys"] == [ - {"resource": resource["id"], "missing": [key]} - ] - assert report["covered"] is False - - -@pytest.mark.parametrize("key", REQUIRED_CEILING_KEYS) -def test_a_null_ceiling_is_not_a_declaration(key): - """flex-auth overlays a registry value only where one exists.""" - inventory, registry = _repo_inventory_and_registry() - resource = registry["resource_manifests"][0]["resources"][0] - resource["attributes"][key] = None - report = audit(inventory, registry) - assert report["covered"] is False - - -def test_builder_output_is_covered_by_construction(tmp_path): - """Every resource the builder emits carries the full ceiling set.""" - inventory = yaml.safe_load((_REPO / "examples" / "inventory.seed.yaml").read_text()) - report = audit(inventory, build_registry(inventory)) - assert report["covered"] is True diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py deleted file mode 100644 index 64ea533..0000000 --- a/tests/test_layer_conformance.py +++ /dev/null @@ -1,355 +0,0 @@ -"""Layer-model conformance (security-layer-model_v0.8 §5, §11). - -Two things are checked here. §11 makes one of them mechanical: every direct -Tooling client maps to a declared shape. §5.2 asks for the other: the conduit's -supplied-authority property covered by a test. - -Deliberately absent: any assertion on a §5.3 review date. A date-triggered -failure breaks the build on a calendar day with no code change, punishing -whoever commits next rather than whoever owns the gap — the same reasoning -recorded in WARDEN-WP-0033-T05 for blocker staleness. -""" -from __future__ import annotations - -import os -import subprocess -import sys -from pathlib import Path - -import yaml - -ROOT = Path(__file__).resolve().parents[1] - - -def _decl() -> dict: - return yaml.safe_load((ROOT / "layer.yaml").read_text()) - - -def _intent_frontmatter() -> dict: - lines = (ROOT / "INTENT.md").read_text().splitlines() - assert lines[0].strip() == "---", "INTENT.md must carry frontmatter — it is the declaration" - end = next(i for i, ln in enumerate(lines[1:], 1) if ln.strip() == "---") - return yaml.safe_load("\n".join(lines[1:end])) - - -def _checker(): - import importlib.util - - spec = importlib.util.spec_from_file_location( - "check_layer_conformance", ROOT / "scripts" / "check_layer_conformance.py" - ) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def _fold(value: str) -> str: - return str(value).strip().encode("ascii", "ignore").decode().lower() - - -class TestDeclaration: - def test_declares_staff_layer_in_its_own_voice(self): - d = _decl() - assert d["repository"] == "ops-warden" - assert _fold(d["layer"]) == "staff" - # §11: "only the repository's own file, in its own voice, conforms." - assert d["declared_by"] == "docs/adr/ADR-0010" - - def test_intent_md_carries_the_governing_declaration(self): - """GH-DEC-2026-017 §1 / A11: INTENT.md's frontmatter is the declaration.""" - assert _fold(_intent_frontmatter()["layer"]) == "staff" - - def test_sidecar_is_marked_derived_and_names_its_source(self): - """§11's derived-artifact rule, applied to the layer sidecar.""" - d = _decl() - assert d["derived"] is True - assert d["derived_from"] == "INTENT.md" - - def test_the_two_forms_agree_once_case_is_folded(self): - """A11: the derived form must agree; A9: comparison folds case. - - `Staff` in INTENT.md and `staff` in layer.yaml are the SAME value. This - test is deliberately a fold rather than an equality: the ruling asked - nobody to re-spell anything, and an equality assertion here would be this - repository quietly doing the re-spelling the ruling declined to order. - The next *real* divergence — a different layer — still fails. - """ - assert _fold(_decl()["layer"]) == _fold(_intent_frontmatter()["layer"]) - - def test_layer_is_in_section_3_closed_vocabulary(self): - """A9: {Taxonomy, Tooling, Engine, Staff}, closed, case-insensitive.""" - vocabulary = {"taxonomy", "tooling", "engine", "staff"} - assert _fold(_intent_frontmatter()["layer"]) in vocabulary - assert _fold(_decl()["layer"]) in vocabulary - - def test_no_declaration_carries_a_standard_version(self): - """GH-DEC-2026-017 §5 / A12 — and the regression guard on its return. - - The field was removed from the estate's reference form, not just from - this file. A field that is present will be branched on, so absence is - asserted rather than trusted. - """ - assert "standard_version" not in _decl() - assert "standard_version" not in _intent_frontmatter() - - def test_no_version_anywhere_in_either_declaration(self): - """A12 r2 / GH-DEC-2026-020 §1-§2: content, not a key name. - - A versioned `standard:` path or a `companion_version` is the same pin as - `standard_version`, so the guard walks every key and value of both forms. - """ - checker = _checker() - assert checker.find_version_pins(_intent_frontmatter()) == [] - assert checker.find_version_pins(_decl()) == [] - assert not str(_intent_frontmatter()["standard"]).endswith(".md") - - def test_checker_catches_a_versioned_standard_path(self): - checker = _checker() - pins = checker.find_version_pins( - {"layer": "Staff", "standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"} - ) - assert pins and pins[0].startswith("standard") - - def test_checker_catches_a_companion_version(self): - checker = _checker() - assert checker.find_version_pins({"layer": "Staff", "companion_version": "0.2"}) - assert checker.find_version_pins({"nested": {"standard_version": "0.7"}}) - - def test_version_token_in_identity_value_is_a_pin(self): - """GH-DEC-2026-021 §3: any `v?N.N` in a standard:/companion: value is a pin.""" - checker = _checker() - pins = checker.find_version_pins({"standard": "security-layer-model v0.7"}) - assert pins and pins[0].startswith("standard") - assert checker.find_version_pins({"companion": "SECURITY-COMPANION 0.2"}) - assert checker.find_version_pins({"nested": {"standard": ["security-layer-model v0.8"]}}) - - def test_prose_citation_and_intent_version_are_not_reached(self): - """GH-DEC-2026-021 §1 (A12 r3): prose provenance and `intent_version` pass.""" - checker = _checker() - assert checker.find_version_pins( - {"layer": "Staff", "note": "Outside §5 by the v0.5 scope rule", "intent_version": "0.1.0"} - ) == [] - - def test_schema_version_is_not_reached(self): - assert _checker().find_version_pins({"schema_version": "0.2", "layer": "Staff"}) == [] - - def test_stance_map_is_outside_the_run(self): - """GH-DEC-2026-020 §3: a stance map keeps its version; the run must not read it.""" - stance = yaml.safe_load((ROOT / "pep-stance.yaml").read_text()) - assert "standard_version" in stance, "pep-stance.yaml keeps its clause-scoped version" - assert "pep-stance" not in _checker().SCOPE - - def test_every_run_states_version_and_scope(self): - """GH-DEC-2026-020 §4: the version belongs to the run, printed every time.""" - checker = _checker() - out = subprocess.run( - [sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")], - capture_output=True, - text=True, - ).stdout - assert f"validated against: {checker.VALIDATED_AGAINST}" in out - assert f"scope: {checker.SCOPE}" in out - pass_line = next(ln for ln in out.splitlines() if ln.startswith("PASS")) - assert checker.VALIDATED_AGAINST in pass_line - - def test_every_tooling_contact_maps_to_a_declared_shape(self): - """§11 mechanical check — the guard against a new undeclared client.""" - result = subprocess.run( - [sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")], - capture_output=True, - text=True, - ) - assert result.returncode == 0, ( - f"undeclared Tooling contact — a finding under §11, not a tracked gap:\n" - f"{result.stdout}{result.stderr}" - ) - - def test_declared_gaps_carry_all_four_fields(self): - """§5.3 is machine-readable or it is prose wearing a schema.""" - for c in _decl()["tooling_contacts"]: - if c["shape"] == "5.3": - for field in ("capability", "intended_owner", "blocked_on", "review"): - assert c.get(field), f"{c['id']} missing {field}" - - def test_gaps_are_not_counted_as_conformance(self): - """§11: a declared gap is tracked non-conformance. Keep that visible.""" - text = (ROOT / "layer.yaml").read_text() - assert "TRACKED NON-CONFORMANCE" in text.upper() - - -class TestConduitSuppliesNoAuthority: - """§5.2: 'MUST NOT present its own credential, MUST NOT widen what the - caller could already do.' The standard says this SHOULD be covered by a - test; this is that test.""" - - def test_conduit_supplies_no_authority_of_its_own(self, monkeypatch): - from warden import proxy - - monkeypatch.setenv("VAULT_TOKEN", "caller-own-token") - monkeypatch.setenv("HOME", "/home/nobody") - before = dict(os.environ) - - env = proxy._caller_env() - - # The child environment IS the caller's environment — nothing added, - # nothing removed, no ops-warden credential injected. - assert env == before, ( - "conduit altered the caller's environment; §5.2 requires it to " - "supply no authority of its own" - ) - assert env["VAULT_TOKEN"] == "caller-own-token" - - def test_conduit_declares_supplied_authority_none(self): - conduits = [c for c in _decl()["tooling_contacts"] if c["shape"] == "5.2"] - assert conduits, "no §5.2 conduit declared — proxy.py is one" - for c in conduits: - assert c["supplied_authority"] == "none" - - def test_proxy_holds_no_credential_constant(self): - """A conduit that presents its own token is not a conduit (§5.2).""" - src = (ROOT / "src" / "warden" / "proxy.py").read_text() - # It may name token ENV VARS to detect caller auth; it must not carry a - # token value or mint one. - for forbidden in ("X-Vault-Token", "auth/approle/login", "token create"): - assert forbidden not in src, ( - f"proxy.py references {forbidden!r} — that is presenting or " - f"minting authority, not conducting the caller's" - ) - - -class TestPepStanceMap: - """§6.4: every PEP-shaped consumer MUST publish its unreachable-engine - stance map, total and per zone, 'published rather than held in code'. - ADR-0009 is named as the reference shape, so it should actually hold.""" - - def _stance(self) -> dict: - return yaml.safe_load((ROOT / "pep-stance.yaml").read_text()) - - def test_published_map_equals_shipped_behaviour(self): - """The whole point. A published map that may drift from the code is - worse than none, because it invites reliance it cannot support.""" - from warden.config import PolicyConfig - - assert self._stance()["stance"] == PolicyConfig().failure_modes - - def test_stance_is_total_over_the_zone_model(self): - """§6.4 obligation 3: total, no implicit default.""" - stance = self._stance()["stance"] - required = { - "z0-experimental", "z1-operational", "z2-protected", - "z2-continuity", "z3-critical", "unknown", "not-applicable", - } - assert required <= set(stance), f"stance not total; missing {required - set(stance)}" - assert set(stance.values()) <= {"fail_open", "fail_closed"} - - def test_critical_zone_fails_closed(self): - """ADR-0009's one non-negotiable row.""" - assert self._stance()["stance"]["z3-critical"] == "fail_closed" - - def test_verdict_is_never_cached(self): - """§6.4 obligation 2: caching an input claim is permitted; caching the - answer is a second decision point deciding early (§6.1).""" - assert self._stance()["verdict_caching"] == "none" - - def test_revocation_visibility_deadline_equals_enforced_ttl_policy(self): - """§9.7.2: a published replay window must not drift from issuance.""" - from warden.models import ActorType, MAX_TTL_HOURS - - published = self._stance()["revocation_visibility"] - expected = {actor.value: MAX_TTL_HOURS[actor] for actor in ActorType} - assert published["deadline_hours"] == expected - assert published["mechanism"] == "ttl_expiry" - assert published["revocation_channel"] == "none" - - def test_attributive_emission_cadence_deferral_carries_measurement(self): - cadence = self._stance()["emission_cadence"] - assert cadence["classification"] == "attributive" - assert cadence["status"] == "deferred" - assert cadence["observed_window"]["signature_records"] == 3 - assert cadence["observed_window"]["active_signature_days"] == 2 - assert cadence["reason"] - - -# --- classification coverage (v0.8 §6.4 obligation 3) ------------------------- - -def test_published_coverage_equals_measured_coverage(): - """The published figure must equal its explicit public declaration snapshot. - - This is a deterministic source check, not a claim about current estate state. - Live measurement remains scripts/report_coverage.py with an explicit estate - root; tests never treat the developer HOME as an authority or fixture. - - ops-warden asked gate-house for §13.1's Coverage column and its figures are - that column's first entries, so their accuracy is ours to hold. The register - explicitly does not compute anyone's coverage, and a stale number beside a - marked cell is worse than a blank -- a blank at least reads as "not reported". - - This is the same property that makes the stance map worth publishing (the map - equals PolicyConfig.failure_modes by test), applied one level up. - """ - import importlib.util - - repo = Path(__file__).resolve().parents[1] - spec = importlib.util.spec_from_file_location( - "report_coverage", repo / "scripts" / "report_coverage.py" - ) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - published = yaml.safe_load((repo / "pep-stance.yaml").read_text())[ - "classification_coverage" - ] - measured = module.measure(repo / "tests/fixtures/workload-coverage") - - for population in ("signing_targets", "routing_lanes"): - assert published[population] == measured[population], population - - -def test_the_unknown_cell_is_marked_as_a_declared_gap(): - """A non-conformant cell must say so where it is declared, not only in a review. - - §11's marking obligation, which ops-warden argued for in the v0.6 round and - then acquired a marked cell under. If the cell is ever flipped to fail_closed - this test fails, which is the correct time to remove the marking. - """ - repo = Path(__file__).resolve().parents[1] - text = (repo / "pep-stance.yaml").read_text() - stance = yaml.safe_load(text)["stance"] - - if stance["unknown"] == "fail_open": - assert "DECLARED GAP" in text - assert "WARDEN-WP-0040" in text - else: - assert stance["unknown"] == "fail_closed" - - -def test_accepted_v08_run_provenance_and_catalog_scope(): - checker = _checker() - assert "security-layer-model_v0.8.md" in checker.VALIDATED_AGAINST - assert "sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b" in checker.VALIDATED_AGAINST - assert "single repository ops-warden (catalogued in §4)" in checker.SCOPE - - -def test_reference_semantics_propagate_identity_through_nested_sequences(): - checker = _checker() - for data in ( - {"nested": {"standard": ["Security Layer Model v0.8"]}}, - {"companion": [["Security Companion 0.3"]]}, - {"standard": {"alternate": ["Security Layer Model v0.7"]}}, - ): - # A dictionary begins a fresh identity key; only sequence descendants - # inherit the immediate standard:/companion: identity in the reference. - if isinstance(data.get("standard"), dict): - assert checker.find_version_pins(data) == [] - else: - assert checker.find_version_pins(data), data - assert checker.find_version_pins({"rationale": "Assented at v0.7; migrated at v0.8."}) == [] - assert checker.find_version_pins({"intent_version": "0.2.0", "schema_version": "0.1"}) == [] - - -def test_ascii_folding_never_discards_non_ascii_letters(): - checker = _checker() - assert checker._fold("Staff") == "staff" - assert checker._fold("Engine") == "engine" - # Deleting non-ASCII bytes would turn this fifth token into Staff. - assert checker._fold("Stαaff") not in checker.LAYER_VOCABULARY diff --git a/tests/test_openbao_caller.py b/tests/test_openbao_caller.py deleted file mode 100644 index 6533380..0000000 --- a/tests/test_openbao_caller.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Sanitized ambient OpenBao caller identity.""" -from __future__ import annotations - -import json -from pathlib import Path - -import pytest - -from warden.openbao_caller import ( - ambient_token_source, - caller_boundary_note, - describe_ambient_openbao_caller, -) - - -def test_source_prefers_bao_token_over_vault_and_file(monkeypatch, tmp_path): - monkeypatch.setenv("BAO_TOKEN", "hvs.bao") - monkeypatch.setenv("VAULT_TOKEN", "hvs.vault") - monkeypatch.setattr(Path, "home", lambda: tmp_path) - (tmp_path / ".vault-token").write_text("file-token") - assert ambient_token_source() == "BAO_TOKEN" - - -def test_source_uses_vault_token_file_when_env_absent(monkeypatch, tmp_path): - monkeypatch.delenv("BAO_TOKEN", raising=False) - monkeypatch.delenv("VAULT_TOKEN", raising=False) - monkeypatch.setattr(Path, "home", lambda: tmp_path) - (tmp_path / ".vault-token").write_text("file-token") - assert ambient_token_source() == "vault-token-file" - - -def test_lookup_drops_token_fields(monkeypatch): - secret = "hvs.this-must-not-appear" - monkeypatch.setenv("BAO_TOKEN", secret) - payload = { - "data": { - "id": secret, - "accessor": "also-secret", - "entity_id": "4a1f663c-0621-0483-edd0-f852274f4b6b", - "display_name": "netkingdom-uid=platform-root", - "path": "auth/netkingdom/oidc/callback", - "policies": ["default", "operator-custody", "platform-admin"], - "meta": {"role": "platform-admin", "email": "hidden@example.invalid"}, - "ttl": 3001, - } - } - - def runner(argv, **kwargs): - class Completed: - returncode = 0 - stdout = json.dumps(payload) - stderr = "" - - assert kwargs["env"]["BAO_TOKEN"] == secret - return Completed() - - monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner) - described = describe_ambient_openbao_caller() - encoded = json.dumps(described) - assert secret not in encoded - assert "accessor" not in described - assert "email" not in encoded - assert described["entity_id"].startswith("4a1f663c-") - assert described["oidc_role"] == "platform-admin" - assert described["policies"] == ["default", "operator-custody", "platform-admin"] - assert described["source"] == "BAO_TOKEN" - assert described["error"] is None - - -def test_lookup_failure_has_no_raw_output(monkeypatch): - monkeypatch.setenv("VAULT_TOKEN", "hvs.caller") - monkeypatch.delenv("BAO_TOKEN", raising=False) - - def runner(argv, **kwargs): - class Completed: - returncode = 2 - stdout = "" - stderr = "Error: permission denied hvs.caller" - - return Completed() - - monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner) - described = describe_ambient_openbao_caller() - assert described["error"] == "token lookup failed" - assert described["entity_id"] is None - assert "hvs.caller" not in json.dumps(described) - - -def test_missing_client(monkeypatch): - monkeypatch.setenv("BAO_TOKEN", "hvs.bao") - - def runner(argv, **kwargs): - raise FileNotFoundError(argv[0]) - - monkeypatch.setattr("warden.openbao_caller.subprocess.run", runner) - assert describe_ambient_openbao_caller()["error"] == "bao client not found" - - -def test_no_token(monkeypatch, tmp_path): - monkeypatch.delenv("BAO_TOKEN", raising=False) - monkeypatch.delenv("VAULT_TOKEN", raising=False) - monkeypatch.setattr(Path, "home", lambda: tmp_path) - described = describe_ambient_openbao_caller() - assert described["source"] == "none" - assert described["error"] == "no ambient OpenBao token" - - -@pytest.mark.parametrize( - ("policies", "high_risk", "fragment"), - [ - ( - ["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"], - True, - "agent-high-risk-boundary", - ), - (["default", "platform-admin"], True, "platform-admin"), - (["workload-kv-read-reuse-surface-runtime"], True, None), - ], -) -def test_boundary_note(policies, high_risk, fragment): - note = caller_boundary_note({"policies": policies}, high_risk=high_risk) - if fragment is None: - assert note is None - else: - assert fragment in note diff --git a/tests/test_openbao_configuration_routing.py b/tests/test_openbao_configuration_routing.py deleted file mode 100644 index 4e831f2..0000000 --- a/tests/test_openbao_configuration_routing.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Configuration intent must never inherit a metadata survey's founder act.""" -from dataclasses import replace -from pathlib import Path - -import pytest - -from warden.plan import build_plan -from warden.routing.catalog import Catalog, load_catalog - -ROOT = Path(__file__).resolve().parents[1] -NEED = ('attended OpenBao platform administration to configure read policies and auth roles ' - 'for Coulomb and email backup delivery') -SIGNER_NEED = ('attended OpenBao platform administration for reviewed flex-auth ' - 'signer custody CAS0 and fixed KV metadata') - - -@pytest.fixture -def catalog(monkeypatch): - monkeypatch.setenv('WARDEN_POSTURE_CATALOG', str(ROOT / 'registry/policy/security-posture.yaml')) - return load_catalog(ROOT / 'registry/routing/catalog.yaml') - - -@pytest.mark.parametrize('need', [NEED, - SIGNER_NEED, - 'Attended OpenBao platform administration login', - 'configure OpenBao policies and Kubernetes auth roles for backup delivery', - 'apply OpenBao read policy for an explicitly reviewed service account']) -def test_configuration_requires_attended_admin_identity(catalog, need): - plan = build_plan(need, catalog=catalog) - assert plan.lane_id == 'openbao-platform-admin-login' - assert plan.verdict == 'founder_required' and plan.founder_act.kind == 'oidc_login' - assert plan.commands == [ - 'warden access openbao-platform-admin-login --exec -- ', - '# owner login is contained by warden; do not invoke it separately'] - assert all('--fetch' not in command and '--out' not in command for command in plan.commands) - - -@pytest.mark.parametrize('need', [ - 'survey existing OpenBao policies and roles for backup delivery', - 'OpenBao read-only metadata survey policy approle consumer backup', - 'survey OpenBao signer custody CAS0 and fixed KV metadata', - 'inspect OpenBao structure metadata policy approle']) -def test_survey_remains_metadata_only(catalog, need): - plan = build_plan(need, catalog=catalog) - assert plan.lane_id == 'openbao-structure-survey' - assert plan.founder_act.kind == 'approve' - - -@pytest.mark.parametrize('need', [NEED, SIGNER_NEED]) -def test_explicit_lane_id_still_wins_and_missing_admin_fails_closed(catalog, need): - assert catalog.find('openbao-structure-survey')[0].id == 'openbao-structure-survey' - unavailable = Catalog(catalog.path, [entry for entry in catalog.entries - if entry.id != 'openbao-platform-admin-login']) - plan = build_plan(need, catalog=unavailable) - assert plan.verdict == 'unroutable' and plan.lane_id is None - assert not plan.commands - - -@pytest.mark.parametrize('changes', [{'status': 'draft'}, {'lane': 'secret'}]) -def test_attended_admin_cannot_use_inactive_or_secret_lane(catalog, changes): - admin = catalog.get('openbao-platform-admin-login') - unavailable = Catalog(catalog.path, [ - replace(entry, **changes) if entry.id == admin.id else entry - for entry in catalog.entries]) - plan = build_plan(SIGNER_NEED, catalog=unavailable, include_draft=True) - assert plan.verdict == 'unroutable' and plan.lane_id is None - assert not plan.commands - - -def test_pair_provisioning_routes_to_custody_not_survey(catalog): - need = "provision a Scaleway object storage backup publisher key pair into OpenBao custody" - plan = build_plan(need, catalog=catalog) - assert plan.lane_id == "openbao-api-key" - assert plan.verdict == "founder_required" - assert plan.founder_act.kind == "paste_once_provision" - assert not any("plan-survey" in command for command in plan.commands) - - -def test_missing_custody_frontdoor_fails_closed(catalog): - unavailable = Catalog(catalog.path, [e for e in catalog.entries if e.id != "openbao-api-key"]) - assert build_plan("provision OpenBao backup key pair", catalog=unavailable).verdict == "unroutable" diff --git a/tests/test_plan.py b/tests/test_plan.py index a3cf827..73fc7e9 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -119,87 +119,3 @@ def test_cli_plan_json(): assert payload["verdict"] == "autonomous" assert payload["organization_posture"] == "build" assert payload["lane_id"] == "agent-harness-forgejo-deploy" - - -# --- mutate intent (WARDEN-WP-0038) ------------------------------------------- -# -# `warden plan` scored a need by keyword overlap with no notion of what the -# caller wanted to DO, so "generate a successor secret and CAS-write it to two -# custodians" matched the lane that READS that path and inherited its -# `autonomous` verdict, answered with --out/--exec/--wrap. Reported independently -# by key-cape (2026-09-08) and railiance-platform (2026-09-09), who added: "this -# is the same verdict your warden plan should have returned; until that is fixed, -# do not let a plan result stand in for this answer." - -QONTO_ROTATION_NEED = ( - "generate a successor client secret for rapp-qonto keycape client and " - "CAS-write it to OpenBao platform/workloads/rapp-qonto/keycape-client " - "and sso/keycape-rapp-qonto-client" -) - - -def test_reported_qonto_write_no_longer_returns_autonomous(): - """The exact need from the report. Regression, in the manner of WP-0033-T06.""" - plan = build_plan(QONTO_ROTATION_NEED) - assert plan.verdict == "founder_required" - assert plan.lane_id == "rapp-qonto-keycape-client" - assert plan.founder_act is not None - assert plan.founder_act.kind == "approve" - assert plan.founder_act.details["write_owner"] == "key-cape" - - -def test_a_write_need_is_never_answered_with_a_read_transport(): - """The half that made the wrong verdict actionable rather than merely wrong.""" - plan = build_plan(QONTO_ROTATION_NEED) - joined = " ".join(plan.commands) - for read_transport in ("--out", "--wrap", "--fetch", "bao kv get", "--exec"): - assert read_transport not in joined, read_transport - - -def test_mutating_need_names_why_it_escalated(): - """WP-0029's property: a verdict carries the reasons that produced it.""" - plan = build_plan(QONTO_ROTATION_NEED) - assert any("change a credential" in r for r in plan.reasons) - assert any("write authority" in r for r in plan.reasons) - # The lane records automatable: false; a driver must be told so. - assert any("not automatable" in r for r in plan.reasons) - - -def test_ops_warden_still_proceeds_autonomously_on_the_lane_it_owns(): - """Signing IS a mutating act. The test is ownership, not the absence of a verb. - - `delegation.mode: permanent` is the whole distinction — if this ever fails, - the guard has started refusing ops-warden's own front door. - """ - for need in ( - "sign an ssh certificate for agt-state-hub-bridge", - "issue a short-lived ssh cert for adm", - ): - plan = build_plan(need) - assert plan.verdict == "autonomous", need - assert plan.lane_id == "ssh-cert-host-access" - - -def test_reading_the_same_lane_is_unaffected(): - plan = build_plan("I need the npm token to publish whynot-design") - assert plan.verdict == "autonomous" - assert plan.lane_id == "whynot-design-npm-publish" - - -@pytest.mark.parametrize( - "verb", ["rotate", "revoke", "regenerate", "reset", "replace"] -) -def test_mutating_verbs_escalate_on_a_lane_ops_warden_does_not_own(verb): - plan = build_plan(f"{verb} the forgejo admin api token") - assert plan.verdict == "founder_required" - assert plan.founder_act.details["write_owner"] == "railiance-platform" - - -def test_intent_classifier_matches_tokens_not_substrings(): - from warden.plan import _need_intent - - assert _need_intent("rotate the forgejo token") == "mutate" - assert _need_intent("CAS-write to two custodians") == "mutate" - # "created" and "updated" are not the verbs; a need describing state is a read. - assert _need_intent("read the token created for whynot-design") == "read" - assert _need_intent("which subsystem owns the npm token") == "read" diff --git a/tests/test_policy.py b/tests/test_policy.py index 8257e23..0e2f948 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -307,47 +307,3 @@ def test_advisory_decision_is_recorded_and_does_not_block(tmp_path): assert check_sign_policy(cfg, spec) == "decision:advisory" assert spec.policy_zone == "unknown" assert spec.policy_outcome == "audit_only" - - -def test_check_request_asserts_no_resource_attributes(tmp_path, monkeypatch): - """The property FLEX-DEC-2026-012 turned on, asserted rather than assumed. - - flex-auth's enrichment used to overlay registry facts additive-if-absent, so - a caller-supplied `resource.attributes` value won and the registry ceiling - never applied. Their exploitability assessment rested on ops-warden sending - no `resource.attributes` at all -- true when they read it, and nothing here - held it true. secrets-engine sends them on every request, so this is a - property of this code rather than of the protocol. - - Ceilings must arrive from the registry. If a future change ever needs to send - an attribute, this test is the place that argument gets made. - """ - from warden import policy as policy_mod - - pubkey = tmp_path / "id.pub" - pubkey.write_text("ssh-ed25519 AAAA test\n") - cfg = PolicyConfig(flex_auth_url="http://flex-auth.test") - seen = {} - - class _Response: - status_code = 200 - - def raise_for_status(self): - return None - - def json(self): - return {"effect": "allow", "id": "decision:49350f1064f674d7"} - - def fake_post(url, json=None, headers=None, timeout=None): - seen["body"] = json - return _Response() - - monkeypatch.setattr(policy_mod.httpx, "post", fake_post) - policy_mod.check_sign_policy(cfg, _spec(pubkey)) - - assert "attributes" not in seen["body"]["resource"] - # The requested TTL is a policy input (ttl_out_of_bounds is denied against - # the registry ceiling), so it must travel as context, never as a resource - # attribute that would be compared against itself. - assert seen["body"]["context"]["ttl_hours"] == 24 - assert set(seen["body"]["resource"]) == {"id", "type", "system", "tenant"} diff --git a/tests/test_policy_fetch_evaluator.py b/tests/test_policy_fetch_evaluator.py deleted file mode 100644 index 72b6ab1..0000000 --- a/tests/test_policy_fetch_evaluator.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Secret-read checks use their own evaluator pin, never the SSH signing pin (CUST-WP-0075).""" -from unittest.mock import patch - -import httpx -import pytest - -from warden.access import policy_gate_status -from warden.ca import CAError -from warden.config import PolicyConfig, load_config -from warden.models import ActorType, CertSpec -from warden.policy import check_fetch_policy, check_sign_policy - -SIGN_URL = "http://127.0.0.1:19090" -FETCH_URL = "http://127.0.0.1:19091" - - -def _allow(url, json=None, headers=None, timeout=None): - return httpx.Response( - 200, - json={"effect": "allow", "id": "decision:fetch", "reason": "secret_read_lane_matched"}, - request=httpx.Request("POST", url), - ) - - -def _fetch(cfg): - return check_fetch_policy( - cfg, need_id="forgejo-admin-api-token", owner_repo="railiance-platform", domain=None - ) - - -def test_fetch_check_goes_to_fetch_pin_never_sign_pin(): - cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL) - with patch("warden.policy.httpx.post", side_effect=_allow) as post: - assert _fetch(cfg) == "decision:fetch" - urls = [c.args[0] for c in post.call_args_list] - assert urls == [f"{FETCH_URL}/v1/check"] - body = post.call_args.kwargs["json"] - assert body["resource"]["system"] == "railiance-platform" - assert body["action"] == "read" - - -@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"]) -def test_unset_fetch_url_fails_closed_without_using_sign_pin(failure_mode): - cfg = PolicyConfig(flex_auth_url=SIGN_URL) - cfg.failure_modes["unknown"] = failure_mode - with patch("warden.policy.httpx.post") as post: - with pytest.raises(CAError, match="policy.fetch_flex_auth_url") as err: - _fetch(cfg) - assert "secret-read evaluator not configured" in str(err.value) - post.assert_not_called() - - -@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"]) -def test_unreachable_fetch_url_fails_closed(failure_mode): - cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL) - cfg.failure_modes["unknown"] = failure_mode - calls = [] - - def boom(url, **kwargs): - calls.append(url) - raise httpx.ConnectError("refused", request=httpx.Request("POST", url)) - - with patch("warden.policy.httpx.post", side_effect=boom): - with pytest.raises(CAError, match="flex-auth-ops-warden-fetch\\) unreachable"): - _fetch(cfg) - assert calls == [f"{FETCH_URL}/v1/check"] - - -def test_fetch_403_keeps_existing_refusal_text(): - cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL) - response = httpx.Response(403, request=httpx.Request("POST", f"{FETCH_URL}/v1/check")) - with patch("warden.policy.httpx.post", return_value=response): - with pytest.raises(CAError) as err: - _fetch(cfg) - assert str(err.value) == ( - "flex-auth refused fetch policy check (HTTP 403); " - "caller authentication or system binding was rejected" - ) - - -def test_sign_check_still_uses_sign_pin(tmp_path): - cfg = PolicyConfig(flex_auth_url=SIGN_URL, fetch_flex_auth_url=FETCH_URL) - pub = tmp_path / "id.pub" - pub.write_text("ssh-ed25519 AAAA test\n") - spec = CertSpec(actor_name="agt-example", actor_type=ActorType.AGT, - pubkey_path=pub, ttl_hours=1, principals=["agt"]) - with patch("warden.policy.httpx.post", side_effect=_allow) as post: - assert check_sign_policy(cfg, spec) == "decision:fetch" - assert [c.args[0] for c in post.call_args_list] == [f"{SIGN_URL}/v1/check"] - assert post.call_args.kwargs["json"]["resource"]["system"] == "ops-warden" - - -def _write_cfg(tmp_path, monkeypatch, policy_block): - cfg = tmp_path / "warden.yaml" - cfg.write_text( - "backend: local\nca_key: /tmp/none\n" + policy_block - ) - monkeypatch.setenv("WARDEN_CONFIG", str(cfg)) - return cfg - - -def test_config_loads_fetch_url(tmp_path, monkeypatch): - _write_cfg(tmp_path, monkeypatch, - f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n") - cfg = load_config() - assert cfg.policy.flex_auth_url == SIGN_URL - assert cfg.policy.fetch_flex_auth_url == FETCH_URL - - -def test_gate_status_names_fetch_evaluator(tmp_path, monkeypatch): - _write_cfg(tmp_path, monkeypatch, - f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n") - status = policy_gate_status() - assert "flex-auth-ops-warden-fetch" in status and FETCH_URL in status - assert SIGN_URL not in status - - -def test_gate_status_unset_fetch_url_says_refused(tmp_path, monkeypatch): - _write_cfg(tmp_path, monkeypatch, f"policy:\n flex_auth_url: {SIGN_URL}\n") - status = policy_gate_status() - assert "fetch_flex_auth_url unset" in status and "refused" in status - - -def test_access_advisory_prints_fetch_evaluator(tmp_path, monkeypatch): - from pathlib import Path - - from typer.testing import CliRunner - - from warden.cli import app - - catalog = Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml" - monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(catalog)) - _write_cfg(tmp_path, monkeypatch, - f"policy:\n flex_auth_url: {SIGN_URL}\n fetch_flex_auth_url: {FETCH_URL}\n") - result = CliRunner().invoke(app, ["access", "forgejo-admin-api-token"]) - assert result.exit_code == 0, result.output - flat = " ".join(result.output.split()) - assert "flex-auth-ops-warden-fetch" in flat and FETCH_URL in flat diff --git a/tests/test_policy_http_refusal.py b/tests/test_policy_http_refusal.py deleted file mode 100644 index 88f88be..0000000 --- a/tests/test_policy_http_refusal.py +++ /dev/null @@ -1,48 +0,0 @@ -"""An explicit caller refusal cannot authorize a CA or credential side effect.""" -from unittest.mock import patch - -import httpx -import pytest - -from warden.ca import CAError -from warden.config import PolicyConfig -from warden.models import ActorType, CertSpec -from warden.policy import check_fetch_policy, check_sign_policy - - -@pytest.mark.parametrize("status", [401, 403]) -@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"]) -@pytest.mark.parametrize("operation", ["sign", "fetch"]) -def test_explicit_refusal_blocks_under_every_outage_mode(tmp_path, status, failure_mode, operation): - cfg = PolicyConfig(flex_auth_url="http://pdp.test", fetch_flex_auth_url="http://pdp.test") - cfg.failure_modes["unknown"] = failure_mode - public_key = tmp_path / "id.pub" - public_key.write_text("ssh-ed25519 AAAA test\n") - spec = CertSpec(actor_name="agt-example", actor_type=ActorType.AGT, - pubkey_path=public_key, ttl_hours=1, principals=["agt"]) - response = httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check"), - text="untrusted response body must not be exposed") - with patch("warden.policy.httpx.post", return_value=response): - with pytest.raises(CAError, match=f"HTTP {status}") as error: - if operation == "sign": - check_sign_policy(cfg, spec) - else: - check_fetch_policy(cfg, need_id="forgejo-admin-api-token", - owner_repo="railiance-platform", domain=None) - assert "untrusted response" not in str(error.value) - if operation == "sign": - assert spec.policy_outcome == "deny" - - -@pytest.mark.parametrize("status", [500, 503]) -@pytest.mark.parametrize("failure_mode", ["fail_open", "fail_closed"]) -def test_actual_evaluator_failure_retains_declared_outage_mode(status, failure_mode): - cfg = PolicyConfig(flex_auth_url="http://sign.test", fetch_flex_auth_url="http://pdp.test") - cfg.failure_modes["unknown"] = failure_mode - response = httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check")) - with patch("warden.policy.httpx.post", return_value=response): - if failure_mode == "fail_closed": - with pytest.raises(CAError, match=f"HTTP {status}"): - check_fetch_policy(cfg, need_id="example", owner_repo="example", domain=None) - else: - assert check_fetch_policy(cfg, need_id="example", owner_repo="example", domain=None) is None diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 3078bbf..cac3a97 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -10,7 +10,6 @@ from typer.testing import CliRunner from warden.cli import app from warden.proxy import ( - AttendedLoginError, ProxyError, ResolvedFetch, caller_auth_present, @@ -204,18 +203,11 @@ def _proxy_env(monkeypatch, tmp_path): monkeypatch.setenv("WARDEN_CONFIG", str(_warden_yaml(tmp_path))) -def _allow_fetch_gate(monkeypatch): - """Stand in for an allowing secret-read evaluator (flex-auth-ops-warden-fetch).""" - monkeypatch.setattr( - "warden.policy.check_fetch_policy", lambda *a, **k: "decision:test-fetch" - ) - - -def test_cli_proxy_unset_fetch_evaluator_fails_closed(monkeypatch, tmp_path): +def test_cli_proxy_unknown_zone_fail_open_reaches_transport_guard(monkeypatch, tmp_path): _proxy_env(monkeypatch, tmp_path) monkeypatch.setenv("VAULT_TOKEN", "caller") - # No policy.fetch_flex_auth_url: the fetch is refused before any transport, - # whatever the unknown-zone failure mode (CUST-WP-0075). + # The unknown-zone profile proceeds when no evaluator is configured, then + # the independent safe-transport boundary still refuses captured stdout. monkeypatch.setattr( "warden.proxy.subprocess.run", lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite gate")), @@ -225,9 +217,8 @@ def test_cli_proxy_unset_fetch_evaluator_fails_closed(monkeypatch, tmp_path): ["access", "npm", "--domain", "coulomb_social", "--field", "NPM_AUTH_TOKEN", "--path", "platform/x/y/z", "--fetch"], ) - assert r.exit_code == 4 - assert "fetch_flex_auth_url" in r.output - assert "fail_open applied" not in r.output + assert r.exit_code == 6 + assert "unknown-zone fail_open" in r.output def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path): @@ -243,31 +234,6 @@ def test_cli_proxy_requires_caller_auth(monkeypatch, tmp_path): assert r.exit_code == 3 -@pytest.mark.parametrize("status", [401, 403]) -def test_cli_explicit_policy_refusal_never_fetches_or_starts_child(monkeypatch, tmp_path, status): - import httpx - - _proxy_env(monkeypatch, tmp_path) - cfg = tmp_path / "warden.yaml" - cfg.write_text( - cfg.read_text() - + "policy:\n flex_auth_url: http://sign.test\n fetch_flex_auth_url: http://pdp.test\n" - ) - monkeypatch.setenv("VAULT_TOKEN", "caller-test-value") - monkeypatch.setattr( - "warden.policy.httpx.post", - lambda *a, **k: httpx.Response(status, request=httpx.Request("POST", "http://pdp.test/v1/check")), - ) - calls = [] - for name in ("proxy_exec", "proxy_fetch", "proxy_fetch_to_file", "proxy_fetch_wrapped"): - monkeypatch.setattr("warden.proxy." + name, lambda *a, **k: calls.append(True)) - result = runner.invoke(app, ["access", "forgejo-admin-api-token", "--exec", "--field", "API_TOKEN", "--", "true"]) - assert result.exit_code == 4 - assert f"HTTP {status}" in result.output - assert "fail_open applied" not in result.output - assert calls == [] - - def test_cli_proxy_rejects_retired_no_policy_bypass(monkeypatch, tmp_path): _proxy_env(monkeypatch, tmp_path) monkeypatch.setenv("VAULT_TOKEN", "caller") @@ -342,77 +308,6 @@ def test_cli_login_lane_rejects_persistent_fetch(monkeypatch, tmp_path): assert "requires --exec" in r.output -@pytest.mark.parametrize( - "phase,code,outcome", - [ - ("success", 0, "ok"), - ("missing_helper", 10, "login_failed"), - ("login_failed", 10, "login_failed"), - ("login_start", 10, "login_failed"), - ("child_start", 11, "child_failed"), - ("child_failed", 11, "child_failed"), - ("child_output", 12, "child_output"), - ("child_whitespace", 12, "child_output"), - ("revoke_failed", 13, "revoke_unconfirmed"), - ("cleanup_failed", 14, "cleanup_failed"), - ], -) -def test_cli_attended_result_and_both_audits(monkeypatch, tmp_path, phase, code, outcome): - _proxy_env(monkeypatch, tmp_path) - monkeypatch.setattr(Path, "home", lambda: tmp_path) - calls = [] - sentinel = b"hvs.NONPRODUCTION_RESULT_SENTINEL" - - def fake_run(argv, **kw): - calls.append(argv) - helper = Path(kw["env"]["HOME"]) / ".vault-token" - if argv[:2] == ["bao", "login"]: - if phase == "login_start": - raise OSError("client unavailable") - if phase != "missing_helper": - helper.write_bytes(sentinel) - login_json = json.dumps({"auth": {"client_token": sentinel.decode()}}) - return subprocess.CompletedProcess( - argv, 1 if phase == "login_failed" else 0, login_json.encode(), b"" - ) - if argv == ["reviewed-child"]: - if phase == "child_start": - raise OSError("child unavailable") - output = {"child_output": sentinel, "child_whitespace": b" \n"}.get(phase, b"") - return subprocess.CompletedProcess( - argv, 9 if phase == "child_failed" else 0, b"", output - ) - assert argv == ["bao", "token", "revoke", "-self"] - return subprocess.CompletedProcess( - argv, 1 if phase == "revoke_failed" else 0, b"", b"" - ) - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - if phase == "cleanup_failed": - def fail_cleanup(*args): - raise OSError("cleanup unavailable") - monkeypatch.setattr("warden.proxy.shutil.rmtree", fail_cleanup) - result = runner.invoke( - app, ["access", "login oidc", "--domain", "coulomb_social", - "--exec", "--", "reviewed-child"] - ) - assert result.exit_code == code, result.output - assert sentinel.decode() not in result.output - if code == 10: - assert ["reviewed-child"] not in calls - if phase not in ("login_start", "cleanup_failed"): - assert calls[-1] == ["bao", "token", "revoke", "-self"] - if phase != "cleanup_failed": - assert not (tmp_path / ".warden-attended-login").exists() - for name in ("access-audit.log", "audit.jsonl"): - raw = (tmp_path / "state" / name).read_text() - assert sentinel.decode() not in raw - records = [json.loads(line) for line in raw.splitlines()] - login = next(record for record in records if record["action"] == "login") - assert login["exit_code"] == code - assert login["outcome"] == outcome - - def test_attended_login_refuses_read_only_home_before_auth(monkeypatch, tmp_path): monkeypatch.setattr(Path, "home", lambda: tmp_path) monkeypatch.setattr( @@ -446,7 +341,7 @@ def test_attended_login_persistence_failure_revokes_and_cleans(monkeypatch, tmp_ child_argv=["should-not-run"], ) assert calls == [ - ["bao", "login", "-format=json", "-no-print"], + ["bao", "login", "-no-print", "-format=json"], ["bao", "token", "revoke", "-self"], ] assert not (tmp_path / ".warden-attended-login").exists() @@ -496,278 +391,6 @@ def test_attended_login_unexpected_output_is_contained_revoked_and_cleaned( assert not (tmp_path / ".warden-attended-login").exists() -def test_attended_login_contained_success_output_never_escapes(monkeypatch, tmp_path, capsys): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - sentinel = "hvs.NONPRODUCTION_CONTAINED_LOGIN" - child_ran = False - - def fake_run(argv, **kw): - nonlocal child_ran - helper = Path(kw["env"]["HOME"]) / ".vault-token" - if argv[:2] == ["bao", "login"]: - helper.write_text(sentinel) - helper.chmod(0o600) - return subprocess.CompletedProcess( - argv, - 0, - stdout=json.dumps(_login_json(sentinel)).encode(), - stderr=_OIDC_PROMPT, - ) - if argv == ["reviewed-child"]: - child_ran = True - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - if argv[:3] == ["bao", "token", "revoke"]: - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - raise AssertionError(argv) - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - assert proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["reviewed-child"], - ) == 0 - captured = capsys.readouterr() - assert child_ran is True - assert sentinel not in captured.out - assert sentinel not in captured.err - assert not (tmp_path / ".warden-attended-login").exists() - - -def test_attended_login_preserves_caller_warden_config_for_reviewed_child( - monkeypatch, tmp_path -): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - monkeypatch.delenv("WARDEN_CONFIG", raising=False) - caller_config = tmp_path / ".config" / "warden" / "warden.yaml" - caller_config.parent.mkdir(parents=True) - caller_config.write_text("backend: local\n") - seen_config = None - - def fake_run(argv, **kw): - nonlocal seen_config - helper = Path(kw["env"]["HOME"]) / ".vault-token" - if argv[:2] == ["bao", "login"]: - helper.write_text("non-production-test-double") - helper.chmod(0o600) - if argv == ["reviewed-child"]: - seen_config = kw["env"].get("WARDEN_CONFIG") - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - assert proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["reviewed-child"], - ) == 0 - assert seen_config == str(caller_config) - - -_OIDC_PROMPT = ( - b"Complete the login via your OIDC provider. Launching browser to:\n\n" - b" https://keycape.example.invalid/auth?state=x&nonce=y\n\n\n" - b"Waiting for OIDC authentication to complete...\n" -) - - -def _login_json(token: str) -> dict: - return { - "request_id": "req", - "lease_id": "", - "lease_duration": 0, - "renewable": False, - "data": None, - "warnings": None, - "mount_type": "", - "auth": { - "client_token": token, - "accessor": "acc", - "policies": ["default"], - "token_policies": ["default"], - "identity_policies": None, - "metadata": {"role": "r"}, - "orphan": True, - "entity_id": "ent", - "lease_duration": 3600, - "renewable": True, - "mfa_requirement": None, - "num_uses": 0, - }, - } - - -def _run_populated_login(monkeypatch, tmp_path, *, stdout: bytes, stderr: bytes): - """Login exits 0 with a populated private helper but the given output.""" - monkeypatch.setattr(Path, "home", lambda: tmp_path) - calls = [] - - def fake_run(argv, **kw): - calls.append(argv) - if argv[:2] == ["bao", "login"]: - helper = Path(kw["env"]["HOME"]) / ".vault-token" - helper.write_text("hvs.NONPRODUCTION_HELPER_VALUE") - helper.chmod(0o600) - return subprocess.CompletedProcess(argv, 0, stdout=stdout, stderr=stderr) - if argv[:3] == ["bao", "token", "revoke"]: - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - raise AssertionError("reviewed child ran after non-allowlisted login output") - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - return calls - - -_LOGIN_SENTINEL = "hvs.NONPRODUCTION_UNEXPECTED_LOGIN" -_OK_SENTINEL = "hvs.NONPRODUCTION_OK" - - -def _assert_login_audits(tmp_path, *, code: int, outcome: str) -> None: - for name in ("access-audit.log", "audit.jsonl"): - raw = (tmp_path / "state" / name).read_text() - assert _LOGIN_SENTINEL not in raw - assert _OK_SENTINEL not in raw - assert "NONPRODUCTION_HELPER_VALUE" not in raw - records = [json.loads(line) for line in raw.splitlines()] - login = next(record for record in records if record["action"] == "login") - assert login["exit_code"] == code - assert login["outcome"] == outcome - - -@pytest.mark.parametrize( - "stdout,stderr", - [ - # unexpected stdout text - (_LOGIN_SENTINEL.encode(), b""), - # unexpected stderr text - (b"", _LOGIN_SENTINEL.encode()), - # unexpected stderr appended to an otherwise valid OIDC prompt - (b"", _OIDC_PROMPT + _LOGIN_SENTINEL.encode()), - # valid JSON followed by extra text - (json.dumps(_login_json(_LOGIN_SENTINEL)).encode() + b"\ntrailing", b""), - # two JSON objects - ((json.dumps(_login_json(_LOGIN_SENTINEL)) * 2).encode(), b""), - # unexpected top-level key - (json.dumps({**_login_json(_LOGIN_SENTINEL), "extra": _LOGIN_SENTINEL}).encode(), b""), - # unexpected auth key - (json.dumps({"auth": {"client_token": _LOGIN_SENTINEL, "x": 1}}).encode(), b""), - # wrong value type - (json.dumps({"auth": {"client_token": _LOGIN_SENTINEL, "num_uses": True}}).encode(), b""), - # JSON without auth block / non-object JSON - (json.dumps({"data": {"v": _LOGIN_SENTINEL}}).encode(), b""), - (json.dumps([_LOGIN_SENTINEL]).encode(), b""), - # wrapping token beside auth (would survive revoke -self) - (json.dumps({**_login_json("a"), "wrap_info": {"token": _LOGIN_SENTINEL}}).encode(), b""), - (json.dumps({**_login_json("a"), "wrap_info": {}}).encode(), b""), - # non-empty data block - (json.dumps({**_login_json("a"), "data": {"v": _LOGIN_SENTINEL}}).encode(), b""), - # empty or null client_token - (json.dumps({"auth": {"client_token": "", "accessor": _LOGIN_SENTINEL}}).encode(), b""), - (json.dumps({"auth": {"client_token": None, "accessor": _LOGIN_SENTINEL}}).encode(), b""), - # leading whitespace before the OIDC prompt - (b"", b" " + _OIDC_PROMPT), - (b"", b"\n" + _OIDC_PROMPT), - # duplicate keys - (b'{"auth": {"client_token": "a"}, "auth": {"client_token": "b"}}', b""), - # malformed JSON - (b'{"auth": {"client_token": "' + _LOGIN_SENTINEL.encode() + b'"', b""), - # undecodable bytes - (b"\xff\xfe", b""), - ], - ids=[ - "stdout-text", "stderr-text", "stderr-after-prompt", "json-trailing", - "json-twice", "top-extra-key", "auth-extra-key", "wrong-type", - "no-auth", "json-array", "wrap-info-token", "wrap-info-empty", - "data-nonempty", "token-empty", "token-null", "stderr-leading-space", - "stderr-leading-newline", "duplicate-key", "malformed-json", "non-utf8", - ], -) -def test_attended_login_non_allowlisted_output_with_populated_helper_fails_closed( - monkeypatch, tmp_path, capsys, stdout, stderr -): - _proxy_env(monkeypatch, tmp_path) - calls = _run_populated_login(monkeypatch, tmp_path, stdout=stdout, stderr=stderr) - result = runner.invoke( - app, ["access", "login oidc", "--domain", "coulomb_social", - "--exec", "--", "should-not-run"] - ) - assert result.exit_code == 10, result.output - assert "non-allowlisted output" in result.output - assert _LOGIN_SENTINEL not in result.output - captured = capsys.readouterr() - assert _LOGIN_SENTINEL not in captured.out - assert _LOGIN_SENTINEL not in captured.err - # No retry: exactly one login, then self-revocation through the populated - # helper; the reviewed child never runs. - assert len(calls) == 2 - assert calls[0][:2] == ["bao", "login"] - assert calls[1] == ["bao", "token", "revoke", "-self"] - assert all(_LOGIN_SENTINEL not in " ".join(argv) for argv in calls) - assert not (tmp_path / ".warden-attended-login").exists() - _assert_login_audits(tmp_path, code=10, outcome="login_failed") - - -def test_attended_login_non_allowlisted_output_direct_raises_value_free( - monkeypatch, tmp_path -): - calls = _run_populated_login( - monkeypatch, tmp_path, stdout=_LOGIN_SENTINEL.encode(), stderr=b"" - ) - with pytest.raises(AttendedLoginError, match="non-allowlisted output") as exc: - proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print"]), - child_argv=["should-not-run"], - ) - assert (exc.value.exit_code, exc.value.outcome) == (10, "login_failed") - assert _LOGIN_SENTINEL not in str(exc.value) - assert calls == [ - ["bao", "login", "-format=json", "-no-print"], - ["bao", "token", "revoke", "-self"], - ] - - -@pytest.mark.parametrize( - "stdout,stderr", - [ - (b"", b""), - (b"", _OIDC_PROMPT), - (json.dumps(_login_json(_OK_SENTINEL)).encode() + b"\n", b""), - (json.dumps({"auth": {"client_token": _OK_SENTINEL}}).encode(), b""), - (json.dumps({"data": {}, "auth": {"client_token": _OK_SENTINEL}}).encode(), b""), - ( - b"", - b"Complete the login via your OIDC provider. Open the following link " - b"in your browser:\n\n https://keycape.example.invalid/auth?s=1\n\n\n", - ), - ], - ids=["empty", "oidc-prompt", "full-json", "minimal-json", "data-empty", "open-link-prompt"], -) -def test_attended_login_allowlisted_output_hands_off( - monkeypatch, tmp_path, capsys, stdout, stderr -): - _proxy_env(monkeypatch, tmp_path) - monkeypatch.setattr(Path, "home", lambda: tmp_path) - calls = [] - - def fake_run(argv, **kw): - calls.append(argv) - if argv[:2] == ["bao", "login"]: - helper = Path(kw["env"]["HOME"]) / ".vault-token" - helper.write_text("hvs.NONPRODUCTION_HELPER_VALUE") - helper.chmod(0o600) - return subprocess.CompletedProcess(argv, 0, stdout=stdout, stderr=stderr) - return subprocess.CompletedProcess(argv, 0, stdout=b"", stderr=b"") - - monkeypatch.setattr("warden.proxy.subprocess.run", fake_run) - result = runner.invoke( - app, ["access", "login oidc", "--domain", "coulomb_social", - "--exec", "--", "reviewed-child"] - ) - assert result.exit_code == 0, result.output - assert calls[1] == ["reviewed-child"] - assert calls[-1] == ["bao", "token", "revoke", "-self"] - assert _OK_SENTINEL not in result.output - captured = capsys.readouterr() - assert _OK_SENTINEL not in captured.out + captured.err - assert not (tmp_path / ".warden-attended-login").exists() - _assert_login_audits(tmp_path, code=0, outcome="ok") - - def test_real_catalog_login_entry_is_login_lane(): from warden.routing import load_catalog e = load_catalog(_repo_catalog()).get("key-cape-oidc-login") @@ -855,15 +478,12 @@ def test_build_wrapped_fetch_refuses_placeholder_path(): def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch): """The anti-pattern: streaming a value to captured stdout is refused (exit 6).""" _proxy_env(monkeypatch, tmp_path) - _allow_fetch_gate(monkeypatch) monkeypatch.setenv("VAULT_TOKEN", "caller-token") # G1 caller-auth precheck # The guard trips before the fetch runs; make a real bao call fail loudly if reached. monkeypatch.setattr( "warden.proxy.subprocess.run", lambda *a, **k: (_ for _ in ()).throw(AssertionError("fetch ran despite stdout guard")), ) - # This case tests the human stdout guard; the agent guard has its own tests. - monkeypatch.setattr("warden.access.agent_read_boundary_identity", lambda: "") # CliRunner captures stdout (not a tty), so the guard trips without --unsafe-stdout. r = runner.invoke(app, ["access", "whynot-design-npm-publish", "--fetch"]) assert r.exit_code == 6 @@ -873,7 +493,6 @@ def test_access_fetch_to_nonterminal_stdout_is_refused(tmp_path, monkeypatch): def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_path): """--fingerprint prints a masked fingerprint (never the value) even to captured stdout.""" _proxy_env(monkeypatch, tmp_path) - _allow_fetch_gate(monkeypatch) monkeypatch.setenv("VAULT_TOKEN", "caller-token") class _Fake: @@ -893,7 +512,6 @@ def test_access_fingerprint_masks_and_bypasses_stdout_guard(monkeypatch, tmp_pat def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch): """WP-0026 T04: WARDEN_AGENT_ID + risk=high refuses raw value stream (exit 7).""" _proxy_env(monkeypatch, tmp_path) - _allow_fetch_gate(monkeypatch) monkeypatch.setenv("VAULT_TOKEN", "caller-token") monkeypatch.setenv("WARDEN_AGENT_ID", "grok") # Prefer high-risk lane; use --unsafe-stdout so T02 would allow if T04 failed. @@ -908,32 +526,9 @@ def test_access_agent_high_risk_raw_stream_refused(tmp_path, monkeypatch): assert "agent read-boundary" in r.output.lower() or "risk=high" in r.output.lower() -def test_access_issued_agent_subject_refuses_raw_stream_without_legacy_marker( - tmp_path, monkeypatch -): - """WP-0034 T02: the issued KeyCape subject is sufficient for exit 7.""" - _proxy_env(monkeypatch, tmp_path) - _allow_fetch_gate(monkeypatch) - monkeypatch.setenv("VAULT_TOKEN", "caller-token") - monkeypatch.delenv("WARDEN_AGENT_ID", raising=False) - monkeypatch.setenv( - "WARDEN_POLICY_SUBJECT", "service:codex:railiance-platform" - ) - r = runner.invoke( - app, - [ - "access", "railiance-backup-offsite-lane", - "--fetch", "--unsafe-stdout", - ], - ) - assert r.exit_code == 7, r.output - assert "service:codex:railiance-platform" in r.output - - def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch): """Agents may use --fingerprint on high-risk lanes (no raw value).""" _proxy_env(monkeypatch, tmp_path) - _allow_fetch_gate(monkeypatch) monkeypatch.setenv("VAULT_TOKEN", "caller-token") monkeypatch.setenv("WARDEN_AGENT_ID", "grok") @@ -948,49 +543,3 @@ def test_access_agent_high_risk_fingerprint_allowed(tmp_path, monkeypatch): ) assert r.exit_code == 0, r.output assert "should-not-appear" not in r.output - - -def test_attended_json_format_precedes_auth_data(monkeypatch, tmp_path): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - calls = [] - def run(argv, **kwargs): - calls.append(argv) - return subprocess.CompletedProcess(argv, 1, b"", b"") - monkeypatch.setattr("warden.proxy.subprocess.run", run) - with pytest.raises(AttendedLoginError): - proxy_attended_login_exec( - ResolvedFetch(argv=["bao", "login", "-no-print", "-method=oidc", "role=platform-admin"]), - child_argv=["must-not-run"], - ) - assert calls[0].index("-format=json") < calls[0].index("role=platform-admin") - - -@pytest.mark.parametrize("shape", ["s", "hvs", "hvb", "hvr"]) -def test_contained_recovery_supports_openbao_token_shapes(monkeypatch, shape): - from warden.proxy import _revoke_contained - calls = [] - sentinel = shape + ".FAKE_RECOVERY_SENTINEL_12345678" - def run(argv, **kwargs): - calls.append((argv, dict(kwargs["env"]))) - return subprocess.CompletedProcess(argv, 1 if len(calls) == 1 else 0, b"", b"") - monkeypatch.setattr("warden.proxy.subprocess.run", run) - assert _revoke_contained("bao", env={}, possible_output=sentinel.encode()) - assert calls[1][1]["BAO_TOKEN"] == sentinel - assert all(sentinel not in " ".join(argv) for argv, _ in calls) - - -@pytest.mark.parametrize("prefix,category", [ - ("timed out waiting for response from provider", "callback_timeout"), - ("Error handling callback", "callback_error"), - ("Error authenticating:", "authentication_error"), - ("unrecognized backend error", "unknown_client_failure"), -]) -def test_attended_failure_is_fixed_category_without_client_bytes(monkeypatch, tmp_path, prefix, category): - monkeypatch.setattr(Path, "home", lambda: tmp_path) - sentinel = "SECRET_CLIENT_OUTPUT_SENTINEL" - def run(argv, **kwargs): - return subprocess.CompletedProcess(argv, 1, b"", (prefix + " " + sentinel).encode()) - monkeypatch.setattr("warden.proxy.subprocess.run", run) - with pytest.raises(AttendedLoginError, match="category=" + category) as exc: - proxy_attended_login_exec(ResolvedFetch(argv=["bao", "login", "-no-print"]), child_argv=["must-not-run"]) - assert sentinel not in str(exc.value) diff --git a/tests/test_reuse_surface_caller_gate.py b/tests/test_reuse_surface_caller_gate.py deleted file mode 100644 index 5843673..0000000 --- a/tests/test_reuse_surface_caller_gate.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The reuse-surface catalog caller stays unnamed until verification.""" -from __future__ import annotations - -from pathlib import Path - -from warden.routing import load_catalog - - -def test_dedicated_caller_is_the_verified_hub_writer_role(): - catalog = load_catalog(Path(__file__).resolve().parents[1] / "registry" / "routing" / "catalog.yaml") - entry = catalog.get("reuse-surface-hub-write-token") - assert entry is not None - assert entry.auth_method == ( - "bao login -method=oidc -path=netkingdom " - "role=reuse-surface-hub-writer-workload-kv-read" - ) diff --git a/tests/test_routing.py b/tests/test_routing.py index 7bf20d9..f280dda 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -137,123 +137,27 @@ def test_ops_warden_warden_sign_lane_has_native_exec(): assert e.resolvable is False # broker lane — owner exec, not warden access --fetch -def test_keycape_client_secret_generate_is_owner_native_and_never_proxied(): - """WARDEN-WP-0044 / RPF-WP-0054: generate-in-custody lane for SecurityCase mint-secret.""" - catalog = load_catalog(_repo_catalog()) - e = catalog.get("keycape-client-secret-generate") - assert e is not None and e.is_active and e.owner_repo == "railiance-platform" - assert e.has_native_exec is True and e.exec_owner == "railiance-platform" - assert "credential.py generate" in e.exec_command - assert "--case-ref" in e.exec_command and "--case-file" in e.exec_command - assert e.warden_executes is False and e.exec_capable is False - assert e.risk == "high" and e.rotation is not None - - -def test_named_client_rotation_outranks_generic_generate_lane(): - """Activating keycape-client-secret-generate must not capture a named client's - rotation need; it is first-generation only and refuses an existing path.""" - catalog = load_catalog(_repo_catalog()) - assert catalog.find("rotate rapp-qonto client secret")[0].id == "rapp-qonto-keycape-client" - assert catalog.find("mint-secret for a security case, generated in custody")[0].id == ( - "keycape-client-secret-generate" - ) - - def test_route_find_vault_token_ops_warden_prefers_broker_lane(): catalog = load_catalog(_repo_catalog()) matches = catalog.find("VAULT_TOKEN ops-warden warden sign", limit=3) assert matches[0].id == "ops-warden-warden-sign-token" -def test_route_find_layer_declaration_resolves_to_reference_playbook(): - catalog = load_catalog(_repo_catalog()) - entry = catalog.find("how do I declare my layer", limit=1)[0] - assert entry.id == "netkingdom-layer-declaration" - assert entry.owner_repo == "ops-warden" - assert entry.wiki_ref.endswith( - "netkingdom-layer-declaration.md#worker-checklist" - ) - assert entry.canon_ref.endswith("security-layer-model_v0.8.md") - assert entry.exec_capable is False - - def test_whynot_design_npm_lane_is_concrete_and_resolvable(): - """The provisioned Forgejo npm lane is exact, concrete, and resolvable.""" + """The provisioned npm publish lane has no placeholders and reports resolvable.""" catalog = load_catalog(_repo_catalog()) e = catalog.get("whynot-design-npm-publish") assert e is not None and e.is_active and e.exec_capable assert e.resolvable is True - assert "Forgejo registry" in e.title - assert "forgejo" in e.need_keywords - assert "gitea" not in e.need_keywords assert "<" not in e.fetch_command and ">" not in e.fetch_command assert "platform/workloads/coulomb/whynot-design/npm-publish" in e.fetch_command - rotation = " ".join(e.rotation.steps) - assert "Forgejo-supported `npm view @`" in rotation - assert "Gitea" not in rotation - - -def test_policy_nexus_source_read_lane_is_exact_high_risk_and_resolvable(): - catalog = load_catalog(_repo_catalog()) - entry = catalog.get("policy-nexus-forgejo-source-read") - assert entry is not None and entry.is_active and entry.exec_capable - assert entry.resolvable is True - assert entry.risk == "high" - assert entry.owner_repo == "railiance-platform" - assert entry.fetch_command == ( - "bao kv get -field=FORGEJO_SOURCE_TOKEN " - "platform/workloads/policy-nexus/forgejo-source-read" - ) - assert entry.path_template == "platform/workloads/policy-nexus/forgejo-source-read" - assert entry.auth_method.endswith( - "role=policy-nexus-forgejo-source-workload-kv-read" - ) - assert entry.delegation is not None and entry.delegation.mode == "native" - - -def test_route_find_policy_nexus_source_read_prefers_concrete_lane(): - catalog = load_catalog(_repo_catalog()) - matches = catalog.find( - "policy nexus Forgejo private source repository read token Actions", limit=1 - ) - assert matches[0].id == "policy-nexus-forgejo-source-read" - - -def test_core_hub_source_read_lane_is_eso_only_and_not_resolvable(): - """CCR-2026-0031: ESO is the only consumer, so no operator fetch lane.""" - catalog = load_catalog(_repo_catalog()) - entry = catalog.get("core-hub-forgejo-source-read") - assert entry is not None and entry.is_active - assert entry.exec_capable is False - assert entry.resolvable is False - assert entry.fetch_command is None - assert entry.lane == "secret" - assert entry.risk == "high" - assert entry.owner_repo == "railiance-platform" - assert entry.path_template == "platform/workloads/core-hub/forgejo-source-read" - assert "role=external-secrets-core-hub-forgejo-source" in entry.auth_method - assert entry.delegation is not None and entry.delegation.mode == "native" - assert entry.canon_ref.endswith("CCR-2026-0031-core-hub-forgejo-source-read.yaml") - - -def test_route_find_core_hub_source_read_prefers_core_hub_lane(): - catalog = load_catalog(_repo_catalog()) - matches = catalog.find( - "core hub repo-manager publisher Forgejo private source repository read token", - limit=1, - ) - assert matches[0].id == "core-hub-forgejo-source-read" def test_generic_and_template_lanes_not_resolvable(): catalog = load_catalog(_repo_catalog()) # generic openbao lane has /; login lane has . assert catalog.get("openbao-api-key").resolvable is False - login = catalog.get("key-cape-oidc-login") - assert login.resolvable is False - assert login.fetch_command == ( - "bao login -no-print -method=oidc -path=netkingdom role=" - ) + assert catalog.get("key-cape-oidc-login").resolvable is False def test_platform_admin_login_lane_is_exact_and_non_value_bearing(): @@ -266,25 +170,6 @@ def test_platform_admin_login_lane_is_exact_and_non_value_bearing(): assert entry.workload_ref.resolution == "not-applicable" -def test_kubernetes_railiance01_admin_lane_is_attended_file_pointer(): - catalog = load_catalog(_repo_catalog()) - entry = catalog.get("kubernetes-railiance01-admin") - assert entry is not None - assert entry.status == "active" - assert entry.risk == "high" - assert entry.lane == "secret" - assert entry.lane != "login" - assert entry.exec_capable is True - assert entry.resolvable is True - assert entry.fetch_command == "test -f /home/worsch/.kube/config-railiance01" - assert entry.path_template == "/home/worsch/.kube/config-railiance01" - assert "cat" not in entry.fetch_command - assert "config view" not in entry.fetch_command - assert "<" not in entry.fetch_command and ">" not in entry.fetch_command - matches = catalog.find("kubernetes railiance01 admin kubeconfig", limit=1) - assert matches[0].id == "kubernetes-railiance01-admin" - - def test_netkingdom_sso_bind_lanes_are_routed_but_not_resolvable(): catalog = load_catalog(_repo_catalog()) for lane_id in ( @@ -424,7 +309,7 @@ def test_exec_capable_without_fetch_command_rejected(tmp_path): @pytest.mark.parametrize( "leaked", [ - "bao write x ghp_abcdef0123456789abcdef0123", # github pat prefix + "bao write x token=ghp_abcdef0123456789abcdef0123", # github token prefix "x=AKIAIOSFODNN7EXAMPLE", # aws key id "header=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", # jwt prefix "val=ZmFrZXNlY3JldDEyMzQ1Njc4OWFiY2RlZmdoaWprbA", # high-entropy run @@ -1061,31 +946,12 @@ def test_asked_and_waiting_is_not_verification(): def test_stale_gaps_flags_unverified_even_when_the_date_is_today(): - """Property, not a named lane. - - Originally pinned to `key-cape-oidc-login` sitting `asked-and-waiting`; - key-cape answered on 2026-09-08, so the fixture lane moved and the test - failed on someone else's good news rather than on a defect. The claim being - made is about the verification method, so assert that instead: any lane - whose blocker was never re-established is stale on the day it was reviewed. - """ catalog = load_catalog(_repo_catalog()) - today = date(2026, 9, 8) - stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=today)} - unverified = [ - e for e in catalog.gaps(include_draft=True) - if not e.effective_delegation.is_verified - ] - for entry in unverified: - assert entry.id in stale, f"{entry.id} is unverified but not stale" - # A lane the owner confirmed today is fresh, so staleness is not vacuous. - fresh = [ - e.id for e in catalog.gaps(include_draft=True) - if e.effective_delegation.is_verified - and e.effective_delegation.reviewed == today.isoformat() - ] - assert fresh, "expected at least one owner-confirmed-today lane" - assert not (set(fresh) & stale) + stale = {e.id for e in catalog.stale_gaps(include_draft=True, today=date(2026, 8, 21))} + # Asked of key-cape on 2026-08-21 and unanswered -- zero days old, still stale. + assert "key-cape-oidc-login" in stale + # Confirmed by the owner the same day -- fresh. + assert "issue-core-ingestion-api-key" not in stale def test_invalid_verification_method_rejected(tmp_path): @@ -1113,10 +979,10 @@ def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env): assert result.exit_code == 3 rows = json.loads(result.stdout) assert any(r["stale"] for r in rows) - # A lane whose blocker was never re-established stays stale regardless of - # how many calendar days have elapsed since it was reviewed. Asserted over - # the verification method rather than one named lane -- see - # test_stale_gaps_flags_unverified_even_when_the_date_is_today. - for row in rows: - if row["verified"] in ("asked-and-waiting", "unverified"): - assert row["stale"], f"{row['id']} is unverified but not stale" + # An asked-and-waiting lane stays stale until it is verified, regardless of + # how many calendar days have elapsed since the request. + assert any( + r["stale"] + and r["verified"] == "asked-and-waiting" + for r in rows + ) diff --git a/tests/test_safe_unit_runner.py b/tests/test_safe_unit_runner.py deleted file mode 100644 index 972854a..0000000 --- a/tests/test_safe_unit_runner.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Exercise installed parent and nested child controls, never real credentials.""" -import json -import os -from pathlib import Path -import socket -import subprocess -import sys - -import pytest - -ROOT = Path(__file__).resolve().parents[1] - - -@pytest.fixture -def guarded(request): - guard = getattr(request.config, '_unit_guard', None) - if guard is None: - pytest.skip('requires make test / scripts/run_unit_tests.py') - return guard - - -def test_parent_environment_is_allowlisted_and_home_is_temporary(guarded): - # The external regression invocation sets synthetic BAO_TOKEN/VAULT_TOKEN/HOME. - assert 'BAO_TOKEN' not in os.environ - assert 'VAULT_TOKEN' not in os.environ - assert Path(os.environ['TMPDIR']) in Path.home().parents - assert 'PYTHONPATH' not in os.environ - - -@pytest.mark.parametrize('command', [ - ['bao', 'token', 'lookup'], ['ssh', 'railiance01'], ['curl', 'http://127.0.0.1:8000'], - [sys.executable, '-c', 'import os; print(os.environ)'], -]) -def test_unexpected_process_refused_before_launch(guarded, command): - with pytest.raises(PermissionError, match='unit isolation'): - subprocess.run(command, check=True) - - -def test_parent_socket_denies_even_unregistered_loopback(guarded): - with pytest.raises(PermissionError, match='unit isolation'): - socket.socket().connect(('127.0.0.1', 8000)) - - -@pytest.mark.parametrize('path', ['/home/synthetic-ambient-unit-fixture/.vault-token', - '/root/.kube/config', '/proc/self/environ', - '/tmp/warden-synthetic-ambient-home/.vault-token']) -def test_ambient_paths_refused_without_opening_them(guarded, path): - with pytest.raises(PermissionError, match='unit isolation'): - Path(path).read_bytes() - - -def test_symlink_cannot_escape_fixture_root(guarded, tmp_path): - link = tmp_path / 'ambient' - link.symlink_to('/home/synthetic-ambient-unit-fixture/.vault-token') - with pytest.raises(PermissionError, match='unit isolation'): - link.read_bytes() - - -def test_child_drops_even_synthetic_token_environment(guarded, monkeypatch): - monkeypatch.setenv('BAO_TOKEN', 'synthetic-only-never-a-real-token') - monkeypatch.setenv('VAULT_TOKEN', 'synthetic-only-never-a-real-token') - monkeypatch.setenv('HOME', '/home/synthetic-ambient-unit-fixture') - result = subprocess.run([sys.executable, str(ROOT / 'tests/unit_isolation_probe.py'), 'state'], - capture_output=True, text=True, check=True, env=dict(os.environ)) - assert json.loads(result.stdout) == {'bao_token_present': False, - 'vault_token_present': False, 'home_in_temporary': True} - - -@pytest.mark.parametrize('mode', ['socket', 'provider', 'home', 'nested']) -def test_guard_exists_in_each_allowed_python_child(guarded, mode): - result = subprocess.run([sys.executable, str(ROOT / 'tests/unit_isolation_probe.py'), mode], - capture_output=True, text=True, check=True) - assert result.stdout == 'denied\n' - - -def test_forged_provider_double_refused(guarded, tmp_path): - provider = tmp_path / 'bao' - provider.write_text('#!/bin/bash\necho not-the-reviewed-fixture\n') - provider.chmod(0o700) - with pytest.raises(PermissionError, match='double bytes differ'): - subprocess.run([str(provider), 'token', 'lookup'], check=True) - - -def test_os_open_truncation_respects_read_only_source_root(guarded, tmp_path, monkeypatch): - # Synthetic readable source outside the guard's writable subtree. - source = tmp_path / 'synthetic-source' - source.write_bytes(b'synthetic public source') - monkeypatch.setattr(guarded, 'temporary', tmp_path / 'writable') - with pytest.raises(PermissionError, match='unit isolation'): - descriptor = os.open(source, os.O_RDONLY | os.O_TRUNC) - os.close(descriptor) - assert source.read_bytes() == b'synthetic public source' diff --git a/tests/test_scaffold.py b/tests/test_scaffold.py deleted file mode 100644 index ad1c2bd..0000000 --- a/tests/test_scaffold.py +++ /dev/null @@ -1,193 +0,0 @@ -from copy import deepcopy -import socket -import subprocess - -import pytest -import yaml -from typer.testing import CliRunner - -from warden.cli import app -from warden.routing.catalog import Catalog, load_catalog -from warden.routing.scaffold import ScaffoldError, render, scaffold - - -@pytest.fixture -def draft_request(): - return { - "schema_version": 1, - "lane_id": "example-workload-kv", - "title": "Example workload read", - "need_keywords": ["example workload"], - "consumer_repo": "example-consumer", - "owner_repo": "railiance-platform", - "workload_ref": { - "applicability": "applicable", - "unknown_reason": "Synthetic fixture has no published deployment identity", - }, - "risk": "standard", - "wiki_ref": "railiance-platform:docs/catalog-lane-scaffold.md", - "canon_ref": "railiance-platform:credential-change-requests/CCR-2026-9999.yaml", - "platform_request_ref": "railiance-platform:examples/catalog-lane-scaffold/platform-request.yaml", - "reviewed": "2026-10-04", - } - - -def empty_catalog(tmp_path): - return Catalog(path=tmp_path / "unused.yaml", entries=[]) - - -def test_deterministic_draft_roundtrip_and_input_unchanged(draft_request, tmp_path): - before = deepcopy(draft_request) - contents = render(draft_request, empty_catalog(tmp_path)) - assert contents == render(draft_request, empty_catalog(tmp_path)) - assert draft_request == before - for name in ["one", "two"]: - source = tmp_path / f"{name}.yaml" - source.write_text(yaml.safe_dump(draft_request)) - paths = scaffold(source, tmp_path / name, catalog=empty_catalog(tmp_path)) - assert len(paths) == 3 - assert (tmp_path / "one/catalog.yaml").read_bytes() == ( - tmp_path / "two/catalog.yaml" - ).read_bytes() - entry = load_catalog(tmp_path / "one/catalog.yaml").entries[0] - assert entry.status == "draft" - assert not entry.exec_capable and not entry.warden_executes - assert entry.fetch_command is None - assert entry.workload_ref.resolution == "unknown" - assert entry.workload_ref.name is None - assert entry.risk == "standard" - assert yaml.safe_load((tmp_path / "one/request.yaml").read_text()) == draft_request - - -@pytest.mark.parametrize("risk", ["high", "ungraded"]) -def test_explicit_risk_is_preserved(draft_request, tmp_path, risk): - draft_request["risk"] = risk - raw = yaml.safe_load(render(draft_request, empty_catalog(tmp_path))["catalog.yaml"]) - assert raw["entries"][0]["risk"] == risk - - -@pytest.mark.parametrize( - "mutation", - [ - lambda r: r.update(exec_capable=True), - lambda r: r.pop("risk"), - lambda r: r.update(risk=""), - lambda r: r.update(schema_version=True), - lambda r: r.update(lane_id="../escape"), - lambda r: r.update(reviewed="yesterday"), - lambda r: r.update(need_keywords=[]), - lambda r: r.update(workload_ref={"applicability": "applicable"}), - lambda r: r.update( - workload_ref={ - "applicability": "applicable", - "unknown_reason": "unknown", - "name": "inferred", - } - ), - lambda r: r.update( - workload_ref={ - "applicability": "applicable", - "unknown_reason": "unknown", - "role": "admin", - } - ), - ], -) -def test_invalid_request_publishes_nothing(draft_request, tmp_path, mutation): - mutation(draft_request) - source = tmp_path / "input.yaml" - source.write_text(yaml.safe_dump(draft_request)) - with pytest.raises(ScaffoldError): - scaffold(source, tmp_path / "stage", catalog=empty_catalog(tmp_path)) - assert not (tmp_path / "stage").exists() - - -@pytest.mark.parametrize( - "field", ["title", "platform_request_ref", "need_keywords", "workload_ref"] -) -def test_secret_rejection_does_not_echo_value(draft_request, tmp_path, field): - token = "ghp_" + "Ab12" * 12 - draft_request[field] = ( - [token] - if field == "need_keywords" - else {"applicability": "applicable", "unknown_reason": token} - if field == "workload_ref" - else token - ) - with pytest.raises(ScaffoldError) as exc: - render(draft_request, empty_catalog(tmp_path)) - assert token not in str(exc.value) - assert "Ab12" not in str(exc.value) - - -def test_existing_lane_collision_even_when_draft(draft_request, tmp_path): - raw = render(draft_request, empty_catalog(tmp_path))["catalog.yaml"] - path = tmp_path / "catalog.yaml" - path.write_text(raw) - with pytest.raises(ScaffoldError, match="already exists"): - render(draft_request, load_catalog(path)) - - -@pytest.mark.parametrize("symlink", [False, True]) -def test_preexisting_destination_is_preserved(draft_request, tmp_path, symlink): - source = tmp_path / "input.yaml" - source.write_text(yaml.safe_dump(draft_request)) - stage = tmp_path / "stage" - if symlink: - stage.symlink_to(tmp_path / "missing") - else: - stage.mkdir() - (stage / "keep").write_text("original") - with pytest.raises(ScaffoldError, match="already exists"): - scaffold(source, stage, catalog=empty_catalog(tmp_path)) - if not symlink: - assert (stage / "keep").read_text() == "original" - else: - assert stage.is_symlink() - - -def test_cli_offline_and_bad_yaml_diagnostic(draft_request, tmp_path, monkeypatch): - def forbidden(*args, **kwargs): - raise AssertionError("credential/network execution forbidden") - - monkeypatch.setattr(socket, "create_connection", forbidden) - monkeypatch.setattr(subprocess, "run", forbidden) - monkeypatch.setenv("WARDEN_MEMORY", "0") - source = tmp_path / "input.yaml" - source.write_text(yaml.safe_dump(draft_request)) - # Override live catalog with a different inert draft lane. - other = deepcopy(draft_request) - other["lane_id"] = "other-example" - catalog_path = tmp_path / "catalog.yaml" - catalog_path.write_text(render(other, empty_catalog(tmp_path))["catalog.yaml"]) - monkeypatch.setenv("WARDEN_ROUTING_CATALOG", str(catalog_path)) - result = CliRunner().invoke( - app, - [ - "route", - "scaffold", - "--input", - str(source), - "--output-dir", - str(tmp_path / "stage"), - ], - ) - assert result.exit_code == 0, result.output - assert ( - load_catalog(tmp_path / "stage/catalog.yaml").entries[0].id - == draft_request["lane_id"] - ) - source.write_text("title: [ghp_private\n") - result = CliRunner().invoke( - app, - [ - "route", - "scaffold", - "--input", - str(source), - "--output-dir", - str(tmp_path / "bad"), - ], - ) - assert result.exit_code == 1 - assert "ghp_private" not in result.output diff --git a/tests/test_workload_join.py b/tests/test_workload_join.py index bbca41f..559a875 100644 --- a/tests/test_workload_join.py +++ b/tests/test_workload_join.py @@ -1,7 +1,5 @@ """Explicit lane-to-workload join tests (WARDEN-WP-0032 / RMGR-WP-0010-T06).""" from pathlib import Path -import hashlib -import json import yaml @@ -12,33 +10,15 @@ ROOT = Path(__file__).resolve().parents[1] def test_repo_catalog_uses_only_explicit_workload_references(): - report = build(ROOT / "registry/routing/catalog.yaml", - ROOT / "tests/fixtures/workload-coverage") + report = build(ROOT / "registry/routing/catalog.yaml", Path.home()) assert report["ok"] is True - assert len(report["resolved"]) == 4 - assert len(report["unknown"]) == 22 - # 15 since 2026-09-21: the layer-declaration route is repository conformance - # guidance, not a workload delivery lane, and the two NetKingdom SSO lanes - # (c374d41) remain provider/control-plane credentials. Lanes added after - # 2026-09-09 moved the denominator without moving `resolved`; these counts - # and pep-stance.yaml's classification_coverage measure the same populations - # and must be updated together. 16 since 2026-10-05: the - # openbao-structure-survey pointer (WARDEN-WP-0042) is a builder survey - # under a founder-minted grant, not a workload credential. 17 since - # 2026-10-08: platform-supplier-credentials (WARDEN-WP-0043) is a generic - # supplier-account path pattern; concrete consumers keep their own lanes. - # unknown 22 since 2026-10-09: fluid-telegram-operator (MASON-WP-0005-T06) - # names fluid-telegram, which has published no workload declaration. - # not_applicable 18 since 2026-10-10: keycape-client-secret-generate is a - # per-case generation lane; each case's consumer gets its own entry. - # not_applicable 22 since 2026-10-17: the four CATALOG-1 draft lanes - # (nk48 1.3 owner review) are per-case lanes like the one above. - assert len(report["not_applicable"]) == 23 + assert len(report["resolved"]) == 3 + assert len(report["unknown"]) == 17 + assert len(report["not_applicable"]) == 9 assert {row["lane"] for row in report["resolved"]} == { "ops-warden-warden-sign-token", "issue-core-ingestion-api-key", "rapp-qonto-keycape-client", - "core-hub-forgejo-source-read", # CCR-2026-0031, 2026-10-03 } @@ -70,14 +50,3 @@ def test_invalid_exact_deployable_resolves_unknown(tmp_path): assert not report["resolved"] assert report["unknown"][0]["lane"] == "x" assert "deployable" in report["unknown"][0]["reason"] - - -def test_public_workload_projection_matches_provenance(): - base = ROOT / 'tests/fixtures/workload-coverage' - provenance = json.loads((base / 'provenance.json').read_text()) - expected = {'rapp-core-hub', 'rapp-issue-core', 'rapp-qonto'} - assert len(provenance) == len(expected) - assert {row['repo'] for row in provenance} == expected - for row in provenance: - projection = base / row['repo'] / 'declarations/rapp.yaml' - assert hashlib.sha256(projection.read_bytes()).hexdigest() == row['projection_sha256'] diff --git a/tests/unit_isolation_probe.py b/tests/unit_isolation_probe.py deleted file mode 100644 index d4c6596..0000000 --- a/tests/unit_isolation_probe.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Closed child regression probes; executed only under the unit runner guard.""" -import json -import os -from pathlib import Path -import socket -import subprocess -import sys - -mode = sys.argv[1] -if mode == 'state': - print(json.dumps({'bao_token_present': 'BAO_TOKEN' in os.environ, - 'vault_token_present': 'VAULT_TOKEN' in os.environ, - 'home_in_temporary': Path(os.environ['TMPDIR']) in Path.home().parents})) -else: - try: - if mode == 'socket': - socket.socket().connect(('127.0.0.1', 8000)) - elif mode == 'provider': - subprocess.run(['bao', 'token', 'lookup'], check=True) - elif mode == 'home': - Path('/home/synthetic-ambient-unit-fixture/.vault-token').read_text() - elif mode == 'nested': - subprocess.run([sys.executable, '-c', 'print("SYNTHETIC-SECRET")'], check=True) - else: - raise AssertionError('unknown probe') - except PermissionError: - print('denied') - else: - raise AssertionError('external operation was not refused') diff --git a/wiki/ActorInventoryPatterns.md b/wiki/ActorInventoryPatterns.md index 0ec6263..23d8c72 100644 --- a/wiki/ActorInventoryPatterns.md +++ b/wiki/ActorInventoryPatterns.md @@ -58,40 +58,6 @@ agt-codex-interhub-bootstrap: description: "Short-lived agent access for Inter-Hub bootstrap execution" ``` -### Supervised interactive agent account (`agt-custodian`) - -CUST-WP-0073-T02 / WARDEN-WP-0045. One principal for the dedicated host account -`custodian-agt` on Railiance01, shared by the Codex, Claude Code and Grok Build -harnesses running in the isolated workstation WSL distro `custodian-agents`. - -```yaml -agt-custodian: - type: agt - principals: [agt-custodian] - ttl_hours: 12 - max_ttl_hours: 12 # --ttl cannot raise it to the 24 h agt maximum - extensions: [permit-pty] # clears agent/port/X11 forwarding and user-rc -``` - -- **TTL 12 h.** Between the attended bootstrap profile (2 h) and the agt type - maximum (24 h): one supervised working session, so a leaked cert expires by the - next day and every new session needs a fresh supervisor issuance. The profile - ceiling (`max_ttl_hours`) stops a `--ttl` override from lengthening it. -- **Extensions.** `permit-pty` only — interactive harness shells need a pty. - Agent, port and X11 forwarding and `~/.ssh/rc` are not granted (`-O clear` - locally; explicit `extensions` map on the Vault role, which must allow - `permit-pty`). -- **No `source-address`.** The workstation egress IP is not stable (residential - dynamic address, WSL NAT), so a pinned address would break issuance silently. - Revisit if agent traffic moves to a fixed egress (VPN or bastion). -- **No self-issuance.** The supervisor (Bernd) signs from the operator account - with `warden sign agt-custodian --pubkey .pub > -cert.pub` - and places the cert into the `custodian-agents` distro. That distro gets no - warden config, CA key, Vault token or `warden-sign` lane credential, so the - signing path is not reachable from the agent account. -- Host mapping (`AuthorizedPrincipalsFile` for `custodian-agt`) is owned by - railiance-infra. - - Prefer **1–2 h TTL** for bootstrap; never multi-day agent certs. - Principal narrower than general ops access (`agt-interhub-bootstrap` not `agt-ops-full`). - Remove or disable actor when lane is retired. diff --git a/wiki/ApprovalConsumption.md b/wiki/ApprovalConsumption.md deleted file mode 100644 index e0b2e6a..0000000 --- a/wiki/ApprovalConsumption.md +++ /dev/null @@ -1,85 +0,0 @@ -# Approval consumption — notes for the signing lane's future consume - -**Status:** notes only. **Nothing is wired.** ops-warden's pre-sign gate consults -flex-auth (`wiki/PolicyGatedSigning.md`); it does not yet consume an approval -artifact. This page exists so the corrections we have already been given survive -until it does, rather than sitting in an inbox thread. - -Owner of the contract: `gate-house`, `docs/contracts/approval-consumption.md`. -ops-warden is one of two PEP-shaped consumers named there (the SSH signing lane). - -## Validate across two artifacts, never one - -`GH-DEC-2026-005` settles the step-1 artifact: `GET /v1/approvals/{id}/claim` -serves the **approval-claim**. flex-auth's proposed `ActionAuthorization` is not -required there and will not be served from that endpoint. - -| Artifact | Carries | -| --- | --- | -| approval-claim (step 1) | the approval fact — binding digest, validity window, consumption state, freshness, issuer | -| `DecisionEnvelope` (step 2) | exact `CheckRequest` match, policy pin | - -v0.8 §6.4 obligation 5 hardened this: each artifact **must** be validated against -the layer that owns its data, and a PIP **must not** republish the PDP's decision. -Neither artifact may be taken from the other. - -**One narrow exception now exists (`GH-DEC-2026-015`, 2026-09-10).** It revised -`GH-DEC-2026-012`'s R3 to permit nesting a binding digest inside a presentation -hash **for one specific pair, conditioned**. It does not reach this lane and -requires nothing here. It is recorded on this page for one reason: if a composed -artifact ever appears on the approval path, read it as a permission that may -exist rather than as a violation of the rule above — and then go and check which -pair the permission covers, because it is not general. - -## Three things that are easy to get wrong - -**1. Do not require `provenance.authority == 'state-hub'`.** - -State Hub is a read model with no runtime approval authority. This is easy to get -wrong precisely *because* the hub is where everything else about a workplan lives, -so a consumer reaches for it as the authority on approval too. - -Worth holding as a shape rather than an instance: a PEP requiring the hub to have -asserted an approval is the same boundary violated from the **consuming** side -that "never register workplans or tasks by hand" forbids from the **writing** -side. Same rule, two directions. - -**2. The digest comparison target was corrected. Use `binding.approval_binding_digest`.** - -`GH-DEC-2026-008` as originally issued required the claim's `pdp_digest` to equal -the decision's `request_digest`. That is unimplementable — a claim travelling -inside a hashed request cannot name the digest of the request containing it — and -a fail-closed consumer obeying it would have denied permanently. Corrected by -`FLEX-DEC-2026-007` to the PDP's published **exclusion-scoped** digest, -`binding.approval_binding_digest`. - -ops-warden never copied the superseded wording anywhere (checked 2026-09-09), so -there was nothing to unwind. Recorded here so it cannot be reintroduced from an -older reading of the contract. - -**3. Digest comparison does not discharge attribution, and attribution is not -available today.** - -`GH-DEC-2026-010` rules that a PEP must be able to **attribute** a decision to -access-engine, and that matching digests does not achieve that. flex-auth's -envelope is unsigned and its pins serve plain HTTP, so that half is a declared -§13 gap owned by access-engine until `FLEX-WP-0024` lands. - -Nothing for ops-warden to build. But when this lane is wired, its validation is -**complete-minus-attribution** and must be described that way: fail-closed -protects against a decision point that is absent, not against one that lies. - -## Open dependency - -`WARDEN-WP-0039-T03` — whether an admitted flex-auth contract exists for a -delegated credential read where caller and resource owner differ. If the answer -changes the caller shape it may reach how this lane's consume is built, and -`gate-house` has asked to hear that before rather than after: a contract admitted -for that shape would reach §6.4 obligation 5, and whether such a delegation -attenuates or widens is doctrine they own. - -## See also - -- `wiki/PolicyGatedSigning.md` — the pre-sign gate as it actually is today -- `pep-stance.yaml` — unreachable-engine stance, and the marked `unknown` cell -- `history/2026-09-09-layer-model-v08-review.md` — the v0.8 assent round diff --git a/wiki/AuditTrail.md b/wiki/AuditTrail.md index aba9036..78b34df 100644 --- a/wiki/AuditTrail.md +++ b/wiki/AuditTrail.md @@ -32,20 +32,6 @@ fresh file starts. high-entropy runs). Signing and proxy paths swallow audit failures so gatekeeping never blocks the primary action — but tests prove values cannot be written. -> **Absence of a record is not evidence of absence.** Because emission never -> blocks the primary action (`src/warden/ca.py`), a failed append loses the event -> while the action still happens. This trail proves that the records it holds were -> not altered or truncated; it does **not** prove that every action produced one. -> Do not reason from a missing entry. This is the estate-wide bound in -> `security-layer-model_v0.4` §9.6 — completeness is the source's obligation, and -> **Ruled 2026-08-29** (`security-layer-model_v0.6` §9.6): this trail is -> **attributive**, not load-bearing — no control branches on the presence of a -> signing record — so the non-atomic trade is legitimate, and the obligations are -> to declare it (this note) and never to claim completeness. Atomicity is required -> only where a control's soundness depends on an event being present or absent. -> Registered in the standard's §13 open-gap table as self-declared. If a future -> control ever gates on this trail, the trade must be revisited before it ships. - --- ## Query diff --git a/wiki/CatalogLaneScaffoldContract.md b/wiki/CatalogLaneScaffoldContract.md deleted file mode 100644 index a0c8a7a..0000000 --- a/wiki/CatalogLaneScaffoldContract.md +++ /dev/null @@ -1,114 +0,0 @@ -# Catalog lane scaffold contract — P4 owner proposal - -Date: 2026-10-03 · Owner: ops-warden · Work: WARDEN-WP-0041 -Origin: CUST-WP-0075; proposal 79549660-4fc8-44d5-989e-074f6c7b6f07. -Status: proposed seam for railiance-platform review. This document authorizes no apply. - -## Existing surfaces - -`src/warden/plan.py::_ccr_stub` offers a prose checklist for an unroutable need. -It does not create files or a CCR-schema-valid request. `RouteEntry` and -`routing/catalog.py` already define and validate the pointer catalog; -`wiki/playbooks/catalog-lane-promotion.md` defines promotion evidence. -The platform's `scripts/credential-change.py` already validates CCR v1, -renders policy/role metadata and operator runbooks, and records lifecycle evidence. -The missing piece is reviewed draft generation across these owner surfaces, -not another policy applier or approval implementation. - -## Ownership and proposed input - -The first supported shape is one exact KV-v2 workload read lane. Login, -ceremony, dynamic-secret and tenant paths need their own reviewed extensions; -no generic fallback may manufacture an identity or auth binding. - -A proposed non-secret input document has `schema_version: 1`, `lane_id`, -`title`, `need_keywords`, `consumer_repo`, `owner_repo`, `workload_ref`, -`risk`, `wiki_ref`, `canon_ref`, and `platform_request_ref`. -`workload_ref` uses the existing catalog structure without inference from a -repository, lane name or secret path. Missing owner identity remains explicit -`unknown_reason`. `risk` must be supplied and covers the entire readable path. -`platform_request_ref` points to the platform-owned draft request rather than -duplicating its path, field set, policies, auth-role claims or ESO bindings. -The two references must point to their reviewed eventual owner destinations. - -ops-warden owns validation of those catalog fields and a draft catalog fragment -plus playbook checklist. It owns no CCR renderer. railiance-platform owns the -CCR schema/input extension, CCR draft, policy/role rendering, ESO manifest and -evidence scaffold under RPF-WP-0050-T04. Its implementation must decide and -publish the precise platform request schema and invocation; this proposal -contains no invented command to invoke it. - -## Output and verification contract - -Generation writes into a caller-selected staging directory and fails on any -existing output by default. It does not insert into a live catalog, modify an -owner repo, call OpenBao, query credentials, approve a request or contact a -cluster. File names and references are deterministic and contain no values. - -The catalog fragment always has `status: draft`, `warden_executes: false`, -`exec_capable: false`, and no executable fetch command. Delegation and rotation -remain explicit owner-reviewed questions, never an inferred permanent proxy. -The playbook is a checklist with owner pointers, not a copied platform procedure. - -Platform output remains `status: proposed`, frontdoor pending-review and -unresolvable, with no approval, bound-claims confirmation or verification -receipt invented. A draft is not an admission or a capability. ExternalSecret -metadata must retain the current annotation-containment requirements. Evidence -scaffolds use metadata/capability checks and contain no secret reads or raw -subprocess error dumps. The platform owner chooses those implementations. - -Offline acceptance covers schema/unknown-field errors, collision refusal, -round-trip catalog validation, missing identity, explicit risk, secret-material -rejection, immutable input, no network/subprocess execution, and all outputs -remaining draft. A representative platform fixture must prove cross-owner -references and field mapping before end-to-end generation is claimed. - -## Activation remains separate - -Review and merge of generated metadata does not replace CCR approval, -confirmed identity/auth bindings, attended value provisioning, deployment -approval or positive/negative evidence. Existing catalog promotion criteria -still apply. Novel authority, trust-root or value-delivery design needs its -own work record; routine lanes fitting an accepted template may become PRs. -The proposal's “only the value step needs the window” is a goal, not a claim -that current approvals disappeared. - -## Evidence receipt - -Source review: catalog models/validator, `_ccr_stub`, promotion checklist, -platform credential-change validator and CCR-2026-0031 (metadata only). -Knowledge: emergent-security 0.2.6, start and local-work, verified-files, -orientation-only; package digest -`8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02`. - - -## Offline catalog command — 2026-10-04 - -`warden route scaffold --input --output-dir ` -validates the closed v1 request against catalog entry rules and refuses an -existing catalog lane ID or staging destination. The request fields above also -require explicit `reviewed: YYYY-MM-DD` text for deterministic review metadata. -Explicit `ungraded` risk is retained with the catalog's existing fail-safe meaning; -no default risk or workload identity is inferred. Outputs are `catalog.yaml`, -`playbook.md` and the unchanged semantic request in `request.yaml`. The output -catalog is draft with execution disabled and no fetch command. This command -does not invoke a platform renderer or replace the owner's admission lifecycle. - - -## Cross-owner offline verification — 2026-10-04 - -Platform owns `scripts/credential-change.py scaffold-lane --input - --catalog-manifest --output-dir -`. Its accepted synthetic paired manifests are at -`railiance-platform/examples/catalog-lane-scaffold/`. The platform schema and -renderer remain authoritative there. Warden emits three staging artifacts; -platform emits six proposed review artifacts, including its CCR, policy/auth -drafts, ESO metadata and unexecuted evidence checklist. - -The exact shared fixture preserves lane, owner, consumer, risk, CCR pointer -and manifest-byte digest across both commands. Warden's unknown workload -identity remains unknown; platform claims remain unconfirmed. Both supported -commands were executed offline, with no credential or production traffic. -The retained receipt is `docs/evidence/2026-10-04-catalog-scaffold.json`. -Routine exact supported lanes may now begin from these draft commands; -novel authority or custody flows retain their separately reviewed work. diff --git a/wiki/CredentialRouting.md b/wiki/CredentialRouting.md index a49acc7..eef4a91 100644 --- a/wiki/CredentialRouting.md +++ b/wiki/CredentialRouting.md @@ -13,14 +13,6 @@ for `exec_capable` catalog lanes, can proxy the owner's tool **as the caller**. That is a transparent conduit, not custody: do not paste secrets into Git, State Hub, agent chat, or workplans. -Practice follows SecurityCanon `SEC-DSM` 1.0.0 -(`security-canon/infospace/standards/devops-secrets-management/SecurityCanonDevOpsSecretsManagementStandard.md`), -which Gate House teaches. Prefer, in order: native identity, federated identity, -an ephemeral credential, an automatically rotated secret, then a vaulted static -secret (§5, decision procedure §37). SSH certificates are the ephemeral path -this repo issues. A static secret stays in the owner's custody. This page routes -to that owner. It does not collect the value. - --- ## Quick decision tree @@ -99,7 +91,6 @@ lists the interim set — WARDEN-WP-0030). | `ops-warden-warden-sign-token` | "railiance-platform broker owns the `warden-sign` lease — use `credential exec`" | `railiance-platform/scripts/credential.py exec --grant ops-warden/warden-sign` (see playbook) | | `openbao-api-key` | "OpenBao owns this — here is the path/command shape" | Call OpenBao directly, or use `warden access --fetch/--exec` as yourself when the lane is `exec_capable` | | `openbao-platform-admin-login` | "This is an attended OpenBao administration identity act, not workload secret retrieval" | Use KeyCape-backed OIDC/MFA at `auth/netkingdom`, role `platform-admin`; never substitute a workload role or root token | -| `kubernetes-railiance01-admin` | "Attended railiance01 admin kubeconfig; the agent default path is ops-mason-builder" | Prefix kubectl with KUBECONFIG=/home/worsch/.kube/config-railiance01. Do not fetch the file. | | `openbao-shamir-recovery-ceremony` | "This is an attended trust-root ceremony, not secret provisioning" | Obtain one explicit approval and follow the railiance-platform recovery checklist with the existing out-of-band custodians; never send shares through Warden | | `flex-auth-policy-check` | "flex-auth decides — here is the policy doc" | Query flex-auth / embed the PEP | | `key-cape-oidc-login` | "key-cape / Keycloak owns identity" | Authenticate via IAM Profile, or use the `warden access` login lane as yourself | @@ -116,8 +107,6 @@ lists the interim set — WARDEN-WP-0030). | `binky-qonto-api` | "tenant Qonto API on `tenants/binky/qonto-api` (API_KEY+API_USER)" | `warden access binky-qonto-api --out FILE` (`risk: high`) | | `net-kingdom-lldap-bind-credential` | "railiance-platform custody for the LLDAP bind credential used by identity-provisioner and the privacyIDEA resolver" | Follow the owner-approved NetKingdom reconciliation runbook; lane is currently unresolved and never fetches a value | | `net-kingdom-privacyidea-admin-token` | "railiance-platform custody for the privacyIDEA administrative token used by attended resolver reconciliation" | Follow the owner-approved NetKingdom reconciliation runbook; lane is currently unresolved and never fetches a value | -| `keycape-client-secret-generate` | "railiance-platform's broker generates a KeyCape client secret inside custody for an accepted SecurityCase. The value is never returned" | `bao login` to role `credential-broker-keycape-client-secret-generate`, then `credential.py generate --case-file ... --case-ref ...` (see playbook). Agents are refused | -| `audit-core-senders` | "ops-mason built the audit-core sender registry in OpenBao. audit-core reads it through ESO only" | Nobody reads the registry. A sender gets its own exact-path projection (see playbook table) | Promotion criteria: `wiki/playbooks/catalog-lane-promotion.md`. High-risk / agent boundary: `wiki/playbooks/agent-read-boundary.md`. @@ -132,7 +121,6 @@ EXPOSED taint: `wiki/playbooks/exposed-taint.md` · `warden taint `. | --- | --- | --- | | `object-storage-sts` | NK-WP-0007 STS vending path | `wiki/playbooks/object-storage-sts.md` | | `database-dynamic-credentials` | OpenBao database secrets engine | `wiki/playbooks/database-dynamic-credentials.md` | -| `fluid-telegram-operator` | ops-mason attended OIDC lane for the Hall of Helix campaign (pending owner-run checks) | `wiki/playbooks/fluid-telegram-operator.md` | ops-warden answers *where + who + how*. The worker still acts on the owning system. When `warden access` proxies a non-SSH lane, it does so as the caller and stores no @@ -140,20 +128,6 @@ value; the owner remains OpenBao, key-cape, flex-auth, or the routed subsystem. --- -## Policy gate for proxied credential reads - -`warden access ` with `--exec`, `--fetch`, `--to-file`, `--wrap` or -`--fingerprint` runs a flex-auth `read` check before any fetch. That check goes -to the **fetch evaluator** `flex-auth-ops-warden-fetch` -(`policy.fetch_flex_auth_url`, workstation `http://127.0.0.1:19091` through -ops-bridge tunnel `flex-auth-ops-warden-fetch-railiance01`). It does not go to -the SSH signing pin `flex-auth-ops-warden` (`policy.flex_auth_url`, -`127.0.0.1:19090`), which admits only the `ops-warden` system. If the fetch -evaluator is unset or unreachable, the fetch is refused (exit 4). It is never -sent to the signing pin and never allowed by default. The `gate` line of -`warden access ` names the evaluator in use. Config: -`wiki/OpsWardenConfig.md` → "Two evaluators" (CUST-WP-0075). - ## Examples — do NOT ask ops-warden to own or vend | Request | Correct path | diff --git a/wiki/OpenBaoSshEngineChecklist.md b/wiki/OpenBaoSshEngineChecklist.md index dfcfe88..a8c17f9 100644 --- a/wiki/OpenBaoSshEngineChecklist.md +++ b/wiki/OpenBaoSshEngineChecklist.md @@ -14,7 +14,7 @@ Cluster bootstrap and unseal are **not** ops-warden scope — see - [ ] `bao status` reports initialized and **unsealed** - [ ] Operator has **scoped token** — not root token in `VAULT_TOKEN` for daily warden use - [ ] `warden.yaml` points `vault.addr` at correct endpoint: - - Workstation: `http://127.0.0.1:18200` (ops-bridge tunnel; public `bao.coulomb.social` is retired) + - Workstation: `https://bao.coulomb.social` - In-cluster: `http://openbao.openbao.svc.cluster.local:8200` - [ ] Actor exists in inventory — `wiki/ActorInventoryPatterns.md` - [ ] Test pubkey available (mode 600 private key, never commit) @@ -87,7 +87,7 @@ path "ssh/sign/agt-role" { ```yaml backend: vault vault: - addr: http://127.0.0.1:18200 # ops-bridge tunnel + addr: https://bao.coulomb.social mount: ssh role_map: adm: adm-role diff --git a/wiki/OpsWardenConfig.md b/wiki/OpsWardenConfig.md index b89eb89..4f65114 100644 --- a/wiki/OpsWardenConfig.md +++ b/wiki/OpsWardenConfig.md @@ -36,12 +36,10 @@ ca_key: ~/.ssh/ops-ca-user inventory_path: ~/.config/warden/inventory.yaml state_dir: ~/.local/state/warden -# Zone-aware flex-auth gate. With no signing URL, the explicit unknown/build -# profile fails open for signing and records that no evaluator decision was -# available. Fetch checks need fetch_flex_auth_url and fail closed without it. +# Zone-aware flex-auth gate. With no URL, the explicit unknown/build profile +# fails open and records that no evaluator decision was available. policy: flex_auth_url: http://127.0.0.1:8080 - fetch_flex_auth_url: http://127.0.0.1:8081 zone_registry_path: /path/to/compiled-flex-auth-registry.json failure_modes: z0-experimental: fail_open @@ -79,10 +77,8 @@ engine that implements the Vault signing API (`POST /v1//sign/`). backend: vault vault: - # OpenBao API through the ops-bridge tunnel (railiance01). The public - # https://bao.coulomb.social endpoint is retired and serves a notice only. - # Prefer short-lived tokens from policy, not root. - addr: http://127.0.0.1:18200 + # OpenBao UI/API (KeyCape OIDC). Prefer short-lived tokens from policy, not root. + addr: https://bao.coulomb.social mount: ssh @@ -100,7 +96,6 @@ state_dir: ~/.local/state/warden # Configure after flex-auth ssh-certificate policies are deployed: # policy: # flex_auth_url: http://flex-auth-ops-warden.flex-auth.svc.cluster.local:8080 -# fetch_flex_auth_url: http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080 # zone_registry_path: /etc/warden/production_registry_snapshot.json ``` @@ -258,7 +253,6 @@ zone. Signing records `policy_decision_id` when present plus `policy_zone`, ```yaml policy: flex_auth_url: http://127.0.0.1:8080 - fetch_flex_auth_url: http://127.0.0.1:8081 zone_registry_path: registry/flex-auth/production_registry_snapshot.json failure_modes: z0-experimental: fail_open @@ -280,32 +274,6 @@ for an unavailable or invalid evaluator. Full request shape and rollout notes: `wiki/PolicyGatedSigning.md`. -### Two evaluators: signing and fetch checks (CUST-WP-0075) - -| Key | Pin | Used by | Admits | -|-----|-----|---------|--------| -| `policy.flex_auth_url` | `flex-auth-ops-warden` (workstation `127.0.0.1:19090`) | `warden sign`, `warden issue` | `resource.system: ops-warden` only | -| `policy.fetch_flex_auth_url` | `flex-auth-ops-warden-fetch` (workstation `127.0.0.1:19091`) | `warden access ` fetch transports (`check_fetch_policy`) | `read` for catalog lanes with status `active`, `exec_capable: true` and lane `secret`, for owner systems bound in access-engine `values/ops-warden-fetch.yaml` | - -Both use the same caller identity (`policy.caller_auth`: the ops-warden service -account token, audience `flex-auth`). For fetches a missing caller token refuses -the fetch instead of sending an unauthenticated request. - -The fetch check never falls back to `flex_auth_url`: the signing pin binds only -the `ops-warden` system, so every fetch for another owner would get 403. If -`fetch_flex_auth_url` is unset or the evaluator is unreachable, the fetch fails -closed (exit 4) with a message naming the missing fetch evaluator, whatever the -`unknown` failure mode says. An explicit 401/403 still maps to "flex-auth -refused fetch policy check (HTTP 403); caller authentication or system binding -was rejected". `warden access ` prints which evaluator gates fetches on -its `gate` line (or `policy` line). - -In-cluster address: `http://flex-auth-ops-warden-fetch.flex-auth.svc.cluster.local.:8080` -(keep the trailing dot). access-engine `tools/gen_ops_warden_fetch.py` generates -the pin's readable set from this repo's routing catalog. When you add or retire a -lane of that kind, tell access-engine so it can regenerate the set and roll a new -image. Deployment notes: access-engine `examples/ops-warden-fetch/README.md`. - --- ## Environment variables @@ -334,29 +302,6 @@ tunnels: cert_command: "warden sign agt-state-hub-bridge --pubkey ~/.ssh/agt-state-hub-bridge_ed25519.pub" ``` -Workstation tunnels to the two flex-auth pins (`remote_host` is each pin -Service's ClusterIP, see access-engine `examples/ops-warden-fetch/README.md`): - -```yaml -tunnels: - flex-auth-ops-warden-railiance01: # signing pin -> policy.flex_auth_url - host: 92.205.62.239 - direction: local - remote_host: - remote_port: 8080 - local_port: 19090 - health_check: - url: http://127.0.0.1:19090/healthz - flex-auth-ops-warden-fetch-railiance01: # fetch pin -> policy.fetch_flex_auth_url - host: 92.205.62.239 - direction: local - remote_host: - remote_port: 8080 - local_port: 19091 - health_check: - url: http://127.0.0.1:19091/healthz -``` - `ops-bridge` runs `cert_command` before each SSH launch, captures stdout as the cert, and passes it alongside the private key via `ssh -i -i `. See `wiki/CertCommandInterface.md` for the full contract and diff --git a/wiki/PolicyGatedSigning.md b/wiki/PolicyGatedSigning.md index 85f53ea..b711a32 100644 --- a/wiki/PolicyGatedSigning.md +++ b/wiki/PolicyGatedSigning.md @@ -78,12 +78,6 @@ python3 scripts/build_flex_auth_registry.py \ -o registry/flex-auth/production_registry_snapshot.json ``` -The committed snapshot is built from `examples/inventory.seed.yaml` with -`--zone-resolutions registry/flex-auth/zone-resolutions.json`, which is zone-engine's -`tools/resolve_zones.py` output over owner declarations (today: activity-core -`declarations/backup-daily/tenancy.yaml`). Run the resolver from the estate root so -`source` paths stay relative. - Omitting `--zone-resolutions` is safe: applicable actors resolve `unknown`, not to a guessed zone. @@ -140,11 +134,6 @@ truth. ## Caller identity -An HTTP 401 or 403 from the policy service refuses the operation under every -outage profile. It is an explicit caller-authentication or system-binding refusal, -not evaluator unavailability. Resolve the admitted caller and resource contract; -do not retry with a different resource owner or disable caller enforcement. - The production flex-auth pin authenticates ops-warden with Kubernetes TokenReview and binds `resource.system: ops-warden` to `system:serviceaccount:ops-warden:ops-warden`. Supported token sources are: @@ -198,14 +187,6 @@ Rollback is a versioned profile or registry rollback. Do not reintroduce `policy.enabled: false`: that would erase per-zone evidence and recreate the global control ADR-0009 supersedes. -## Approval consumption (not wired) - -This lane does not yet consume an approval artifact. The corrections already -received about how it must validate when it does — two artifacts never taken from -each other, the corrected `binding.approval_binding_digest` comparison target, and -the attribution gap that digest matching does not discharge — are held in -`wiki/ApprovalConsumption.md` so they are not lost between now and then. - ## See also - `tenancy.yaml` diff --git a/wiki/playbooks/audit-core-senders.md b/wiki/playbooks/audit-core-senders.md index 85c878d..77c32cb 100644 --- a/wiki/playbooks/audit-core-senders.md +++ b/wiki/playbooks/audit-core-senders.md @@ -10,21 +10,10 @@ does not duplicate the operating procedure. - Database leases (separate lane): `warden route show database-dynamic-credentials` - Authoritative senders shape: `audit-core/docs/senders.example.json` (placeholders only) -The registry lives in OpenBao KV v2 at `platform/workloads/audit-core/senders`. -It was filled by a Mason wrap-migrate, never by a founder `bao kv put`. -audit-core reads it only through ESO (policy `external-secrets-audit-core`). -`agent-high-risk-boundary` denies agents the data path. Checked by ops-mason on -2026-10-10, from metadata and policy text only. - -## Sender projections - -A sender that needs its own token gets an exact-path projection of it. That is a -separate reader of a single sender's token, never read access to the registry. -Each projection is verified on its own and does not activate the registry lane. - -| Sender | KV path | Reader | Verified | -| --- | --- | --- | --- | -| tenant-engine | `platform/workloads/tenant-engine/audit-core-sender` | policy `workload-kv-read-tenant-engine-audit-core-sender-eso`, role `tenant-engine-audit-core-sender-eso` (SA `tenant-engine/tenant-engine-eso`), ExternalSecret `tenant-engine/tenant-engine-audit-core-sender` | 2026-10-09, ops-mason MASON-WP-0007-T06 (metadata and policy text only) | +First deploy mints sender tokens in-cluster into Secret +`audit-core/audit-core-senders`. The OpenBao path +`platform/workloads/audit-core/senders` is the later authority, filled by a +Mason wrap-migrate — not by a founder `bao kv put`. Never place a sender token, bearer, or `senders.json` value in Git, State Hub, logs, or chat. diff --git a/wiki/playbooks/core-hub-forgejo-source-read.md b/wiki/playbooks/core-hub-forgejo-source-read.md deleted file mode 100644 index af43f09..0000000 --- a/wiki/playbooks/core-hub-forgejo-source-read.md +++ /dev/null @@ -1,52 +0,0 @@ -# Core Hub Forgejo source-read token - -Date: 2026-10-03 -Catalog: `core-hub-forgejo-source-read` (status `active`, `exec_capable: false`, `resolvable: false`) -Owner: `railiance-platform` (OpenBao and credential lifecycle) - -This lane carries the dedicated Forgejo PAT that the Core Hub repo-manager -publisher (`rapp-core-hub`, deployable `repository-classification-publisher`) -uses to read private sources of the repository registry. The Forgejo identity -`core-hub-source` is restricted to organization-wide repository-code read in org -`coulomb`, and the PAT scope is exactly `read:repository`. It has no repository -write, package, organization administration, instance administration, Actions, -cluster, or deployment authority. - -The authoritative lifecycle and evidence record is -`railiance-platform/credential-change-requests/CCR-2026-0031-core-hub-forgejo-source-read.yaml` -(design: `railiance-platform/docs/credential-lane-designs/core-hub-forgejo-source-read.md`). -It is distinct from `policy-nexus-forgejo-source-read` (CCR-2026-0014); neither -identity nor token is reused across the two lanes. - -## Owner-confirmed lane - -| Field | Value | -| --- | --- | -| OpenBao path | `platform/workloads/core-hub/forgejo-source-read` | -| Fields | `FORGEJO_SOURCE_TOKEN`, `API_USER`, `API_BASE_URL`, `TOKEN_SCOPES`, `GENERATED_AT` | -| Read policy | `workload-kv-read-core-hub-forgejo-source` | -| Auth | Kubernetes auth role `external-secrets-core-hub-forgejo-source` (bound to `external-secrets/external-secrets`) | -| Delivery | ClusterSecretStore `openbao-core-hub-forgejo-source` → ExternalSecret `core-hub/repo-manager-forgejo-source` → Secret key `token` | -| Forgejo identity | `core-hub-source` | -| Forgejo team | `core-hub-source-readers` | - -## Worker checklist - -1. There is no operator or agent fetch lane. External Secrets is the only reader, - so `warden access core-hub-forgejo-source-read --fetch/--exec` is not offered - and the fetch-policy pin does not list this lane. Do not request one; if a - legitimate operator read need appears, it goes through a new CCR first. - -2. Check delivery health without reading values: ExternalSecret - `core-hub/repo-manager-forgejo-source` Ready, and `make verify-publisher` in - `rapp-core-hub`. Verify OpenBao allow/deny with `bao token capabilities`, never - `bao kv get` (see `catalog-lane-promotion.md`). - -3. For rotation (every 180 days or on owner change), deactivation, or compromise, - follow the CCR-2026-0031 lifecycle; `warden rotate-guide core-hub-forgejo-source-read` - prints the rotation steps. A replacement is not active until `make - verify-publisher` passes; revoke the predecessor afterward. - -4. Never place the value in argv, chat, Git, State Hub, logs, or a persistent - temporary file. Never substitute the Forgejo admin PAT, a personal token, or - the policy-nexus-source token. diff --git a/wiki/playbooks/flex-auth-decision-envelope-signing-key.md b/wiki/playbooks/flex-auth-decision-envelope-signing-key.md deleted file mode 100644 index 31c7650..0000000 --- a/wiki/playbooks/flex-auth-decision-envelope-signing-key.md +++ /dev/null @@ -1,35 +0,0 @@ -# flex-auth decision-envelope signing key - -Date: 2026-09-14 -Catalog: `flex-auth-decision-envelope-signing-key` (status `draft` until the OpenBao lane exists) -Owner of **shape**: `flex-auth` (`docs/decision-envelope-signature.md`) -Owner of **key material**: OpenBao / `railiance-platform` — flex-auth does not mint this key - -Detached Ed25519 key used to sign `flex-auth.decision-record.v1` envelopes so a -stored allow remains attributable after the HTTP connection is gone -(`FLEX-WP-0024`, `FLEX-DEC-2026-010`). - -**This lane is a pointer.** ops-warden routes it and executes nothing. - -**Ask first:** `warden plan "decision envelope signing key" --json`. - ---- - -## Worker checklist - -1. Do **not** generate a key in `flex-auth` or paste a seed into Git, State Hub, or chat. -2. The proposed KV path is `platform/workloads/flex-auth/envelope-signing`. - Fields: `ED25519_PRIVATE_KEY` (secret), `ED25519_PUBLIC_KEY` and `kid` (public). -3. flex-auth loads the private key the same way it loads TokenReview credentials. - With no key configured it emits `signature.mode=unsigned`. -4. Consumers verify with `kid` against the published public key list, not a pinned - single key. Rotation overlap is the allow TTL (default 15m) plus a short margin. - -## Verify (capabilities-safe) - -```bash -bao token capabilities platform/data/workloads/flex-auth/envelope-signing -# expect: read on the workload identity; deny for unrelated paths -``` - -Do not print the private field. diff --git a/wiki/playbooks/fluid-telegram-operator.md b/wiki/playbooks/fluid-telegram-operator.md deleted file mode 100644 index 2e81897..0000000 --- a/wiki/playbooks/fluid-telegram-operator.md +++ /dev/null @@ -1,26 +0,0 @@ -# fluid-telegram operator lane (Hall of Helix) - -Catalog: `fluid-telegram-operator` (status `draft`, pointer only) -Owner: `ops-mason` (construction plan `ops-mason/plans/fluid-telegram-operator-credential-lane.md`, CCR-2026-0039) -Consumer: `fluid-telegram` attended provisioner (FT-WP-0002) - -## Worker checklist - -This page is a pointer. ops-warden does not issue, read, or hold these values. - -1. You must be a member of the LLDAP group `fluid-telegram-operators`, signed - in at aal2 (MFA). -2. Log in attended: - `bao login -no-print -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv`. - The token lives 15 minutes. Agents are denied on these paths. -3. Run the fluid-telegram provisioner per FT-WP-0002. It reads the operator - app entry, writes the MTProto session and bot token, and creates - `redaction-salt` once. -4. Verify with `bao token capabilities` only, never by reading a value. The - owner-run check is `ops-mason/scripts/verify-fluid-telegram-lane.sh`. - -The bot adapter's runtime read of `bot-token` and `redaction-salt` is a -separate lane (MASON-IN-0003). It never uses this attended role. - -Status: draft until the owner-run member and non-member checks and the salt -create-once probe pass. ops-warden then activates the entry. diff --git a/wiki/playbooks/inbound-library-imap-approle.md b/wiki/playbooks/inbound-library-imap-approle.md deleted file mode 100644 index 11b30a4..0000000 --- a/wiki/playbooks/inbound-library-imap-approle.md +++ /dev/null @@ -1,36 +0,0 @@ -# inbound-library AppRole for the library@coulomb.social IMAP read -Date: 2026-10-05 -Catalog: `inbound-library-imap-approle` (status `active`, risk `high`) -Owner: `ops-mason` (`plans/inbound-library-imap-approle.md`, built 2026-10-05) · consumer: `inbound-library` -Demand: `inbound-library/workplans/ILIB-WP-0003` - -inbound-library's systemd user timer reads the dedicated mailbox -`library@coulomb.social` read-only every 15 minutes, with no operator present. -Each run logs in with this AppRole, reads the IMAP entry once, and revokes its -token. No copy of the password is kept on disk. - -## OpenBao pointers - -| Field | Value | -| --- | --- | -| Path | `platform/workloads/inbound-library/imap` (fields `IMAP_USERNAME`, `IMAP_PASSWORD`, `IMAP_HOST`, `IMAP_PORT`, `IMAP_TLS`, `IMAP_FOLDER`) | -| Policy | `workload-kv-read-inbound-library-imap` (read on that entry's data and metadata only) | -| AppRole | `inbound-library` — `token_ttl` 15m, `token_max_ttl` 30m, `token_num_uses` 4, `secret_id_ttl` 0 | -| Delivery | `~/.local/inbound-library/approle/{role_id,secret_id}` (0600) | -| Entry custody | `railiance-platform/docs/inbound-library-imap-custody.md` | - -## Worker checklist - -This lane is for the inbound-library process only. Coding agents do not read the -AppRole files or the KV entry. To check the lane, run the consumer and read its -report: - -```bash -systemctl --user start inbound-library.service -journalctl --user -u inbound-library.service -n 30 -o cat # intake.ok, never a secret -``` - -`credential_unavailable` means one of these: OpenBao is unreachable (the -`openbao-ui-railiance01` bridge forward), the AppRole files are missing, or the -read was denied. -For the procedure itself, see the ops-mason plan; this page only points to it. diff --git a/wiki/playbooks/informed-decision-sitting-requester-login.md b/wiki/playbooks/informed-decision-sitting-requester-login.md deleted file mode 100644 index 8cc3e1a..0000000 --- a/wiki/playbooks/informed-decision-sitting-requester-login.md +++ /dev/null @@ -1,42 +0,0 @@ -# Sitting-requester reader session - -CCR-2026-0027 admits only -`platform/workloads/informed-decision/sitting-requester`. The contained Warden -login uses `informed-decision-sitting-requester-workload-kv-read`. Its owner -command checks exact reader policies, sibling denial of -`secrets-engine/approval-requester`, and a create-only KeyCape token exchange. -It never prints the client secret. - -Use the current source catalog explicitly (the installed bundle may not list -this id yet): - -`WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml` - -OpenBao is reached through `http://127.0.0.1:18200` (public -`bao.coulomb.social` is retracted). Warden self-revokes after the child exits. - -## Proof-only child (no sitting POST) - -`/home/worsch/railiance-platform/scripts/prove-sitting-requester-exchange.sh` - -Already verified 2026-09-15. Do not re-run as a substitute for create. - -## Sitting-create child (INFD-WP-0002-T03) - -This is the analog of the T03 requester playbook's create child. It POSTs -seven unapproved `human_control` objects (`c01` skipped) and does not bind -or consume. - -Requires an Approval Engine tunnel at `127.0.0.1:18281`. Use the attended -wrapper so `BAO_ADDR` is the OpenBao tunnel, not public `bao.coulomb.social`. -A failed receipt is overwritten; a `created` receipt is not. - -```sh -kubectl -n approval-engine port-forward svc/approval-engine 18281:8080 -python3 /home/worsch/railiance-platform/scripts/openbao-attended-exec.py \ - --lane informed-decision-sitting-requester-login -- \ - /home/worsch/informed-decision/tools/create_sitting_approvals.sh -``` - -Receipt: `informed-decision/docs/evidence/2026-09-15-sitting-approval-creates.json` -(metadata only). Human disposition stays on `informed-decision-approver`. diff --git a/wiki/playbooks/keycape-client-secret-generate.md b/wiki/playbooks/keycape-client-secret-generate.md deleted file mode 100644 index 225abe4..0000000 --- a/wiki/playbooks/keycape-client-secret-generate.md +++ /dev/null @@ -1,42 +0,0 @@ -# Generate a KeyCape client secret in custody - -Catalog: `keycape-client-secret-generate` (status `active`, pointer only) -Owner: `railiance-platform` credential broker (RPF-WP-0054, grant applied by CCR-2026-0040) -Used by: SecurityCaseType `oidc-browser-relying-party` 1.0, step `mint-secret` - -## Worker checklist - -This page is a pointer. ops-warden does not generate, read or hold the value. - -1. The case must be `materialized`, `client_type: confidential`, with a - `case-ref` from `warden case plan `. -2. Log in to the issuer role. You must be in `net-kingdom-admins` at aal2. The - token lasts 15m and holds only the issuer policy. Its uses are unlimited, - because a limited-use token cannot mint the grant token: - - ```bash - bao login -no-print -method=oidc -path=netkingdom role=credential-broker-keycape-client-secret-generate - ``` - -3. Run the owner command from `~/railiance-platform` in the same terminal: - - ```bash - scripts/credential.py generate --grant keycape/client-secret-generate \ - --client-id --case-file \ - --case-ref @sha256: --receipt - ``` - - The broker re-derives the case-ref, checks status, type and `client_id`, - proves the path is absent, and writes once with CAS 0 to - `platform/workloads/keycape/`. It prints nothing. -4. Exit codes: 0 generated (or dry-run ok), 2 precondition, 3 already present - (never overwritten), 4 token or write failure, 5 verify failure. -5. Verify from the receipt and metadata only (version 1). Never read the value. - -Refused for agent sessions (`WARDEN_AGENT_ID`), root, and callers without the -issuer policy. - -Status: active since 2026-10-10. The issuer role is bound (CCR-2026-0043), and -the RPF-WP-0054-T05 disposable probe passed (railiance-platform -`docs/evidence/2026-10-10-keycape-client-secret-generate-probe.json`). key-cape -confirmed the secret format the same day. diff --git a/wiki/playbooks/kubernetes-railiance01-admin.md b/wiki/playbooks/kubernetes-railiance01-admin.md deleted file mode 100644 index b4d19b2..0000000 --- a/wiki/playbooks/kubernetes-railiance01-admin.md +++ /dev/null @@ -1,16 +0,0 @@ -# Attended railiance01 cluster-admin kubeconfig - -## Worker checklist - -This lane is the attended path required by MASON-IN-0006 choice (b). It names -the admin kubeconfig file. It does not read that file. - -- The admin file is `/home/worsch/.kube/config-railiance01` and stays that path. -- After the scoped identity is installed, `~/.kube/config` is the ops-mason-builder context named `default`, and is not cluster-admin. -- Reach admin by prefixing the command: `KUBECONFIG=/home/worsch/.kube/config-railiance01 kubectl ...` -- Do not cat the kubeconfig, do not run `kubectl config view`, and do not use `warden access --fetch` to obtain the file. The fetch_command only tests that the file exists. -- Do not copy the admin client key into the builder kubeconfig. - -The catalog fetch command is `test -f /home/worsch/.kube/config-railiance01`. -A passing test means the file is present. It does not mean the file was read, -copied, or made the agent default. diff --git a/wiki/playbooks/netkingdom-layer-declaration.md b/wiki/playbooks/netkingdom-layer-declaration.md deleted file mode 100644 index 24af535..0000000 --- a/wiki/playbooks/netkingdom-layer-declaration.md +++ /dev/null @@ -1,195 +0,0 @@ -# NetKingdom layer declaration route - -Date: 2026-09-04 -Catalog: `netkingdom-layer-declaration` -Doctrine owner: `gate-house` -Path steward: `ops-warden` - -This page is a route from the accepted NetKingdom security companion to the -reference declaration artifacts. It does not redefine the layer model. - -## Worker checklist - -1. Read `net-kingdom/SECURITY-COMPANION.md`, then use the accepted - `net-kingdom/canon/standards/security-layer-model_v0.8.md` for the normative - layer, Tooling-contact, and PEP obligations. Exact publication acceptance is - `net-kingdom/docs/security-layer-v0.8-acceptance.md`. Assent to a previous - boundary is provenance; it is not blanket assent to this current text. -2. **Declare in your own `INTENT.md` frontmatter.** That is the declaration - (`GH-DEC-2026-017` §1). Use ops-warden's `layer.yaml` as the machine-readable - reference *form*, which is a **derived** artifact that must be marked derived, - must name `INTENT.md` as its source, and must agree with it. Adapt its - repository, layer, contacts, and ownership facts; do not copy ops-warden-specific - claims as your own. -3. If the repository is PEP-shaped, use `pep-stance.yaml` as the stance-map - reference and publish the resulting path in the repository's layer declaration. -4. Adapt `scripts/check_layer_conformance.py` and - `tests/test_layer_conformance.py`, then run both checks in the declaring repo. -5. Send the declaration and any stance-map inventory pointer to `gate-house` for - doctrine/register review. Route credential or operational-lane questions back - through `warden route`; do not place doctrine in the routing catalog. - -Reference checks in this checkout: - -```bash -python3 scripts/check_layer_conformance.py -pytest tests/test_layer_conformance.py -``` - -## Reference-form change set — 2026-09-21 (`GH-DEC-2026-017`) - -**If you copied ops-warden's `layer.yaml` before 2026-09-21, it is now the wrong -shape in three ways.** The change is to the reference form, which is why -`gate-house` asked ops-warden to make it here rather than asking each adopter to -work it out. Apply all three in your own repository; ops-warden does not edit -anyone else's files. - -1. **Remove `standard_version:` from the sidecar *and* from your `INTENT.md` - frontmatter.** A layer declaration MUST NOT carry a standard version - (`GH-DEC-2026-017` §5, amendment A12): the declared layer is a standing - property that does not change when the standard is revised, and a version in - the declaration makes every revision read as though it invalidated every - declaration. Keeping it "for information" was declined explicitly — a field - that is present will be branched on. Version-scoped state belongs to the - conformance *run* (see the 2026-09-21 `GH-DEC-2026-020` section below; the - "derived conformance record" this line used to name was a defect in §5 and - nobody is required to emit one). If your checker lists `standard_version` as a - required key, or prints it in a report line, it will now **reject a conforming - declaration** — fix the checker in the same commit. -2. **Add `derived: true` and `derived_from: INTENT.md`.** The sidecar is a derived - artifact under §11's derived-artifact rule and does not govern - (`GH-DEC-2026-017` §1, amendment A11). If your `INTENT.md` has no frontmatter - `layer:` key, add one — that, not the sidecar and not a prose line, is your - declaration. -3. **Fold ASCII case before comparing a layer value, and re-spell nothing.** §3's - vocabulary is closed and has **four** tokens — `Taxonomy`, `Tooling`, - `Engine`, `Staff` — and comparison is case-insensitive (`GH-DEC-2026-017` §2 - and §3, amendment A9). `Staff` and `staff` are the same value; a lowercase - declaration is conforming, not tolerated. Two traps: a validator that admits - only three tokens and rejects `Taxonomy` carries a defect — the layer this - standard itself occupies is in the vocabulary; and an equality assertion - between your two forms silently performs the re-spelling the ruling declined - to order. Assert the **fold**, so a real layer divergence still fails. - -A disagreement between the two forms, after folding, is a **finding in its own -right** and must be reported rather than resolved away by precedence. Precedence -says which value is your answer; it does not say the disagreement did not happen. - -`pep-stance.yaml` is **not** a layer declaration and is out of scope here: its -`standard_version` / `standard_version_reviewed` pair is a stance map's record of -what was reviewed, and stays. - -ops-warden's applied instance of this change set is commit-local: `INTENT.md`, -`layer.yaml`, `scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py`. -Read those four together rather than the sidecar alone. - -## Reach and run-version change set — 2026-09-21 (`GH-DEC-2026-020`, A12 r2) - -**If you copied ops-warden's checker before this change, it enforces A12 by key -name only, and prints no version.** Both are now defects. `GH-DEC-2026-020` -refines A12 as A12 r2; verify it in `gate-house` `decisions/decisions.md` and -`docs/amendments/v0.8-section-11-declaration-amendments.md` (§ "A12 r2"), not -from this page. - -1. **A version anywhere in the declaration counts.** The declaration is every key - and value of your `INTENT.md` frontmatter and of your derived sidecar. No key - or value carries a version of the standard **or of its companion**, including - a version-bearing path: `standard: .../security-layer-model_v0.7.md` becomes - `standard: .../security-layer-model` (`GH-DEC-2026-020` §1). A de-versioned - path was a required change, not a voluntary one. -2. **`companion_version` counts** and comes out of the declaration (§2). -3. **Not reached:** comments, and a file's own `schema_version`. Keeping or - removing them is equally fine; do not edit them just to tidy. -4. **Not a declaration, not checked:** stance maps (`pep-stance.yaml`), claims - maps, evidence classifications. Their version is what makes them re-readable - when clause text changes; keep it. Your checker **MUST NOT** apply A12 to - them (§3). -5. **The version belongs to the run.** A re-runnable checker is sufficient — - not "for now" — if **every** run prints the standard version or commit it - checks against and the scope it ranged over, including the PASS line (§4). - No repository must emit a durable conformance record; whoever retains a run's - output owes that copy's version. The pattern is kings-guard's: a - `VALIDATED_AGAINST` constant in the checker, printed on every run. -6. **Widen your checker from key name to content.** ops-warden's - `scripts/check_layer_conformance.py` now carries `VALIDATED_AGAINST`, `SCOPE` - and `find_version_pins()`, which walks every parsed key and value, skips - `schema_version`, and flags any key naming a standard/companion version and - any value carrying a versioned file name or path. It prints version and scope - before loading anything, so even a MALFORMED run states them. Copy that, set - `VALIDATED_AGAINST` to what *your* run checks against, and add tests that fail - if a versioned `standard:` path or a `companion_version` comes back - (`tests/test_layer_conformance.py` has the reference set). Change declaration, - checker and tests in the same commit, and re-spell no layer value. - -### Reference detector and `VALIDATED_AGAINST` — the estate reference (`GH-DEC-2026-021`) - -The estate's checkers each wrote their own version detector in the 020 round, and -they diverge: some flag any key *ending* in `version`, some any key *containing* -it, one flags `vN.N` anywhere in prose while the others ignore prose. The -checkers also disagree on what `VALIDATED_AGAINST` names. `GH-DEC-2026-021` §3 -rules that the detector below is the **estate reference** every checker's copy -converges on — when the copy is next edited, and no later than the commit that -re-points its `VALIDATED_AGAINST` after the v0.8 flip (`GH-WP-0004-T11`). Each -repository implements its own copy. - -**One detector.** Copy this from ops-warden's checker as-is: - -```python -VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)", re.I) -VERSION_IN_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.I) -NOT_REACHED_KEYS = {"schema_version"} -IDENTITY_KEYS = {"standard", "companion"} -IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I) - -def find_version_pins(node, where="", identity=False): # walk every parsed key and value - ... # see scripts/check_layer_conformance.py -``` - -- **Keys:** flag a key that names a *standard or companion* version - (`standard_version`, `companion_version`, `standard_version_reviewed`) — not - every key containing `version`. `schema_version` is the file's own schema and - is skipped (A12 r2). **Do not flag a key such as `intent_version`**: it names - neither the standard nor its companion. `informed-decision`'s declaring - frontmatter carries `intent_version: 0.1.0` (the version of its own - `INTENT.md`), and a detector that flags every key containing or ending in - `version` fails it wrongly (`GH-DEC-2026-021`, Context). -- **Values:** flag a version carried in a *file name or path* (`_v0.7`, - `-v0.8.md`, `@0.7`) — the `standard: …_v0.7.md` case §1 rules on. -- **Identity-bearing values:** any version token (`v?N.N`) in the value of a - `standard:` or `companion:` key is a pin (`GH-DEC-2026-021` §3), so - `standard: security-layer-model v0.7` fails. -- **Deliberate over-reach:** the path pattern flags a versioned path of *any* - document in a declaration, wider than A12's "this standard or its companion". - `GH-DEC-2026-021` §3 accepts this; a repository that needs such a path raises - it, and it is not waived by allowlist. -- **Comments:** never read; the detector runs on parsed YAML, so they are gone. -- **Prose citations in a value** (e.g. a rationale string saying "the v0.5 scope - rule"): **not reached** (`GH-DEC-2026-021` §1, A12 r3). A revision cited in - prose is provenance, not a pin. The reference detector does not flag a bare - `vN.N` outside a `standard:`/`companion:` value, and no allowlist is needed. - Accepted v0.8 carries A12 r3; arbitrary prose versions remain provenance. - -**Name the text in force.** Following the accepted v0.8 flip, -`VALIDATED_AGAINST` names accepted `security-layer-model_v0.8.md` and its exact -publication digest. The output states single-repository scope, the repository's -§4 membership, and the owner-specific declaration/source checks. These are -run provenance, never standard-version pins in standing declarations. The -ops-warden constant is the reference: - -```text -net-kingdom/canon/standards/security-layer-model_v0.8.md (accepted; sha256:3d8298560069ef471fba28d2b1245fb2309328f231dad1a667cdf0ce5a5a7c8b) -``` - -Old versions remain in rejection fixtures and historical review citations. -They must not be replaced mechanically: those fixtures prove that declaration -pins stay forbidden after the normative text changes. - -**Citation.** Cite the ruling by the decision's body section: `GH-DEC-2026-017` -§5, statute A12 (now A12 r2). This page's "§5" is canonical -(`GH-DEC-2026-020`); the decision's `rationale:` part numbers are a summary and -are not cited. - -## Ownership boundary - -`gate-house` owns what the model requires. Each repository owns the truth of its -own declaration. `ops-warden` owns only this discoverable path to those sources. diff --git a/wiki/playbooks/openbao-platform-admin-login.md b/wiki/playbooks/openbao-platform-admin-login.md index 0cd809d..c1c86ae 100644 --- a/wiki/playbooks/openbao-platform-admin-login.md +++ b/wiki/playbooks/openbao-platform-admin-login.md @@ -33,10 +33,8 @@ workload KV-read lane and it does not provision a secret value. and remain silent. Warden self-revokes the session and removes the helper on every success or failure path. - Safety does not rely on `-no-print`. Login output stays contained and is - accepted only after successful helper persistence. Any child output (including - whitespace), persistence defect, non-zero exit, or revocation/cleanup defect - fails closed. + Safety does not rely on `-no-print`. Any client or child output, helper + persistence defect, non-zero exit, or revocation/cleanup defect fails closed. Captured bytes are never returned, logged, excerpted, hashed, or fingerprinted. Do not paste a token into chat, State Hub, a shell argument, or a handoff file. Root is offline break-glass authority, not a fallback for failure. @@ -55,56 +53,9 @@ mount, `platform-admin` role, and allowed callback with `railiance-platform` and `key-cape`. Do not retry with a workload-specific OIDC role: it is intentionally incapable of OpenBao control-plane administration. -## Exit status and audit - -After argument/configuration validation, the contained envelope returns: - -| Exit | Audit outcome | Meaning | -| --- | --- | --- | -| 0 | `ok` | Login, silent child, self-revocation and cleanup succeeded. | -| 10 | `login_failed` | Login or private-home preflight failed; child did not run. | -| 11 | `child_failed` | Child could not start or exited non-zero. | -| 12 | `child_output` | Child emitted stdout/stderr, even whitespace; takes precedence over child exit failure. | -| 13 | `revoke_unconfirmed` | Child succeeded silently, but self-revocation was not confirmed. | -| 14 | `cleanup_failed` | Private storage cleanup failed; overrides the prior phase. | - -Both `access-audit.log` and `audit.jsonl` record the Warden exit code and phase -outcome, never captured bytes. Revocation warnings accompany child failures too. -Codes 11–14 do not establish that the child made no changes: check its permitted -metadata receipt before retrying. A zero exit from `bao token revoke -self` is -Warden's revocation confirmation; no post-revoke lookup is used. An arbitrary -lookup failure would not prove revocation. - -On the WSL workstation, use the ops-bridge `openbao-ui-railiance01` tunnel at -`http://127.0.0.1:18200` for `BAO_ADDR` and `VAULT_ADDR`, with the founder's TTY -and a working browser opener (`xdg-open`, or the configured OpenBao browser). -`bao.coulomb.social` serves a public notice, not OpenBao. The captured OIDC output -is not a browser fallback. Check the opener before starting an attended login. -The reviewed child must redirect both streams if its tools normally print success -messages; it should write only approved, value-free evidence to its own receipt. - ## Authority - OpenBao policy and role owner: `railiance-platform/docs/openbao.md` - Human identity and MFA provider: key-cape / Keycloak - Database-engine procedure owner: `rapp-postgres` - Routing decision and founder-act surface: WARDEN-WP-0029 - -## 2026-10-15 contained failure diagnostics - -A failed login now reports only fixed categories (`callback_timeout`, -`callback_error`, `authentication_error`, `unknown_client_failure`, -`helper_persistence_missing`, `output_shape_rejected`), the numeric client exit -code, helper-present and allowed-output booleans. These are observations, not -authorization or proof of an issued session. The original captured bytes remain -discarded, and the existing strict output allowlist is unchanged. - -Synthesized JSON-format flags precede positional auth data. OpenBao 2.5.5 also -scans global format flags across argv; this normalization is not a proven fix -for the October 15 timeout. Emergency self-revocation recognizes the estate's -`s.` service-token shape as well as `hvs.`, `hvb.` and `hvr.`. Recovery still uses -a value only in memory for `bao token revoke -self`, never a new handoff. - -The two CUST81 failed attempts remain failed and revocation-unconfirmed. Fresh -Firefox profiles prove browser initialization, not callback completion. Source -receipt: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`. diff --git a/wiki/playbooks/openbao-structure-survey.md b/wiki/playbooks/openbao-structure-survey.md deleted file mode 100644 index c245386..0000000 --- a/wiki/playbooks/openbao-structure-survey.md +++ /dev/null @@ -1,26 +0,0 @@ -# OpenBao existing-structure survey (construction plan section 2) - -Catalog: `openbao-structure-survey` (status `active`, pointer only) -Owner: `ops-mason` — `scripts/plan-survey.py`, `docs/construction-plan-format.md` § 2 -Routing records: `WARDEN-WP-0042`, `WARDEN-WP-0043`; owner change: `ops-mason/MASON-IN-0004` (2026-10-07) - -ops-warden routes this need and does nothing else. It holds no survey code and -no OpenBao token; the survey runs under ops-mason's own scoped -`ops-mason-survey` read-only grant, which reads structure metadata and is denied every -secret value. - -## Worker checklist - -1. Ask the founder for an ops-mason grant for the task. The founder runs - `./scripts/bao-session.sh grant ` in `~/ops-mason`; an agent does not - log in or grant. -2. Run `./scripts/plan-survey.py` in `~/ops-mason` with the demand's KV path, - proposed policy, role and consumer, exactly as documented in - `ops-mason/docs/construction-plan-format.md` § 2. -3. Paste its output as section 2 of the plan and turn the draft answer into - a reviewed one. - -Anything the grant refuses is ops-mason's to resolve with the founder; do not -reach for a broader session through ops-warden. - -The builder grant was retired by MASON-IN-0004. Surveys cannot apply policies or auth roles; reviewed structure changes belong to railiance-platform's delegated metadata applier. diff --git a/wiki/playbooks/platform-supplier-credentials.md b/wiki/playbooks/platform-supplier-credentials.md deleted file mode 100644 index 3a09567..0000000 --- a/wiki/playbooks/platform-supplier-credentials.md +++ /dev/null @@ -1,37 +0,0 @@ -# Supplier account credentials (`platform/suppliers/`) - -Catalog: `platform-supplier-credentials` (status `active`, pointer only) -Owner: `railiance-platform`, which owns the KV write and the `credential-change.py` allowlist -Routing record: `WARDEN-WP-0043` - -Founder decision, 2026-10-08: credentials we hold with a supplier live at -`platform/suppliers//` on the existing `platform` mount. -Tenant commercial secrets stay on `tenants/`. ops-warden routes this shape. It -does not store values and does not apply OpenBao. - -## Worker checklist - -1. Pick the path `platform/suppliers//` (two levels, lowercase). -2. To create or change the path, open a delegated credential change with - railiance-platform. The `credential-change.py` allowlist does not yet - accept `platform/suppliers/` (WARDEN-WP-0043-T02). -3. The founder provisions values on a trusted terminal from a mode-0600 - file. Never put a value in git, State Hub, a workplan, a log, or chat. -4. Verify with metadata and capabilities only: - `bao kv metadata get `, `bao token capabilities `. - -## Existing accounts (not moved yet) - -A path with a live reader stays where it is until that reader is retargeted. - -| Account | Path today | Future home | -| --- | --- | --- | -| OpenRouter, llm-connect | `platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets` | `platform/suppliers/openrouter/llm-connect` | -| OpenRouter, rein-openweights | `reins/rein-openweights/openrouter` | `platform/suppliers/openrouter/rein-openweights` | -| Scaleway bootstrap | `platform/workloads/railiance/scaleway/bootstrap` | `platform/suppliers/scaleway/bootstrap` | -| Scaleway backup space | `platform/workloads/railiance/backup/object-storage` | `platform/suppliers/scaleway/backup` | -| IONOS transactional mailbox | `platform/workloads/email-connect/transactional` | `platform/suppliers/ionos/email-connect-smtp` | -| IONOS library mailbox | `platform/workloads/inbound-library/imap` | `platform/suppliers/ionos/library-imap` | - -llm-connect already tries the supplier path first and falls back to the -workload path. diff --git a/wiki/playbooks/policy-nexus-forgejo-source-read.md b/wiki/playbooks/policy-nexus-forgejo-source-read.md deleted file mode 100644 index 6e704c7..0000000 --- a/wiki/playbooks/policy-nexus-forgejo-source-read.md +++ /dev/null @@ -1,55 +0,0 @@ -# Policy Nexus Forgejo source-read token - -Date: 2026-09-01 -Catalog: `policy-nexus-forgejo-source-read` (status `active`, `resolvable: true`) -Owner: `railiance-platform` (OpenBao and credential lifecycle) - -This lane carries the dedicated Forgejo PAT used by Policy Nexus Actions to -fetch exact archives from private owner repositories. The Forgejo identity is -restricted to organization-wide repository-code read and the PAT scope is -exactly `read:repository`. It has no repository write, package, organization -administration, instance administration, cluster, or deployment authority. - -The authoritative lifecycle and evidence record is -`railiance-platform/credential-change-requests/CCR-2026-0014-policy-nexus-forgejo-source-read.yaml`. -Warden is a governed conduit and never owns, persists, caches, or logs the PAT. - -## Owner-confirmed lane - -| Field | Value | -| --- | --- | -| OpenBao path | `platform/workloads/policy-nexus/forgejo-source-read` | -| Primary field | `FORGEJO_SOURCE_TOKEN` | -| Read policy | `workload-kv-read-policy-nexus-forgejo-source` | -| OIDC role | `policy-nexus-forgejo-source-workload-kv-read` | -| Forgejo identity | `policy-nexus-source` | -| Forgejo team | `policy-nexus-source-readers` | -| Actions consumer | `coulomb/policy-nexus` secret `FORGEJO_SOURCE_TOKEN` | - -## Worker checklist - -1. Authenticate as an approved operator through the exact OIDC role. Do not - substitute the Forgejo admin PAT or a broader OpenBao identity. - -2. Confirm the route before use: - - ```bash - warden route show policy-nexus-forgejo-source-read --json | jq .resolvable - ``` - - The result must be `true`, and the rendered fetch must contain no placeholder. - -3. Agent callers must use a sanctioned high-risk transport. Pass the value only - to the bounded source-consuming child: - - ```bash - warden access policy-nexus-forgejo-source-read \ - --field FORGEJO_SOURCE_TOKEN --exec -- - ``` - - Do not use raw `--fetch`, place the value on argv, or write it to chat, Git, - State Hub, workflow logs, or a persistent temporary file. - -4. For rotation or compromise, follow CCR-2026-0014. A replacement is not active - until its scope and negative permissions are verified and one exact-commit - Policy Nexus candidate workflow succeeds; revoke the predecessor afterward. diff --git a/wiki/playbooks/reuse-surface-hub-write-token.md b/wiki/playbooks/reuse-surface-hub-write-token.md index 79ef529..8907aa0 100644 --- a/wiki/playbooks/reuse-surface-hub-write-token.md +++ b/wiki/playbooks/reuse-surface-hub-write-token.md @@ -15,7 +15,7 @@ ops-warden does not vend this token — custody belongs to `railiance-platform` | Field | Value | | --- | --- | -| OpenBao server | railiance01 (`92.205.62.239`); operator access via the ops-bridge tunnel `http://127.0.0.1:18200` — public `bao.coulomb.social` is retired | +| OpenBao server | `https://bao.coulomb.social` (railiance01, `92.205.62.239`) | | KV path | `platform/workloads/reuse/reuse-surface/runtime-secrets` | | Hub write field | `REUSE_SURFACE_TOKEN` | | Webhook HMAC field | `REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET` | @@ -28,29 +28,6 @@ ops-warden does not vend this token — custody belongs to `railiance-platform` `SecretSynced`, positive hub + webhook verification and negative default-policy denial recorded in CCR-2026-0005. -### Which identity this route evaluates - -`warden access` does not pick a principal. Two identities are involved, and they are not the same: - -| Plane | Identity | -| --- | --- | -| OpenBao data read | The ambient token: `BAO_TOKEN`, else `VAULT_TOKEN`, else `~/.vault-token`. `warden access` prints the entity, OIDC role, and policies before a proxy. | -| flex-auth gate | `policy.caller_auth` in `warden.yaml`. On this workstation that mints a short-lived token for ServiceAccount `ops-warden/ops-warden`. An allow here does not mean OpenBao allowed the read. | - -A capabilities check on one shell and a proxy in another can disagree. The OpenBao CLI in the proxy uses that process's ambient token. An attended `platform-admin` login (entity `4a1f663c-0621-0483-edd0-f852274f4b6b`, policies `default`, `operator-custody`, `platform-admin`) can show `read` on both exact paths while `auth/netkingdom/role/coding-agent` is denied: that role's `agent-high-risk-boundary` policy denies data read on this path. The 2026-10-14 hub registration attempt was that shape. PDP decision `f730e3da14beebdd` allowed the fetch; OpenBao then returned HTTP 403 and the helper did not run. - -Do not attach `workload-kv-read-reuse-surface-runtime` to the platform-admin entity `4a1f663c-0621-0483-edd0-f852274f4b6b`. CCR-2026-0005 remains the KV and ESO authority. ESO still uses Kubernetes role `auth/kubernetes/role/reuse-surface-runtime-eso` with policy `workload-kv-read-reuse-surface-runtime-eso`. - -The attended caller is OIDC role `reuse-surface-hub-writer-workload-kv-read` on `auth/netkingdom`, group `reuse-surface-hub-writers`, assurance `aal2`, policy `workload-kv-read-reuse-surface-runtime` only. railiance-platform `CCR-2026-0044` is applied. Log in with: - -```bash -bao login -method=oidc -path=netkingdom role=reuse-surface-hub-writer-workload-kv-read -``` - -`warden access` does not perform that login. It reads the ambient token. Use this role's token for the proxy. - -ops-mason verified both modes on 2026-10-14. The member receipt (`docs/evidence/2026-10-14-reuse-surface-hub-writer-member.txt`) logged in as entity `c204099b-cb94-363f-e207-5ff4d4aa6e94` with only `workload-kv-read-reuse-surface-runtime`: read on the exact data and metadata paths, deny on the parent and on `issue-core-runtime`, then revoked. The non-member receipt refused the login. The CCR approval comment names member entity `f94b7c34-e151-30bb-b473-df3ccdbb9bf1`; the passing receipt's entity is the one above. - ### Break-glass (cluster read) If OpenBao is unreachable, operators with Railiance01 kubeconfig may read the diff --git a/wiki/playbooks/secrets-engine-approval-client-login.md b/wiki/playbooks/secrets-engine-approval-client-login.md deleted file mode 100644 index 71b4bc4..0000000 --- a/wiki/playbooks/secrets-engine-approval-client-login.md +++ /dev/null @@ -1,29 +0,0 @@ -# Scoped approval-client reader login - -CCR-2026-0019 names the operator-confirmed `net-kingdom-admins` group and -live OIDC role `secrets-engine-approval-client-workload-kv-read`. This login -lane establishes only that reader session. Warden applies its normal policy -gate, contained OIDC helper, silent child and self-revocation/cleanup. - -Before delivery, run the platform metadata-only preflight: - -```sh -warden access secrets-engine-approval-client-login --exec -- python3 /home/worsch/railiance-platform/scripts/approval-client-reader-preflight.py -``` - -The command verifies effective exact-path read and sibling/parent/write denial. -It reads no KV value. Login readiness does not activate credential delivery. -CCR-2026-0019 retains its separate pending native delivery acceptance. No raw -fetch, retained token, admin fallback or verifier Kubernetes Secret substitution. - -Origin: operator-authorized SECRETS-WP-0010-T03 continuation, 2026-09-14. - -Live reader preflight and protected file/token-exchange checks passed on -2026-09-14. Platform evidence names this session's scope and its remaining -wrong-group and real claim/consume checks. The acceptance command is -`/home/worsch/secrets-engine/.venv/bin/python -B /home/worsch/railiance-platform/scripts/approval-client-delivery-check.py`. -It requires the owner-bound Approval Engine tunnel on 127.0.0.1:18281, removes -its private runtime file, and never creates/binds/consumes an approval. - -When the installed package uses its older bundled catalog, explicitly select -`WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml`. diff --git a/wiki/playbooks/secrets-engine-requester-login.md b/wiki/playbooks/secrets-engine-requester-login.md deleted file mode 100644 index f0cdb7b..0000000 --- a/wiki/playbooks/secrets-engine-requester-login.md +++ /dev/null @@ -1,16 +0,0 @@ -# T03 requester session - -CCR-2026-0025 admits only the new requester path. The contained Warden login -uses `secrets-engine-requester-workload-kv-read`. Its owner command checks exact -reader policies and denial of the existing consumer path, obtains one in-memory -create-only token and submits only the three frozen T03 records. It cannot bind -or consume an approval and never reads the KeyCape verifier Secret. - -Use the current source catalog explicitly while the installed bundle is older: -`WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml`. - -The reviewed child is -`/home/worsch/informed-decision/.venv/bin/python -B /home/worsch/railiance-platform/scripts/create-t03-approval-requests.py`. -It requires the owner-bound Approval Engine tunnel at 127.0.0.1:18281 and frozen -native evaluator receipts. Its output is metadata-only persisted by the owner; -Warden self-revokes and removes the reader helper after the child exits. diff --git a/wiki/playbooks/state-hub-forge-derivation-read.md b/wiki/playbooks/state-hub-forge-derivation-read.md index 5bcc9a2..3969485 100644 --- a/wiki/playbooks/state-hub-forge-derivation-read.md +++ b/wiki/playbooks/state-hub-forge-derivation-read.md @@ -1,7 +1,7 @@ # State Hub Forge Derivation Read Token -Date: 2026-08-26 (policy and auth role built and verified 2026-08-27; catalog active 2026-09-14) -Catalog: `state-hub-forge-derivation-read` (status `active`) +Date: 2026-08-26 (policy and auth role built and verified 2026-08-27) +Catalog: `state-hub-forge-derivation-read` (status `draft` until the token and ServiceAccount land) Owner: `ops-mason` (built under `ops-mason/plans/state-hub-forge-derivation-read.md`, approved 2026-08-26) Read-only Forgejo token so the State Hub can clone **private** repositories and @@ -11,10 +11,9 @@ repositories were invisible to derivation entirely (`STATE-WP-0083-T04`, 2026-08-26). **This lane is a pointer.** ops-warden routes it and executes nothing: it issues -SSH certificates only and owns no secret store. The Kubernetes auth role, policy, -and KV path are ops-mason's build; the token value is minted by the forge owner -and delivered through `paste_once_provision`. The credential was delivered -2026-08-27; `STATE-WP-0084` mounted it into State Hub. +SSH certificates only and owns no secret store. The AppRole, policy, and KV path +are ops-mason's build; the token value is minted by the forge owner and +delivered through `paste_once_provision`. **Ask first:** `warden plan "state hub read private repository" --json`. diff --git a/wiki/playbooks/whynot-design-npm-publish.md b/wiki/playbooks/whynot-design-npm-publish.md index 3c45014..af0c92a 100644 --- a/wiki/playbooks/whynot-design-npm-publish.md +++ b/wiki/playbooks/whynot-design-npm-publish.md @@ -1,18 +1,11 @@ # whynot-design npm publish token -Date: 2026-09-04 +Date: 2026-06-29 Catalog: `whynot-design-npm-publish` (status `active`, `resolvable: true`) Owner: `railiance-platform` (OpenBao) · provisioning CCR-2026-0001 (commit 8f617fc) -> **Rotation required (2026-09-04).** The OIDC role and OpenBao read path are -> healthy, but the stored credential failed a real Forgejo publish. Version -> `@whynot/design@0.4.2` was published and integrity-verified through the -> plan-authorized Forgejo admin recovery lane. Treat this dedicated lane as -> unverified for writes until its package token is rotated and re-proven. - -The npm publish token for `@whynot/design` on the coulomb Forgejo npm registry -(KV field `NPM_AUTH_TOKEN`, which is also the name the publish command reads it under) -(`https://forgejo.coulomb.social/api/packages/coulomb/npm/`). ops-warden **does not hold +The `NPM_AUTH_TOKEN` that publishes `@whynot/design` to the coulomb Gitea npm registry +(`https://gitea.coulomb.social/api/packages/coulomb/npm/`). ops-warden **does not hold this token** — it is the access front door: `warden access` proxies the read from OpenBao **as the caller** and never persists, caches, or logs the value. @@ -23,8 +16,7 @@ this token** — it is the access front door: `warden access` proxies the read f | Field | Value | | --- | --- | | OpenBao path | `platform/workloads/coulomb/whynot-design/npm-publish` | -| KV field | `NPM_AUTH_TOKEN` — the only field present at this path (attended enumeration 2026-09-10, railiance-platform `docs/evidence/2026-09-10-npm-lane-field-resolution.json`) | -| Injected env var | `NPM_AUTH_TOKEN` — same name as the KV field, which is what made the 2026-09-09 `npm_token` change look plausible | +| Field | `NPM_AUTH_TOKEN` | | KV mount | `platform` | | Read policy | `workload-kv-read-whynot-design-npm-publish` | | OIDC login | `bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read` | @@ -35,17 +27,6 @@ this token** — it is the access front door: `warden access` proxies the read f > The `platform/workloads/whynot-design/whynot-design/npm-publish` path from early in the > provisioning thread is **superseded** — the live path is under the `coulomb` tenant. -> **A second, ungoverned location exists and is not this lane.** -> `secret/coulomb/whynot-design/npm/publish` (v1, created 2026-07-03, never updated) sits -> outside this lane's exact-path policy and outside any CCR. It was found by metadata read -> only; its field names were not enumerated, its value was not read, and it was not -> deleted — a location holding real credential material is disposed of deliberately by its -> owner, not tidied away by whoever finds it. railiance-platform tracks it as -> `RPF-WP-0035-T07`, with an open hypothesis that the native `secrets-engine exec` front -> door below may be reading it rather than the governed path. If that proves true, this -> lane's acceptance evidence describes a path its consumer does not use. Do not route -> around the governed path on that suspicion; the question is with its owners. - --- ## Worker checklist @@ -61,20 +42,13 @@ this token** — it is the access front door: `warden access` proxies the read f for this lane (SECRETS-WP-0003, decision e6381a56); ops-warden routes to it: ```bash secrets-engine route whynot-design-npm-publish --json # pointer / readiness - secrets-engine exec --catalog whynot-design-npm-publish -- \ - npm view @whynot/design@ version \ - --registry=https://forgejo.coulomb.social/api/packages/coulomb/npm/ secrets-engine exec --catalog whynot-design-npm-publish -- npm publish ``` **ops-warden transparent fallback** — same lane via the `warden access` proxy (fetches as - you, holds nothing). The project `.npmrc` must point both the `@whynot` scope and - token fragment at `forgejo.coulomb.social`: + you, holds nothing). Field-verified flags (whynot-design, @whynot/design@0.4.0): ```bash # --exec needs the env-var name. The zone-aware policy gate always runs first. - warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \ - --exec -- npm view @whynot/design@ version \ - --registry=https://forgejo.coulomb.social/api/packages/coulomb/npm/ warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN \ --exec -- npm publish warden access whynot-design-npm-publish --field NPM_AUTH_TOKEN --fetch @@ -89,19 +63,9 @@ this token** — it is the access front door: `warden access` proxies the read f `resolvable: true` means the lane is concrete and `--fetch` will run; a template lane reports `false`. -4. **Publish is outward-facing and immutable.** Before publishing, confirm that - `package.json#publishConfig.registry` is exactly the Forgejo URL above, verify the - intended version and `npm pack --dry-run` contents, and obtain explicit operator - approval. `npm publish` is irreversible; do not auto-run it from an agent. - -5. **Record non-secret release evidence.** After the owner publishes, record only the - package coordinate (for example `@whynot/design@0.4.2`), registry URL, authenticated - install result, and release-content verification. Never record the token or npm - configuration generated for its delivery. - - Forgejo advertises `npm view`, search, install, publish, unpublish, and dist-tag - support; it does not advertise `npm whoami`. Use the exact-version lookup above - rather than treating `npm whoami` failure as a credential failure. +4. **Publish is outward-facing and immutable.** `npm publish` is irreversible and public. + Even once the token resolves, hold for an explicit operator "yes, publish" — do not + auto-run it from an agent. --- diff --git a/workplans/ADHOC-2026-06-27.md b/workplans/ADHOC-2026-06-27.md index d75908f..cda844a 100644 --- a/workplans/ADHOC-2026-06-27.md +++ b/workplans/ADHOC-2026-06-27.md @@ -1,5 +1,5 @@ --- -id: WARDEN-WP-ADHOC-2026-06-27 +id: ADHOC-2026-06-27 type: workplan title: "Ad Hoc Tasks — 2026-06-27" domain: infotech @@ -9,7 +9,7 @@ owner: claude topic_slug: custodian created: "2026-06-27" updated: "2026-06-27" -state_hub_workstream_id: "a222c91f-3bb5-58a4-b6b2-f0fb18cdd5c3" +state_hub_workstream_id: "142b171b-c34b-4a45-91a5-c77e6d07ec6f" --- # Ad Hoc Tasks — 2026-06-27 @@ -19,10 +19,10 @@ Low-risk opportunistic fixes completed directly during the consolidation session ### T01 — Fix stale `warden` CLI install + make it usable outside the repo ```task -id: WARDEN-WP-ADHOC-2026-06-27-T01 +id: ADHOC-2026-06-27-T01 status: done priority: medium -state_hub_task_id: "9176b560-8ca5-5143-888d-479857fe60f0" +state_hub_task_id: "867c72c9-9904-400f-8542-04264e5856c2" ``` issue-core reported (msg `70bcf238`) that the `warden` CLI on `~/.local/bin` lacked diff --git a/workplans/ADHOC-2026-06-29.md b/workplans/ADHOC-2026-06-29.md index b46be0f..34aac41 100644 --- a/workplans/ADHOC-2026-06-29.md +++ b/workplans/ADHOC-2026-06-29.md @@ -1,5 +1,5 @@ --- -id: WARDEN-WP-ADHOC-2026-06-29 +id: ADHOC-2026-06-29 type: workplan title: "Ad Hoc Tasks — 2026-06-29" domain: infotech @@ -9,7 +9,7 @@ owner: claude topic_slug: custodian created: "2026-06-29" updated: "2026-06-29" -state_hub_workstream_id: "13fa845f-852e-55ec-a2a5-2296996e0216" +state_hub_workstream_id: "1c0460b7-bc8a-48db-96d4-681bce18ac91" --- # Ad Hoc Tasks — 2026-06-29 @@ -17,10 +17,10 @@ state_hub_workstream_id: "13fa845f-852e-55ec-a2a5-2296996e0216" ### T01 — Joint-smoke mode for the deployed flex-auth (assist FLEX-WP-0007 T4) ```task -id: WARDEN-WP-ADHOC-2026-06-29-T01 +id: ADHOC-2026-06-29-T01 status: done priority: medium -state_hub_task_id: "62540533-f4ca-5176-9237-32adbeb292ee" +state_hub_task_id: "371235cc-b9d3-4103-b09f-e4e01cc83c5b" ``` flex-auth (msg `ea00620b`) asked ops-warden to help close FLEX-WP-0007 T4 (joint OpenBao diff --git a/workplans/ADHOC-2026-08-11.md b/workplans/ADHOC-2026-08-11.md index 3d858fe..4f234fb 100644 --- a/workplans/ADHOC-2026-08-11.md +++ b/workplans/ADHOC-2026-08-11.md @@ -1,5 +1,5 @@ --- -id: WARDEN-WP-ADHOC-2026-08-11 +id: ADHOC-2026-08-11 type: workplan title: "Ad Hoc Tasks — 2026-08-11" domain: infotech @@ -9,7 +9,7 @@ owner: claude topic_slug: custodian created: "2026-08-11" updated: "2026-08-15" -state_hub_workstream_id: "9f99cc64-4682-5f20-b13e-89af2b6f7c70" +state_hub_workstream_id: "bc6dd3e3-ab2b-567a-9574-30d2650f20a2" --- # Ad Hoc Tasks — 2026-08-11 @@ -17,10 +17,10 @@ state_hub_workstream_id: "9f99cc64-4682-5f20-b13e-89af2b6f7c70" ### T01 — Repair stale `rapp-qonto-keycape-client` wiki anchor (restore green routing suite) ```task -id: WARDEN-WP-ADHOC-2026-08-11-T01 +id: ADHOC-2026-08-11-T01 status: done priority: medium -state_hub_task_id: "0771d121-278c-556e-9509-841cf6e657c3" +state_hub_task_id: "ddd0c95c-1196-5251-a4ca-6ef441c2f377" ``` rapp-postgres (msg `96907986`, residual from RAPP-POSTGRES-WP-0002-T04) reported the @@ -39,10 +39,10 @@ an anchor that does not exist. The intended heading is `## Routing catalog index ### T02 — Triage the stale ops-warden inbox (11 unread, C-28/C-29) ```task -id: WARDEN-WP-ADHOC-2026-08-11-T02 +id: ADHOC-2026-08-11-T02 status: done priority: medium -state_hub_task_id: "0ed58145-732f-5102-b6a8-b931d9b6ba08" +state_hub_task_id: "fda35ad4-f526-5012-a2be-ef6b5dcbc8af" ``` `fix-consistency` flagged 11 unread messages older than 3 days, two of them as possible @@ -73,10 +73,10 @@ read. Inbox is now empty. ### T03 — warden-sign AppRole: PARKED pending WP-0027 break-glass + ops-bridge cutover ```task -id: WARDEN-WP-ADHOC-2026-08-11-T03 +id: ADHOC-2026-08-11-T03 status: done priority: medium -state_hub_task_id: "337ae793-c6b0-59e9-8a07-3a7ccba237aa" +state_hub_task_id: "7ef4b6b0-26bf-5456-8429-8cf8458c2ddc" ``` secrets-engine (msg `80456912`, 2026-06-29) is holding a validated non-mutating dry-run diff --git a/workplans/ADHOC-2026-08-17.md b/workplans/ADHOC-2026-08-17.md index 65d7595..15f35e5 100644 --- a/workplans/ADHOC-2026-08-17.md +++ b/workplans/ADHOC-2026-08-17.md @@ -1,5 +1,5 @@ --- -id: WARDEN-WP-ADHOC-2026-08-17 +id: ADHOC-2026-08-17 type: workplan title: "Ad Hoc Tasks — 2026-08-17" domain: infotech @@ -9,7 +9,7 @@ owner: claude topic_slug: custodian created: "2026-08-17" updated: "2026-08-17" -state_hub_workstream_id: "5c6c2bbb-b944-5afd-b89c-20d865518849" +state_hub_workstream_id: "8376eb89-36f2-52b0-a59a-2954b2e49349" --- # Ad Hoc Tasks — 2026-08-17 @@ -21,10 +21,10 @@ workload-identity owner. ### T01 — Answer flex-auth: how should `/v1/check` authenticate its callers? ```task -id: WARDEN-WP-ADHOC-2026-08-17-T01 +id: ADHOC-2026-08-17-T01 status: done priority: high -state_hub_task_id: "04a2f8f9-e70b-5eed-ad87-343c8f9ef501" +state_hub_task_id: "b980d1f7-2fcc-58dd-97d8-6e9b9ba57bc9" ``` flex-auth (msg `130a148c`, FLEX-WP-0015 T02) reported that `POST /v1/check` and @@ -69,10 +69,10 @@ Their four questions answered in `wiki/NetKingdomSecurityMap.md` ### T02 — user-engine: USER_ENGINE_PROXY_SECRET stays railiance-apps; record consumer-only ```task -id: WARDEN-WP-ADHOC-2026-08-17-T02 +id: ADHOC-2026-08-17-T02 status: done priority: medium -state_hub_task_id: "0e815282-2fad-5c8d-be34-398e492737d0" +state_hub_task_id: "e76b9b3e-189b-5a4f-8815-4ccf2057507c" ``` user-engine (msg `2af4a124`) answered the open confirm question on @@ -95,10 +95,10 @@ consumes it and has no authority over custody, rotation, or issuance. ### T03 — key-cape: `rapp-qonto-keycape-client` interim accepted; refresh the blocker ```task -id: WARDEN-WP-ADHOC-2026-08-17-T03 +id: ADHOC-2026-08-17-T03 status: done priority: medium -state_hub_task_id: "e21781d9-a35d-5916-b335-d12131f97a22" +state_hub_task_id: "edfb0626-5022-5f49-aaf9-08e0d1002b90" ``` key-cape (msg `099b7cba`) acknowledged the lane staying interim on ops-warden. @@ -118,10 +118,10 @@ workplan. ### T04 — Session hygiene ```task -id: WARDEN-WP-ADHOC-2026-08-17-T04 +id: ADHOC-2026-08-17-T04 status: done priority: low -state_hub_task_id: "b15724e0-c27a-5260-a810-4dd25bff2228" +state_hub_task_id: "62109af6-f095-5e7b-9258-0fcec29d30e8" ``` - [x] `uv run pytest -q` → 338 passed, 4 deselected (routing anchor + no-double-source diff --git a/workplans/ADHOC-2026-09-08.md b/workplans/ADHOC-2026-09-08.md deleted file mode 100644 index c1ae7c3..0000000 --- a/workplans/ADHOC-2026-09-08.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -id: WARDEN-WP-ADHOC-2026-09-08 -type: workplan -title: "Ad Hoc Tasks — 2026-09-08" -domain: infotech -repo: ops-warden -status: finished -owner: claude -topic_slug: netkingdom -created: "2026-09-08" -updated: "2026-09-08" -state_hub_workstream_id: "182f3aff-8c8a-5766-8a68-5fe35f423f7a" ---- - -# Ad Hoc Tasks — 2026-09-08 - -Four key-cape messages, all follow-on from `WARDEN-WP-0033`. Two carry answers -ops-warden's catalog was waiting for; both make a recorded blocker less true than -it reads. This is the T05 mechanism paying out — and this time the owner ran it -against us rather than us running it against them. - -### T01 — Narrow the `rapp-qonto-keycape-client` blocker (msg `08d42f47`) - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T01 -status: done -priority: high -state_hub_task_id: "29cef28a-d93b-5318-bfdb-69dbe11fa3ba" -``` - -The blocker recorded "still no key-cape-native exchange/rotation command", -`verified: source-read` against key-cape source on 2026-08-28. Accurate then; -two things landed after that read: - -- `keycape service-token` (2026-09-05, KEY-WP-0014-T03) — the client_secret_basic - exchange the blocker records as absent. -- `keycape verify-client` (2026-09-08) — rotation step 3 as one command, - including predecessor rejection and identical-secret detection. - -**Narrowed, not cleared**, as key-cape asked. What remains genuinely blocked is -steps 1-2: successor generation and the CAS write to both custodians. Those are -custody and deployment acts, key-cape deliberately ships no command for them, and -no admitted ops-warden lane authorizes them either (T03). - -`verified` moved to `owner-confirmed` — the correction came from the owner. - -`rotation.automatable` flipped `true` → `false`, which key-cape explicitly asked -us not to do "either way" without precision. Done with the precision they wanted, -in the only place the schema has room for it: the flag is a single hint consumed -by a future executable driver (`WARDEN-WP-0027` Strand B), and a driver told -`true` would attempt custody acts with no admitted transport — the dangerous -direction. So the flag fails safe and the *steps* now carry the per-step truth, -each naming whether it is automatable and why. Offered them a schema change if a -per-step field would serve better; not built on speculation. - -### T02 — Record key-cape's acceptance of `key-cape-oidc-login` (msg `12f1bdfa`) - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T02 -status: done -priority: high -state_hub_task_id: "8efb8013-7b90-52ef-a17c-d63f5ce3752c" -``` - -key-cape **accepted** ownership: interactive login, OIDC identity-token issuance -and MFA. This confirms the 2026-08-28 correction of `intended_owner` from -secrets-engine to key-cape, and confirms secrets-engine's 2026-08-21 refusal was -right. `verified: asked-and-waiting` → `owner-confirmed`. - -**The lane stays `interim`.** Acceptance covers the identity half only. The -`fetch_command` yields an *OpenBao* token — the `netkingdom` mount, the -`role=` mapping, the token store and its enforcement are OpenBao's, with -role and policy definition railiance-platform's. `keycape login` yields a -key-cape issuer JWT to a private file: different credential type, different -verifier, different enforcement owner. Accepting an owner is not the same as the -owner fronting the lane, and `ADR-0003` says the interim cover is retired when -the front door exists, not when ownership is agreed. Retirement condition is now -recorded on the entry. - -Their reading of the risk is right and we confirmed it: `warden_executes: false` -with a `wiki_ref` pointer, so per `ADR-0001` this is a pointer layer, not a code -path with programmatic consumers. `risk: standard` and -`workload_ref: not-applicable` are correct — a human identity act with no KV -path, consistent with how `ADR-0008` treats the other non-KV lanes. - -### T03 — Answer "who executes Qonto rotation steps 1-2" (msg `0a6c9d88`) - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T03 -status: done -priority: high -state_hub_task_id: "bd621821-2116-51c9-a09a-a3660b5e5557" -``` - -Answer: **nobody, admittedly, today** — and ops-warden must not invent one. - -Steps 1-2 mutate custody on `platform/workloads/rapp-qonto/keycape-client` and -`sso/keycape-rapp-qonto-client`. Custody of platform workload paths is -railiance-platform's; the admitted execution front door for provisioned -secret-exec is `secrets-engine exec`. ops-warden owns neither and absorbing it -would be `ADR-0002` and `ADR-0005` violated in one act. Routed to -railiance-platform, with secrets-engine named as the transport owner — and -secrets-engine independently confirmed the same boundary three days earlier -(msg `15f0c0ca`): they will not rewrite a production lane's custody state from a -coordination message without owner confirmation. - -key-cape was right not to treat a generic `warden route` match as authorization. -See T04 for what our own tool did when asked. - -### T04 — `warden plan` returned `autonomous` for a custody *write* (defect) - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T04 -status: done -priority: high -state_hub_task_id: "9ada01d2-b14b-579e-921c-275820f255db" -``` - -Verifying T03 against our own front door: - -``` -warden plan "generate a successor client secret ... and CAS-write it to - OpenBao platform/workloads/... and sso/..." --json - verdict: autonomous - lane_id: rapp-qonto-keycape-client - commands: warden access ... --exec / --out / --wrap -``` - -A need that says *generate* and *CAS-write* resolved to `autonomous` and was -answered with three **read** transports. This is the `WARDEN-WP-0033-T06` shape -recurring — a generic keyword match producing a confident wrong act — except T06 -fixed one route by adding an exact lane, and the defect is in the classifier. -`warden plan` has no notion of read versus mutate intent, so every write need -matches the lane that *reads* the same path and comes back `autonomous`. - -Not fixed here: a classifier is design work with an approval-semantics blast -radius, which the convention says is a workplan, not an adhoc. -Promoted to **`WARDEN-WP-0038`** (`proposed`). - -Worth stating plainly: key-cape distrusted this output on principle and was right -to. Had they trusted it, our tool would have told them to proceed autonomously on -an unadmitted custody mutation. - -### T05 — Acknowledge the KEY-WP-0009 handoff (msg `5bc58847`) - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T05 -status: done -priority: low -state_hub_task_id: "06bf8508-d1a2-51b4-b951-02539524ebb9" -``` - -Informational; no route retired or changed. Their routing note matches ours: -"login / OIDC / MFA" → key-cape means **issuance only**, and secret *values* -route to OpenBao custody. Nothing to change in `wiki/CredentialRouting.md`. - -Also records that the KEY-WP-0009 reply ops-warden reported never arriving indeed -never existed (KEY-WP-0014-T02, audited 2026-09-05). Our 2026-08-28 finding was -right, and the correction is theirs, recorded. - -### T06 — Unpin two tests from a transient catalog state - -```task -id: WARDEN-WP-ADHOC-2026-09-08-T06 -status: done -priority: medium -state_hub_task_id: "a0b88db9-2886-54dd-a925-9e0521044aa0" -``` - -`test_stale_gaps_flags_unverified_even_when_the_date_is_today` and -`test_cli_route_gaps_fail_on_stale_exits_3` both asserted that -`key-cape-oidc-login` specifically sat `asked-and-waiting`. T02 answering that -question broke them — **the tests failed on good news**, not on a defect, and a -test that fails when a counterparty replies will get suppressed rather than read. - -Both now assert the property they were always about: any lane whose blocker was -never re-established is stale regardless of its date, over whatever lanes are in -that state. The named-lane version of the claim already exists as a unit test on -`Delegation.is_verified`, which is where a fixture belongs. diff --git a/workplans/ADHOC-2026-10-03.md b/workplans/ADHOC-2026-10-03.md deleted file mode 100644 index e76cd01..0000000 --- a/workplans/ADHOC-2026-10-03.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -id: WARDEN-WP-ADHOC-2026-10-03 -type: workplan -title: "Documentary feedback on Emergent Security repository pilot" -domain: infotech -repo: ops-warden -status: finished -owner: codex -topic_slug: ops-warden -created: "2026-10-03" -updated: "2026-10-03" -state_hub_workstream_id: "246dc069-0f9b-516b-9db4-9914323da16f" ---- - -## Review retained pilot evidence against owner boundaries - -```task -id: WARDEN-WP-ADHOC-2026-10-03-T01 -status: done -priority: medium -state_hub_task_id: "c532cd72-88c4-543b-b6db-caf1bcb74a3d" -``` - -Evidence: docs/evidence/2026-10-03-emergent-pilot-owner-feedback.md. -Completed authorized documentary feedback on the retained read-only 0.2.0 -Claude scenario; preserved original version and evidence limits, qualified -owner boundary wording and identified bounded 0.2.2 follow-up scenarios. -No provider session, runtime control, deployment, credential or institutional -adoption was performed. Next pilot/adoption gates remain existing -EMERGENT-WP-0002-T08/T09 and decision d20a9750-2b29-534e-8227-fb866581a6ca. -No new operational residual was created. diff --git a/workplans/WARDEN-WP-0027-credential-governance-lockdown.md b/workplans/WARDEN-WP-0027-credential-governance-lockdown.md index c14e9c3..840f291 100644 --- a/workplans/WARDEN-WP-0027-credential-governance-lockdown.md +++ b/workplans/WARDEN-WP-0027-credential-governance-lockdown.md @@ -4,14 +4,13 @@ type: workplan title: "Tamper-resistant credential governance + mass rotation/lockdown (Strand B)" domain: infotech repo: ops-warden -status: blocked -flavor: implementation +status: active owner: codex topic_slug: custodian planning_priority: medium planning_order: 27 created: "2026-07-16" -updated: "2026-10-03" +updated: "2026-08-23" state_hub_workstream_id: "21528e8d-a049-523d-9ae1-da7a27cb8bbf" --- @@ -28,7 +27,7 @@ Strand A makes accidental disclosure structurally hard and gives every lane layer: turning that advisory guidance into one-command action and hardening policy governance against silent drift or malicious change. -## Status: blocked on T02 (reviewed 2026-09-28) +## Status: active, narrowly on T02 Activated by the operator on 2026-08-22 after the second activation gate became concrete. `RAILIANCE-WP-0024-T03` now requires an approved recovery window, @@ -40,7 +39,7 @@ heavyweight machinery. Activation is deliberately narrow. No mass-disclosure incident triggers T01, and no fleet policy currently mandates T03's signed policy-manifest reconcile. Those tasks remain `cancel`; cancellation here continues to mean deferred, not -abandoned. T02 alone was activated; it now waits on a fresh attended drill. +abandoned. T02 alone is `progress`. ## Activation gate (promote to `ready` only when ≥1 holds) @@ -89,14 +88,11 @@ each verified capabilities-safe, taint cleared only on success. ```task id: WARDEN-WP-0027-T02 -status: todo -decision_id: 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa +status: progress priority: medium state_hub_task_id: "cae498ee-6307-5d32-9f1b-a471cfcc2536" ``` -2026-10-11: requalified after founder decision D3 (trust root, build-mode default) resolved — see the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. - Design and document a graded lockdown: (a) soft — deny agent roles read on all high-risk lanes; (b) hard — seal + re-key with a pre-designed trust-root and quorum. Define the trust-root (who holds unseal shares, recovery keys), the break-glass @@ -226,41 +222,6 @@ test results, and infra acceptance. T02 remains `progress` until that owner gate is open; acceptance will permit preparation of a new scenario, not execution or reuse of the terminal one. -**Owner gate CLOSED — accepted 2026-08-23, found 2026-08-28.** -`railiance-platform` accepted the exact revision `0fae0904`. Recorded in their -`RPF-WP-0017-attended-login-output-containment.md` (`status: finished`), which -also records railiance-infra's independent approval at `186b030` and marks all -five acceptance criteria met. The receipt digest was verified here rather than -taken on trust: `docs/evidence/RAILIANCE-WP-0026-T01-ops-warden-receipt.json` at -ops-warden `bc1966da` hashes to -`d2ba444ed16989590325697e69d25283dc75a9432c29a72e627e80bf9fd987e4`, matching -their record exactly. - -Note the identifier: the remediation interface recorded above as -`RAILIANCE-WP-0026-T01` is `RPF-WP-0017-T01` in the owner's repo. Searching for -the cited id finds an unrelated workplan, which is part of why this sat unnoticed. - -**Their acceptance is source acceptance only and authorizes no live OIDC or -drill** — their words, and the boundary holds. So T02's state changes but its -`Done when` does not: what was blocked was *preparing a new scenario*, and that -is now permitted. The terminal NO-GO scenario and its receipts remain unusable. - -**Third instance of the same failure this session.** The acceptance existed for -five days; no message reached ops-warden. Identically, `key-cape` accepted the -WP-0033-T04 question on 2026-08-23 with no message, and nine unread messages -were sitting on already-superseded threads. The `verified:` field added by -WP-0033-T05 was built for exactly this and it works — what does not work is -waiting for a counterparty to tell you. Re-checking a blocker means reading the -owner's repository. - -**Remaining to close T02:** one attended production emergency seal/unseal drill, -requiring a new scenario id, fresh owner receipts from platform/infra/master, a -fully parameterized green preflight, and a new human GO. It is executed by the -platform owner, never by a coding agent. That is an operator decision, not an -agent one, so T02 stays `progress` and the decision is surfaced rather than taken. - -2026-10-03: human gate routed to hub decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa (D3: Trust root and secret custody: unseal/recovery holders, break-glass, and the deferred rotation of exposed material) by CUST-WP-0075; resolving that decision raises C-40 here. - ## Task: Tamper-evident policy governance + reconcile ```task @@ -292,12 +253,3 @@ manifest, and an attended reconcile can restore it. approved ops-bridge unattended-signing design may still re-evaluate it cert_command cutover - `secrets-engine` `SECRETS-WP-0004` — the parked AppRole apply/handoff - -### 2026-09-28 loose-end review - -T02 now waits and the workplan is blocked. Source containment acceptance is -already complete in railiance-platform's archived RPF-WP-0017; it is not a live -drill receipt. The terminal August scenario remains consumed. Completion still -requires a fresh platform-driven attended emergency seal/unseal scenario, fresh -platform/infra/master receipts and a new founder GO. The cancelled T01/T03 stay -cancelled. No ceremony, rotation or production mutation was performed. diff --git a/workplans/WARDEN-WP-0033-native-lane-handoff.md b/workplans/WARDEN-WP-0033-native-lane-handoff.md index 55cd015..1e20015 100644 --- a/workplans/WARDEN-WP-0033-native-lane-handoff.md +++ b/workplans/WARDEN-WP-0033-native-lane-handoff.md @@ -4,14 +4,14 @@ type: workplan title: "Native lane handoff — review secrets-engine's catalog admission, and fix what it exposed" domain: infotech repo: ops-warden -status: finished +status: active owner: ops-warden topic_slug: netkingdom planning_priority: P1 depends_on_workplans: - WARDEN-WP-0030 created: "2026-08-21" -updated: "2026-08-28" +updated: "2026-08-23" state_hub_workstream_id: "4627d89b-4b00-562a-81e9-76e96f90fa7e" --- @@ -160,7 +160,7 @@ railiance-platform may deny more, deny less, or dispute a grade (`ADR-0002`). ```task id: WARDEN-WP-0033-T04 -status: done +status: wait priority: medium state_hub_task_id: "5acac140-a586-5db3-b231-bbf236710786" ``` @@ -212,33 +212,6 @@ accept that target identity with an authoritative workplan/interface, or refuse and name the actual owner. T04 remains `wait` until one of those two answers is recorded; the live AppRole is operational evidence, not an ownership answer. -**Answered — accepted. Closed 2026-08-28.** `key-cape` accepted issuance -ownership in `KEY-WP-0009-T03` (`status: done`, 2026-08-23): the non-secret static -registration `codex-railiance-platform` is published in -`key-cape/config/service-clients.example.yaml` — subject -`service:codex:railiance-platform`, tenant `tenant:coulomb`, role `coding-agent`, -scope `openbao:login`, `client_credentials`, 15-minute lifetime — with the -service-auth claims, renewal, expiry and failure semantics in -`key-cape/docs/openbao-service-auth-contract.md` (`T02`). The split is exactly the -one the routing asked for: KeyCape owns JWT issuance and client disablement, -railiance-platform owns the exact-bound OpenBao role and policy, OpenBao owns -enforcement, and no secret value enters either repository. - -**The answer was found by reading their repo, not by receiving it.** -`KEY-WP-0009-T04` records replying to ops-warden; no such message exists in the -ops-warden inbox — 0 messages from `key-cape`, read or unread. The acceptance had -been sitting done for five days while this task sat `wait` on it. - -That is the T05 lesson landing on T04: a blocker is a claim about the world at a -date, and this one had stopped being true. `verified: source-read` is doing real -work here — it is the difference between "we asked" and "we checked". The -mechanism is right; what it needs is for someone to run it, which is why the -lanes below were re-verified in the same pass rather than date-bumped. - -Consequence for `ADR-0004`: the honour-system `WARDEN_AGENT_ID` marker now has a -real issuance identity behind it on the OpenBao side. ops-warden remains a -consumer of that identity and does not own it. - ```task id: WARDEN-WP-0033-T05 status: done diff --git a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md b/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md deleted file mode 100644 index d6ebfdc..0000000 --- a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md +++ /dev/null @@ -1,354 +0,0 @@ ---- -id: WARDEN-WP-0034 -type: workplan -title: "Layer model v0.7 conformance — state the deadline, bind the agent boundary, steward the estate's newest rule" -domain: infotech -repo: ops-warden -status: active -flavor: residual -depends_on: - - WARDEN-WP-0030 -owner: ops-warden -topic_slug: netkingdom -planning_priority: P1 -depends_on_workplans: - - WARDEN-WP-0030 -created: "2026-08-29" -updated: "2026-09-28" -state_hub_workstream_id: "ae3ff76f-883d-5e2f-b6aa-144d61e8fdef" ---- - -# WARDEN-WP-0034 — Layer model v0.7 conformance - -`security-layer-model_v0.7` is **accepted**. ops-warden declared Staff and PEP-shaped, -shipped the two declaration artifacts the standard now cites as estate reference forms, -and had four findings adopted into the text between v0.4 and v0.7. - -The assessment in `history/2026-08-29-v07-scope-intent-assessment.md` checked every -v0.7 obligation against shipped code rather than intent. Three gaps survive, plus a -role the companion assigns that no conformance check will ever catch. - -## Why a workplan and not an adhoc - -T01 is a `MUST` that is currently unstated. T02 changes an enforcement boundary and -touches `ADR-0004`. T04 is a cross-repo stewardship commitment against a standard eight -repositories have yet to adopt. The convention reserves adhocs for low-risk work -completed directly; none of these qualify. - -## Tasks - -```task -id: WARDEN-WP-0034-T01 -status: done -priority: high -state_hub_task_id: "8b3bdb9f-d2c2-5b3e-89e2-417bf3e37484" -``` - -**State the revocation visibility deadline (§9.7.2, a MUST).** - -ops-warden states none, and the honest value is the certificate TTL: **up to 48 hours**. -A cert issued under an allow remains valid for its full TTL even if the authorizing -decision is revoked the next minute. There is no CRL, no KRL distribution, and host-side -`auth_principals` belongs to `railiance-infra`. - -Add the deadline to `pep-stance.yaml` as what it is — `adm` 48h / `agt` 24h / `atm` 8h — -with the mechanism named (TTL expiry, no revocation channel) rather than implied. Assert -it against the shipped `ActorType` TTL policy by test, the same way the stance map is -asserted equal to shipped behaviour: a stated deadline free to drift from the code has -the same defect as a stated stance free to drift. - -**Done when:** the deadline is published, test-bound to the TTL policy, and the absence -of a revocation channel is stated rather than left to inference. - -**Not in scope:** shortening it. Whether 48h is acceptable is a joint question with -`railiance-infra` (KRL distribution) and is T05's to raise, not this task's to decide. - -Completed 2026-09-04. `pep-stance.yaml` now publishes TTL expiry as the only -revocation mechanism, explicitly records that no CRL/KRL channel exists, and -states `adm=48h`, `agt=24h`, and `atm=8h`. The conformance test derives the -expected map from `ActorType`/`MAX_TTL_HOURS`, so declaration and issuance -policy cannot drift independently. The layer and stance declarations now name -the accepted v0.7 standard. - -```task -id: WARDEN-WP-0034-T02 -status: done -priority: high -state_hub_task_id: "3318ee1a-b5d9-5d39-baf7-9c42a8bc7b55" -``` - -**Bind the agent read-boundary to an issued identity (§3.4 rule 1).** - -`ADR-0004`'s boundary triggers on `WARDEN_AGENT_ID` — a variable the agent sets about -itself. An agent that omits it is not recognised as one. §3.4 rule 1 now requires -authority to be *issued* per task and attributable to the principal acted for. - -The identity exists as of `KEY-WP-0009-T03`: `codex-railiance-platform`, subject -`service:codex:railiance-platform`, role `coding-agent`, 15-minute lifetime, with -`railiance-platform` enforcing the OpenBao-side policy. - -Key the boundary on the issued identity where one is present; keep `WARDEN_AGENT_ID` as -a fallback that fails **toward** the boundary, never away from it. State plainly in the -ADR trail which half is enforced and which is advisory: the OpenBao-side deny is real, -the ops-warden-side refusal is a courtesy that a determined caller can decline. - -**Done when:** an agent presenting the issued identity is recognised without setting -`WARDEN_AGENT_ID`, the fallback still refuses on `risk: high` lanes, and the -enforced/advisory split is written down. - -**Watch:** do not turn this into ops-warden validating a token — that is verifying an -identity claim, adjacent to deciding, and `ADR-0002`/§6 both point away from it. Read -the identity; do not adjudicate it. - -Completed 2026-09-04. The advisory CLI guard now prefers the exact issued -KeyCape subject `service:codex:railiance-platform` from -`WARDEN_POLICY_SUBJECT`, then falls back to `WARDEN_AGENT_ID`. Tests prove the -issued subject triggers exit 7 without the legacy marker, the fallback remains, -and an operator subject does not manufacture an agent identity. ADR-0004 -revision 2 records the enforced/advisory split: Warden reads the marker and -does not validate a token; OpenBao's `agent-high-risk-boundary` is the real -enforcement. - -```task -id: WARDEN-WP-0034-T03 -status: done -priority: medium -state_hub_task_id: "a891b32c-b0a7-59f6-a5cd-977be65c09ca" -``` - -**Derive an emission cadence, or defer it with a reason (§9.6).** - -ops-warden's trail is attributive, so cadence is a SHOULD rather than v0.7's MUST for -load-bearing sources. It has been silent through two reviews, which is the one outcome -that is not defensible. - -Derive a baseline from the existing `audit.jsonl` and signatures log. If the signal is -too bursty to support a threshold — plausible, since volume is operator-driven — record -that as the finding with the distribution that shows it, and declare the deferral in -`pep-stance.yaml`. A measured "no useful baseline" is a result; silence is not. - -**Done when:** either a declared cadence with its derivation, or a declared deferral -carrying the data that justifies it. - -Completed 2026-09-04 with an explicit measured deferral. The signatures log -contains three issuances across the 79-day observation window, on only two -active days: one on 2026-06-17 and two within 40 minutes on 2026-08-22. The -unified audit carries the latter two events. `pep-stance.yaml` records the -counts, dates, attributive classification, and reason that this sparse, -operator-driven burst cannot support a meaningful rate threshold. - -```task -id: WARDEN-WP-0034-T04 -status: done -priority: medium -state_hub_task_id: "94e73daa-f74d-51fd-8639-68896a4066ee" -``` - -**Answer the question the companion sends the estate here to ask.** - -> *"For how to get something done — which lane, which credential, which route — ask -> `ops-warden`."* - -Today the repo answers credential questions and no others. `warden route` and -`warden plan` cover lanes, owners and acts. Nothing answers *"which layer am I"*, *"how -do I declare"*, *"I am PEP-shaped, what do I owe"* — and eight of fifteen catalogued -repositories have yet to declare. - -Provide the path, not the doctrine (that boundary is `ADR-0010`'s and does not move): -a routing entry and a short playbook that carry a reader from the companion to the files -to copy — `layer.yaml`, `pep-stance.yaml`, `check_layer_conformance.py`, -`test_layer_conformance.py` — and the check to run. The standard already names these in -§11 and §6.4; what is missing is the route to them. - -**Done when:** `warden route find "how do I declare my layer"` resolves, and the -playbook is reachable from the catalog. **Not** a restatement of the companion — a -pointer layer, per `ADR-0001`. - -Completed 2026-09-04. Catalog entry `netkingdom-layer-declaration` resolves the -exact query to `wiki/playbooks/netkingdom-layer-declaration.md`. The short -playbook points at the accepted companion/statute, the four reference artifacts -and checks, and the gate-house review path while explicitly leaving doctrine -with gate-house and declaration truth with each repository. - -```task -id: WARDEN-WP-0034-T05 -status: wait -blocked_on: message-from:ops-mason -blocking_reason: "Two items to route, not absorb: ops-mason has published no stance map; the second is another repository's to own." -priority: low -state_hub_task_id: "7d1b3c82-9b96-5087-a53a-496212909029" -``` - -**Two things to raise rather than absorb.** - -Both are other repositories' to own; ops-warden's obligation is to route them, not to -fix them (`ADR-0003`, `ADR-0005`). - -1. **`ops-mason` has published no stance map.** §13.1's register has one row and the - standard says that is itself the finding. `ops-mason` is catalogued PEP-shaped in - the same paragraph and is ops-warden's peer lane owner. Offer the reference form; - do not write their map. -2. **The 48-hour replay window from T01.** Once stated, raise with `railiance-infra` - whether KRL distribution is worth building, and with `access-engine` whether a - decision lifetime shorter than the cert TTL is meaningful when nothing can recall - the cert. State the question; let the owners answer. - -**Done when:** both are routed with reasoning, and the answers recorded either way — -including a refusal, which is an equally good answer. - -Raised 2026-09-04 and waiting for answers. Ops-mason received the stance-map -reference and classification question (message `6ddd6cc7`). Railiance-infra -received the KRL-versus-TTL question (message `3bfa306e`). Flex-auth, as the -current access-engine implementation, received the decision-lifetime semantics -question (message `02c99a74`). No implementation or policy decision was assigned -to another owner by these messages. - -**One of three answered, 2026-09-09.** `flex-auth` answered the decision-lifetime -question as `FLEX-DEC-2026-004` (flex-auth commit `6a6464f`), and answered it -rather than declining it: a decision lifetime shorter than the certificate TTL is -meaningful, but only as authority to **issue**, never as authority to **use** an -already-issued certificate. The action is `sign` and it completes at issuance, so -the decision is consumed once the certificate exists and no field in the decision -record reaches the artifact. 15m against a 48h `adm` certificate was never an -inconsistency to reconcile — the question mistook a decision lifetime for a -credential lifetime. - -Two obligations land on this side and are accepted: no replay of an allow past -`expires_at`, and a fresh Check per sign. The second is where the value is, and it -is real rather than nominal because claims are not cached PDP-side (deadline 0 for -the claim class), so a revoked principal stops obtaining *new* certificates -immediately. - -`pep-stance.yaml` §9.7.2 is unchanged and stays here: flex-auth explicitly declined -to move the residue to their side, on the ground that a PDP recalling an issued -credential would be "the same shape of lie as fail-open". Declining it was the -right call even though absorbing it would have made their record look tidier. - -The TTL lever they named — capping requested TTL per actor class or zone, already a -policy input since `ttl_out_of_bounds` is denied before OpenBao is reached — is -noted and deliberately not requested. Shortening `adm` from 48h has an operator -cost, and issuance on this lane is too sparse (3 signature records across 79 days, -`pep-stance.yaml` `emission_cadence`) to price it from intuition. - -T05 stays `wait` on the remaining two: `ops-mason` (no stance map published) and -`railiance-infra` (KRL distribution versus TTL expiry). Neither has replied. - -## Await gate-house's §11 precedence and case-sensitivity ruling - -```task -id: WARDEN-WP-0034-T06 -status: done -priority: low -state_hub_task_id: "b3b573df-2d2d-5a2c-9662-a13e435be57b" -``` - -ops-warden declares `layer: Staff` in `INTENT.md` frontmatter and `layer: staff` -in `layer.yaml`. §11 permits both forms and is silent on which governs when both -exist and disagree, so two conformance runs over this repository can disagree -while both follow the standard. Nine of nine repositories carrying both forms do -the same thing (flex-auth `tools/survey_layer_declarations.go`, receipt -`docs/evidence/2026-09-21-layer-declaration-survey.json`). - -**RULED 2026-09-21, APPLIED 2026-09-21.** `GH-DEC-2026-017` -(`gate-house@def0af2`, amendments A9-A13) answers both questions, and the answer -to the second dissolves the first's consequence: - -- **`INTENT.md` governs**; `layer.yaml` is a derived artifact that must be marked - derived, must name `INTENT.md` as its source, and must agree with it. -- **The §3 vocabulary is case-insensitive**, closed, and has four tokens - (`Taxonomy`, `Tooling`, `Engine`, `Staff`). A conformance run MUST ASCII-fold - case before comparing, and **no repository was asked to re-spell anything**. - `Staff` and `staff` are one value. -- **A declaration MUST NOT carry a standard version** — and that field is the - *reference form's*, not this repository's habit, so ops-warden was asked to - remove it here rather than each of seven adopters deciding. - -Verified before editing against gate-house's own committed files rather than the -inbox message: the decision at `gate-house@def0af2`, amendments A9-A13 in -`docs/amendments/v0.8-section-11-declaration-amendments.md`, and §3/§4/§11 of -`net-kingdom/canon/standards/security-layer-model_v0.8.md`. - -**What was applied.** `standard_version` removed from `layer.yaml` **and** from -`INTENT.md` frontmatter (the ruling's subject is the declaration, and `INTENT.md` -is the declaration), with the version-pinned `standard:` path de-versioned for the -same reason; `derived: true` / `derived_from: INTENT.md` added; **no layer value -changed**; `scripts/check_layer_conformance.py` rewritten to read `INTENT.md` as -governing, fold case, validate the closed four-token vocabulary, require the -derived marking, and reject a returning `standard_version` — it previously -*required* that field and so would have rejected the conforming declaration this -ruling produces; `tests/test_layer_conformance.py` asserts the **fold** rather -than equality, because an equality assertion would perform the re-spelling the -ruling declined to order. `pep-stance.yaml` untouched: a stance map is not a layer -declaration, and the schema beyond the derived marking and the version is -explicitly not ruled. - -**Evidence.** `python3 scripts/check_layer_conformance.py --report` exits 0 and -prints the governing form and the derived one separately; `pytest -tests/test_layer_conformance.py` green; full suite and `ruff` green. Reasoning -and the adopter change set: `docs/layer-declaration-precedence.md` and -`wiki/playbooks/netkingdom-layer-declaration.md`. - -**Left open, and not decided here.** Where the removed version now lives. A12 -says version-scoped state belongs in the derived conformance record, which -"already MUST" carry the version it was derived at; ops-warden has a re-runnable -checker that emits nothing durable, not an emitted record. Whether §11 expects an -emitted artifact or a re-runnable check discharges it is asked of gate-house in -message `4220413a` and unanswered. Nothing applied above depends on the answer. -**Closed 2026-09-21 by `GH-DEC-2026-020` §4 (gate-house@104f3fc):** §5 was wrong -that the record exists; a re-runnable checker is sufficient if every run prints -the version it checks against and its scope. Applied: the checker now prints -`VALIDATED_AGAINST` and `SCOPE` on every run and enforces A12 r2 by content, not -key name (versioned `standard:` path, `companion_version`); `pep-stance.yaml` -untouched per §3; the playbook carries the adopter change set. - -**Ruled the same day, and still `wait` — deliberately.** `GH-DEC-2026-017` -(message `3715e247`) landed hours later: `INTENT.md` governs, the sidecar is a -derived artifact that must be marked as such, §3's vocabulary is case-insensitive -so **nobody re-spells anything**, and `standard_version` must come out of the -declaration. Two asks land on ops-warden specifically because `layer.yaml` is the -form seven repositories copied. - -This task stays `wait` rather than flipping to `progress`, because applying it -edits this repository's security-layer declaration — and the estate's reference -shape — on the strength of an instruction delivered in an inbox message. The -session that applies it should be mandated to, and should verify the decision at -`gate-house@def0af2` and amendments A9–A13 in the repository rather than from the -message text. The exact change set and its four conditions are written down in -`docs/layer-declaration-precedence.md`; the casing half of the original "done -when" above is moot rather than outstanding, since the ruling folds case. - -Raised by flex-auth's corrected B1 (message `719b3e9e`, 2026-09-21), which -withdrew an earlier claim that the estate disagreed *across* repositories. The -withdrawal matters to this task: the original finding would have had ops-warden -changing a file to match an estate consensus that did not exist. - -## Related - -Review 2026-09-05: reviewed the pending implementation and fixed the identity -helper so an explicitly empty environment does not inherit the process identity. -Regression coverage also proves an operator subject cannot override the legacy -agent marker. Refreshed the generated high-risk path artifact after the catalog -changes. T05 remains waiting: no corresponding reply was present in the inbox, -and the checked owner workplans contained no resolution of these questions. -Validation: 415 unit tests and four SSH integration tests pass; Ruff, the -layer-conformance script, and the declaration-route CLI smoke check pass. - -- `history/2026-08-29-v07-scope-intent-assessment.md` — the gap analysis behind this plan -- `history/2026-08-29-layer-model-v04-review.md`, `-v06-review.md` — the two prior reviews -- `security-layer-model_v0.7.md` §3.4, §6.4, §9.6, §9.7, §11, §13.1 -- `net-kingdom/SECURITY-COMPANION.md` v0.2 -- `ADR-0002`, `ADR-0003`, `ADR-0004`, `ADR-0005`, `ADR-0009`, `ADR-0010` - -### 2026-09-28 loose-end review - -T05 remains wait; the workplan is blocked on the two outstanding owner answers. -The local ops-mason checkout still has no published stance map, and the available -railiance-infra workplans/docs contain no KRL-versus-TTL acceptance or refusal. -The flex-auth decision-lifetime answer remains accepted. Warden's declaration, -TTL-bound tests and fresh-check behavior are complete; inventing an owner answer -would not satisfy T05. No certificate lifetime or revocation policy was changed. - -> 2026-10-11: demoted to `flavor: residual` / `backlog` by founder-authorized triage — off the -> critical path (identity → authz → secrets → SecurityCase execution). Tasks are unchanged; -> promote back when demand or risk appears. See the-custodian -> `history/20261011-netkingdom-security-progress-assessment.md`. diff --git a/workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md b/workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md deleted file mode 100644 index 1210b15..0000000 --- a/workplans/WARDEN-WP-0035-policy-nexus-forgejo-source-read-route.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: WARDEN-WP-0035 -type: workplan -title: "Register the Policy Nexus Forgejo source-read route" -domain: infotech -repo: ops-warden -status: finished -owner: codex -topic_slug: policy-nexus-forgejo-source-read -created: "2026-09-01" -updated: "2026-09-01" -state_hub_workstream_id: "45aec8d3-94b3-586e-b019-a47e656efafa" ---- - -## Register the exact high-risk lane - -```task -id: WARDEN-WP-0035-T01 -status: done -priority: high -state_hub_task_id: "dd84f2be-0143-540c-9c16-74f0fd129260" -``` - -Add the exact OpenBao path, field, OIDC role, owner pointer, and rotation -boundary from railiance-platform CCR-2026-0014. The entry must be concrete and -resolvable while remaining subject to Warden's high-risk agent read boundary. - -## Verify routing and governed use - -```task -id: WARDEN-WP-0035-T02 -status: done -priority: high -state_hub_task_id: "1fa8f778-3e46-5f44-86c4-cab8628b7e60" -``` - -Pass catalog, route-selection, proxy, and policy tests; reinstall the CLI; prove -the installed route resolves and can hand the value only to a sanctioned child -transport without printing or persisting it. - -Completed 2026-09-01. All 406 selected tests passed, including the generated -high-risk data-path boundary. The no-cache installed CLI resolves the exact -lane, and `warden plan` returns only sanctioned `--exec`, `--out`, and `--wrap` -transports for an agent caller. Policy Nexus Actions run 32 separately proved -the installed credential against the complete private-source fetch and release -path without exposing the value. diff --git a/workplans/WARDEN-WP-0036-attended-login-openbao-output.md b/workplans/WARDEN-WP-0036-attended-login-openbao-output.md deleted file mode 100644 index 452eb41..0000000 --- a/workplans/WARDEN-WP-0036-attended-login-openbao-output.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: WARDEN-WP-0036 -type: workplan -title: "Accept contained OpenBao login output only after helper persistence" -domain: infotech -repo: ops-warden -status: finished -owner: codex -topic_slug: attended-login-openbao-output -created: "2026-09-01" -updated: "2026-10-15" -state_hub_workstream_id: "d844c96e-152d-53fa-bff6-e072125ef66c" ---- - -## Repair attended-login handoff - -```task -id: WARDEN-WP-0036-T01 -status: done -priority: high -state_hub_task_id: "7eb8b9c9-1285-5ada-a17b-1d5bfbb8ba59" -``` - -Allow a successful OpenBao login to proceed when its output is fully contained -and the private mode-0600 token helper is populated. Continue failing closed on -non-zero login, missing persistence, child output, revocation failure, or cleanup -failure. - -## Verify live contained operation - -```task -id: WARDEN-WP-0036-T02 -status: done -priority: high -state_hub_task_id: "d22bab05-c38b-561f-95de-6c146ce7c6cf" -``` - -Run the proxy regression suite, reinstall the CLI, and complete one governed -OpenBao platform-admin operation with deterministic self-revocation. - -Completed 2026-09-01. The installed CLI completed the governed Policy Nexus -Forgejo source bootstrap with all child output contained, then revoked and -removed its isolated helper session. - -### 2026-09-28 contained-result correction (existing T01/T02) - -Addressed the September 21 attended-login failure/audit report under the existing -handoff repair and verification tasks. Failed envelopes now use distinct -non-zero codes 10–14 for login, child failure, child output, unconfirmed revocation -and cleanup failure. Both audit files retain the actual Warden exit code and -phase outcome. Any child bytes, including whitespace, fail closed; the access -advisory now states the silent-child requirement and the playbook documents codes, -retry limits and the WSL tunnel/browser requirements. - -The pre-change checkout already raised exit 5 on ProxyError, so the reported -historical exit-zero failure was not reproduced here. The actual reproduced defect -was unconditional success auditing; the new CLI regression covers failed login -with returncode zero but missing persistence as well as non-zero login, start -failures, child output/failure, revocation and cleanup. Revocation still checks -revoke-self's exit status; it does not misinterpret an arbitrary failed lookup as -proof. The September 23 platform return confirms revoke-self is already granted. - -Validation: focused proxy suite 48 passed; full suite 483 passed, 4 integration -tests deselected by repository default; changed Python files pass Ruff. No live -OIDC, credential read or production operation was used. The earlier September 1 -live operation remains historical evidence, not a live validation of this change. -Automated endpoint/browser preflight and OIDC URL presentation remain optional -inbox suggestions, outside these completed handoff acceptance criteria. - -### 2026-10-14 login-output fail-closed regression and fix (existing T01) - -railiance-platform's RPF-WP-0017 re-acceptance review -(`railiance-platform/reviews/RPF-WP-0017-ops-warden-reacceptance-2026-10-14.json`, -decision `reject`) found that T01's commit `b4c1d39` (2026-09-01) dropped -`login_output.strip()` from the login fail-closed condition. From then until this -fix, a login that exited 0 with a populated helper proceeded to the child whatever -it wrote to stdout/stderr. Output stayed contained (criterion 3 held) but -criterion 4 (non-allowlisted output fails closed with self-revocation) and the -criterion 5 unexpected-output coverage regressed; the existing unexpected-output -test passed only through the empty-helper persistence path. - -Fix: login output is still captured and never forwarded, and is now accepted only -when allowlisted by structure (values are never logged, returned, stored, or put -in exceptions/audit): - -- stdout: empty, or exactly one JSON object with an `auth` object containing - `client_token`; top-level keys limited to `request_id, lease_id, - lease_duration, renewable, data, warnings, wrap_info, mount_type, auth`, auth - keys to `client_token, accessor, policies, token_policies, identity_policies, - metadata, orphan, entity_id, lease_duration, renewable, mfa_requirement, - num_uses`, each type-checked; duplicate keys rejected. -- stderr: empty, or exactly the OpenBao OIDC CLI prompt ("Complete the login via - your OIDC provider. Launching browser to:" or "... Open the following link in - your browser:", one `https://` URL, optional "Waiting for OIDC authentication to - complete..."). These prompt strings were confirmed present in the local `bao` - binary; this stderr allowlist entry is an ops-warden proposal pending - railiance-platform review under RPF-WP-0017. - -Anything else (extra text, other stderr, multiple objects, unexpected keys or -types, parse/decoding failure) fails closed: self-revocation through the existing -path, helper cleanup, exit 10 / `login_failed`, no retry. Added a parametrized -populated-helper test for 13 non-allowlisted shapes and an allowlisted hand-off -test; the success test now uses a full JSON shape plus the OIDC prompt. The -September 1 T02 live operation was recorded in `9770d6a` (00:51 +0200), after -`b4c1d39` (00:13 +0200) and `8f01eef` (00:24 +0200); `proxy.py` at that time -contained the relaxed check (the installed CLI's exact revision was not recorded). No live OIDC, OpenBao call, or credential read was used for this fix. - -Tightened the same day after an independent re-review (containment gap): stdout -`wrap_info` must be null or absent (a wrapping token would survive `token revoke --self`), `data` null, absent or `{}`, and `auth.client_token` a non-empty string; -the stderr OIDC prompt must start at the first byte (no leading whitespace; -trailing newlines remain fine). The fail-closed and hand-off tests now run through -the CLI and assert sentinel absence from both `access-audit.log` and `audit.jsonl`. - -### 2026-10-15 CUST81 dependency correction (existing T01/T02) - -Prepared source correction after the two CUST81 attended platform-admin attempts -failed before child handoff. The strict login-output allowlist remains unchanged. -Synthesized format flags now precede auth data; the recovery-only matcher handles -OpenBao `s.` tokens; failure messages expose only fixed categories, client exit -code and helper/output-shape booleans. No credential bytes or URLs are surfaced. - -Do not attribute the observed 121-second failure to the argv order: Bao 2.5.5 -scans global format throughout argv. Actual callback/auth failure remains unknown. -Browser profile initialization was verified from metadata only; server revocation -remains unconfirmed and the failed attempts are retained. No login, admin token -read, policy application or provisioning was performed by this correction. - -Source verification is distinct from live verification. The already-approved -CUST81 execution scope and its open attended gate remain with CUST-WP-0081-T02. -Owner evidence: `docs/evidence/2026-10-15-cust81-attended-dependency-repair-r1.json`. diff --git a/workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md b/workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md deleted file mode 100644 index a3eba2d..0000000 --- a/workplans/WARDEN-WP-0037-whynot-design-forgejo-npm-lane.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: WARDEN-WP-0037 -type: workplan -title: "Repoint the whynot-design npm lane to Forgejo" -domain: infotech -repo: ops-warden -status: blocked -flavor: planning -owner: codex -topic_slug: whynot-design-forgejo-npm-lane -created: "2026-09-04" -updated: "2026-10-03" -state_hub_workstream_id: "42a097db-1c24-558e-a724-030bb2b4443e" ---- - -## Verify the existing credential against Forgejo - -```task -id: WARDEN-WP-0037-T01 -status: done -priority: high -state_hub_task_id: "afc4d325-1c6d-5c25-aaf7-2118aa8f86c6" -``` - -Use only the governed execution transport to test the existing publish identity -against the canonical Forgejo npm endpoint. Do not print or persist the token. - -2026-09-04: `warden plan` classified the exact verification as autonomous and -selected this lane. A contained login using the documented -`whynot-design-workload-kv-read` role proved `read` on the exact OpenBao data -path, and the governed child proved the secret is present. A real 0.4.2 publish -through that credential then failed. No value was printed or persisted and each -contained session self-revoked. The lane therefore needs token rotation, not an -OpenBao policy repair. - -## Repoint the catalog and playbook - -```task -id: WARDEN-WP-0037-T02 -status: done -priority: high -state_hub_task_id: "7ceb2f74-496c-54e8-bf5a-3c49f994ff23" -``` - -Replace the retired Gitea endpoint and discovery language with Forgejo while -preserving the established OpenBao path, field, and least-privilege boundary. -This delivers the npm facet of routed intake -`01a06e07-f2f9-7586-9745-b359eb0025b6`; its inter-hub SSH facet remains separate. - -Completed 2026-09-04. The catalog title, discovery keywords, rotation guidance, -and worker playbook now name the canonical Forgejo registry at -`https://forgejo.coulomb.social/api/packages/coulomb/npm/`. The OpenBao path, -field, owner, native-exec pointer, and high-risk boundary are unchanged. A -regression assertion prevents the retired Gitea discovery term and guidance -from returning. - -## Prove routing and publication - -```task -id: WARDEN-WP-0037-T03 -status: wait -priority: high -decision_id: 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa -depends_on: [SECRETS-WP-0006-T06] -blocking_reason: "Hold kept by D3 until SECRETS-WP-0006-T06 moves publishing off the legacy whynot-design npm path; then rotate and prove a fresh publish." -intervention_note: "HOLD (founder, 2026-09-21): do not rotate yet. secrets-engine exec reads the ungoverned legacy path secret/coulomb/whynot-design/npm/publish (field npm_token); this runbook writes the governed path platform/workloads/coulomb/whynot-design/npm-publish (NPM_AUTH_TOKEN). A rotation now would not reach the live publish path. Resume after SECRETS-WP-0006-T06 moves the lane. Then: rotate the dedicated Forgejo package token, and prove a fresh publish and exact npm view through this lane." -state_hub_task_id: "a8b1b855-ab34-5835-b9fd-5f48bc0b6817" -``` - -2026-10-11: requalified after founder decision D3 (trust root, build-mode default) resolved — see the-custodian docs/decisions/2026-10-11-security-unblock-decisions.md. - -Pass catalog and routing tests, verify the checkout route, and record the exact -Forgejo package release proven through the lane. - -2026-09-04: repo-local verification is complete: the checkout route ranks -`whynot-design-npm-publish` first for a Forgejo npm publish need, reports the -canonical Forgejo title and verification command, all focused -routing/access/proxy tests pass (`145 passed`). - -The sibling repo's `@whynot/design@0.4.2` release (105 files; IR -`designVersion` 0.4.2; five visual tests pass) was published through the -plan-authorized `forgejo-admin-api-token` one-time recovery lane. An -authenticated exact-version lookup proved that Forgejo's remote integrity -matches the local dry-run. T03 waits only on rotating the dedicated package -token and proving the next fresh version through `whynot-design-npm-publish`; -the package migration itself is complete. - -**2026-09-09 — field claim corrected, path routed.** `secrets-engine` (msg -`15f0c0ca`) corrected two things and declined a third, all correctly. - -The KV field is `npm_token`. `NPM_AUTH_TOKEN` is the environment variable their -publication-scope policy injects, and ops-warden had copied the env var in as the -field name — so the catalog's `fetch_command` named a field that does not exist -and could only ever have failed. Corrected in `registry/routing/catalog.yaml` and -`wiki/playbooks/whynot-design-npm-publish.md` from the owner's statement rather -than re-derived here. This was `ADR-0001`'s failure mode rather than a typo: a -pointer layer restating an owner's procedure and getting a detail wrong. - -The endpoint claim already agreed; `delivery_config.npm.registry` has been the -Forgejo URL throughout. - -The **path** is routed to `railiance-platform` and the catalog is unchanged pending -their answer. Which location backs the lane for reads is custody state they own; -`secrets-engine` has no lane read authority to confirm it and refused to rewrite a -production lane pointer from a coordination message (`SECRETS-WP-0006-T06`). Their -reasoning is right, and asserting our own pointer is authoritative because it is -ours would route around it. The ask names a location only and flags that a -`bao kv get` answer would be the 2026-07-16 disclosure vector on a `risk: high` -lane. - -`ready: false` is not being read as path evidence: a source checkout with no -production authority reports not-ready regardless of which path the catalog names. - -T03 still waits on the human rotation of the dedicated Forgejo package token; the -path question does not block that, it determines whether the pointer is correct -once it rotates. - -**2026-09-21 — field reverted to `NPM_AUTH_TOKEN`; path confirmed; a second -location found.** `railiance-platform`, who holds custody of the path, answered -both halves (msgs `c6841bdb`, `79f3e047`) and the answer reverses the 2026-09-09 -change above. - -An attended founder session enumerated the field names present at -`platform/workloads/coulomb/whynot-design/npm-publish`: read-only, no mutation, -no value emitted, `attended_identity` true. **Exactly one field is present, and -it is `NPM_AUTH_TOKEN`.** Receipt: `railiance-platform -docs/evidence/2026-09-10-npm-lane-field-resolution.json`, commit `9d24086`. The -both-fields reconciliation offered the day before is ruled out by the same -enumeration. `CCR-2026-0001` has carried two dated receipts naming that field -since 2026-06-28, one of them an attended founder fetch that exited zero — which -is itself proof the field exists. - -So `npm_token` is wrong *for this path*, and the catalog and playbook are -reverted to `NPM_AUTH_TOKEN`. Reverting is a correction back to the evidenced -value, not a new lane change. - -The lesson is the one this plan already recorded, one level in. On 2026-09-09 -this file wrote that adopting a correction from `secrets-engine` rather than -re-deriving it was the right shape. It was the right *source* and the wrong -*standard of evidence*: a correction adopted from a coordination message is -still an unverified mutation, even when it is only a field name in a catalog, -and this one would have failed at the rotation T03 is waiting for — the moment -it is most expensive to discover. railiance-platform's phrasing, kept because it -is better than ours: it will break "at the moment you least want it to". - -**The second location, which is not ours to dispose of.** -`secret/coulomb/whynot-design/npm/publish` exists — version 1, created -2026-07-03T15:00:44Z, never updated, five days *after* the governed lane was -verified. It sits outside this lane's exact-path policy and outside any CCR. -railiance-platform read metadata only: field names were not enumerated, the -value was not read, nothing was deleted, because a location holding real -credential material is disposed of deliberately by its owner rather than tidied -away by whoever finds it. - -Their unconfirmed hypothesis, recorded because it changes what this lane's -acceptance evidence means if true: `secrets-engine`'s lowercase field may belong -to *that* path, their catalog may declare that location, and the proven pilot -publish may have been reading the duplicate all along. If so, a working -production lane has been running ungoverned and this lane's acceptance evidence -describes a path its consumer does not use. Tracked as `RPF-WP-0035-T07`; -railiance-platform has asked `secrets-engine` which location their publish -reads, and ops-warden has asked the same about the native -`secrets-engine exec` front door this catalog points at (`exec_owner`). - -Recorded in the catalog and in `wiki/playbooks/whynot-design-npm-publish.md` as -an explicit non-lane rather than deleted from the record. Not routed around: the -governed path stays the pointer. - -### 2026-09-27 owner follow-up - -Consumed the September 21 secrets-engine source return: the active catalog -still names `secret/coulomb/whynot-design/npm/publish`, field `npm_token`. -The governed field remains `NPM_AUTH_TOKEN`; no pointer correction is needed -in Warden. RPF-WP-0035-T07 now requires native governed-consumer proof before -legacy destruction and no longer asks for comparison of secret values. - -T03 remains wait and the founder's no-rotation hold remains effective. -Resume only on the SECRETS-WP-0006-T06 migration receipt, then rotate through -the dedicated package lane and prove a fresh publish plus exact npm view. -Do not treat the September 16 OpenRouter key-check receipt as npm acceptance. - -### 2026-09-28 loose-end review - -Workplan state corrected to blocked to match T03's wait and the September 27 -owner return. The governed catalog field and path are already correct. The -founder's no-rotation hold remains binding until SECRETS-WP-0006-T06 supplies -native governed-consumer migration evidence; a dedicated token rotation and a -fresh package publish/exact-version lookup then remain. No credential fetch, -rotation or publication was attempted. - -2026-10-03 (CUST-WP-0075): the gate was only in intervention_note. Encoded as blocking_reason, depends_on SECRETS-WP-0006-T06, and hub decision 1591daa1-6e85-4dfd-aadd-5a9fe1ef47fa (D3: trust root and secret custody), which also carries the matching RPF-WP-0035-T07 hold. diff --git a/workplans/WARDEN-WP-0038-plan-mutation-intent.md b/workplans/WARDEN-WP-0038-plan-mutation-intent.md deleted file mode 100644 index 541a238..0000000 --- a/workplans/WARDEN-WP-0038-plan-mutation-intent.md +++ /dev/null @@ -1,150 +0,0 @@ ---- -id: WARDEN-WP-0038 -type: workplan -title: "warden plan must distinguish reading a secret from mutating one" -domain: infotech -repo: ops-warden -status: finished -owner: ops-warden -topic_slug: netkingdom -planning_priority: P1 -depends_on_workplans: - - WARDEN-WP-0029 -created: "2026-09-08" -updated: "2026-10-16" -state_hub_workstream_id: "751ad530-e44b-52c4-b70e-cb47568a0179" ---- - -# WARDEN-WP-0038 — Read versus mutate intent in the policy front door - -## The defect - -`warden plan` scores a need against catalog lanes by keyword overlap and returns -a verdict. It has no notion of what the caller wants to *do*. So a need that -says *generate a successor secret and CAS-write it to two custodians* matches the -lane that **reads** that path, scores well on the shared nouns, and returns: - -``` -verdict: autonomous -commands: warden access --exec / --out / --wrap -``` - -Three read transports offered as the answer to a write, under the verdict that -means "proceed without the founder". Reproduced 2026-09-08 against -`rapp-qonto-keycape-client` (`ADHOC-2026-09-08-T04`). - -## Why this is worse than a bad suggestion - -`warden plan` is the documented first step before drafting any founder credential -step (`.claude/rules/credential-routing.md`), and `autonomous` is consumed as an -authorization signal by agents that have no other check. `WARDEN-WP-0029` shipped -the verdict *because* instinct was not auditable; a verdict that is confidently -wrong in the permissive direction is worse than no verdict. - -key-cape hit this and refused to rely on it — "we are not treating a generic -match as authorization" — which is the only reason it surfaced as a design -question rather than as an unadmitted custody mutation. The control that caught -it was a counterparty's caution. That is not a control. - -`WARDEN-WP-0033-T06` is the same shape (rapp-postgres, generic -`openbao-api-key` match → `paste_once_provision` for an act that could not work) -and was fixed by adding one exact lane plus one regression. That fixed the -instance. This is the class. - -## Shape of the fix (to be designed, not assumed) - -- Classify need intent — read / mutate / establish — before scoring, and never - answer a mutate need with a read lane's transports. -- A mutate need against a lane ops-warden does not own is `unroutable` with a CCR - stub naming the custody owner, not `autonomous`. `ADR-0003`: cover gaps, never - silently own them; a permissive verdict on someone else's custody act is a - silent claim of ownership. -- Consider whether `rotation.steps` should be reachable from `plan` at all, given - they are advisory prose and several are explicitly not automatable. -- Regression from the reproduced case, in the manner of T06. - -## Open question for design - -Whether intent belongs in the matcher or in the lane. A lane could declare which -acts it admits (`read`, `rotate`, `provision`) and `plan` could refuse any act a -lane does not declare — stricter, and it fails closed on lanes that say nothing, -which is the majority today. That is a catalog schema change and needs its own -argument. - -## Confirmed independently, then fixed — 2026-09-09 - -`railiance-platform` hit the same defect from the other side while answering the -Qonto custody routing question (msg `7c7228ac`). Having stated that the correct -verdict is `founder_required` — attended OIDC via `netkingdom` `role=platform-admin`, -through the governed `openbao-platform-admin-login` lane — they added: - -> Note this is the same verdict your `warden plan` should have returned; it -> returning `autonomous` and offering read transports for a write is a real -> defect and WARDEN-WP-0038 is the right place for it. Until that is fixed, do -> not let a plan result stand in for this answer. - -Two counterparties reporting the same wrong verdict in two days, one of whom is -the write authority being wrongly bypassed, moved this from proposed to shipped. - -### What shipped - -`_need_intent()` classifies a need as `mutate` or `read` by token match, and -`_owns_write_authority()` tests `delegation.mode == "permanent"`. A mutating need -on a lane ops-warden does not permanently own can no longer reach any branch that -returns `autonomous`: - -- lane declares a `rotation` → `founder_required`, with an `approve` act naming - the write owner, the rotation method, whether it is automatable, and - `warden rotate-guide` as guidance; -- lane declares none → `unroutable` with a CCR stub whose `owner_hint` names the - write authority. - -**Commands are empty of read transports either way.** That is the half that made -the wrong verdict actionable rather than merely wrong: `--out`, `--exec`, -`--wrap` and a bare `bao kv get` were being offered as the answer to a write, and -a test now asserts none of them appears. - -### The design question in the plan, answered by use - -The plan asked whether intent belongs in the matcher or in the lane. It went in -the matcher, with the *ownership* test doing the work that a verb list cannot: -SSH certificate issuance is itself a mutating act, and `warden sign` must stay -`autonomous`. `delegation.mode: permanent` is what separates ops-warden's own -front door from someone else's custody — not the absence of a verb from a list. -A regression asserts the SSH lane still proceeds, because a guard that refuses -our own lane would be a worse defect than the one it fixes. - -The stricter alternative — lanes declaring which acts they admit, `plan` refusing -anything undeclared — is not built. It is a catalog schema change that would fail -closed on the majority of lanes, which say nothing today, and it needs its own -argument rather than arriving as a side effect of this fix. - -## Recognize explicit attended OpenBao platform administration - -```task -id: WARDEN-WP-0038-T01 -status: done -priority: high -state_hub_task_id: "ef28df7c-2c72-56be-ad90-4e079f37639f" -``` - -2026-10-16 source follow-up: the exact request "attended OpenBao platform -administration for reviewed flex-auth signer custody CAS0 and fixed KV metadata" -fell through to keyword ranking and selected `openbao-structure-survey` / -`approve`. The configuration override required policy/auth-role nouns even -though the caller explicitly named attended platform administration. Recognize -that explicit intent before keyword ranking. The plan now selects -`openbao-platform-admin-login` / `founder_required` / `oidc_login`; the governed -reviewed-command transport and identity gate are unchanged. Survey requests -remain metadata-only, and a missing administration lane stays unroutable. - -Prepared and tested in an isolated checkout for independent review. This task -records source repair only; it neither authorizes nor claims installation, -attended login, signer provisioning or full16TSA native acceptance. Integration -and the existing platform custody work retain those next actions. - -### Not claimed - -This narrows what `warden plan` will assert; it does not make the tool an -authority on custody. railiance-platform's instruction stands and is the right -standing posture: a plan result does not substitute for the owner's answer. diff --git a/workplans/WARDEN-WP-0039-explicit-policy-refusal.md b/workplans/WARDEN-WP-0039-explicit-policy-refusal.md deleted file mode 100644 index ca0828d..0000000 --- a/workplans/WARDEN-WP-0039-explicit-policy-refusal.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -id: WARDEN-WP-0039 -type: workplan -title: "Preserve explicit policy caller refusals before credential and CA effects" -domain: infotech -repo: ops-warden -status: finished -flavor: residual -owner: codex -topic_slug: custodian -created: "2026-09-08" -updated: "2026-10-03" -origin: residual -origin_ref: HFACT-WP-0001 -state_hub_workstream_id: "ae44a935-6fca-514c-a385-4550dd2b1fe8" ---- - -The factory critical-path review reproduced a live HTTP 403 from the reachable -PDP for the configured ops-warden caller and a railiance-platform secret read. -`check_fetch_policy` treated it as evaluator unavailability and used unknown-zone -fail-open. Caller-auth rejection is an explicit refusal, not a transport outage. - -## Preserve explicit authentication and binding refusals - -```task -id: WARDEN-WP-0039-T01 -status: done -flavor: residual -priority: high -assignee: the-custodian -state_hub_task_id: "eaad08d9-ae47-5f50-8278-ea402a0e2869" -``` - -HTTP 401 and 403 stop signing and credential fetch irrespective of the outage -profile. Do not expose response bodies or alter system identity to bypass the -refusal. Preserve configured behavior for genuine evaluator failures. Prove -both permissive and closed outage profiles and refusal before child execution. - -## Verify and publish the correction - -```task -id: WARDEN-WP-0039-T02 -status: done -flavor: residual -priority: high -assignee: the-custodian -state_hub_task_id: "ac85cf00-391c-5c63-b8f4-ed5bd57684e5" -``` - -Run the policy and proxy suites, retain a value-free live refusal receipt, -publish the reviewed source, and verify the installed CLI refuses before any -credential transport. A refusal is not a successful credential-read admission. - -The owner installer initially reused a cached wheel despite `--force`, leaving -the pre-fix policy code installed. Both Makefile installation targets now use -`--reinstall-package ops-warden` to refresh this package from the reviewed source. -Verify the installed module as well as the checkout before the live refusal test. - -## Resolve the credential proxy's admitted policy binding - -```task -id: WARDEN-WP-0039-T03 -status: done -flavor: residual -priority: high -assignee: the-custodian -state_hub_task_id: "8ca28b63-2e04-5333-b635-011b124c1806" -``` - -Consume the existing native-lane handoff (WARDEN-WP-0033 / SECRETS-WP-0006) -and flex-auth caller contract. Establish whether this interim transport needs an -admitted dedicated policy route or must finish its native handoff. Retain the -refusal until that contract yields positive and wrong-caller/owner/tenant -negative evidence. No credential read, secret generation, or policy grant is -authorized by this workplan alone. HFACT-WP-0001-T03 consumes this return. - -Validation: 429 tests passed (4 integration tests deselected by the repository default); Ruff passed for changed Python files. Full tests used the declared phase-memory source and an isolated temporary memory store. The focused policy/proxy suite passed 69 tests. The existing authenticated SSH policy probe still returns HTTP 200/ALLOW, decision:f3f7c88f9585582a; the credential-owner request returns 403. No CA issue or credential read was performed by these probes. - -Installed verification: source guard present after refreshed owner installation (`19bb75c`). `warden access forgejo-admin-api-token --exec --field API_TOKEN -- true` exits 4 with explicit HTTP 403 before credential transport or child execution. The previous fail-open warning is absent. The probe requested no output value; no credential was fetched. T01/T02 are complete; T03 owns the remaining exact policy-binding admission. - -**T03 routed 2026-09-09.** Asked `flex-auth` whether an admitted contract exists -for a delegated credential read where the caller (ops-warden) and the resource -owner differ, since that is exactly the shape drawing the live 403. Three outcomes -were named as equally acceptable answers, with no preference stated between the -first two: an existing contract to conform to; no contract yet, making this a -design question they should raise rather than one ops-warden invents a shape for; -or that there should be **no** such contract — the interim proxy transport is -itself the defect and the answer is to finish the native handoff -(`WARDEN-WP-0033` / `SECRETS-WP-0006`). - -The third is a real possibility and was not argued against. Eleven catalog lanes -are ops-warden proxies with a named intended owner precisely because no owner -front door exists; if delegated reads should not be made policy-admissible, that -shortens WP-0033 rather than blocking it. - -Two fixes were explicitly ruled out in the message rather than left unmentioned: -broadening the ops-warden caller binding so it may read other systems' resources, -and relabelling `resource.system` as ops-warden so the existing binding matches. -The second is worse — it would make the audit trail assert ops-warden owns -credentials it deliberately does not, which is the claim `ADR-0002` exists to -prevent, achieved by editing a field instead of making an argument. - -The refusal stays in place until an answer yields positive evidence plus -wrong-caller, wrong-owner and wrong-tenant negatives. No grant was requested. -`secrets-engine` was told this bears on SECRETS-WP-0007-T04 and on how many lanes -stay proxied; `gate-house` was told it may reach how the signing lane's -approval-consume is built (GH-DEC-2026-005). - -### 2026-09-28 loose-end review - -T03 remains wait and the workplan remains blocked. Re-read the owner's finished -FLEX-WP-0026: no admitted delegated-read binding exists, and its native OpenRouter -contract explicitly does not close WARDEN-WP-0039-T03. FLEX-DEC-2026-015 also -confirms resource.system is runtime policy vocabulary, not a repository rename. -Retain the explicit refusal. A native owner route's success does not grant this -interim proxy general delegated-read authority; exact positive and caller/owner/ -tenant negative evidence is still required for the route that replaces it. - -**T03 code side done 2026-10-03 (CUST-WP-0075, founder build-mode directive).** -access-engine answered the contract question with a dedicated fetch evaluator: -pin `flex-auth-ops-warden-fetch` (access-engine 02b15a7, `values/ops-warden-fetch.yaml`), -same caller (ops-warden SA, audience flex-auth), owner systems bound per lane, -request shape unchanged. ops-warden now sends `check_fetch_policy` only to the new -key `policy.fetch_flex_auth_url`; signing keeps `policy.flex_auth_url`. An unset or -unreachable fetch evaluator fails closed with a message naming it, with no fallback -to the signing pin and no fail-open. 401/403 keep the existing refusal text. -`warden access ` names the fetch evaluator on its `gate` (or `policy`) line. Tests: -`tests/test_policy_fetch_evaluator.py`, plus updated proxy and refusal tests. Full -suite: 494 passed, 4 deselected. Docs: `wiki/OpsWardenConfig.md` ("Two -evaluators", tunnel example), `wiki/CredentialRouting.md`, production example. - -Still open (founder go, `needs_human`): deploy the pin (access-engine -ADHOC-2026-10-03-T02), add the ops-bridge tunnel (local 19091 → the pin's Service -ClusterIP:8080), set `fetch_flex_auth_url: http://127.0.0.1:19091` in the live -`~/.config/warden/warden.yaml`, then record exact positive evidence and -wrong-owner/caller negative evidence before T03 closes. Until the key is set, -every proxied fetch refuses with "secret-read evaluator not configured". Before -that change, those fetches got a 403 from the signing pin. - -**T03 done 2026-10-10.** access-engine reported the fetch pin -`flex-auth-ops-warden-fetch` live since 2026-10-03 (Helm revision 1). The tunnel -`flex-auth-ops-warden-fetch-railiance01` (127.0.0.1:19091) is connected, and the -live `~/.config/warden/warden.yaml` carries -`fetch_flex_auth_url: http://127.0.0.1:19091`. Receipts were taken with -ops-warden's own caller token (SA `ops-warden/ops-warden`, audience -`flex-auth`, 10m). The requests are access-engine's fixtures in -`examples/ops-warden-fetch/`. Policy decisions only; no credential was read. - -| Case | Result | -| --- | --- | -| Positive: `forgejo-admin-api-token`, owner railiance-platform | HTTP 200 allow `secret_read_lane_matched`, `decision:014afa428f776db5` | -| Unknown need | HTTP 200 deny `unknown_need`, `decision:d792b0d6b1cb8122` | -| Wrong owner: `resource.system` ops-warden | HTTP 403 | -| Wrong tenant: `tenant:binky` on subject and resource | HTTP 200 deny `wrong_tenant`, `decision:71da3ff8b50dad74` | -| Wrong caller: anonymous | HTTP 401 | - -This is the exact positive evidence plus the caller, owner and tenant negatives -this task required. The explicit refusal stays for anything the pin does not -admit. access-engine closes its ADHOC-2026-10-03-T02 on this reply. diff --git a/workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md b/workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md deleted file mode 100644 index f07fb53..0000000 --- a/workplans/WARDEN-WP-0040-unknown-zone-fail-closed-adoption.md +++ /dev/null @@ -1,240 +0,0 @@ ---- -id: WARDEN-WP-0040 -type: workplan -title: "Adopt unknown -> fail_closed behind signing-target classification coverage" -domain: infotech -repo: ops-warden -status: active -flavor: residual -depends_on: - - WARDEN-WP-0032 - - WARDEN-WP-0034 -owner: ops-warden -topic_slug: netkingdom -planning_priority: P1 -depends_on_workplans: - - WARDEN-WP-0032 - - WARDEN-WP-0034 -created: "2026-09-09" -updated: "2026-09-28" -state_hub_workstream_id: "c8ee441e-1be1-5219-910c-e79ff23cc9ec" ---- - -# WARDEN-WP-0040 — `unknown` → `fail_closed`, in the right order - -security-layer-model v0.8 §6.4 obligation 3 (proposed, 2026-09-06) rules that -`unknown` is not a zone and must resolve to `fail_closed`. ops-warden's -`pep-stance.yaml` declares `unknown: fail_open` under `ADR-0009`. - -**We agree with the rule.** `history/2026-09-09-layer-model-v08-review.md` records -the assent and why our falsifier check failed: the cell governs certificate -issuance, not a §5.1 read-only diagnostic, so nothing rescues it. - -**We disagree with adopting it first.** Measured 2026-09-09: of four signing -targets, zero resolve to a zone, three are `unknown`, one is `not-applicable`. -Flipping the cell today makes flex-auth a hard dependency of essentially every -certificate — `ADR-0006`'s rejected configuration, reached by a different route, -including the continuity path needed to repair flex-auth itself. - -So: coverage first, then the cell. That ordering is the whole holding of -`ADR-0006` and the reason `ADR-0009` replaced a global switch with a zone map. - -## Tasks - -```task -id: WARDEN-WP-0040-T01 -status: wait -blocked_on: message-from:ops-bridge -blocking_reason: "Warden's own declaration cannot classify the repair-path target workloads; the owner must declare the continuity actor/target and its z2-continuity resolution (see 2026-09-28 review)." -priority: high -state_hub_task_id: "611f0901-9fb5-5954-8dd0-a860c5f0cbec" -``` -2026-09-30: gate stated. The 2026-09-28 review found the classification is not ours to make (target workloads belong to ops-bridge, the Inter-Hub execution owner and the backup owner), so T01 waits on the owner declaration, same as T02. Qualified per Task Wait Qualifiers v0.1. - - -**Classify the continuity path before anything else changes.** - -The deadlock is specific and worth naming before it is designed around: flex-auth -unreachable → operator needs an SSH certificate to reach the host → target is -`unknown` because nobody classified the repair path → `fail_closed` denies it. - -`z2-continuity` exists for this and its stance is `fail_open`, so the fix is -classification, not an exception. Determine which actor(s) constitute the -repair path, establish whether the declaration is ops-warden's to make (our own -`tenancy.yaml` covers ops-warden as a workload; the actors' target workloads may -not be ours), and route what is not. - -Blocks T03. Converting the cell with the repair path unclassified is the one -outcome this workplan exists to prevent. - -```task -id: WARDEN-WP-0040-T02 -status: wait -blocked_on: message-from:ops-bridge -blocking_reason: "Owners of agt-codex-interhub-bootstrap (ops-hub) and agt-state-hub-bridge (ops-bridge) must publish authoritative workload identity declarations; atm-backup-daily declared by activity-core and compiled 2026-10-09 (z2-protected, admission unsatisfied: M1 below M2 floor)." -priority: high -state_hub_task_id: "54ad4864-7bcf-592e-a187-9239a81a0b11" -``` - -**Raise signing-target coverage, by asking owners — never by inferring.** - -Three actor resources report `workload_resolution_absent`: -`agt-codex-interhub-bootstrap`, `agt-state-hub-bridge`, `atm-backup-daily`. -Their owners (ops-bridge and the backup execution unit among them) have not -published an authoritative workload identity declaration. - -`ADR-0009` rule 3 forbids closing this with path or repository-name inference, -and that prohibition holds under pressure from this workplan specifically — -adopting a stricter stance is not a licence to manufacture the membership that -makes it survivable. - -Route to each owner with the concrete consequence: while the declaration is -absent their actor cannot be issued a certificate during a flex-auth outage once -the cell converts. That is a better ask than a generic request to publish a -declaration, and it is true. - -Report coverage with `scripts/report_workload_join.py`; this task is done when -coverage is stated, not when it reaches a threshold — the threshold is T03's -gate, and owners may legitimately decline. - -```task -id: WARDEN-WP-0040-T03 -status: wait -depends_on: [WARDEN-WP-0040-T01] -blocking_reason: "Gated on T01 and on layer model v0.8 (or successor) reaching accepted; never convert against a proposed standard." -priority: high -state_hub_task_id: "93eaf1f2-daaa-5e22-8804-85ca9433571c" -``` - -**Convert the cell and supersede `ADR-0009`'s unknown row.** - -Gated on T01, and on v0.8 (or its successor) reaching `accepted`. Do not convert -against a `proposed` standard. - -`ADR-0009` is `owner: ops-warden`, so changing it means a superseding ADR, never -an in-place edit (`.claude/rules/architecture.md`). The superseding record should -carry the v0.8 argument rather than restate it: unknown is not a zone, so a §9.3 -per-zone trade was never made for that request. - -`pep-stance.yaml` is asserted equal to `PolicyConfig.failure_modes` by -`tests/test_layer_conformance.py`, so the map and the code convert in one commit -or the test fails — which is the property that makes the map worth publishing. - -```task -id: WARDEN-WP-0040-T04 -status: done -priority: medium -state_hub_task_id: "222a8c5d-0c0f-5a1d-98d8-02be7dca3358" -``` - -**Return findings to gate-house and track the transition ask.** - -Sent 2026-09-09 with the review. Two asks, in preference order: obligation 3 -names a dated transitional state gated on coverage; or failing that, §13.1 -records classification coverage alongside stance, so a register row cannot read -`unknown: fail_closed` while every target is unknown. - -If both are declined, that is an answer and this workplan proceeds unchanged — -the cell stays a **declared gap** under §11 with this workplan as its route, -which is what the `declared-gap` mark (our own v0.6 finding) exists to express. - -**Answered 2026-09-09 — GH-DEC-2026-011.** Ask 1 declined, ask 2 adopted; -standard amended at `net-kingdom@64394e9`. - -Ask 1's refusal is correct and we are not appealing it: a sanctioned transitional -`fail_open` is indistinguishable at runtime from the stance the rule forbids, and -would make the rule optional at the only moment it costs anything. Ask 2 gave us -what we actually needed — §13.1 carries a Coverage column with this repo's -figures as its first entries, guarded so that coverage never softens a stance, -never gates one, and never makes a non-conformant cell conformant. - -The reversal condition on that column binds ops-warden first, since the column -exists because we asked: if a row is ever argued conformant *because* its coverage -is low, the column comes out. Our row reads non-conformant and low-coverage, and -the second is not a defence of the first. - -gate-house confirmed explicitly that they are **not** asking for the cell to be -flipped before coverage exists, and that this workplan's order is right. The cell -stays a declared gap under §11 with WP-0040 recorded as its route — the outcome -this task named as acceptable if both asks were declined, reached with one of them -adopted. - -T01–T03 are unchanged and still gate the conversion. Coverage is now measured -rather than asserted (`scripts/report_coverage.py`), so T02's reporting obligation -has a tool behind it and the published figure cannot drift from the register's. - -### 2026-09-28 loose-end review - -T04 is done: GH-DEC-2026-011 already answered both transition asks on September 9; -the accepted coverage column and declared-gap disposition are recorded above. -No additional response is required to meet that task's acceptance criterion. - -T01/T02 now wait; the workplan is blocked. Re-ran both coverage reports: -signing targets = 0 resolved / 3 unknown / 1 not-applicable; routing lanes = -3 resolved / 20 unknown / 15 not-applicable. Refreshed pep-stance.yaml's measured -date. These figures describe the checked-in registry and local owner declarations, -not a live PDP query. No zone membership was inferred or changed. - -The exact missing workload resolutions are `ops-bridge-tunnel` for -`agt-state-hub-bridge`, `codex-interhub-bootstrap` for -`agt-codex-interhub-bootstrap`, and `backup-daily` for `atm-backup-daily`. -The seed inventory's `adm-example` is not an admitted repair actor. -Warden's own workload declaration cannot classify those target workloads. -T01 needs an owner-declared continuity actor/target and its authoritative -z2-continuity resolution; T02 retains the owner-routing/declaration follow-up -(ops-bridge, the Inter-Hub execution owner and the backup execution owner). -Without it, changing unknown to fail_closed would deny their issuance during a -PDP outage. Coverage reporting is complete, but owner coordination is not. - -T03 also remains gated on standard acceptance: the local authoritative -net-kingdom/canon/standards/security-layer-model_v0.8.md still says proposed. -No superseding ADR or runtime stance change is warranted before these gates. - -### 2026-09-30 owner asks sent - -Routed the concrete-consequence ask (T01 classification + T02 declaration) to `ops-bridge` (agt-state-hub-bridge), `ops-hub` (agt-codex-interhub-bootstrap; owner unconfirmed, asked to name the owner if not theirs) and `railiance-platform` (atm-backup-daily; same caveat). Declining is an accepted answer. No membership inferred. T01/T02 stay `wait` on these replies. - -### 2026-10-08 owner replies recorded - -`atm-backup-daily`: railiance-platform declined in its replies of 2026-10-02 -(msgs 53234ede, a3691df9). It declares only the credential lane -`railiance-backup-offsite-lane`, and no workload in railiance-platform has that -name. It names activity-core as the declaring owner, because activity-core is -the source repo of `daily-cnpg-option-a-backup`. It classifies the actor as -**ordinary**, not z2-continuity repair-path. activity-core will not declare -without founder confirmation (ACTIVITY-WP-0042-T03, decision 89f1058f). So this -actor now waits on that founder decision, not on railiance-platform. No -membership is inferred. `agt-state-hub-bridge` (ops-bridge) and -`agt-codex-interhub-bootstrap` (ops-hub) are still unanswered. - -2026-10-08 (later): the founder confirmed in session that activity-core's -`daily-cnpg-option-a-backup` worker is the `atm-backup-daily` unit, and that it -is classed as ordinary. The confirmation was relayed to activity-core for -ACTIVITY-WP-0042-T03. Decision 89f1058f (D2) was deliberately left open, -because it also gates production ArgoCD go-aheads. `atm-backup-daily` now waits -only on activity-core publishing its declaration. - -### 2026-10-09 atm-backup-daily compiled - -activity-core published `declarations/backup-daily/tenancy.yaml` (c1533fe). -zone-engine's `resolve_zones.py` resolves it with the -`netkingdom-build-v0.1` profile: - -- declared zone: `z2-protected` (ordinary, not z2-continuity) -- admission: `unsatisfied`, reason `M1_below_M2_zone_floor` -- effective zone: `unknown` - -The resolved view is committed as `registry/flex-auth/zone-resolutions.json`, -and the snapshot was rebuilt from it. Resolution is no longer absent. The actor -stays `unknown` because the owner declared M1, below the M2 floor, which is -accurate rather than a gap ops-warden may close. Coverage, stated as T02 -requires: signing targets 0/3 resolved, 3 unknown, 1 not-applicable. The -figure is unchanged because `report_coverage.py` counts effective zones. -`atm-backup-daily` now waits on owner M2 evidence. It does not wait on a -declaration. Still unanswered: `agt-state-hub-bridge` (ops-bridge) and -`agt-codex-interhub-bootstrap` (ops-hub). - -> 2026-10-11: demoted to `flavor: residual` / `backlog` by founder-authorized triage — off the -> critical path (identity → authz → secrets → SecurityCase execution). Tasks are unchanged; -> promote back when demand or risk appears. See the-custodian -> `history/20261011-netkingdom-security-progress-assessment.md`. diff --git a/workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md b/workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md deleted file mode 100644 index 8a1ca34..0000000 --- a/workplans/WARDEN-WP-0041-catalog-lane-scaffolding.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -id: WARDEN-WP-0041 -type: workplan -title: "Draft catalog lane scaffolding with platform-owned credential rendering" -domain: infotech -repo: ops-warden -status: finished -flavor: implementation -owner: ops-warden -topic_slug: netkingdom -created: "2026-10-03" -updated: "2026-10-04" -depends_on: [] -state_hub_workstream_id: "eb42b59c-1e32-5e1b-917d-34625f7a041d" ---- - -Accepts the ops-warden portion of P4 from CUST-WP-0075, proposal message -`79549660-4fc8-44d5-989e-074f6c7b6f07`. The platform owns its renderer and -CCR schema; no production act or approval is included here. - -## Establish the catalog and platform seam - -```task -id: WARDEN-WP-0041-T01 -status: done -priority: medium -state_hub_task_id: "85bfd209-d2f1-59e4-aa02-e8e374598538" -``` - -Reviewed existing catalog validation, plan CCR stub, promotion checklist and -platform CCR rendering. The stub does not scaffold; platform rendering already -exists for reviewed CCRs. Proposed exact owner seam, inputs, fail-safe draft -outputs and offline acceptance in `wiki/CatalogLaneScaffoldContract.md`. -Platform's matching owner task is `RPF-WP-0050-T04`. - -## Implement offline draft catalog and playbook generation - -```task -id: WARDEN-WP-0041-T02 -status: done -priority: medium -state_hub_task_id: "68bf72d8-a798-5e88-bfda-79b8910f7cda" -``` - -Implement the ops-warden portion of the seam proposal: deterministic staging -output, validated catalog fields, draft-only defaults, collision refusal, -explicit workload unknowns and risk, no secret inputs, no credential execution. -Add meaningful rejection/round-trip fixtures; reuse catalog validation. Do not -write platform-owned artifacts or invent its CLI. The local fragment generator -can progress independently while platform contract review is pending. - -## Verify the accepted cross-owner scaffold - -```task -id: WARDEN-WP-0041-T03 -status: done -priority: medium -depends_on: - - WARDEN-WP-0041-T02 - - RPF-WP-0050-T04 -state_hub_task_id: "33587361-2b8a-5540-9c4f-d7b4f8ef25e0" -``` - -Integrate through reviewed owner references and demonstrate one exact KV-v2 -workload-read fixture end to end. Record both owner commits and validation -results. Generation success alone does not admit or activate a lane. Update -`warden plan`'s unroutable guidance only once a real supported command exists. - -## Report owner uptake and gates - -```task -id: WARDEN-WP-0041-T04 -status: done -priority: medium -state_hub_task_id: "4bc926af-04d4-5a27-9578-d2f83c0ecf9c" -``` - -Return the scoped P4 acceptance and this live owner record to the-custodian's -original proposal thread. Existing admission, approval, identity confirmation, -attended values and deployment evidence gates remain; no secret read or -production action was taken. Source/knowledge receipt is in the seam document. - -## Offline delivery acceptance — 2026-10-04 - -T02 and T03 completed the bounded draft-generation scope. Actual-source -generator/routing/plan tests: 139 passed. Both actual owner CLIs staged the -paired synthetic fixture; lane/consumer/owner/risk/CCR pointer and manifest -digest match, unknown workload remains unknown, catalog execution is disabled, -CCR remains proposed with empty review, auth claims unconfirmed, frontdoor -pending/unresolvable and evidence not executed. Independent platform owner -verification remains separately attributed in the receipt. - -Source implementation commits: ops-warden `dfe436e4e05610cae96085dadd582e5a63525580`; -railiance-platform `f5b7ff5118326f66be8ecd8330d4cda9e4d94520`. -Evidence: `docs/evidence/2026-10-04-catalog-scaffold.json`. -`warden plan` now names the supported offline command for unroutable needs. -The workplan finishes its local draft scaffold contract; CCR approval, binding -confirmation, attended values, deployment and actual capability receipts -remain separate lane-activation work, never implied by fixture acceptance. -No unfinished in-scope implementation or residual is parked by this closure. diff --git a/workplans/WARDEN-WP-0042-openbao-structure-survey-route.md b/workplans/WARDEN-WP-0042-openbao-structure-survey-route.md deleted file mode 100644 index 097d647..0000000 --- a/workplans/WARDEN-WP-0042-openbao-structure-survey-route.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -id: WARDEN-WP-0042 -type: workplan -title: "Route the OpenBao existing-structure survey to ops-mason" -domain: infotech -repo: ops-warden -status: finished -flavor: implementation -owner: claude -topic_slug: netkingdom -created: "2026-10-05" -updated: "2026-10-05" -depends_on: [] -state_hub_workstream_id: "bd5edebd-2988-5685-881e-13931464f92f" ---- - -ops-mason construction plans need an existing-structure survey (section 2) -built from live OpenBao metadata. Founder decision: ops-warden routes, -ops-mason surveys. ops-mason implements `scripts/plan-survey.py` under its -own `ops-mason-build` grant (`ops-mason/MASON-WP-0008`). ops-warden gets no -survey code and no token, per the no-double-source rule -(`WARDEN-WP-0010`, `docs/adr/ADR-0001-catalog-is-a-pointer-layer.md`). - -## Add the pointer-only catalog entry - -```task -id: WARDEN-WP-0042-T01 -status: done -priority: medium -state_hub_task_id: "ca8cd3e9-83ac-5732-8ec4-525e5ce1b113" -``` - -`registry/routing/catalog.yaml` entry `openbao-structure-survey`: owner -`ops-mason`, `warden_executes: false`, `exec_capable: false`, no `steps`, -native delegation to ops-mason, lane `ceremony` (the grant is a founder act), -`risk: high` (the grant can write policies and auth roles), `auth_method` -naming the ops-mason grant, `exec_owner`/`exec_command` pointing at the -survey script, `canon_ref` at `ops-mason/docs/construction-plan-format.md`. -Short pointer playbook `wiki/playbooks/openbao-structure-survey.md`. - -## Verify routing offline - -```task -id: WARDEN-WP-0042-T02 -status: done -priority: medium -state_hub_task_id: "05bb75bd-2de9-55d5-8bc8-41b306ddcf16" -``` - -`make test` passes, including catalog validation and layer conformance. -`warden access "openbao structure survey"`, `warden access "construction -plan section 2"` and `warden route find "existing policies auth roles -survey"` resolve to the entry; `warden plan` returns `founder_required` -with the survey command; `warden access ... --exec` refuses (not -exec_capable). diff --git a/workplans/WARDEN-WP-0043-platform-supplier-credentials.md b/workplans/WARDEN-WP-0043-platform-supplier-credentials.md deleted file mode 100644 index d359328..0000000 --- a/workplans/WARDEN-WP-0043-platform-supplier-credentials.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -id: WARDEN-WP-0043 -type: workplan -title: "Route supplier credentials under platform/suppliers" -domain: infotech -repo: ops-warden -status: finished -flavor: implementation -owner: grok -topic_slug: platform-suppliers -created: "2026-10-08" -updated: "2026-10-09" -depends_on: [] -related: - - repo:railiance-platform - - repo:llm-connect -state_hub_workstream_id: "16aac061-977a-536b-a074-aba059f2475f" ---- - -# WARDEN-WP-0043 — Supplier credentials under platform/suppliers - -Founder decision 2026-10-08: account credentials we hold with a supplier -live at `platform/suppliers//` on the existing `platform` -KV mount. ops-warden routes that shape. It does not store the values and it -does not apply OpenBao. The KV write and the credential-change allowlist -belong to railiance-platform. Tenant commercial secrets stay on `tenants/`. - -Workload and rein paths that already have a reader stay where they are -until that reader is retargeted: - -| Account | Path today | -| --- | --- | -| OpenRouter, llm-connect | `platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets` | -| OpenRouter, rein-openweights | `reins/rein-openweights/openrouter` | -| Scaleway bootstrap | `platform/workloads/railiance/scaleway/bootstrap` | -| Scaleway backup space | `platform/workloads/railiance/backup/object-storage` | -| IONOS transactional mailbox | `platform/workloads/email-connect/transactional` | -| IONOS library mailbox | `platform/workloads/inbound-library/imap` | - -The future homes are `platform/suppliers/openrouter/llm-connect`, -`platform/suppliers/openrouter/rein-openweights`, -`platform/suppliers/scaleway/bootstrap`, `platform/suppliers/scaleway/backup`, -`platform/suppliers/ionos/email-connect-smtp`, and -`platform/suppliers/ionos/library-imap`. Further suppliers use the same -two-level shape. llm-connect already tries the llm-connect supplier path -and then the live workload path before `OPENROUTER_API_KEY`. - -## Catalog pointer - -```task -id: WARDEN-WP-0043-T01 -status: done -priority: high -state_hub_task_id: "7a86d91a-fcbd-5dc8-b3cf-4c28ad963474" -``` - -Add a pointer catalog entry and a short playbook for -`platform/suppliers//`. `warden_executes: false`. The -owner of the KV write is railiance-platform. No secret values, no fetch -of a live field, and no new `warden` verb. - -Done 2026-10-08: catalog `platform-supplier-credentials` plus -`wiki/playbooks/platform-supplier-credentials.md`. - -## Allow the path in credential changes - -```task -id: WARDEN-WP-0043-T02 -status: done -priority: high -state_hub_task_id: "c384b228-e710-580d-a3de-a3046a86e666" -``` - -`railiance-platform/scripts/credential-change.py` accepts `platform/workloads/` -and `tenants/` for a delegated change. `platform/suppliers/` needs that -allowlist widened before a CCR can create these paths. ops-warden does not -apply the change. - -Done 2026-10-09: railiance-platform `b77cb41` maps mount `platform` to -`platform/workloads/` and `platform/suppliers/`. Under `suppliers/` it requires -exactly two non-empty segments, and tests cover `platform/suppliers/ionos/main`. -The IONOS CCR can now be drafted against this shape. - -## Provision supplier accounts - -```task -id: WARDEN-WP-0043-T03 -status: done -priority: medium -needs_human: true -state_hub_task_id: "87996bc7-2f7a-578d-9f7c-8bd876605a47" -``` - -New supplier material is written from a mode-0600 file on a trusted -terminal, into `platform/suppliers//`. IONOS is the -first account that still needs that home. Existing workload and rein -readers keep their current paths until each one is retargeted. Values do -not go into git, State Hub, workplans, logs, or chat. - -2026-10-09: the IONOS stub exists. The founder ran the write through the -contained `openbao-platform-admin-login` envelope: -`bao kv put -cas=0 platform/suppliers/ionos/main` as version 1, with keys -`CUSTOMER_NUMBER`, `LOGIN`, `PASSWORD`, `API_KEY_PREFIX` and `API_KEY_SECRET`, -each set to the placeholder `FILL_ME`. A metadata-only check through the same -envelope exited 0. The founder fills the real values as version 2 in the -OpenBao UI. This task closes once that is done. No reader policy exists for the -path; a reader is requested through a railiance-platform CCR when a workload -needs it. - -2026-10-09: done. The founder filled in the real values as version 2 through -the OpenBao UI. ops-warden did not read or verify any value. Further supplier -accounts follow the same playbook; existing workload and rein readers move only -when each reader is retargeted. diff --git a/workplans/WARDEN-WP-0044-security-case-follow-through.md b/workplans/WARDEN-WP-0044-security-case-follow-through.md deleted file mode 100644 index 6c09ae9..0000000 --- a/workplans/WARDEN-WP-0044-security-case-follow-through.md +++ /dev/null @@ -1,410 +0,0 @@ ---- -id: WARDEN-WP-0044 -type: workplan -title: "Follow through on KnownSecurityCases along their type's route" -domain: infotech -repo: ops-warden -status: active -flavor: implementation -owner: claude-code -topic_slug: netkingdom -created: "2026-10-08" -updated: "2026-10-11" -depends_on: [] -related_repos: - - net-kingdom - - ops-mason - - key-cape - - railiance-platform -state_hub_workstream_id: "f7e81013-6868-5d99-8528-ba75a442eca9" ---- - -# WARDEN-WP-0044 — Follow through on KnownSecurityCases along their type's route - -## Why - -NetKingdom is introducing SecurityCaseTypes (net-kingdom `NK-WP-0048`, canon -`security-case-types_v0.1`, proposed). Once a case of an implemented type is -accepted and ops-mason has materialized its structure (`MASON-WP-0010`), -ops-warden carries out the remaining steps. These are the owners' declared entry -points, run on the authentication path the type defines, followed by the type's -verification. - -Today ops-warden routes needs and, for `exec_capable` lanes, proxies fetches as -the caller. It does not yet know about cases, and each new integration ends with -a founder running a hand-written script (for example the proposed KEY-WP-0038-T02 -rollout). This plan gives ops-warden the follow-through role, without widening -what it may do: each step still runs under its owner's entry point and the -caller's identity. - -## Tasks - -```task -id: WARDEN-WP-0044-T01 -status: done -depends_on: [NK-WP-0048-T03] -priority: high -state_hub_task_id: "ef0920fc-418e-52cb-acff-9a5c0c728bf5" -``` - -### T01 — Define the type's follow-through section - -Propose the `follow_through` part of the SecurityCaseType schema together with -net-kingdom. For each step it names the owner entry point, `warden_executes`, -the auth method and lane, whether an attended human login is unavoidable (with -the type's justification), and the verification command. Write it for the first -type, `oidc-browser-relying-party`, covering key-cape client registration, -custody minting and LLDAP group membership. - -2026-10-08: NK-WP-0048-T03/T04 landed with a drafted section. Review is in -`history/2026-10-08-oidc-rp-follow-through-review.md`. It requires three -changes before T02: a per-step `lane`, a narrow `warden_executes`, and -`value_handling`. It also lists four refinements. This task is done when -net-kingdom acknowledges the review (NK-WP-0048-T05). - -2026-10-09: R4 was withdrawn in favour of ops-mason's per-case -`{consuming_service}-keycape-client`, with the entry body templated in the type -(R4′, see the addendum in the review). - -2026-10-09: the revised 1.0 type and canon now carry R1–R3, R4′, R5–R7. -`warden case check-type ` (`src/warden/case_type.py`, tests in -`tests/test_case_type.py`) checks a type's follow-through mechanically: the -live type is schema-conformant, with one attended session -(`key-cape-oidc-login`). It is not yet plannable, because two lanes are not -in the catalog: `identity-provisioner-groups` (net-kingdom) and -`keycape-client-secret-generate` (railiance-platform). These gate T02, not -T01. T01 still closes on net-kingdom's acknowledgement (NK-WP-0048-T05). - -2026-10-09: ops-warden cannot add `identity-provisioner-groups` as the type -declares it. The provisioner accepts only user-engine's runtime token and -admits only user-engine pods, so no caller-identity path exists. R8 (review -addendum) asks net-kingdom either to move `ensure-groups` into the existing -attended session (recommended) or to give the provisioner an operator -credential. `keycape-client-secret-generate` needs a generate-in-custody entry -point from railiance-platform, and the broker does not have one yet. Both were -sent to their owners. - -2026-10-09: done. net-kingdom took R8 option 1, so `ensure-groups` runs attended -in the `register-client` session and `identity-provisioner-groups` is dropped -(net-kingdom `0e86f95`). The founder acknowledged the type with memo r4 (hub -decision `e418263e`, NK-WP-0048-T05 done). - -```task -id: WARDEN-WP-0044-T02 -status: done -depends_on: [WARDEN-WP-0044-T01] -priority: high -state_hub_task_id: "5f68c991-f490-58f8-a121-92f74073d272" -``` - -### T02 — Plan a case run - -`warden case plan ` checks that the case is accepted and materialized, -resolves each step to its catalog entry and entry point, and prints the run: -which steps warden executes, which run as the attended caller, what the -verification is. No secret values; no execution. - -Tests: refuse an unaccepted or unmaterialized case; refuse a step whose catalog -entry is missing or not reviewed; show attended steps explicitly. - -2026-10-09: done. `warden case plan [--type] [--json]` -(`src/warden/case_plan.py`, 14 tests in `tests/test_case_plan.py`). It refuses: -- a case that is not `materialized`, or has no acceptance record -- a type that is mismatched or not acknowledged -- any `case check-type` finding, including a missing or unreviewed lane -- undeclared, type-fixed or secret-sensitivity parameters -- a placeholder the case does not set, or an unsupported `when` - -It fills type defaults (`issuer`), evaluates ` == ` conditions, -renders owner, entry point, auth and ready_check, groups attended steps per -lane, and lists the applicable verification. - -The live Forgejo case is correctly refused: it is `accepted`, not -materialized. A materialized copy, with a stand-in lane for -`keycape-client-secret-generate`, renders six steps: two attended steps plus -`register-client` in one `key-cape-oidc-login` session, one warden step -(`mint-secret`), and two owner steps. Seven checks follow. - -Observation for net-kingdom (not blocking): `bind-env` runs before -`register-client`, in type order. Its entry carries a literal -`KEYCAPE__CLIENT_SECRET`, which is not a `{param}` placeholder, so it -reaches the plan unrendered. - -2026-10-09 follow-up: -- **Format specs.** `_render` supports net-kingdom's `{param:env}` spec (upper - case, `-` as `_`, the same as `keycape-client.py env_name`) and refuses any - other spec or `!conversion`. -- **Case reference.** The plan prints `case-ref: @sha256:` over the - case file's exact bytes. This is the reference proposed to railiance-platform - for `credential.py generate --case-ref` (RPF-WP-0054-T01); the broker - re-derives it from the file. -- 18 tests now. - -```task -id: WARDEN-WP-0044-T03 -status: progress -depends_on: [WARDEN-WP-0044-T02, MASON-WP-0010-T03] -blocking_reason: "Local durable execution kernel and injected failure harness implemented. Native acceptance still requires authenticated owner machine descriptors/adapters, actual Forgejo materialization receipt, contained session integration, ordered verification and first native run." -priority: high -state_hub_task_id: "bcbf6c91-2c04-5c16-ae8c-5d6c06fd6b89" -``` - -### T03 — Run and verify a case - -2026-10-18 current-session continuation: the new optional -`Executor.admit(original_session_reference=...)` separates immutable original -run-sheet provenance from fresh independently verified current session evidence. -It requires the exact existing journal and original running intent under the -normal lock, refuses missing/orphan/changed context and retains all original -pins. Current authority is rechecked before every subsequent write/callback; -neither the original reference nor a fresh login can replay a completed effect -or clear uncertainty. No descriptor/Invocation/receipt schema or default -admission behavior changes. Source integration uses the actual KeyCape CLI over -HTTPS, a fresh Python owner process and actual Warden SQLite, with synthetic -case/catalog/owner/effect attestations only. Exact source/proof is retained by -CUST-WP-0080 under `keycape-current-client-r1`. Native owner mappings, current -operation/effect authority, credential custody, case/type/descriptor refreeze, -Forgejo inverse semantics and first native acceptance remain open; no lane, -adapter, runtime or caller scope is activated by this source change. - -`warden case run ` executes the plan. Owner entry points run through -the existing proxy as the caller; steps that need an attended login use the -existing contained login envelope (silent command, session revoked afterwards), -grouped into as few attended sessions as possible. Then it runs the type's -verification and writes a receipt without values. `warden case rollback` -reverses exactly this case's steps. - -Acceptance: the Forgejo reference case (NK-WP-0048-T06) reaches `live` through -this path, with no case-specific script. - -2026-10-17: original-intent lookup/context interface for NK-WP-0048-T15, source -and synthetic tests only. `src/warden/case_run.py`: `Executor.run` writes an -`OriginalIntent` (schema `warden.case.original-intent.v1`: the dispatched run -Invocation — attempt, step, owner, target, actor, principal, lane, all six pins, -tenant, realm — plus admitted adapter source) in the same SQLite transaction as -the pre-dispatch journal row. `Journal.original_intent(binding, current, source)` -returns it after restart and refuses on a missing record or any -pins/source/step/owner/target/actor/principal/lane/tenant/realm mismatch, before any -journal write, so the step stays uncertain. An adapter's -`reconcile_original(ReconcileContext(original, attempt))` receives the original -intent separately from the new reconcile attempt. This is Warden's record of what -it dispatched, not owner created-only provenance. Current Executor.admit leaves -tenant/realm empty; the reserved coordinates establish no admitted provenance. -Owner descriptors/adapters, contained session integration, Forgejo -materialization and the native run remain open. - -2026-10-17 source consistency follow-through: retained the additive intents table -and reconcile_original callback and fixed four reproduced local defects: original -actor mismatch, payload/SQL attempt-ID mismatch, orphan intent without its -matching original running receipt, and arbitrary nonmetadata values accepted by -the original-intent serializer. Exact bounded field validation, duplicate-free -bounded decode and save-time intent/invocation/running-state equality now preserve -the original receipt linkage before owner lookup. No new context store, native -verifier, trust root, adapter activation or authority migration was introduced. - -The actual canonical focused kernel and new consistency suite passes 54 checks; -owner-installed Ruff check/format passes. The existing generated-data/synthetic -owner fixture and a distinct interpreter prove source correspondence/refusal, -not independent native custody or creation. Earlier comparison probes and failed -preparation are retained. Current caller and fresh session/grant revocation checks -remain required; original owner payload/revocation, exact native readback and -created-only provenance still need independent authenticated owner admission. -No JSON/SQL context, unknown tenant/realm, changed run-sheet or legacy missing -intent supplies recovery, replay or undo authority. T03 remains progress and all -native acceptance gates/statuses/dependencies/UUIDs remain unchanged. Evidence: -Custodian original-context-compare-r2 and original-context-owner-followthrough-r3 -under docs/changes/netkingdom-target-2026-10-17/; publication/projection is separate. - -Agreed with railiance-platform (RPF-WP-0054-T01, 2026-10-09): -- `mint-secret` runs `credential.py generate --grant keycape/client-secret-generate - --client-id --case-ref --case-file ` as the caller's - child process. The broker re-derives the case-ref and checks status and - client_id. -- warden refuses that step when `WARDEN_AGENT_ID` is set. - -2026-10-10: railiance-platform implemented the command (RPF-WP-0054-T03). It is -catalogued as draft `keycape-client-secret-generate`, with `exec_owner` -railiance-platform and `exec_capable: false`. `warden case check-type` now -reports the lane as LANE-UNREVIEWED rather than LANE-MISSING. Before it can be -activated: -- the founder binds the issuer policy to the operator OIDC role -- key-cape confirms the secret format -- the RPF-WP-0054-T05 probe passes - -2026-10-10 (later): -- **Broker lane.** The issuer role is bound (CCR-2026-0043) and the T05 probe - passed (railiance-platform `21b8af3`). Only key-cape's format confirmation is - left. The playbook now includes the issuer-role login, a 15m token with - unlimited uses that `warden case run` must group with the attended session. -- **Env spec.** net-kingdom adopted `{client_id:env}` as a 1.0 patch - (`9a9b8e3`). The plan renders `KEYCAPE_FORGEJO_CLIENT_SECRET`. -- **Forgejo case.** Rendered and built by ops-mason. It is not yet - `materialized`: MASON-WP-0010-T03 waits on the railiance-platform CCRs and - their founder apply. - -2026-10-10: `keycape-client-secret-generate` activated on railiance-platform's -go-ahead (message 20973a9c) after key-cape confirmed the format (4e0213b3). -`warden case check-type` on the live type now reports `plannable=True`. The -only thing T03 still waits on is the Forgejo case reaching `materialized` -(MASON-WP-0010-T03). - -2026-10-10: net-kingdom moved the type to 1.1 (`e2174ce`) and re-pinned the -Forgejo case. 1.1 adds the verification `consumer-enforces-groups` and requires -the hand-over to enforce `access_groups`. `check-type` on 1.1 reports -`plannable=True`, and a materialized copy renders six steps and the new check. -The case-ref digest covers the whole case file, including `status`, so it -changes when the case becomes `materialized`. `warden case run` therefore takes -it from the file at run time, and the broker re-derives it from the same file. - -```task -id: WARDEN-WP-0044-T04 -status: wait -depends_on: [WARDEN-WP-0044-T03] -blocking_reason: "Catalog integration follows a proven run." -priority: medium -state_hub_task_id: "4a0fa4a7-5ca9-518a-ab92-a79a5e2cf93e" -``` - -### T04 — Catalog entries from types - -Let a type contribute reviewed catalog entries once, so that every case of that -type routes without a new catalog edit. Cases add parameter values, not routes. -`warden route find` should be able to answer "which SecurityCaseType covers this -need?" before anyone builds by hand. - - -2026-10-10 consolidation source prototype (not an accepted owner adapter API): -`src/warden/case_receipt_state.py` provides a pure immutable receipt reducer, -with exact case/type/adapter/catalog/acceptance/run-sheet pins, running intent -before dispatch, uncertain recovery after a lost result, no success replay, -explicit retry-safe failed-before-change retry and created-only reverse undo. -It executes nothing and accepts no command output. The caller must durably -persist running before any future side effect; storage/authenticated owner -receipts/live adapters are not implemented. Focused offline tests cover changed -pins, lost receipts, retry/refusal, uncertain rollback and shared-object -preservation. Source proposal: `docs/security-case-execution-proposal.md`. - -T03 remains open: typed owner execution/receipt contracts require review, the -Forgejo structural materialization needs owner receipts, and CLI dispatch, -contained sessions, native verification and provenance remain unimplemented. -There is a circularity to resolve with net-kingdom/ops-mason: materialization -cannot require mint-secret if mint-secret is itself a follow-through step whose -planning requires materialized. No case status is changed by this prototype. - - -2026-10-11: the exact execution-design memo version1 has confirmed native human -acceptance (approval e46b149b-ec3c-4b3b-98a7-dcb4934448ca). The approved proposal -bytes remain unchanged. `src/warden/case_run.py` now implements a closed v1 local -execution seam, independent admission/evidence callbacks, private transactional -SQLite receipt storage, process locking, running-before-dispatch, crash recovery -to uncertainty, explicit safe retry, separately required broker-grant and parent-session revocation -proofs, native owner reconciliation and created-only -reverse rollback. Thirty-seven kernel tests and fourteen reducer tests pass; -ruff checks pass. See `docs/security-case-execution-kernel-v1.md` for exact scope -and receipt provenance. No installed native adapter or live CLI execution is -claimed. Current prose types stay plan-only; acknowledged descriptor/type machine -seams and authenticated owner/caller receipts are still required. T03 progresses -for implementation but cannot finish before ordered native Forgejo acceptance. - -2026-10-16 source preparation (isolated checkout; no native acceptance): -T03's browser parameter mismatch now has a producer-validator-backed projection -in `case_inputs.py`, explicitly configured on the real Executor. Exact 1.1/1.2 -parameter definitions and all four reviewed producer modules are pinned; full case -validation admits reviewed security-sensitive metadata while unknown/fixed/secret -inputs and type/producer drift refuse. Only the reviewed descriptor target -reaches the owner callback; no parameter journal payload is added. Actual -Executor/producer tests cover positive versions and refusal/containment paths. -Acknowledged machine descriptors, authenticated owner evidence, real Forgejo -materialization and contained native sessions remain mandatory and unimplemented; -T03 stays progress, T04 stays wait. No type or case status changed. - -2026-10-16 producer source refresh: independently reviewed NetKingdom -`015694ee414c802dc2ef1fa0120dfed610086e69` optional descriptor-map repair. -Only the validator/evidence source pins change; exact parameter and transitive -source identity remain enforced. Actual canonical producer, Executor and reducer -suite passes 87 checks, including 14 new malformed-map integration cases; all -four missing/changed modules still refuse. Review: -`docs/reviews/2026-10-16-browser-producer-pin-refresh.md`. No adapter attestation, -native acceptance, credential use or privileged dispatch. T03 remains progress. - - -## T03 source correction and safe-test follow-through — 2026-10-17 - -The reviewed KERNEL-2/3 source now authenticates and parses the actual catalog -snapshot, checks lane owner/risk/delegation, and resolves each type step's Warden, -attended or owner dispatch mode. Four proposed attended-change lanes remain -DRAFT; no lane is promoted and PARAMETER_PINS still contains only 1.1/1.2. - -Independent review KR1-F1/F2 found cached attended run/rollback/reconcile could -lose the human/runtime boundary, and actual admission lacked the planner's -establishing-login checks. The exact successor retains the mode and authenticated -catalog bytes and repeats caller/session, agent marker and attended-lane checks -before any journal write or callback on all three operations. Planner and executor -share active/reviewed login/session-lane validation. Two unresolved Apps session -lanes correctly remain refused. Integration tests now explicitly disclose all -fixture authority substitutions; they establish no owner or native authority. - -The new 19-test regression matrix reproduces 13 failures on its predecessor and -passes on the candidate. A fresh independent reviewer adds changed-authority and -catalog-snapshot probes: 90 focused checks and six guarded integration checks -pass; the combined predecessor comparison has 22 expected failures. Author's -isolated full suite passes 765 with four integration-marked tests deselected. -Evidence: Custodian warden-1.3-kernel-fixes-r1 and warden-1.3-kernel-review-r2 -under docs/changes/netkingdom-target-2026-10-17. Original unsuccessful review, -invalid setup attempts and unknown ambient-lookup outcome are retained. - -A separate reviewed test-only fix stubs ambient OpenBao identity in access CLI -unit tests and adds a synthetic sentinel regression which fails before any -subprocess can launch on the predecessor; 26 access/helper checks pass. The real -production helper is unchanged. The first old review's unisolated launches may -have invoked ambient `bao token lookup`; remote reach/success remain unknown, -with no secret displayed. This patch does not erase that adverse outcome. - -Explicit remaining safe-test work is retained in this live T03 scope, owned by -ops-warden: replace or wrap the ordinary unit-test entry path with allowlisted -environment/empty synthetic HOME and guarded subprocess/network execution, -including nested validator children. Completion requires representative access -and case tests to refuse token-file/environment and live provider access under -synthetic negative controls while permitted local fixtures remain usable. Until -then use the preserved isolated review runner, not the ambient `make test` path. -No credential lookup, live investigation or rotation is authorized by this note. -T03 remains progress; contained sessions, authenticated owner provenance, -materialization, exact type acknowledgment/pin and first native case remain open. - - -2026-10-17 safe unit-runner source candidate: `make test` now selects an existing -interpreter and `scripts/run_unit_tests.py`, which constructs the environment and -empty temporary HOME before pytest collection, denies unexpected process/socket -use and ambient file opens, and guards the pinned BrowserInputs child too. -Closed fixture exceptions cover the synthetic desk endpoint, metadata checkers, -exact-byte contract doubles and fixed transport prints; they confer no provider -authority. Explicit public declaration fixtures replace ambient HOME source -lookups. See `docs/development/safe-unit-tests.md` and Custodian packet -`docs/changes/netkingdom-target-2026-10-17/warden-safe-unit-runner-r1`. -Synthetic token, ambient-path, forged-provider and nested-child regressions are -retained alongside the initial failed isolation runs and corrected results. This -is a source candidate for the existing safe-test handoff, not an OS sandbox or -native case acceptance. T03 remains progress; contained sessions, authenticated -owner provenance, materialization, exact type acknowledgment/pin and first native -case remain open. No credential lookup, live investigation or rotation occurred. - -Candidate validation: ordinary `make test` under synthetic ambient token/HOME -controls passes 783 tests with four integration-marked tests deselected. Affected -focused checks pass 117 tests; new runner code/regressions pass Ruff. Retained -initial failures exposed fixture dependencies and were corrected, not suppressed. - - -2026-10-17 root follow-through: the original safe-runner source candidate was -independently rejected for O_TRUNC open-write misclassification and unchecked -public fixture projection hashes. Both counterexamples are retained. The minimal -correction treats truncating/appending opens as writes and adds the closed -projection-byte provenance regression. Root inspected the two exact corrections -and ran ordinary make test on stable corrected source with only synthetic parent -HOME/token/Python/pytest sentinels: 785 passed, four deselected. No real credential -or native provider action occurred. Initial author failures and rejected review -are preserved in the Custodian warden-safe-unit-runner-r1 and -warden-safe-unit-runner-review-r1 packets, with root disposition/full result in -warden-safe-unit-integration-r1. This completes the known trusted-unit-entry -accident-prevention follow-through; direct pytest bypass, native/non-open/preopened -file-descriptor limits remain explicit. No OS sandbox, isolated agent assignment, -production grant or native T03 acceptance follows. T03 stays progress. diff --git a/workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md b/workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md deleted file mode 100644 index 1e0a1e0..0000000 --- a/workplans/WARDEN-WP-0045-agt-custodian-issuance-profile.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -id: WARDEN-WP-0045 -type: workplan -title: "Issuance profile for the supervised custodian agent principal (agt-custodian)" -domain: infotech -repo: ops-warden -status: finished -flavor: implementation -owner: claude-code -created: "2026-10-20" -updated: "2026-10-20" -depends_on: [] -related_repos: - - the-custodian - - railiance-infra - - railiance-platform -state_hub_workstream_id: "1bc9683e-51a7-55e0-9eaa-56b7f8d854a8" ---- - -# WARDEN-WP-0045 — Issuance profile for agt-custodian - -ops-warden's share of owner task `CUST-WP-0073-T02` (the-custodian, -`workplans/CUST-WP-0073-agent-credential-separation.md`). - -## Why - -Founder decision 2026-10-20: a new agent principal `agt-custodian` for the -dedicated host account `custodian-agt` on Railiance01. One account and principal -serves the Codex, Claude Code and Grok Build harnesses. The harnesses run in the -isolated workstation WSL distro `custodian-agents`. Certificates are short-lived. -The supervisor (Bernd) re-issues them from his operator account and places them -in the agent distro. The agent must never be able to self-issue. - -Before this plan the issuance contract had no per-actor certificate extensions. -Every cert got the backend defaults: agent, port and X11 forwarding, pty and -user-rc. A `--ttl` override could also reach the 24 h agt type maximum whatever -the actor declared. - -## Tasks - -```task -id: WARDEN-WP-0045-T01 -status: done -priority: high -state_hub_task_id: "a7dec8f1-8846-5a16-9773-5ef836a02c43" -``` - -Extend the issuance contract. An inventory actor may now declare `extensions`, -an allow-list of OpenSSH user-cert extensions. LocalCA issues `-O clear` plus -`-O `, and Vault receives an explicit `extensions` map. An actor may also -declare `max_ttl_hours`, a ceiling below the type maximum that `_enforce_ttl` -applies to `--ttl` overrides. Actors without these fields behave as before. - -```task -id: WARDEN-WP-0045-T02 -status: done -priority: high -state_hub_task_id: "6d4300a2-561f-5d2d-9575-9f648b9158f3" -``` - -Add the `agt-custodian` profile to `examples/inventory.seed.yaml`: principal -`agt-custodian`, TTL 12 h with a ceiling of 12 h, and `permit-pty` only. Pty -stays because the harness shells are interactive. There is no `source-address` -because the workstation egress IP is not stable. Add the ceremony routing lane -`agt-custodian-ssh-cert` (the supervisor issues; the agent never runs -`warden sign`) and document it in `wiki/ActorInventoryPatterns.md`. Rebuild the -flex-auth snapshot, regenerate the high-risk paths and update the pep-stance -coverage counts. Tests: `tests/test_agt_custodian_profile.py`. - -```task -id: WARDEN-WP-0045-T03 -status: done -priority: high -state_hub_task_id: "166447a5-e704-52f4-9d08-d0fb3061a406" -``` - -2026-10-20 (recorded 2026-10-21): railiance-infra mapped `custodian-agt` (uid 1001) to principal `agt-custodian`. railiance-platform `agt-role` allowed_extensions = `permit-pty`. The founder issued the first attended certificate via `credential.py exec --grant ops-warden/warden-sign`; it carried no extensions. The founder then reissued it after the role fix, valid 2026-10-20T23:24Z-2026-10-21T11:25Z, with `permit-pty` only, and a pty session worked from the custodian-agents distro. Receipt: the-custodian `docs/evidence/2026-10-20-railiance01-cutover-steps-2-4.md` (CUST-WP-0073-T02). Done. - -Do the operator rollout. Copy the `agt-custodian` block into the operator's -`~/.config/warden/inventory.yaml`, out of the agent's reach. railiance-platform -confirms that `ssh/roles/agt-role` has `allowed_users` ⊇ `agt-custodian` and -`allowed_extensions` ⊇ `permit-pty`. railiance-infra deploys the principal -mapping for `custodian-agt`. The supervisor then issues the first cert with -`warden sign agt-custodian --pubkey .pub > -cert.pub`, -places it in the `custodian-agents` distro, and runs `ssh-keygen -L` to check -the principal, the 12 h validity and that only `permit-pty` is present. Verify -that the agent distro holds no warden config, CA key or Vault token. diff --git a/workplans/archived/260707-ADHOC-2026-07-07.md b/workplans/archived/260707-ADHOC-2026-07-07.md index 31e9d27..d2929fb 100644 --- a/workplans/archived/260707-ADHOC-2026-07-07.md +++ b/workplans/archived/260707-ADHOC-2026-07-07.md @@ -1,5 +1,5 @@ --- -id: WARDEN-WP-ADHOC-2026-07-07 +id: ADHOC-2026-07-07 type: workplan title: "Ad Hoc Tasks — 2026-07-07" domain: infotech @@ -9,7 +9,7 @@ owner: grok topic_slug: custodian created: "2026-07-07" updated: "2026-07-07" -state_hub_workstream_id: "90568b1e-8395-5c67-9c69-851ed08ff3d3" +state_hub_workstream_id: "2e8bc783-2e37-4b86-a691-177b18329783" --- > **Terminology note:** Historical text in this archived workplan may use the legacy term "workstream". The fleet term is **workplan** (`canon/standards/workplan-terminology-fleet_v0.1.md`). @@ -19,10 +19,10 @@ state_hub_workstream_id: "90568b1e-8395-5c67-9c69-851ed08ff3d3" ### T01 — Roll out proxy pipe fix (be3b4a2) ```task -id: WARDEN-WP-ADHOC-2026-07-07-T01 +id: ADHOC-2026-07-07-T01 status: done priority: high -state_hub_task_id: "bf985c95-bea6-5057-94f9-9cfa7e1c9dd8" +state_hub_task_id: "55b6de52-b958-4b72-94eb-4ba1b8da2629" ``` `warden access` failed on `reuse-surface-hub-write-token` because `shlex.split`