WARDEN-WP-0033-T05: split the stale cadences, and record how a blocker was verified
The 90-day --stale-days default on `warden route gaps` was not a loose threshold, it was an inert one: the delegation register was created 2026-08-15, so it could not have fired before November. It was inherited from the catalog pointer cadence and applied to a claim with a completely different half-life. Two changes. DEFAULT_BLOCKER_STALE_DAYS = 14 now governs interim blockers, while DEFAULT_STALE_DAYS = 90 keeps governing pointer freshness -- "is this the right owner and page" is quarterly, "has the owner answered" is not. 14 is calibrated on blockers that actually cost something: ten days for the secrets-engine lanes, one for RISK-F-0001, roughly fifty for FLEX-WP-0007. The second change matters more. `reviewed` records when someone touched an entry, which is indistinguishable from re-checking it -- six lanes read as freshly reviewed today because I typed in them. `verified:` now says how the claim was established, and asked-and-waiting explicitly does NOT count: that is the state the secrets-engine blocker sat in for ten days while looking current. A lane in that state is stale at zero days old, and key-cape-oidc-login proves it works. 8 of 14 interim lanes are honestly marked unverified rather than given a fresh date they did not earn. --fail-on-stale exits 3 for a cron or gate. No CI test on age: a date-triggered failure breaks the build for whoever commits next instead of whoever owns the blocker. The CI test is structural -- every interim lane must record how it was verified -- so it fails on the commit that introduces the omission. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a565e62b2f
commit
55f0f47a02
8 changed files with 335 additions and 24 deletions
|
|
@ -10,11 +10,11 @@
|
||||||
# declares it, and is null where the field set has not been established --
|
# declares it, and is null where the field set has not been established --
|
||||||
# null means unknown, never 'one field'.
|
# null means unknown, never 'one field'.
|
||||||
|
|
||||||
generated_at: "2026-08-21T07:04:54Z"
|
generated_at: "2026-08-21T11:24:43Z"
|
||||||
source: ops-warden/registry/routing/catalog.yaml
|
source: ops-warden/registry/routing/catalog.yaml
|
||||||
catalog_revision: "675e04e8e67869f0e47a3ae55ab37b00112582f2"
|
catalog_revision: "675e04e8e67869f0e47a3ae55ab37b00112582f2"
|
||||||
catalog_revision_date: "2026-08-21T09:04:54+02:00"
|
catalog_revision_date: "2026-08-21T09:04:54+02:00"
|
||||||
catalog_dirty: false
|
catalog_dirty: true
|
||||||
high_risk_lane_count: 19
|
high_risk_lane_count: 19
|
||||||
concrete_path_count: 14
|
concrete_path_count: 14
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
# Concrete broker lane — RAILIANCE-WP-0005 pilot (live 2026-07-01):
|
# Concrete broker lane — RAILIANCE-WP-0005 pilot (live 2026-07-01):
|
||||||
# credential exec injects VAULT_TOKEN only into the child process; ops-warden
|
# credential exec injects VAULT_TOKEN only into the child process; ops-warden
|
||||||
# issues SSH certs and never mints or holds OpenBao tokens.
|
# issues SSH certs and never mints or holds OpenBao tokens.
|
||||||
|
|
@ -109,6 +110,7 @@ entries:
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
blocked_on: "NOT A DELEGABLE LANE. Refused by secrets-engine 2026-08-21: this is a generic routing template (path_template is a <domain>/<workload>/<bundle> pattern), not one secret lane, so there is no front door for anyone to own. ops-warden agrees. The concrete lanes it resolves to are delegated individually; this entry stays a pointer and should not be counted as an interim cover."
|
blocked_on: "NOT A DELEGABLE LANE. Refused by secrets-engine 2026-08-21: this is a generic routing template (path_template is a <domain>/<workload>/<bundle> pattern), not one secret lane, so there is no front door for anyone to own. ops-warden agrees. The concrete lanes it resolves to are delegated individually; this entry stays a pointer and should not be counted as an interim cover."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: unverified
|
||||||
# Structured handoff (WP-0014) — reference example. Templates only, no values.
|
# Structured handoff (WP-0014) — reference example. Templates only, no values.
|
||||||
# ops-warden does not own this secret; it advises and (exec_capable) proxies the
|
# ops-warden does not own this secret; it advises and (exec_capable) proxies the
|
||||||
# fetch *as the caller* via `warden access`, never holding or persisting the value.
|
# fetch *as the caller* via `warden access`, never holding or persisting the value.
|
||||||
|
|
@ -143,6 +145,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0001 (commit 8f617fc):
|
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0001 (commit 8f617fc):
|
||||||
# status=active, access_frontdoor.readiness=ready, resolvable=true; positive fetch
|
# status=active, access_frontdoor.readiness=ready, resolvable=true; positive fetch
|
||||||
# passed and negative (non-whynot) login denied. Zero-placeholder fetch: an automated
|
# passed and negative (non-whynot) login denied. Zero-placeholder fetch: an automated
|
||||||
|
|
@ -186,6 +189,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: flex-auth
|
intended_owner: flex-auth
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: key-cape-oidc-login
|
- id: key-cape-oidc-login
|
||||||
title: Interactive login, OIDC token, or MFA
|
title: Interactive login, OIDC token, or MFA
|
||||||
|
|
@ -204,6 +208,7 @@ entries:
|
||||||
intended_owner: key-cape
|
intended_owner: key-cape
|
||||||
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."
|
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"
|
reviewed: "2026-08-21"
|
||||||
|
verified: asked-and-waiting
|
||||||
# Login lane (WP-0014 T4) — interactive auth bootstrap, not a secret read. No
|
# 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
|
# 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
|
# point is to obtain one). warden runs it interactively as the caller and never
|
||||||
|
|
@ -229,6 +234,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: ops-bridge
|
intended_owner: ops-bridge
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: railiance-infra-principals
|
- id: railiance-infra-principals
|
||||||
title: Host SSH principal file or force-command deployment
|
title: Host SSH principal file or force-command deployment
|
||||||
|
|
@ -246,6 +252,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: railiance-infra
|
intended_owner: railiance-infra
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: inter-hub-bootstrap-ssh
|
- id: inter-hub-bootstrap-ssh
|
||||||
title: Inter-Hub bootstrap SSH envelope
|
title: Inter-Hub bootstrap SSH envelope
|
||||||
|
|
@ -263,6 +270,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: railiance-infra
|
intended_owner: railiance-infra
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: activity-core-issue-sink
|
- id: activity-core-issue-sink
|
||||||
title: activity-core IssueSink → issue-core REST emission
|
title: activity-core IssueSink → issue-core REST emission
|
||||||
|
|
@ -280,6 +288,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: activity-core
|
intended_owner: activity-core
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: issue-core-ingestion-api-key
|
- id: issue-core-ingestion-api-key
|
||||||
title: issue-core ingestion API key (OpenBao KV + ESO)
|
title: issue-core ingestion API key (OpenBao KV + ESO)
|
||||||
|
|
@ -306,6 +315,7 @@ entries:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: owner-confirmed
|
||||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0002 / RAILIANCE-WP-0009
|
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0002 / RAILIANCE-WP-0009
|
||||||
# (promoted 2026-07-02): policy workload-kv-read-issue-core-runtime and k8s auth role
|
# (promoted 2026-07-02): policy workload-kv-read-issue-core-runtime and k8s auth role
|
||||||
# external-secrets-issue-core applied; ExternalSecret issue-core/issue-core-runtime
|
# external-secrets-issue-core applied; ExternalSecret issue-core/issue-core-runtime
|
||||||
|
|
@ -353,6 +363,7 @@ entries:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: owner-confirmed
|
||||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0005 / RAILIANCE-WP-0011
|
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0005 / RAILIANCE-WP-0011
|
||||||
# (promoted 2026-07-07): policy workload-kv-read-reuse-surface-runtime; ExternalSecret
|
# (promoted 2026-07-07): policy workload-kv-read-reuse-surface-runtime; ExternalSecret
|
||||||
# reuse/reuse-surface-runtime SecretSynced to reuse-surface-env on Railiance01;
|
# reuse/reuse-surface-runtime SecretSynced to reuse-surface-env on Railiance01;
|
||||||
|
|
@ -390,6 +401,7 @@ entries:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: owner-confirmed
|
||||||
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
# High-risk: provider API key with spend impact + prompt-adjacent (WP-0026 T04).
|
||||||
risk: high
|
risk: high
|
||||||
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0003 / RAILIANCE-WP-0010
|
# Concrete, owner-confirmed lane — railiance-platform CCR-2026-0003 / RAILIANCE-WP-0010
|
||||||
|
|
@ -430,6 +442,7 @@ entries:
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
blocked_on: "Rotation is re-establish, a multi-step procedure ops-warden only describes"
|
blocked_on: "Rotation is re-establish, a multi-step procedure ops-warden only describes"
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
|
verified: unverified
|
||||||
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
# High-risk: WebDAV upload token + AGE recovery escrow (WP-0026 T04).
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0004: policy + OIDC role applied; values provisioned 2026-07-07.
|
# CCR-2026-0004: policy + OIDC role applied; values provisioned 2026-07-07.
|
||||||
|
|
@ -475,6 +488,7 @@ entries:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: owner-confirmed
|
||||||
# High-risk: site-admin PAT (WP-0026 T04).
|
# High-risk: site-admin PAT (WP-0026 T04).
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0006: approved by platform-operator 2026-07-12; policy
|
# CCR-2026-0006: approved by platform-operator 2026-07-12; policy
|
||||||
|
|
@ -517,6 +531,7 @@ entries:
|
||||||
intended_owner: tenant-engine
|
intended_owner: tenant-engine
|
||||||
blocked_on: "Custody at tenants/binky/... but rotation owner is binky-control — split lifecycle, no front door reconciling it"
|
blocked_on: "Custody at tenants/binky/... but rotation owner is binky-control — split lifecycle, no front door reconciling it"
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0007: tenants/ mount + policy + OIDC role applied; founder provisioned
|
# CCR-2026-0007: tenants/ mount + policy + OIDC role applied; founder provisioned
|
||||||
# values via UI (version ≥2, not placeholder). Capabilities-safe verify 2026-07-17:
|
# values via UI (version ≥2, not placeholder). Capabilities-safe verify 2026-07-17:
|
||||||
|
|
@ -551,6 +566,7 @@ entries:
|
||||||
intended_owner: tenant-engine
|
intended_owner: tenant-engine
|
||||||
blocked_on: "Same split lifecycle as binky-company-email-imap; no tenant-engine front door"
|
blocked_on: "Same split lifecycle as binky-company-email-imap; no tenant-engine front door"
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0008: policy + OIDC role applied; secret at tenants/binky/qonto-api
|
# CCR-2026-0008: policy + OIDC role applied; secret at tenants/binky/qonto-api
|
||||||
# (fields API_KEY, API_USER). Map to QONTO_API_KEY / QONTO_ORGANIZATION_ID for
|
# (fields API_KEY, API_USER). Map to QONTO_API_KEY / QONTO_ORGANIZATION_ID for
|
||||||
|
|
@ -586,6 +602,7 @@ entries:
|
||||||
intended_owner: key-cape
|
intended_owner: key-cape
|
||||||
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)"
|
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"
|
reviewed: "2026-08-17"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange"
|
auth_method: "OpenBao platform workload lane; KeyCape client_secret_basic exchange"
|
||||||
path_template: "platform/workloads/rapp-qonto/keycape-client"
|
path_template: "platform/workloads/rapp-qonto/keycape-client"
|
||||||
|
|
@ -616,6 +633,7 @@ entries:
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
blocked_on: "re-establish plus an alternative host-local key path; two ways in, neither owner-fronted (also agent-harness)"
|
blocked_on: "re-establish plus an alternative host-local key path; two ways in, neither owner-fronted (also agent-harness)"
|
||||||
reviewed: "2026-08-11"
|
reviewed: "2026-08-11"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
# Provisioned 2026-07-17 on railiance01: ed25519 keypair on host, OpenBao copy at
|
# Provisioned 2026-07-17 on railiance01: ed25519 keypair on host, OpenBao copy at
|
||||||
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
|
# platform/workloads/agent-harness/forgejo-deploy-key, write deploy key on
|
||||||
|
|
@ -651,6 +669,7 @@ entries:
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
blocked_on: "AppRole is a host-standing credential; no owner front door for minting or rotating role_id+secret_id"
|
blocked_on: "AppRole is a host-standing credential; no owner front door for minting or rotating role_id+secret_id"
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
# Provisioned 2026-07-17: role agent-harness-binky-mail bound to existing policy
|
# Provisioned 2026-07-17: role agent-harness-binky-mail bound to existing policy
|
||||||
# workload-kv-read-binky-company-email-imap; role_id/secret_id delivered to
|
# workload-kv-read-binky-company-email-imap; role_id/secret_id delivered to
|
||||||
|
|
@ -689,6 +708,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: net-kingdom
|
intended_owner: net-kingdom
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
|
|
||||||
- id: database-dynamic-credentials
|
- id: database-dynamic-credentials
|
||||||
title: Database dynamic credentials (OpenBao secrets engine)
|
title: Database dynamic credentials (OpenBao secrets engine)
|
||||||
|
|
@ -704,6 +724,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: rapp-postgres
|
intended_owner: rapp-postgres
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
exec_capable: false
|
exec_capable: false
|
||||||
|
|
||||||
|
|
@ -722,6 +743,7 @@ entries:
|
||||||
intended_owner: ops-mason
|
intended_owner: ops-mason
|
||||||
blocked_on: "AppRole is a host-standing credential; no owner-fronted exec for the OpenRouter key read"
|
blocked_on: "AppRole is a host-standing credential; no owner-fronted exec for the OpenRouter key read"
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
risk: standard
|
risk: standard
|
||||||
# Built 2026-07-27 by ops-mason (MASON-WP-0001-T05), approved by Bernd
|
# Built 2026-07-27 by ops-mason (MASON-WP-0001-T05), approved by Bernd
|
||||||
# Worsch 2026-07-27. Policy + AppRole live; reins/ KV v2 mount created
|
# Worsch 2026-07-27. Policy + AppRole live; reins/ KV v2 mount created
|
||||||
|
|
@ -776,6 +798,7 @@ entries:
|
||||||
intended_owner: railiance-apps
|
intended_owner: railiance-apps
|
||||||
blocked_on: "Runtime Secret applied via railiance-apps make target; OpenBao path planned but CCR not applied"
|
blocked_on: "Runtime Secret applied via railiance-apps make target; OpenBao path planned but CCR not applied"
|
||||||
reviewed: "2026-08-17"
|
reviewed: "2026-08-17"
|
||||||
|
verified: unverified
|
||||||
# USER_ENGINE_PROXY_SECRET ownership settled 2026-08-16 (Bernd; State Hub decision
|
# USER_ENGINE_PROXY_SECRET ownership settled 2026-08-16 (Bernd; State Hub decision
|
||||||
# 8fe22037-5bbb-4487-bb86-e4beccee454b, USER-WP-0021): it is infrastructure trust
|
# 8fe22037-5bbb-4487-bb86-e4beccee454b, USER-WP-0021): it is infrastructure trust
|
||||||
# between ingress and workload, not a user-domain fact. intended_owner stays
|
# between ingress and workload, not a user-domain fact. intended_owner stays
|
||||||
|
|
@ -816,6 +839,7 @@ entries:
|
||||||
mode: native
|
mode: native
|
||||||
intended_owner: ops-mason
|
intended_owner: ops-mason
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
auth_method: "in-cluster generated Secret audit-core-senders; later OpenBao KV via Mason wrap-migrate"
|
auth_method: "in-cluster generated Secret audit-core-senders; later OpenBao KV via Mason wrap-migrate"
|
||||||
path_template: "platform/workloads/audit-core/senders"
|
path_template: "platform/workloads/audit-core/senders"
|
||||||
policy_ref: "external-secrets-audit-core"
|
policy_ref: "external-secrets-audit-core"
|
||||||
|
|
@ -838,6 +862,7 @@ entries:
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
blocked_on: "ACCEPTED by secrets-engine 2026-08-21 (SECRETS-WP-0006, decision ae676382). They drafted and hold the catalog entry; ops-warden reviewed it and both sides agree. Interim proxy remains with ops-warden until this lane passes approved native positive/negative verification (SECRETS-WP-0006-T05) — retire only then, not on acceptance."
|
||||||
reviewed: "2026-08-21"
|
reviewed: "2026-08-21"
|
||||||
|
verified: owner-confirmed
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0010 approved 2026-08-12; applied same day (EMAIL-WP-0004-T03):
|
# CCR-2026-0010 approved 2026-08-12; applied same day (EMAIL-WP-0004-T03):
|
||||||
# policies external-secrets-email-connect + workload-kv-read-email-connect-transactional,
|
# policies external-secrets-email-connect + workload-kv-read-email-connect-transactional,
|
||||||
|
|
@ -879,6 +904,7 @@ entries:
|
||||||
intended_owner: railiance-platform
|
intended_owner: railiance-platform
|
||||||
blocked_on: "Founder bootstrap API key; draft until provisioned; no owner-fronted exec"
|
blocked_on: "Founder bootstrap API key; draft until provisioned; no owner-fronted exec"
|
||||||
reviewed: "2026-08-15"
|
reviewed: "2026-08-15"
|
||||||
|
verified: unverified
|
||||||
risk: high
|
risk: high
|
||||||
# CCR-2026-0011. Values via founder paste-once or local tfvars ingest.
|
# CCR-2026-0011. Values via founder paste-once or local tfvars ingest.
|
||||||
# Not the Barman runtime key (platform-pg-backup-s3).
|
# Not the Barman runtime key (platform-pg-backup-s3).
|
||||||
|
|
|
||||||
|
|
@ -771,6 +771,24 @@ def route_list(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS
|
||||||
|
|
||||||
|
|
||||||
|
def _gap_is_stale(delegation, reviewed: str, stale_days: int) -> bool:
|
||||||
|
"""An interim lane needs attention on either of two independent grounds.
|
||||||
|
|
||||||
|
Age is the obvious one. The other is that the review was never a
|
||||||
|
verification: an `asked-and-waiting` lane is fresh on the day the question
|
||||||
|
goes out and stays fresh while nobody answers, which is exactly how the
|
||||||
|
secrets-engine blocker looked current for ten days (WARDEN-WP-0033-T05).
|
||||||
|
"""
|
||||||
|
from warden.routing.catalog import days_since_review
|
||||||
|
|
||||||
|
if days_since_review(reviewed) > stale_days:
|
||||||
|
return True
|
||||||
|
return delegation.verified is not None and not delegation.is_verified
|
||||||
|
|
||||||
|
|
||||||
@route_app.command("gaps")
|
@route_app.command("gaps")
|
||||||
def route_gaps(
|
def route_gaps(
|
||||||
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
output_json: Annotated[bool, typer.Option("--json", help="Output JSON")] = False,
|
||||||
|
|
@ -779,12 +797,20 @@ def route_gaps(
|
||||||
int,
|
int,
|
||||||
typer.Option(
|
typer.Option(
|
||||||
"--stale-days",
|
"--stale-days",
|
||||||
help="Days since delegation review before an interim lane is stale (default 90)",
|
help="Days since a blocker was verified before an interim lane is stale "
|
||||||
|
"(default 14 — see DEFAULT_BLOCKER_STALE_DAYS)",
|
||||||
min=1,
|
min=1,
|
||||||
),
|
),
|
||||||
] = 90,
|
] = DEFAULT_BLOCKER_STALE_DAYS,
|
||||||
|
fail_on_stale: Annotated[
|
||||||
|
bool,
|
||||||
|
typer.Option(
|
||||||
|
"--fail-on-stale",
|
||||||
|
help="Exit 3 if any interim lane needs re-verifying (for cron or a gate)",
|
||||||
|
),
|
||||||
|
] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""List interim lanes: intended owner, blocker, and age since review."""
|
"""List interim lanes: intended owner, blocker, and age since verification."""
|
||||||
from warden.routing.catalog import days_since_review
|
from warden.routing.catalog import days_since_review
|
||||||
|
|
||||||
catalog = _load_catalog()
|
catalog = _load_catalog()
|
||||||
|
|
@ -805,11 +831,15 @@ def route_gaps(
|
||||||
"blocked_on": d.blocked_on,
|
"blocked_on": d.blocked_on,
|
||||||
"reviewed": reviewed,
|
"reviewed": reviewed,
|
||||||
"days_since_review": days_since_review(reviewed),
|
"days_since_review": days_since_review(reviewed),
|
||||||
|
"verified": d.verified,
|
||||||
|
"is_verified": d.is_verified,
|
||||||
"implicit": d.implicit,
|
"implicit": d.implicit,
|
||||||
"stale": days_since_review(reviewed) > stale_days,
|
"stale": _gap_is_stale(d, reviewed, stale_days),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
print(json.dumps(payload, indent=2))
|
print(json.dumps(payload, indent=2))
|
||||||
|
if fail_on_stale and any(row["stale"] for row in payload):
|
||||||
|
raise typer.Exit(3)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
|
|
@ -822,13 +852,18 @@ def route_gaps(
|
||||||
table.add_column("Blocked on")
|
table.add_column("Blocked on")
|
||||||
table.add_column("Reviewed")
|
table.add_column("Reviewed")
|
||||||
table.add_column("Days")
|
table.add_column("Days")
|
||||||
|
table.add_column("Verified")
|
||||||
table.add_column("Status")
|
table.add_column("Status")
|
||||||
for e in entries:
|
for e in entries:
|
||||||
d = e.effective_delegation
|
d = e.effective_delegation
|
||||||
reviewed = d.reviewed or e.reviewed
|
reviewed = d.reviewed or e.reviewed
|
||||||
days = days_since_review(reviewed)
|
days = days_since_review(reviewed)
|
||||||
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if days > stale_days else reviewed
|
stale = _gap_is_stale(d, reviewed, stale_days)
|
||||||
days_styled = f"[yellow]{days}[/yellow]" if days > stale_days else str(days)
|
reviewed_styled = f"[yellow]{reviewed}[/yellow]" if stale else reviewed
|
||||||
|
days_styled = f"[yellow]{days}[/yellow]" if stale else str(days)
|
||||||
|
verified_styled = (
|
||||||
|
d.verified if d.is_verified else f"[yellow]{d.verified or 'unrecorded'}[/yellow]"
|
||||||
|
)
|
||||||
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
|
status_styled = e.status if e.status == "active" else f"[yellow]{e.status}[/yellow]"
|
||||||
table.add_row(
|
table.add_row(
|
||||||
e.id,
|
e.id,
|
||||||
|
|
@ -836,18 +871,41 @@ def route_gaps(
|
||||||
d.blocked_on or "",
|
d.blocked_on or "",
|
||||||
reviewed_styled,
|
reviewed_styled,
|
||||||
days_styled,
|
days_styled,
|
||||||
|
verified_styled,
|
||||||
status_styled,
|
status_styled,
|
||||||
)
|
)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
stale_n = sum(
|
|
||||||
1
|
stale_entries = [
|
||||||
for e in entries
|
e for e in entries
|
||||||
if days_since_review(e.effective_delegation.reviewed or e.reviewed) > stale_days
|
if _gap_is_stale(
|
||||||
)
|
e.effective_delegation,
|
||||||
if stale_n:
|
e.effective_delegation.reviewed or e.reviewed,
|
||||||
console.print(
|
stale_days,
|
||||||
f"[yellow]{stale_n} interim lane(s) past {stale_days}d review cadence.[/yellow]"
|
|
||||||
)
|
)
|
||||||
|
]
|
||||||
|
if stale_entries:
|
||||||
|
# Say which of the two reasons applies. "Past cadence" and "never actually
|
||||||
|
# checked" call for different actions, and collapsing them is how an
|
||||||
|
# asked-and-waiting lane reads as reviewed.
|
||||||
|
aged = [
|
||||||
|
e for e in stale_entries
|
||||||
|
if days_since_review(e.effective_delegation.reviewed or e.reviewed) > stale_days
|
||||||
|
]
|
||||||
|
unverified = [e for e in stale_entries if e not in aged]
|
||||||
|
if aged:
|
||||||
|
console.print(
|
||||||
|
f"[yellow]{len(aged)} interim lane(s) past the {stale_days}d blocker "
|
||||||
|
f"cadence — re-check the blocker, do not just bump the date.[/yellow]"
|
||||||
|
)
|
||||||
|
if unverified:
|
||||||
|
console.print(
|
||||||
|
f"[yellow]{len(unverified)} interim lane(s) reviewed but not verified "
|
||||||
|
f"(asked-and-waiting or unverified) — the claim was never "
|
||||||
|
f"re-established.[/yellow]"
|
||||||
|
)
|
||||||
|
if fail_on_stale and stale_entries:
|
||||||
|
raise typer.Exit(3)
|
||||||
|
|
||||||
|
|
||||||
@route_app.command("show")
|
@route_app.command("show")
|
||||||
|
|
|
||||||
|
|
@ -69,9 +69,24 @@ _VALID_STATUS = ("active", "draft")
|
||||||
_VALID_LANES = ("secret", "login")
|
_VALID_LANES = ("secret", "login")
|
||||||
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
|
_VALID_ROTATION_METHODS = ("rotate", "re-establish")
|
||||||
|
|
||||||
# Default review cadence — see wiki/AccessRouting.md#drift-review-cadence
|
# Default review cadence for a catalog pointer — "is this still the right owner
|
||||||
|
# and page?" That is a genuinely quarterly question, so 90 days is right for it.
|
||||||
|
# See wiki/AccessRouting.md#drift-review-cadence
|
||||||
DEFAULT_STALE_DAYS = 90
|
DEFAULT_STALE_DAYS = 90
|
||||||
|
|
||||||
|
# Cadence for an interim lane's *blocker*, which is a different kind of claim
|
||||||
|
# with a much shorter half-life: "has the intended owner answered / can they
|
||||||
|
# front this yet?" (WARDEN-WP-0033-T05).
|
||||||
|
#
|
||||||
|
# 14 rather than 90 because 90 was never a loose default, it was an inert one --
|
||||||
|
# the delegation register was created 2026-08-15, so a 90-day threshold could not
|
||||||
|
# fire before November and never had. Calibrated instead against blockers that
|
||||||
|
# actually went stale: the secrets-engine lanes cost ten days, RISK-F-0001
|
||||||
|
# invalidated an ops-warden blocker in one, and the FLEX-WP-0007 claim was
|
||||||
|
# repeated by two repos for roughly fifty. 14 catches the ten-day cases and, at
|
||||||
|
# ~15 interim lanes, surfaces about one lane a day rather than a wall of them.
|
||||||
|
DEFAULT_BLOCKER_STALE_DAYS = 14
|
||||||
|
|
||||||
|
|
||||||
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
def days_since_review(reviewed: str, *, today: Optional[date] = None) -> int:
|
||||||
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
"""Calendar days between reviewed date (YYYY-MM-DD) and today."""
|
||||||
|
|
@ -214,16 +229,25 @@ class Catalog:
|
||||||
def stale_gaps(
|
def stale_gaps(
|
||||||
self,
|
self,
|
||||||
include_draft: bool = False,
|
include_draft: bool = False,
|
||||||
threshold_days: int = DEFAULT_STALE_DAYS,
|
threshold_days: int = DEFAULT_BLOCKER_STALE_DAYS,
|
||||||
*,
|
*,
|
||||||
today: Optional[date] = None,
|
today: Optional[date] = None,
|
||||||
) -> List[RouteEntry]:
|
) -> List[RouteEntry]:
|
||||||
"""Interim lanes whose delegation review is past the cadence threshold."""
|
"""Interim lanes whose blocker is due a re-check.
|
||||||
|
|
||||||
|
A lane counts as stale when its review date is past the threshold **or**
|
||||||
|
when the review was never a verification at all. An `asked-and-waiting`
|
||||||
|
entry is the case that motivated this: it looks freshly reviewed on the
|
||||||
|
day the question is asked and stays that way while nobody answers.
|
||||||
|
"""
|
||||||
out: List[RouteEntry] = []
|
out: List[RouteEntry] = []
|
||||||
for e in self.gaps(include_draft=include_draft):
|
for e in self.gaps(include_draft=include_draft):
|
||||||
reviewed = e.effective_delegation.reviewed or e.reviewed
|
d = e.effective_delegation
|
||||||
|
reviewed = d.reviewed or e.reviewed
|
||||||
if is_review_stale(reviewed, threshold_days=threshold_days, today=today):
|
if is_review_stale(reviewed, threshold_days=threshold_days, today=today):
|
||||||
out.append(e)
|
out.append(e)
|
||||||
|
elif d.verified is not None and not d.is_verified:
|
||||||
|
out.append(e)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def freshness(
|
def freshness(
|
||||||
|
|
@ -261,14 +285,16 @@ class Catalog:
|
||||||
f"{stale_count} catalog entr{'y' if stale_count == 1 else 'ies'} "
|
f"{stale_count} catalog entr{'y' if stale_count == 1 else 'ies'} "
|
||||||
f"past {stale_threshold_days}d review cadence"
|
f"past {stale_threshold_days}d review cadence"
|
||||||
)
|
)
|
||||||
|
# Interim blockers run on their own, much shorter cadence -- a stale
|
||||||
|
# pointer and an unanswered blocker are not the same kind of drift.
|
||||||
stale_interim = len(self.stale_gaps(
|
stale_interim = len(self.stale_gaps(
|
||||||
include_draft=True, threshold_days=stale_threshold_days, today=today
|
include_draft=True, threshold_days=DEFAULT_BLOCKER_STALE_DAYS, today=today
|
||||||
))
|
))
|
||||||
if stale_interim:
|
if stale_interim:
|
||||||
warnings.append(
|
warnings.append(
|
||||||
f"{stale_interim} interim delegation"
|
f"{stale_interim} interim delegation"
|
||||||
f"{'' if stale_interim == 1 else 's'} past "
|
f"{'' if stale_interim == 1 else 's'} need re-verifying "
|
||||||
f"{stale_threshold_days}d review — see `warden route gaps`"
|
f"({DEFAULT_BLOCKER_STALE_DAYS}d blocker cadence) — see `warden route gaps`"
|
||||||
)
|
)
|
||||||
|
|
||||||
return CatalogFreshness(
|
return CatalogFreshness(
|
||||||
|
|
@ -447,11 +473,19 @@ def _parse_delegation(entry_id: str, raw: Optional[dict]) -> Optional[Delegation
|
||||||
entry_id, "delegation.blocked_on", blocked_on, prose=True
|
entry_id, "delegation.blocked_on", blocked_on, prose=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
verified = str(raw.get("verified", "")).strip() or None
|
||||||
|
if verified is not None and verified not in Delegation.VERIFICATION_METHODS:
|
||||||
|
raise CatalogError(
|
||||||
|
f"entry {entry_id!r} delegation.verified {verified!r} invalid "
|
||||||
|
f"(expected one of {Delegation.VERIFICATION_METHODS})"
|
||||||
|
)
|
||||||
|
|
||||||
return Delegation(
|
return Delegation(
|
||||||
mode=mode,
|
mode=mode,
|
||||||
intended_owner=intended_owner,
|
intended_owner=intended_owner,
|
||||||
blocked_on=blocked_on,
|
blocked_on=blocked_on,
|
||||||
reviewed=reviewed,
|
reviewed=reviewed,
|
||||||
|
verified=verified,
|
||||||
implicit=False,
|
implicit=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,38 @@ class Delegation:
|
||||||
intended_owner: Optional[str] = None
|
intended_owner: Optional[str] = None
|
||||||
blocked_on: Optional[str] = None
|
blocked_on: Optional[str] = None
|
||||||
reviewed: Optional[str] = None
|
reviewed: Optional[str] = None
|
||||||
|
verified: Optional[str] = None
|
||||||
implicit: bool = False
|
implicit: bool = False
|
||||||
|
|
||||||
|
#: How `reviewed` was established. The distinction exists because a date
|
||||||
|
#: bumped by editing the entry looks identical to one bumped by re-checking
|
||||||
|
#: the blocker, and on 2026-08-21 six lanes read as freshly reviewed when
|
||||||
|
#: only some had actually been re-verified (WARDEN-WP-0033-T05).
|
||||||
|
#:
|
||||||
|
#: `asked-and-waiting` deliberately does NOT count as verification: it is the
|
||||||
|
#: state the secrets-engine lanes sat in for ten days while looking fresh.
|
||||||
|
VERIFICATION_METHODS = (
|
||||||
|
"owner-confirmed", # the intended owner stated the blocker's status
|
||||||
|
"source-read", # re-derived from the owner's code, canon, or CCR
|
||||||
|
"asked-and-waiting", # a question is outstanding — NOT verification
|
||||||
|
"unverified", # carried forward without a check
|
||||||
|
)
|
||||||
|
#: Methods that mean the claim was actually re-established.
|
||||||
|
VERIFYING_METHODS = ("owner-confirmed", "source-read")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_verified(self) -> bool:
|
||||||
|
"""True only when the blocker was re-established, not merely re-edited."""
|
||||||
|
return self.verified in self.VERIFYING_METHODS
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"mode": self.mode,
|
"mode": self.mode,
|
||||||
"intended_owner": self.intended_owner,
|
"intended_owner": self.intended_owner,
|
||||||
"blocked_on": self.blocked_on,
|
"blocked_on": self.blocked_on,
|
||||||
"reviewed": self.reviewed,
|
"reviewed": self.reviewed,
|
||||||
|
"verified": self.verified,
|
||||||
|
"is_verified": self.is_verified,
|
||||||
"implicit": self.implicit,
|
"implicit": self.implicit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -779,3 +779,78 @@ def test_low_risk_vocabulary_is_explicit():
|
||||||
entry = _bare_entry(risk=grade)
|
entry = _bare_entry(risk=grade)
|
||||||
assert entry.is_high_risk is False, grade
|
assert entry.is_high_risk is False, grade
|
||||||
assert entry.is_graded is True, grade
|
assert entry.is_graded is True, grade
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Blocker staleness cadence + verification (WARDEN-WP-0033-T05)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_blocker_cadence_is_separate_from_pointer_cadence():
|
||||||
|
"""Two claims with different half-lives must not share one threshold.
|
||||||
|
|
||||||
|
"Is this still the right owner and page?" is quarterly. "Has the owner
|
||||||
|
answered yet?" is not. Sharing 90 days made the second one inert -- the
|
||||||
|
register was six days old, so it could not have fired for months.
|
||||||
|
"""
|
||||||
|
from warden.routing.catalog import DEFAULT_BLOCKER_STALE_DAYS, DEFAULT_STALE_DAYS
|
||||||
|
|
||||||
|
assert DEFAULT_STALE_DAYS == 90
|
||||||
|
assert DEFAULT_BLOCKER_STALE_DAYS == 14
|
||||||
|
assert DEFAULT_BLOCKER_STALE_DAYS < DEFAULT_STALE_DAYS
|
||||||
|
|
||||||
|
|
||||||
|
def test_asked_and_waiting_is_not_verification():
|
||||||
|
"""The failure this whole change exists to catch.
|
||||||
|
|
||||||
|
A lane asked today reads as reviewed today. The secrets-engine blocker sat
|
||||||
|
in exactly that state for ten days while looking current.
|
||||||
|
"""
|
||||||
|
from warden.routing.models import Delegation
|
||||||
|
|
||||||
|
asked = Delegation(mode="interim", intended_owner="x", blocked_on="y",
|
||||||
|
reviewed="2026-08-21", verified="asked-and-waiting")
|
||||||
|
assert asked.is_verified is False
|
||||||
|
|
||||||
|
for method in ("owner-confirmed", "source-read"):
|
||||||
|
d = Delegation(mode="interim", intended_owner="x", blocked_on="y",
|
||||||
|
reviewed="2026-08-21", verified=method)
|
||||||
|
assert d.is_verified is True, method
|
||||||
|
|
||||||
|
|
||||||
|
def test_stale_gaps_flags_unverified_even_when_the_date_is_today():
|
||||||
|
catalog = load_catalog(_repo_catalog())
|
||||||
|
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):
|
||||||
|
entry = dict(ROUTED_ENTRY)
|
||||||
|
entry["delegation"] = {
|
||||||
|
"mode": "interim", "intended_owner": "secrets-engine",
|
||||||
|
"blocked_on": "pending", "reviewed": "2026-08-21", "verified": "probably-fine",
|
||||||
|
}
|
||||||
|
with pytest.raises(CatalogError, match="verified"):
|
||||||
|
load_catalog(_write_catalog(tmp_path, [SSH_ENTRY, entry]))
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_interim_lane_records_how_it_was_verified():
|
||||||
|
"""Structural, not time-based, so it never fails on a calendar day alone."""
|
||||||
|
catalog = load_catalog(_repo_catalog())
|
||||||
|
missing = [
|
||||||
|
e.id for e in catalog.gaps(include_draft=True)
|
||||||
|
if e.effective_delegation.verified is None
|
||||||
|
]
|
||||||
|
assert not missing, f"interim lanes with no `verified`: {missing}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_route_gaps_fail_on_stale_exits_3(repo_catalog_env):
|
||||||
|
result = runner.invoke(app, ["route", "gaps", "--fail-on-stale", "--json"])
|
||||||
|
assert result.exit_code == 3
|
||||||
|
rows = json.loads(result.stdout)
|
||||||
|
assert any(r["stale"] for r in rows)
|
||||||
|
# A lane can be stale on age or on never having been verified; both must be
|
||||||
|
# expressible, or asked-and-waiting silently passes the gate.
|
||||||
|
assert any(r["stale"] and r["days_since_review"] == 0 for r in rows)
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,56 @@ owner repo's shipped path.
|
||||||
| **On canon change** | When net-kingdom security docs change, review affected `canon_ref` entries immediately |
|
| **On canon change** | When net-kingdom security docs change, review affected `canon_ref` entries immediately |
|
||||||
| **On owner ship** | When an owning repo merges a new OpenBao path or playbook, promote `draft` → `active` and bump `reviewed` |
|
| **On owner ship** | When an owning repo merges a new OpenBao path or playbook, promote `draft` → `active` and bump `reviewed` |
|
||||||
| **On agent confusion** | If `warden route find` misses a common query, add `need_keywords` or a playbook — do not restate owner procedure in the catalog |
|
| **On agent confusion** | If `warden route find` misses a common query, add `need_keywords` or a playbook — do not restate owner procedure in the catalog |
|
||||||
| **On interim review** | Run `warden route gaps` — re-check each `blocked_on` against the intended owner; flip to `native` when their front door exists |
|
| **Fortnightly** (default 14 days) | Run `warden route gaps` — re-check each `blocked_on` against the intended owner; flip to `native` when their front door exists |
|
||||||
|
|
||||||
|
### Two cadences, because they are two different claims
|
||||||
|
|
||||||
|
A catalog pointer and an interim blocker both carry a `reviewed:` date, and for a
|
||||||
|
while they shared one 90-day threshold. They should not.
|
||||||
|
|
||||||
|
| Claim | Question | Default | Where |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Pointer freshness | Is this still the right owner and page? | **90 days** | `warden route list --stale` |
|
||||||
|
| Interim blocker | Has the intended owner answered / can they front this yet? | **14 days** | `warden route gaps` |
|
||||||
|
|
||||||
|
A pointer genuinely is a quarterly question. A blocker is not: it is a claim
|
||||||
|
about another repo's state at a date, and this estate invalidates those in days.
|
||||||
|
`RISK-F-0001` invalidated an ops-warden blocker in one. The `secrets-engine`
|
||||||
|
lanes carried one for ten while it was answerable from that repo's source the
|
||||||
|
whole time. A `FLEX-WP-0007` blocker was repeated by two repos for about seven
|
||||||
|
weeks after that workplan read `finished`.
|
||||||
|
|
||||||
|
Sharing 90 days did not make the blocker check loose, it made it **inert**: the
|
||||||
|
delegation register was created 2026-08-15, so the threshold could not have fired
|
||||||
|
before November and never had.
|
||||||
|
|
||||||
|
### Reviewed is not verified
|
||||||
|
|
||||||
|
The more important half. A `reviewed:` date records when someone last *touched*
|
||||||
|
the entry, which looks identical to re-checking it. So every interim lane also
|
||||||
|
carries `verified:`, saying how the claim was established:
|
||||||
|
|
||||||
|
| Value | Meaning | Counts as verification |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `owner-confirmed` | the intended owner stated the blocker's status | **yes** |
|
||||||
|
| `source-read` | re-derived from the owner's code, canon, or a CCR | **yes** |
|
||||||
|
| `asked-and-waiting` | a question is outstanding | **no** |
|
||||||
|
| `unverified` | carried forward without a check | **no** |
|
||||||
|
|
||||||
|
`asked-and-waiting` deliberately does not reset the clock. A lane asked today
|
||||||
|
reads as reviewed today, and that is precisely how a blocker stays fresh-looking
|
||||||
|
while nobody answers it. `warden route gaps` flags such a lane as stale at zero
|
||||||
|
days old.
|
||||||
|
|
||||||
|
**Re-check the blocker; do not bump the date.** Bumping `reviewed` without
|
||||||
|
re-establishing the claim is the failure this section exists to prevent, and it
|
||||||
|
is cheap to avoid — most of these are answerable by reading the owner's repo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
warden route gaps # 14-day blocker cadence, plus unverified lanes
|
||||||
|
warden route gaps --stale-days 30 # looser threshold
|
||||||
|
warden route gaps --fail-on-stale # exit 3 — for a cron job or a gate
|
||||||
|
```
|
||||||
|
|
||||||
### Stale check (operators and agents)
|
### Stale check (operators and agents)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,51 @@ avoid is the 2026-08-17 one recorded in `.claude/rules/finding-routing.md`:
|
||||||
ops-warden answered a question well and never routed it, and another repo ended
|
ops-warden answered a question well and never routed it, and another repo ended
|
||||||
up filing it.
|
up filing it.
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: WARDEN-WP-0033-T05
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
**Make blocker staleness a mechanism instead of an intention.**
|
||||||
|
|
||||||
|
Four stale blockers surfaced in twelve hours: an OpenBao token recorded expired
|
||||||
|
that was valid, a verification script recorded ready that had never been written,
|
||||||
|
a ten-day `secrets-engine` question answerable from their source, and
|
||||||
|
`FLEX-WP-0007` cited as blocking `policy.enabled` seven weeks after it read
|
||||||
|
`finished`. `.claude/rules/finding-routing.md` has said *re-read a blocker before
|
||||||
|
trusting it* since `RISK-F-0001`. Saying it has not worked.
|
||||||
|
|
||||||
|
`warden route gaps` had a `--stale-days` threshold defaulting to 90, which was
|
||||||
|
not loose but **inert** — the register was created 2026-08-15, so it could not
|
||||||
|
fire before November.
|
||||||
|
|
||||||
|
**Done 2026-08-21.** Two changes, and the second matters more:
|
||||||
|
|
||||||
|
1. **Split the cadences.** `DEFAULT_STALE_DAYS` (90) still governs pointer
|
||||||
|
freshness — "is this the right owner and page" genuinely is quarterly. New
|
||||||
|
`DEFAULT_BLOCKER_STALE_DAYS` (14) governs interim blockers, calibrated on the
|
||||||
|
cases that cost something: ten days for the secrets-engine lanes, one day for
|
||||||
|
`RISK-F-0001`, ~50 for `FLEX-WP-0007`. At ~15 lanes it surfaces about one a
|
||||||
|
day rather than a wall, and it fires on zero lanes on age today.
|
||||||
|
|
||||||
|
2. **`verified:` distinguishes re-checking from re-editing.** `reviewed` records
|
||||||
|
when someone touched the entry; nothing recorded whether the claim was
|
||||||
|
re-established. `owner-confirmed` and `source-read` count;
|
||||||
|
**`asked-and-waiting` and `unverified` do not**, and a lane in those states is
|
||||||
|
stale regardless of its date. `key-cape-oidc-login` is the proof: asked today,
|
||||||
|
zero days old, correctly flagged.
|
||||||
|
|
||||||
|
Current state: 14 non-draft interim lanes — 5 `owner-confirmed`, 1
|
||||||
|
`asked-and-waiting`, 8 `unverified`. The 8 are honest; they were carried forward
|
||||||
|
this session without a check and now say so instead of hiding behind a fresh date.
|
||||||
|
|
||||||
|
`--fail-on-stale` exits 3 so a cron job or gate can act. **Deliberately not a CI
|
||||||
|
test on age**: a date-triggered failure breaks the build on a calendar day with
|
||||||
|
no code change, punishing whoever commits next rather than whoever owns the
|
||||||
|
blocker. The CI test is structural instead — every interim lane must record
|
||||||
|
*how* it was verified, which fails on the commit that introduces the omission.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- `secrets-engine` `SECRETS-WP-0006` — catalog admission, decision `ae676382`
|
- `secrets-engine` `SECRETS-WP-0006` — catalog admission, decision `ae676382`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue