Close CCR drift and high-risk policy gaps
This commit is contained in:
parent
852a8ab661
commit
382f04412a
12 changed files with 577 additions and 68 deletions
|
|
@ -20,9 +20,10 @@
|
|||
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-apps-pg-resource-evidence.md |
|
||||
| workplan | RAILIANCE-WP-0016 | finished | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||
| workplan | RAILIANCE-WP-0017 | finished | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
||||
| workplan | RAILIANCE-WP-0022 | blocked | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| workplan | RPF-WP-0018 | finished | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| workplan | RPF-WP-0019 | finished | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| workplan | RPF-WP-0020 | proposed | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| workplan | RPF-WP-0020 | finished | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| workplan | RPF-WP-0021 | finished | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||
| task | RAILIANCE-WP-0005-T01 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
| task | RAILIANCE-WP-0005-T02 | done | — | workplans/RAILIANCE-WP-0005-credential-request-and-lease-broker.md |
|
||||
|
|
@ -85,6 +86,11 @@
|
|||
| task | RAILIANCE-WP-0016-T04 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||
| task | RAILIANCE-WP-0016-T05 | done | — | workplans/RAILIANCE-WP-0016-architecture-cleanup-backlog.md |
|
||||
| task | RAILIANCE-WP-0017-T01 | done | — | workplans/RAILIANCE-WP-0017-consumption-mode-enforcement.md |
|
||||
| task | RAILIANCE-WP-0022-T01 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| task | RAILIANCE-WP-0022-T02 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| task | RAILIANCE-WP-0022-T03 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| task | RAILIANCE-WP-0022-T04 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| task | RAILIANCE-WP-0022-T05 | wait | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
|
||||
| task | RPF-WP-0018-T01 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T02 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
| task | RPF-WP-0018-T03 | done | — | workplans/RPF-WP-0018-policy-surface-alignment.md |
|
||||
|
|
@ -96,10 +102,10 @@
|
|||
| task | RPF-WP-0019-T02 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0019-T03 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0019-T04 | done | — | workplans/RPF-WP-0019-apps-pg-recoverability-and-controls.md |
|
||||
| task | RPF-WP-0020-T01 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T02 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T03 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T04 | todo | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T01 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T02 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T03 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0020-T04 | done | — | workplans/RPF-WP-0020-ccr-schema-drift.md |
|
||||
| task | RPF-WP-0021-T01 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||
| task | RPF-WP-0021-T02 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||
| task | RPF-WP-0021-T03 | done | — | workplans/RPF-WP-0021-core-hub-platform-onboarding.md |
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ request_type: workload-kv-read
|
|||
title: email-connect transactional SMTP and ingest token lane
|
||||
status: active
|
||||
created: '2026-08-12'
|
||||
updated: '2026-08-12'
|
||||
updated: '2026-08-21'
|
||||
requester:
|
||||
agent: grok
|
||||
reason: >-
|
||||
|
|
@ -117,6 +117,15 @@ verification:
|
|||
railiance01, namespace-scoped to email-connect.
|
||||
- Secret value provisioned directly in OpenBao through approved operator custody.
|
||||
- Positive and negative verification recorded with non-secret audit ids or timestamps.
|
||||
evidence:
|
||||
- at: '2026-08-20T22:56:00+00:00'
|
||||
actor: codex
|
||||
kind: auth_path_reconciliation
|
||||
result: passed
|
||||
details:
|
||||
- Live ClusterSecretStore openbao-email-connect uses tokenSecretRef external-secrets/openbao-email-connect-eso-token,
|
||||
is namespace-limited to email-connect, and reports Valid.
|
||||
- ExternalSecret email-connect-runtime reports SecretSynced. No Secret value was read.
|
||||
lifecycle:
|
||||
deactivate: Disable ops-warden catalog entry and detach ESO role policy.
|
||||
rotate: >-
|
||||
|
|
|
|||
|
|
@ -3,9 +3,16 @@ kind: credential-change-request
|
|||
schema_version: 1
|
||||
request_type: workload-kv-read
|
||||
title: Scaleway bootstrap API key for reef-storage / WP-0002 bucket create
|
||||
status: apply_pending
|
||||
status: in_flight
|
||||
created: '2026-08-14'
|
||||
updated: '2026-08-14'
|
||||
in_flight:
|
||||
missing_fields:
|
||||
- openbao.policy_file
|
||||
- openbao.auth
|
||||
blocking_reason: Founder bootstrap API key and final operator authentication design
|
||||
are pending; do not invent metadata or fill placeholders.
|
||||
owner: platform-operator
|
||||
requester:
|
||||
agent: grok
|
||||
reason: >-
|
||||
|
|
@ -47,7 +54,7 @@ access_frontdoor:
|
|||
selector: scaleway bootstrap api
|
||||
command: bao kv put platform/workloads/railiance/scaleway/bootstrap
|
||||
resolvable: false
|
||||
readiness: waiting-on-ui-replace-of-xxx-placeholders
|
||||
readiness: approved-pending-apply
|
||||
delivery:
|
||||
surface: operator-workstation
|
||||
target: reef-storage/tools/create-platform-audit-bucket.sh (reads, never prints)
|
||||
|
|
@ -62,6 +69,11 @@ verification:
|
|||
- Field names present on the KV path; values not printed.
|
||||
negative:
|
||||
- default-policy token denied on the data path.
|
||||
activation_conditions:
|
||||
- Founder supplies the bootstrap credential through attended custody outside Git,
|
||||
chat, argv, and State Hub.
|
||||
- Platform operator records the exact policy artifact and authentication method
|
||||
before the request leaves in_flight status.
|
||||
lifecycle:
|
||||
deactivate: Delete bootstrap key at Scaleway after the scoped bucket key works.
|
||||
rotate: Put a new bootstrap key; do not reuse the scoped backup key.
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ Suggested states:
|
|||
|
||||
```text
|
||||
draft
|
||||
in_flight
|
||||
proposed
|
||||
needs_changes
|
||||
approved
|
||||
|
|
@ -89,6 +90,12 @@ superseded
|
|||
cancelled
|
||||
```
|
||||
|
||||
`in_flight` is the only state that may explicitly omit completion-only fields.
|
||||
It must declare every omission in `in_flight.missing_fields`, name the owner and
|
||||
blocking reason, and remain non-resolvable. The validator still checks every
|
||||
other field. This is not an applyable state and must never be used to hide a
|
||||
malformed active lane.
|
||||
|
||||
Only `approved` requests may be applied. Only `verified` requests may become
|
||||
`active`.
|
||||
|
||||
|
|
@ -141,6 +148,12 @@ Version 1 should be boring:
|
|||
- prompt or delegate separately for secret value entry;
|
||||
- record non-secret evidence in State Hub.
|
||||
|
||||
When the schema adds or strengthens a required field, the same change must
|
||||
include a migration pass over every existing CCR. Active declarations must
|
||||
describe the live authentication path; incomplete requests must move to the
|
||||
explicit `in_flight` state rather than relying on a filename exception or a
|
||||
loosened repository-wide assertion.
|
||||
|
||||
The first implemented CLI slice is:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
43
docs/evidence/agent-high-risk-boundary-2026-08-21.md
Normal file
43
docs/evidence/agent-high-risk-boundary-2026-08-21.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Agent high-risk OpenBao boundary — 2026-08-21
|
||||
|
||||
## Scope
|
||||
|
||||
This is capabilities and metadata evidence for `RAILIANCE-WP-0022`. No
|
||||
credential value was read, no token was minted, and no operator role was
|
||||
modified.
|
||||
|
||||
## Source and live coverage
|
||||
|
||||
- State Hub message `828e4903-30fe-4903-acfd-cd2ecdda437d` reported that the
|
||||
live `agent-high-risk-boundary` lacked the Core Hub path and that six other
|
||||
concrete high-risk catalog paths were absent from both source and live.
|
||||
- Source now denies KV-v2 data and permits metadata only for all concrete
|
||||
high-risk catalog paths. Pattern-only and non-KV lanes do not generate an
|
||||
invented address.
|
||||
- Under attended `platform-admin` OIDC, OpenBao accepted the updated policy.
|
||||
A normalized readback matched the source file.
|
||||
- The ops-warden audit used `policy_source: server` and reported 17 high-risk
|
||||
lanes: 12 covered catalog entries, zero uncovered, and five with no concrete
|
||||
KV address. The policy itself contains 12 unique deny paths because two
|
||||
catalog entries share the Binky IMAP path and Core Hub is an additional
|
||||
reviewed deny without a catalog lane.
|
||||
|
||||
## Attachment audit and residual blocker
|
||||
|
||||
A metadata-only scan listed and read role configuration under netkingdom OIDC,
|
||||
Kubernetes auth, AppRole, and token roles. It found:
|
||||
|
||||
- roles attaching `agent-high-risk-boundary`: **0**;
|
||||
- roles combining it with any `workload-kv-read-*` policy: **0**.
|
||||
|
||||
The live policy is therefore complete but is not automatically attached to a
|
||||
coding-agent identity. The documented manual short-lived token example is not
|
||||
a standing identity and carries no workload-read policy. Attaching the boundary
|
||||
to `platform-admin` would incorrectly constrain the attended operator role and
|
||||
erase the human/agent distinction, so that change was not made.
|
||||
|
||||
The remaining work is an identity-owner decision: define a distinct coding-
|
||||
agent issuance path, attach the boundary, and prove that deny wins when a
|
||||
workload read policy is also present. A versioned generated list of concrete
|
||||
high-risk deny paths is also requested from ops-warden so policy coverage does
|
||||
not depend on manual catalog transcription.
|
||||
|
|
@ -476,7 +476,8 @@ IONOS STARTTLS credentials and the shared user-engine ingest bearer for the
|
|||
| Policy file | `openbao/policies/workload-kv-read-email-connect-transactional.hcl` |
|
||||
| ESO policy | `external-secrets-email-connect` |
|
||||
| ESO policy file | `openbao/policies/external-secrets-email-connect.hcl` |
|
||||
| K8s auth role | `external-secrets-email-connect` (ESO delivery) |
|
||||
| Current ESO auth | policy-limited orphan token in Secret `external-secrets/openbao-email-connect-eso-token` |
|
||||
| K8s auth follow-up | role `external-secrets-email-connect` after the railiance01 auth mount is wired |
|
||||
| ClusterSecretStore | `openbao-email-connect` (namespace `email-connect` only) |
|
||||
| Primary consumer | ExternalSecret `email-connect/email-connect-runtime` → Secret `email-connect-runtime` |
|
||||
| Package manifests | `email-connect/deploy/k8s/railiance/` |
|
||||
|
|
|
|||
|
|
@ -28,6 +28,42 @@ path "platform/data/workloads/core-hub/runtime" {
|
|||
path "platform/metadata/workloads/core-hub/runtime" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/coulomb/whynot-design/npm-publish" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/coulomb/whynot-design/npm-publish" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/rapp-qonto/keycape-client" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/rapp-qonto/keycape-client" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/agent-harness/forgejo-deploy-key" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/agent-harness/forgejo-deploy-key" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/audit-core/senders" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/audit-core/senders" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/email-connect/transactional" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/email-connect/transactional" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "platform/data/workloads/railiance/scaleway/bootstrap" {
|
||||
capabilities = ["deny"]
|
||||
}
|
||||
path "platform/metadata/workloads/railiance/scaleway/bootstrap" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# --- tenant high-risk (WARDEN-WP-0028) ---
|
||||
path "tenants/data/binky/company-email/imap" {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ required_top_level:
|
|||
|
||||
allowed_statuses:
|
||||
- draft
|
||||
- in_flight
|
||||
- proposed
|
||||
- needs_changes
|
||||
- approved
|
||||
|
|
@ -68,10 +69,6 @@ workload_kv_read:
|
|||
- auth
|
||||
openbao.auth:
|
||||
- method
|
||||
- mount
|
||||
- role
|
||||
- bound_claims
|
||||
- bound_claims_confirmed
|
||||
- policies
|
||||
access_frontdoor:
|
||||
- type
|
||||
|
|
@ -87,8 +84,35 @@ workload_kv_read:
|
|||
- rotate
|
||||
- compromised
|
||||
conditional:
|
||||
status=in_flight:
|
||||
required:
|
||||
- in_flight.missing_fields
|
||||
- in_flight.blocking_reason
|
||||
- in_flight.owner
|
||||
allowed_missing_fields:
|
||||
- openbao.policy_file
|
||||
- openbao.auth
|
||||
openbao.auth.method=token:
|
||||
required:
|
||||
- openbao.eso_policy_name
|
||||
- openbao.eso_policy_file
|
||||
- openbao.auth.token_secret
|
||||
- openbao.auth.bootstrap_script
|
||||
- openbao.auth.ttl
|
||||
- openbao.auth.kubernetes_followup
|
||||
note: Transitional ESO token auth; delegated applier does not create the token.
|
||||
openbao.auth.method=kubernetes:
|
||||
required:
|
||||
- mount
|
||||
- role
|
||||
- bound_claims
|
||||
- bound_claims_confirmed
|
||||
openbao.auth.method=oidc:
|
||||
required:
|
||||
- mount
|
||||
- role
|
||||
- bound_claims
|
||||
- bound_claims_confirmed
|
||||
- allowed_redirect_uris
|
||||
allowed_redirect_uris: non-empty list of OpenBao callback URIs accepted by the role
|
||||
groups_claim: requires openbao.auth.oidc_scopes to include groups
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ REPO_DIR = Path(__file__).resolve().parents[1]
|
|||
DEFAULT_CCR_DIR = REPO_DIR / "credential-change-requests"
|
||||
ALLOWED_STATUSES = {
|
||||
"draft",
|
||||
"in_flight",
|
||||
"proposed",
|
||||
"needs_changes",
|
||||
"approved",
|
||||
|
|
@ -189,6 +190,28 @@ def reject_secret_text(text: str, field: str) -> None:
|
|||
|
||||
|
||||
def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings: list[str]) -> None:
|
||||
in_flight = ccr.get("status") == "in_flight"
|
||||
missing_fields: set[str] = set()
|
||||
if in_flight:
|
||||
declaration = require_object(ccr.get("in_flight"), "in_flight", errors)
|
||||
listed_missing = require_list(
|
||||
declaration.get("missing_fields"), "in_flight.missing_fields", errors
|
||||
)
|
||||
missing_fields = {str(field) for field in listed_missing}
|
||||
if not missing_fields:
|
||||
errors.append("in_flight.missing_fields must not be empty")
|
||||
allowed_missing = {"openbao.policy_file", "openbao.auth"}
|
||||
unsupported_missing = missing_fields - allowed_missing
|
||||
if unsupported_missing:
|
||||
errors.append(
|
||||
"in_flight.missing_fields contains unsupported fields: "
|
||||
+ ", ".join(sorted(unsupported_missing))
|
||||
)
|
||||
require_string(
|
||||
declaration.get("blocking_reason"), "in_flight.blocking_reason", errors
|
||||
)
|
||||
require_string(declaration.get("owner"), "in_flight.owner", errors)
|
||||
|
||||
target = require_object(ccr.get("target"), "target", errors)
|
||||
for field in ("domain", "tenant", "workload", "environment", "purpose"):
|
||||
require_string(target.get(field), f"target.{field}", errors)
|
||||
|
|
@ -203,9 +226,16 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
|||
policy_name = require_string(
|
||||
openbao.get("policy_name"), "openbao.policy_name", errors
|
||||
)
|
||||
policy_file = require_string(
|
||||
openbao.get("policy_file"), "openbao.policy_file", errors
|
||||
)
|
||||
policy_file = ""
|
||||
if "openbao.policy_file" in missing_fields:
|
||||
if openbao.get("policy_file") is not None:
|
||||
errors.append(
|
||||
"openbao.policy_file is declared missing but is present"
|
||||
)
|
||||
else:
|
||||
policy_file = require_string(
|
||||
openbao.get("policy_file"), "openbao.policy_file", errors
|
||||
)
|
||||
fields = [str(field) for field in require_list(openbao.get("fields"), "openbao.fields", errors)]
|
||||
if not fields:
|
||||
errors.append("openbao.fields must contain at least one field")
|
||||
|
|
@ -220,12 +250,20 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
|||
if not resolved_policy.exists():
|
||||
errors.append(f"openbao.policy_file does not exist: {policy_file}")
|
||||
|
||||
auth = require_object(openbao.get("auth"), "openbao.auth", errors)
|
||||
method = require_string(auth.get("method"), "openbao.auth.method", errors)
|
||||
if method not in {"oidc", "kubernetes"}:
|
||||
errors.append("openbao.auth.method must be oidc or kubernetes")
|
||||
require_string(auth.get("mount"), "openbao.auth.mount", errors)
|
||||
require_string(auth.get("role"), "openbao.auth.role", errors)
|
||||
auth: dict[str, Any] = {}
|
||||
if "openbao.auth" in missing_fields:
|
||||
if openbao.get("auth") is not None:
|
||||
errors.append("openbao.auth is declared missing but is present")
|
||||
else:
|
||||
auth = require_object(openbao.get("auth"), "openbao.auth", errors)
|
||||
method = ""
|
||||
if auth:
|
||||
method = require_string(auth.get("method"), "openbao.auth.method", errors)
|
||||
if method and method not in {"oidc", "kubernetes", "token"}:
|
||||
errors.append("openbao.auth.method must be oidc, kubernetes, or token")
|
||||
if method in {"oidc", "kubernetes"}:
|
||||
require_string(auth.get("mount"), "openbao.auth.mount", errors)
|
||||
require_string(auth.get("role"), "openbao.auth.role", errors)
|
||||
if method == "oidc":
|
||||
redirect_uris = require_list(
|
||||
auth.get("allowed_redirect_uris"),
|
||||
|
|
@ -252,22 +290,95 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
|
|||
errors.append(
|
||||
f"openbao.auth.oidc_scopes[{index}] must be a non-empty string"
|
||||
)
|
||||
policies = [str(policy) for policy in require_list(auth.get("policies"), "openbao.auth.policies", errors)]
|
||||
if policies != [policy_name]:
|
||||
errors.append("openbao.auth.policies must contain exactly openbao.policy_name")
|
||||
for policy in policies:
|
||||
if policy in DISALLOWED_POLICY_NAMES:
|
||||
errors.append(f"openbao.auth.policies contains disallowed policy {policy}")
|
||||
ttl = auth.get("ttl")
|
||||
if ttl is not None and (not isinstance(ttl, str) or not TTL_RE.match(ttl)):
|
||||
errors.append("openbao.auth.ttl must match <positive integer><s|m|h|d>")
|
||||
bound_claims = require_object(
|
||||
auth.get("bound_claims"), "openbao.auth.bound_claims", errors
|
||||
)
|
||||
if not bound_claims:
|
||||
errors.append("openbao.auth.bound_claims must not be empty")
|
||||
if auth.get("bound_claims_confirmed") is not True:
|
||||
warnings.append("OIDC/Kubernetes bound claim is not confirmed; apply is blocked")
|
||||
policies: list[str] = []
|
||||
if auth:
|
||||
policies = [
|
||||
str(policy)
|
||||
for policy in require_list(
|
||||
auth.get("policies"), "openbao.auth.policies", errors
|
||||
)
|
||||
]
|
||||
expected_policy = policy_name
|
||||
if method == "token":
|
||||
expected_policy = require_string(
|
||||
openbao.get("eso_policy_name"), "openbao.eso_policy_name", errors
|
||||
)
|
||||
eso_policy_file = require_string(
|
||||
openbao.get("eso_policy_file"), "openbao.eso_policy_file", errors
|
||||
)
|
||||
if eso_policy_file and not resolve_repo_path(eso_policy_file).exists():
|
||||
errors.append(
|
||||
f"openbao.eso_policy_file does not exist: {eso_policy_file}"
|
||||
)
|
||||
if policies != [expected_policy]:
|
||||
errors.append(
|
||||
"openbao.auth.policies must contain exactly the policy used by the auth method"
|
||||
)
|
||||
for policy in policies:
|
||||
if policy in DISALLOWED_POLICY_NAMES:
|
||||
errors.append(
|
||||
f"openbao.auth.policies contains disallowed policy {policy}"
|
||||
)
|
||||
ttl = auth.get("ttl")
|
||||
if ttl is not None and (
|
||||
not isinstance(ttl, str) or not TTL_RE.match(ttl)
|
||||
):
|
||||
errors.append("openbao.auth.ttl must match <positive integer><s|m|h|d>")
|
||||
if method in {"oidc", "kubernetes"}:
|
||||
bound_claims = require_object(
|
||||
auth.get("bound_claims"), "openbao.auth.bound_claims", errors
|
||||
)
|
||||
if not bound_claims:
|
||||
errors.append("openbao.auth.bound_claims must not be empty")
|
||||
if auth.get("bound_claims_confirmed") is not True:
|
||||
warnings.append(
|
||||
"OIDC/Kubernetes bound claim is not confirmed; apply is blocked"
|
||||
)
|
||||
elif method == "token":
|
||||
token_secret = require_string(
|
||||
auth.get("token_secret"), "openbao.auth.token_secret", errors
|
||||
)
|
||||
if token_secret and not re.match(
|
||||
r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?/[a-z0-9]([-a-z0-9]*[a-z0-9])?$",
|
||||
token_secret,
|
||||
):
|
||||
errors.append("openbao.auth.token_secret must be namespace/name")
|
||||
require_string(
|
||||
auth.get("bootstrap_script"), "openbao.auth.bootstrap_script", errors
|
||||
)
|
||||
if auth.get("ttl") is None:
|
||||
errors.append("openbao.auth.ttl is required for token auth")
|
||||
followup = require_object(
|
||||
auth.get("kubernetes_followup"),
|
||||
"openbao.auth.kubernetes_followup",
|
||||
errors,
|
||||
)
|
||||
if followup.get("method") != "kubernetes":
|
||||
errors.append(
|
||||
"openbao.auth.kubernetes_followup.method must be kubernetes"
|
||||
)
|
||||
require_string(
|
||||
followup.get("mount"),
|
||||
"openbao.auth.kubernetes_followup.mount",
|
||||
errors,
|
||||
)
|
||||
require_string(
|
||||
followup.get("role"),
|
||||
"openbao.auth.kubernetes_followup.role",
|
||||
errors,
|
||||
)
|
||||
followup_claims = require_object(
|
||||
followup.get("bound_claims"),
|
||||
"openbao.auth.kubernetes_followup.bound_claims",
|
||||
errors,
|
||||
)
|
||||
if not followup_claims:
|
||||
errors.append(
|
||||
"openbao.auth.kubernetes_followup.bound_claims must not be empty"
|
||||
)
|
||||
warnings.append(
|
||||
"token auth is transitional; complete the declared Kubernetes-auth follow-up"
|
||||
)
|
||||
|
||||
frontdoor = require_object(ccr.get("access_frontdoor"), "access_frontdoor", errors)
|
||||
require_string(frontdoor.get("type"), "access_frontdoor.type", errors)
|
||||
|
|
@ -342,14 +453,11 @@ def validate_ccr(path: Path) -> tuple[dict[str, Any], list[str], list[str]]:
|
|||
|
||||
def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
auth = openbao.get("auth") or {}
|
||||
frontdoor = ccr["access_frontdoor"]
|
||||
risk = ccr["risk"]
|
||||
verification = ccr["verification"]
|
||||
fields = ", ".join(openbao["fields"])
|
||||
claim_bits = ", ".join(
|
||||
f"{key}={value}" for key, value in auth.get("bound_claims", {}).items()
|
||||
)
|
||||
lines = [
|
||||
f"Request: {ccr['title']}",
|
||||
f"CCR: {ccr['id']} ({ccr['status']})",
|
||||
|
|
@ -361,13 +469,37 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
|
|||
"Policy:",
|
||||
f" {openbao['policy_name']}",
|
||||
"Auth binding:",
|
||||
f" {auth['mount']} {auth['method']} role {auth['role']}",
|
||||
f" bound claims: {claim_bits}",
|
||||
f" confirmed: {auth.get('bound_claims_confirmed') is True}",
|
||||
"Access front door:",
|
||||
f" {frontdoor['type']} {frontdoor['catalog_id']}",
|
||||
f" readiness: {frontdoor.get('readiness')} resolvable={frontdoor.get('resolvable') is True}",
|
||||
]
|
||||
if auth.get("method") == "token":
|
||||
lines.extend(
|
||||
[
|
||||
f" transitional token via Secret {auth['token_secret']}",
|
||||
f" policy: {', '.join(auth.get('policies', []))}",
|
||||
f" ttl: {auth.get('ttl')}",
|
||||
]
|
||||
)
|
||||
elif auth:
|
||||
claim_bits = ", ".join(
|
||||
f"{key}={value}"
|
||||
for key, value in auth.get("bound_claims", {}).items()
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
f" {auth['mount']} {auth['method']} role {auth['role']}",
|
||||
f" bound claims: {claim_bits}",
|
||||
f" confirmed: {auth.get('bound_claims_confirmed') is True}",
|
||||
]
|
||||
)
|
||||
else:
|
||||
missing = ", ".join(ccr.get("in_flight", {}).get("missing_fields", []))
|
||||
lines.append(f" in flight; declared missing: {missing}")
|
||||
lines.extend(
|
||||
[
|
||||
"Access front door:",
|
||||
f" {frontdoor['type']} {frontdoor['catalog_id']}",
|
||||
f" readiness: {frontdoor.get('readiness')} resolvable={frontdoor.get('resolvable') is True}",
|
||||
]
|
||||
)
|
||||
if frontdoor.get("command"):
|
||||
lines.append(f" command: {frontdoor['command']}")
|
||||
lines.append(f"Risk: {risk['classification']}")
|
||||
|
|
@ -1408,8 +1540,15 @@ def apply_blockers(ccr: dict[str, Any]) -> list[str]:
|
|||
return blockers
|
||||
if status not in APPLY_ALLOWED_STATUSES:
|
||||
blockers.append(f"apply requires status approved, got {status}")
|
||||
if ccr["openbao"]["auth"].get("bound_claims_confirmed") is not True:
|
||||
auth = ccr["openbao"].get("auth") or {}
|
||||
if auth.get("method") in {"oidc", "kubernetes"} and auth.get(
|
||||
"bound_claims_confirmed"
|
||||
) is not True:
|
||||
blockers.append("apply requires confirmed OpenBao auth binding")
|
||||
if auth.get("method") == "token":
|
||||
blockers.append(
|
||||
"delegated apply does not create transitional token-auth bootstrap identities"
|
||||
)
|
||||
return blockers
|
||||
|
||||
|
||||
|
|
@ -1432,7 +1571,7 @@ def status_payload(ccr: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
|
|||
frontdoor_blocked_by = frontdoor_blockers(ccr)
|
||||
frontdoor = ccr["access_frontdoor"]
|
||||
openbao = ccr["openbao"]
|
||||
auth = openbao["auth"]
|
||||
auth = openbao.get("auth") or {}
|
||||
return {
|
||||
"id": ccr["id"],
|
||||
"title": ccr["title"],
|
||||
|
|
@ -1449,9 +1588,10 @@ def status_payload(ccr: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
|
|||
"kv_path": openbao["kv_path"],
|
||||
"fields": openbao["fields"],
|
||||
"policy_name": openbao["policy_name"],
|
||||
"auth_mount": auth["mount"],
|
||||
"auth_method": auth["method"],
|
||||
"auth_role": auth["role"],
|
||||
"auth_mount": auth.get("mount"),
|
||||
"auth_method": auth.get("method"),
|
||||
"auth_role": auth.get("role"),
|
||||
"token_secret": auth.get("token_secret"),
|
||||
"bound_claims_confirmed": auth.get("bound_claims_confirmed") is True,
|
||||
},
|
||||
"access_frontdoor": {
|
||||
|
|
|
|||
|
|
@ -76,6 +76,55 @@ class CredentialChangeTests(unittest.TestCase):
|
|||
self.assertEqual(errors, [])
|
||||
self.assertEqual(ccr["target"]["rapp"], "rapp-qonto")
|
||||
|
||||
def test_email_connect_declares_live_transitional_token_auth(self) -> None:
|
||||
path = (
|
||||
REPO_DIR
|
||||
/ "credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml"
|
||||
)
|
||||
ccr, errors, warnings = credential_change.validate_ccr(path)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(ccr["openbao"]["auth"]["method"], "token")
|
||||
self.assertEqual(
|
||||
ccr["openbao"]["auth"]["token_secret"],
|
||||
"external-secrets/openbao-email-connect-eso-token",
|
||||
)
|
||||
self.assertTrue(any("token auth is transitional" in item for item in warnings))
|
||||
rendered = credential_change.render_summary(ccr, warnings)
|
||||
self.assertIn("transitional token via Secret", rendered)
|
||||
|
||||
def test_in_flight_ccr_declares_each_completion_only_omission(self) -> None:
|
||||
path = (
|
||||
REPO_DIR
|
||||
/ "credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml"
|
||||
)
|
||||
ccr, errors, warnings = credential_change.validate_ccr(path)
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(warnings, [])
|
||||
self.assertEqual(ccr["status"], "in_flight")
|
||||
self.assertEqual(
|
||||
set(ccr["in_flight"]["missing_fields"]),
|
||||
{"openbao.policy_file", "openbao.auth"},
|
||||
)
|
||||
payload = credential_change.status_payload(ccr, warnings)
|
||||
self.assertFalse(payload["apply_allowed"])
|
||||
self.assertFalse(payload["frontdoor_resolvable"])
|
||||
self.assertIn("got in_flight", " ".join(payload["apply_blockers"]))
|
||||
rendered = credential_change.render_summary(ccr, warnings)
|
||||
self.assertIn("in flight; declared missing", rendered)
|
||||
|
||||
def test_in_flight_ccr_cannot_omit_an_undeclared_field(self) -> None:
|
||||
source = (
|
||||
REPO_DIR
|
||||
/ "credential-change-requests/CCR-2026-0011-scaleway-object-storage-bootstrap.yaml"
|
||||
)
|
||||
path = self.unapproved_ccr(source)
|
||||
data = credential_change.load_yaml(path)
|
||||
data["status"] = "in_flight"
|
||||
data["in_flight"]["missing_fields"] = ["openbao.auth"]
|
||||
credential_change.dump_yaml(path, data)
|
||||
_ccr, errors, _warnings = credential_change.validate_ccr(path)
|
||||
self.assertTrue(any("openbao.policy_file" in error for error in errors))
|
||||
|
||||
def test_core_hub_runtime_lane_is_split_and_agent_denied(self) -> None:
|
||||
path = (
|
||||
REPO_DIR
|
||||
|
|
@ -105,6 +154,30 @@ class CredentialChangeTests(unittest.TestCase):
|
|||
self.assertEqual(store["spec"]["provider"]["vault"]["path"], "platform")
|
||||
self.assertEqual(store["spec"]["conditions"][0]["namespaces"], ["core-hub"])
|
||||
|
||||
def test_agent_boundary_denies_every_current_concrete_high_risk_catalog_path(self) -> None:
|
||||
boundary = (
|
||||
REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
|
||||
).read_text()
|
||||
data_paths = {
|
||||
"platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets",
|
||||
"platform/data/workloads/railiance/backup/offsite-lane",
|
||||
"platform/data/workloads/forgejo/forgejo-admin",
|
||||
"tenants/data/binky/company-email/imap",
|
||||
"tenants/data/binky/qonto-api",
|
||||
"platform/data/workloads/coulomb/whynot-design/npm-publish",
|
||||
"platform/data/workloads/rapp-qonto/keycape-client",
|
||||
"platform/data/workloads/agent-harness/forgejo-deploy-key",
|
||||
"platform/data/workloads/audit-core/senders",
|
||||
"platform/data/workloads/email-connect/transactional",
|
||||
"platform/data/workloads/railiance/scaleway/bootstrap",
|
||||
}
|
||||
for path in data_paths:
|
||||
with self.subTest(path=path):
|
||||
self.assertRegex(
|
||||
boundary,
|
||||
rf'path "{path}" \{{\s*capabilities = \["deny"\]',
|
||||
)
|
||||
|
||||
database_policy = (
|
||||
REPO_DIR / "openbao/policies/external-secrets-core-hub-database.hcl"
|
||||
).read_text()
|
||||
|
|
|
|||
124
workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md
Normal file
124
workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
---
|
||||
id: RAILIANCE-WP-0022
|
||||
type: workplan
|
||||
title: "Close agent high-risk OpenBao boundary coverage"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: blocked
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-21"
|
||||
updated: "2026-08-21"
|
||||
related:
|
||||
- WARDEN-WP-0032
|
||||
- RISK-F-0009
|
||||
origin: routed
|
||||
origin_ref: "State Hub message 828e4903-30fe-4903-acfd-cd2ecdda437d"
|
||||
---
|
||||
|
||||
# RAILIANCE-WP-0022 — Agent high-risk boundary coverage
|
||||
|
||||
## Goal
|
||||
|
||||
Make the OpenBao `agent-high-risk-boundary` deny every concrete high-risk KV
|
||||
data path in the ops-warden routing catalog, verify the deployed policy, and
|
||||
establish whether any agent identity actually carries the boundary.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Read policy documents, token-role metadata and capabilities only; never read
|
||||
a Secret value.
|
||||
- A deny is added only for a concrete catalog path graded `risk: high`.
|
||||
- Pattern-only and non-KV lanes are reported but do not produce invented paths.
|
||||
- Operator identities do not receive this boundary; it is for coding-agent
|
||||
identities where deny must override any coincident workload read policy.
|
||||
|
||||
## T01 — Reconcile catalog coverage
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0022-T01
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Run the capabilities-only ops-warden audit against the policy. The 2026-08-21
|
||||
reconciliation found 17 high-risk lanes: six covered, six concrete uncovered,
|
||||
and five without a concrete KV address. No credential value was read.
|
||||
|
||||
## T02 — Close the concrete deny gaps
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0022-T02
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Add deny-data/read-metadata pairs for the six catalog paths: whynot-design npm,
|
||||
rapp-qonto Keycape client, agent-harness Forgejo deploy key, audit-core senders,
|
||||
email-connect transactional, and Scaleway bootstrap. Add regression coverage
|
||||
for every concrete path currently emitted by the catalog audit.
|
||||
|
||||
Completed 2026-08-21. The source policy covers all 12 unique concrete paths
|
||||
(including the Core Hub path, which has no catalog lane), and the local
|
||||
catalog audit reports all 12 catalog entries covered with none uncovered.
|
||||
|
||||
## T03 — Apply and verify live
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0022-T03
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
Under attended platform authority, upload the reviewed policy, read it back,
|
||||
and rerun the catalog audit with `policy_source: server`. Inspect policy/token
|
||||
role metadata to determine whether an agent identity carries the boundary and
|
||||
whether any role combines it with a workload-read policy. Do not mint a token.
|
||||
|
||||
Completed 2026-08-21 under attended `platform-admin` OIDC. OpenBao accepted the
|
||||
policy; normalized readback matched source, and the server-backed catalog audit
|
||||
reported 17 high-risk lanes, 12 covered entries, zero uncovered, and five
|
||||
pattern/non-KV lanes without a concrete address. Metadata-only inspection of
|
||||
all discoverable netkingdom, Kubernetes, AppRole, and token roles found zero
|
||||
attachments of `agent-high-risk-boundary` and therefore zero roles combining
|
||||
it with a workload-read policy. No token was minted and no Secret was read.
|
||||
|
||||
## T04 — Route the result
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0022-T04
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Reply to ops-warden with the deployment evidence and remaining attachment
|
||||
finding. Request a generated, versioned concrete-deny artifact so future policy
|
||||
updates consume catalog output rather than relying on a hand-maintained list.
|
||||
|
||||
Completed 2026-08-21 via State Hub message
|
||||
`fe727451-163a-4e14-8ce3-187fea8ce5b3`, including live audit counts, the zero-
|
||||
attachment finding, the distinct-agent-identity blocker, and the requested
|
||||
versioned generated artifact shape.
|
||||
|
||||
## T05 — Establish a distinct coding-agent identity
|
||||
|
||||
```task
|
||||
id: RAILIANCE-WP-0022-T05
|
||||
status: wait
|
||||
priority: high
|
||||
```
|
||||
|
||||
The policy is live but no role attaches it. Do not add the boundary to
|
||||
`platform-admin`: that is an attended human/operator role whose legitimate
|
||||
recovery work may require the protected values. The identity owner must define
|
||||
a distinct coding-agent issuance path, attach this boundary there, and prove
|
||||
deny-wins behavior when combined with an otherwise readable workload policy.
|
||||
This is blocked on an identity-owner decision and is not invented here.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- [x] Every concrete high-risk catalog path is denied in the source policy.
|
||||
- [x] The live policy matches source and the server-backed audit passes.
|
||||
- [x] Agent boundary attachment is established from metadata (currently zero).
|
||||
- [x] Result and generated-artifact follow-up are routed to ops-warden.
|
||||
- [ ] A distinct coding-agent identity actually attaches the boundary.
|
||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
|||
title: "Close CCR schema drift: one active lane unmigrated, one draft the suite cannot express"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-18"
|
||||
updated: "2026-08-18"
|
||||
updated: "2026-08-21"
|
||||
related:
|
||||
- RPF-WP-0014
|
||||
origin: residual
|
||||
|
|
@ -38,22 +38,22 @@ one makes the suite green first.
|
|||
|
||||
## The two problems
|
||||
|
||||
**P1 — an active credential lane is unmigrated.**
|
||||
**P1 — an active credential lane uses an unrepresented auth mode.**
|
||||
`credential-change-requests/CCR-2026-0010-email-connect-transactional.yaml`
|
||||
carries `status: active` and `readiness: ready`, and is missing the entire
|
||||
`openbao.auth` block: `method`, `mount`, `role`, `policies`, `bound_claims`.
|
||||
The validator gained those requirements and this CCR was never brought
|
||||
forward.
|
||||
carries `status: active` and `readiness: ready`. It has always declared the
|
||||
live transitional ESO token Secret, bootstrap script, policy and TTL, but the
|
||||
validator understood only OIDC and Kubernetes auth and consequently reported
|
||||
the role/bound-claim fields for those modes as missing.
|
||||
|
||||
This is the one that matters. A live lane whose declaration does not describe
|
||||
how the workload authenticates is a governance gap, not a lint failure — the
|
||||
document that is supposed to be the authority on the lane cannot answer the
|
||||
first question anyone would ask of it. The lane itself is presumably working,
|
||||
which is exactly what makes it easy to leave.
|
||||
first question anyone would ask of it. The live lane is working, which is
|
||||
exactly what made this representation gap easy to leave.
|
||||
|
||||
**P2 — a genuine in-flight draft the suite cannot express.**
|
||||
**P2 — a genuine in-flight request the suite could not express.**
|
||||
`CCR-2026-0011-scaleway-object-storage-bootstrap.yaml` carries
|
||||
`status: apply_pending` and `readiness: waiting-on-ui-replace-of-xxx-placeholders`.
|
||||
`status: apply_pending` and an out-of-enum placeholder readiness string.
|
||||
It is a founder-bootstrap credential still holding placeholder values, and
|
||||
`ops-warden` already tracks it as a draft lane. Its errors include a
|
||||
`readiness` value outside the permitted enum, which is the file honestly
|
||||
|
|
@ -76,7 +76,7 @@ draft and a real gap produce identical output.
|
|||
|
||||
```task
|
||||
id: RPF-WP-0020-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3bf8bf9c-ac33-4ce7-8b3f-5b40135b227c"
|
||||
```
|
||||
|
|
@ -88,9 +88,18 @@ policy file, not by inventing plausible values. If the live configuration and
|
|||
the declaration disagree, the live configuration is the fact and the
|
||||
disagreement is the finding.
|
||||
|
||||
Completed 2026-08-21. Live read-only Kubernetes metadata showed
|
||||
`ClusterSecretStore/openbao-email-connect` using
|
||||
`external-secrets/openbao-email-connect-eso-token`, limited to namespace
|
||||
`email-connect`, and reporting Valid; its ExternalSecret reported
|
||||
SecretSynced. The schema now represents this transitional token mode directly,
|
||||
requires the ESO policy artifact, Secret reference, bootstrap script, bounded
|
||||
TTL and explicit Kubernetes-auth follow-up, and keeps delegated token creation
|
||||
out of scope. No Secret value was read.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0020-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "12933d69-82a3-470d-b01c-0c40c28a7984"
|
||||
```
|
||||
|
|
@ -100,9 +109,16 @@ a `status` the validator recognises as not-yet-complete, with the test
|
|||
asserting that such files are still well-formed in every other respect. An
|
||||
allowlist of filenames would work today and rot on the next draft.
|
||||
|
||||
Completed 2026-08-21. Added the non-applyable `in_flight` status. It requires a
|
||||
named owner, blocking reason and exact `missing_fields`; only
|
||||
`openbao.policy_file` and `openbao.auth` may be declared incomplete, while all
|
||||
other CCR structure remains validated. CCR-2026-0011 now uses this state and a
|
||||
valid non-resolvable front-door readiness without inventing policy/auth
|
||||
metadata or filling credential placeholders.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0020-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "da52b518-c6fb-40b8-acb9-c64724ca4eee"
|
||||
```
|
||||
|
|
@ -111,9 +127,15 @@ requirement was added without migrating existing active CCRs, other repos
|
|||
carrying CCRs may have the same gap and no failing test to reveal it. Confirm
|
||||
whether the requirement originated here or upstream, and notify accordingly.
|
||||
|
||||
Completed 2026-08-21. Git history traces the validator requirement to local
|
||||
commit `815b124`; a filesystem-wide declaration search found CCR files and the
|
||||
validator/schema implementation only in `railiance-platform`. There is no
|
||||
upstream CCR implementation to migrate or notify. The migration obligation is
|
||||
now documented locally.
|
||||
|
||||
```task
|
||||
id: RPF-WP-0020-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "2259ee69-4914-42c4-9175-8c61b2206888"
|
||||
```
|
||||
|
|
@ -122,6 +144,12 @@ suite passes. Record in `docs/credential-change-approval.md` that a new
|
|||
required field obliges a migration pass over existing active CCRs — the
|
||||
omission that produced P1.
|
||||
|
||||
Completed 2026-08-21. Regression coverage proves the live token-auth shape,
|
||||
explicit in-flight omissions, rejection of undeclared omissions, and safe
|
||||
status/summary rendering. The approval guide now requires a migration pass over
|
||||
all existing CCRs whenever required fields are added or strengthened. The full
|
||||
repository test suite passes.
|
||||
|
||||
## Risks
|
||||
|
||||
**T01 invents values to make the test pass.** The likeliest failure and the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue