Add credential lane readiness proposals

This commit is contained in:
2026-06-27 23:30:29 +02:00
parent 815b124ab1
commit aee0dcefad
13 changed files with 425 additions and 25 deletions

View File

@@ -200,6 +200,12 @@ credential-change-render: ## Render a credential change request review summary
credential-change-plan: ## Render a credential change request apply plan for review credential-change-plan: ## Render a credential change request apply plan for review
scripts/credential-change.py plan $(CREDENTIAL_CHANGE) scripts/credential-change.py plan $(CREDENTIAL_CHANGE)
credential-change-status: ## Render credential change request readiness status
scripts/credential-change.py status $(CREDENTIAL_CHANGE)
credential-change-status-json: ## Render credential change request readiness status as JSON
scripts/credential-change.py status --json $(CREDENTIAL_CHANGE)
credential-change-apply-plan: ## Render approved-only operator apply plan credential-change-apply-plan: ## Render approved-only operator apply plan
scripts/credential-change.py apply-plan $(CREDENTIAL_CHANGE) scripts/credential-change.py apply-plan $(CREDENTIAL_CHANGE)

View File

@@ -8,7 +8,7 @@ created: "2026-06-27"
updated: "2026-06-27" updated: "2026-06-27"
requester: requester:
agent: ops-warden agent: ops-warden
message_id: "551031d1-335e-4db8-9535-820fea52d0a3" message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
reason: "Allow ops-warden to proxy caller-scoped access to whynot-design's npm publish token." reason: "Allow ops-warden to proxy caller-scoped access to whynot-design's npm publish token."
review: review:
required: true required: true
@@ -43,13 +43,17 @@ openbao:
ttl: 15m ttl: 15m
access_frontdoor: access_frontdoor:
type: ops-warden type: ops-warden
catalog_id: whynot-design-npm-token catalog_id: whynot-design-npm-publish
selector: "npm auth token" selector: "npm publish token"
command: "warden access whynot-design-npm-publish --exec -- npm publish"
resolvable: false
readiness: template
activation: "draft-until-ccr-verified" activation: "draft-until-ccr-verified"
risk: risk:
classification: high classification: high
notes: notes:
- "Grants read access to the credential used to publish npm packages." - "Grants read access to the credential used to publish npm packages."
- "Uses a publish-specific catalog id; a future read-only npm token must use a separate catalog id."
- "The proposed OIDC bound claim must be confirmed before apply." - "The proposed OIDC bound claim must be confirmed before apply."
- "ops-warden must proxy the read as the caller and must not retain the token value." - "ops-warden must proxy the read as the caller and must not retain the token value."
verification: verification:
@@ -70,3 +74,4 @@ state_hub:
workplan_id: RAILIANCE-WP-0007 workplan_id: RAILIANCE-WP-0007
related_workplan_id: RAILIANCE-WP-0006 related_workplan_id: RAILIANCE-WP-0006
ops_warden_reply_message_id: "b175c561-7858-43f5-a309-949b0dede1b4" ops_warden_reply_message_id: "b175c561-7858-43f5-a309-949b0dede1b4"
ops_warden_batch_message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"

View File

@@ -0,0 +1,80 @@
id: CCR-2026-0002
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: "issue-core runtime ingestion key lane"
status: proposed
created: "2026-06-27"
updated: "2026-06-27"
requester:
agent: ops-warden
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
reason: "Confirm and provision the issue-core workload KV lane requested in the ops-warden batch."
review:
required: true
required_approvers:
- platform-operator
- issue-core-owner
comments: []
target:
domain: financials
tenant: issue-core
workload: issue-core
environment: production
purpose: "issue-core runtime ingestion through OpenBao workload KV and External Secrets"
openbao:
mount: platform
kv_path: platform/workloads/issue-core/issue-core/issue-core-runtime
fields:
- ISSUE_CORE_API_KEY
- GITEA_BACKEND_TOKEN
policy_name: workload-kv-read-issue-core-runtime
policy_file: openbao/policies/workload-kv-read-issue-core-runtime.hcl
auth:
method: kubernetes
mount: kubernetes
role: issue-core-runtime-workload-kv-read
bound_claims:
service_account_names:
- issue-core
service_account_namespaces:
- issue-core
bound_claims_confirmed: false
policies:
- workload-kv-read-issue-core-runtime
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: issue-core-ingestion-api-key
selector: "issue-core ingestion API key"
command: "warden access issue-core-ingestion-api-key --fetch ISSUE_CORE_API_KEY"
resolvable: false
readiness: template
activation: "draft-until-ccr-verified"
delivery:
surface: external-secrets
target: "issue-core namespace"
risk:
classification: high
notes:
- "Grants read access to issue-core runtime ingestion credentials."
- "GITEA_BACKEND_TOKEN is included because ops-warden asked to confirm whether it is used; remove it before approval if issue-core does not require it."
- "The Kubernetes service account and namespace binding must be confirmed before apply."
- "ops-warden must proxy reads as the caller and must not retain token values."
verification:
positive:
- "Approved issue-core service account can read the configured fields through OpenBao or External Secrets without printing values."
negative:
- "A service account outside the approved issue-core binding cannot read the path."
activation_conditions:
- "Policy applied with platform-admin/operator authority."
- "Kubernetes auth role bound to the confirmed issue-core service account and namespace."
- "Secret values provisioned directly in OpenBao through approved operator custody."
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
lifecycle:
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
rotate: "Replace issue-core runtime secret values directly in OpenBao and record non-secret rotation evidence."
compromised: "Immediately deactivate access front door, rotate affected values, record blast-radius notes, and open incident follow-up tasks."
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"

View File

@@ -0,0 +1,78 @@
id: CCR-2026-0003
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: "llm-connect OpenRouter provider key lane"
status: proposed
created: "2026-06-27"
updated: "2026-06-27"
requester:
agent: ops-warden
message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"
reason: "Confirm and provision the llm-connect OpenRouter workload KV lane requested in the ops-warden batch."
review:
required: true
required_approvers:
- platform-operator
- activity-core-owner
comments: []
target:
domain: financials
tenant: activity-core
workload: llm-connect
environment: production
purpose: "llm-connect provider access through OpenBao workload KV and External Secrets"
openbao:
mount: platform
kv_path: platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets
fields:
- OPENROUTER_API_KEY
policy_name: workload-kv-read-llm-connect-provider-secrets
policy_file: openbao/policies/workload-kv-read-llm-connect-provider-secrets.hcl
auth:
method: kubernetes
mount: kubernetes
role: llm-connect-provider-secrets-read
bound_claims:
service_account_names:
- llm-connect
service_account_namespaces:
- activity-core
bound_claims_confirmed: false
policies:
- workload-kv-read-llm-connect-provider-secrets
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: llm-connect-openrouter-api-key
selector: "llm-connect OpenRouter API key"
command: "warden access llm-connect-openrouter-api-key --fetch OPENROUTER_API_KEY"
resolvable: false
readiness: template
activation: "draft-until-ccr-verified"
delivery:
surface: external-secrets
target: "Secret llm-connect-provider-secrets in the activity-core namespace"
risk:
classification: high
notes:
- "Grants read access to the provider key used by llm-connect for OpenRouter requests."
- "The Kubernetes service account and namespace binding must be confirmed before apply."
- "ops-warden must proxy reads as the caller and must not retain token values."
verification:
positive:
- "Approved llm-connect service account can read field OPENROUTER_API_KEY through OpenBao or External Secrets without printing the value."
negative:
- "A service account outside the approved activity-core/llm-connect binding cannot read the path."
activation_conditions:
- "Policy applied with platform-admin/operator authority."
- "Kubernetes auth role bound to the confirmed llm-connect service account and namespace."
- "Secret value provisioned directly in OpenBao through approved operator custody."
- "Positive and negative verification recorded with non-secret audit ids or timestamps."
lifecycle:
deactivate: "Disable ops-warden catalog entry and remove or detach auth role policy."
rotate: "Replace OPENROUTER_API_KEY directly in OpenBao and record non-secret rotation evidence."
compromised: "Immediately deactivate access front door, rotate the provider key, record blast-radius notes, and open incident follow-up tasks."
state_hub:
workplan_id: RAILIANCE-WP-0007
ops_warden_batch_message_id: "fe5b1696-8956-4bd5-9d6f-dbde1901a076"

View File

@@ -50,6 +50,8 @@ The CCR must be non-secret. It may contain:
- proposed auth bindings and bound claims; - proposed auth bindings and bound claims;
- delivery surface such as ops-warden, External Secrets, CSI, or direct caller - delivery surface such as ops-warden, External Secrets, CSI, or direct caller
fetch; fetch;
- machine-readable front-door readiness, including `readiness` and
`resolvable`;
- risk classification and approval requirements; - risk classification and approval requirements;
- generated apply plan; - generated apply plan;
- verification plan; - verification plan;
@@ -109,7 +111,9 @@ Auth binding:
netkingdom OIDC role whynot-design-workload-kv-read netkingdom OIDC role whynot-design-workload-kv-read
bound claim: groups includes whynot-design bound claim: groups includes whynot-design
Access front door: Access front door:
ops-warden whynot-design-npm-token ops-warden whynot-design-npm-publish
readiness: template
resolvable: false
Risk: Risk:
grants read access to npm publish credential grants read access to npm publish credential
Checks: Checks:
@@ -143,6 +147,8 @@ The first implemented CLI slice is:
make credential-change-validate make credential-change-validate
make credential-change-render CREDENTIAL_CHANGE=CCR-2026-0001 make credential-change-render CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-plan CREDENTIAL_CHANGE=CCR-2026-0001 make credential-change-plan CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-status CREDENTIAL_CHANGE=CCR-2026-0001
make credential-change-status-json CREDENTIAL_CHANGE=CCR-2026-0001
scripts/credential-change.py approve CCR-2026-0001 --reviewer <name> --comment "..." scripts/credential-change.py approve CCR-2026-0001 --reviewer <name> --comment "..."
scripts/credential-change.py deny CCR-2026-0001 --reviewer <name> --comment "..." scripts/credential-change.py deny CCR-2026-0001 --reviewer <name> --comment "..."
scripts/credential-change.py needs-changes CCR-2026-0001 --reviewer <name> --comment "..." scripts/credential-change.py needs-changes CCR-2026-0001 --reviewer <name> --comment "..."
@@ -193,7 +199,8 @@ For draft requests, ops-warden may create a draft catalog entry that points to
the CCR, but it should not activate the entry until the CCR is verified. the CCR, but it should not activate the entry until the CCR is verified.
For `warden access --fetch` / `--exec`, the catalog should include the CCR id For `warden access --fetch` / `--exec`, the catalog should include the CCR id
and refuse active use when the CCR state is not `active`. and refuse active use when the CCR state is not `active`, `readiness` is not
`ready`, or `resolvable` is not `true`.
## Interactive Runbook Role ## Interactive Runbook Role

View File

@@ -16,15 +16,19 @@ The first lane is for ops-warden `warden access --fetch` / `--exec`.
## whynot-design npm Publish Token ## whynot-design npm Publish Token
Ops-warden request: Ops-warden original request:
`551031d1-335e-4db8-9535-820fea52d0a3` `551031d1-335e-4db8-9535-820fea52d0a3`
Ops-warden batch follow-up:
`fe5b1696-8956-4bd5-9d6f-dbde1901a076`
| Item | Value | | Item | Value |
| --- | --- | | --- | --- |
| ops-warden catalog id | `whynot-design-npm-token` | | ops-warden catalog id | `whynot-design-npm-publish` |
| KV mount | `platform` | | KV mount | `platform` |
| OpenBao CLI path | `platform/workloads/whynot-design/whynot-design/npm-publish` | | OpenBao CLI path | `platform/workloads/whynot-design/whynot-design/npm-publish` |
| Secret field | `NPM_AUTH_TOKEN` | | Secret field | `NPM_AUTH_TOKEN` |
| Front-door readiness | `template`, `resolvable=false` until CCR verification |
| Read policy | `workload-kv-read-whynot-design-npm-publish` | | Read policy | `workload-kv-read-whynot-design-npm-publish` |
| Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` | | Policy file | `openbao/policies/workload-kv-read-whynot-design-npm-publish.hcl` |
| OIDC auth mount | `netkingdom` | | OIDC auth mount | `netkingdom` |
@@ -38,12 +42,18 @@ Expected caller login shape:
bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read bao login -method=oidc -path=netkingdom role=whynot-design-workload-kv-read
``` ```
Expected fetch shape: Expected OpenBao fetch shape:
```bash ```bash
bao kv get -field=NPM_AUTH_TOKEN platform/workloads/whynot-design/whynot-design/npm-publish bao kv get -field=NPM_AUTH_TOKEN platform/workloads/whynot-design/whynot-design/npm-publish
``` ```
Expected ops-warden exec shape after activation:
```bash
warden access whynot-design-npm-publish --exec -- npm publish
```
The fetch command returns the secret value to the authenticated caller. Run it The fetch command returns the secret value to the authenticated caller. Run it
only in an attended shell or through a process that consumes the value without only in an attended shell or through a process that consumes the value without
logging it. logging it.
@@ -139,7 +149,7 @@ Negative verification:
Send ops-warden only these pointers: Send ops-warden only these pointers:
```text ```text
catalog id: whynot-design-npm-token catalog id: whynot-design-npm-publish
mount: platform mount: platform
path: platform/workloads/whynot-design/whynot-design/npm-publish path: platform/workloads/whynot-design/whynot-design/npm-publish
field: NPM_AUTH_TOKEN field: NPM_AUTH_TOKEN
@@ -151,4 +161,5 @@ runbook: docs/workload-kv-access-lanes.md
``` ```
Until live provisioning and verification are complete, ops-warden should keep Until live provisioning and verification are complete, ops-warden should keep
the catalog entry in `draft` or equivalent non-active state. the catalog entry in `template`/`draft` or equivalent non-active state with
`resolvable=false`.

View File

@@ -0,0 +1,7 @@
path "platform/data/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["read"]
}
path "platform/metadata/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["read"]
}

View File

@@ -0,0 +1,7 @@
path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["read"]
}
path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["read"]
}

View File

@@ -71,6 +71,8 @@ workload_kv_read:
access_frontdoor: access_frontdoor:
- type - type
- catalog_id - catalog_id
- readiness
- resolvable
verification: verification:
- positive - positive
- negative - negative
@@ -80,6 +82,20 @@ workload_kv_read:
- rotate - rotate
- compromised - compromised
access_frontdoor_readiness:
allowed:
- template
- pending-review
- approved-pending-apply
- applied-pending-verify
- ready
- disabled
- compromised
resolvable_true_requires_status: active
ops_warden_should_consume_only:
readiness: ready
resolvable: true
guardrails: guardrails:
apply_plan_requires_status: apply_plan_requires_status:
- approved - approved

View File

@@ -46,6 +46,15 @@ SECRET_MARKERS = [
"sk-", "sk-",
] ]
DISALLOWED_POLICY_NAMES = {"root", "platform-admin"} DISALLOWED_POLICY_NAMES = {"root", "platform-admin"}
FRONTDOOR_READINESS = {
"template",
"pending-review",
"approved-pending-apply",
"applied-pending-verify",
"ready",
"disabled",
"compromised",
}
SAFE_ID_RE = re.compile(r"^[A-Z0-9][A-Z0-9_.-]*$") SAFE_ID_RE = re.compile(r"^[A-Z0-9][A-Z0-9_.-]*$")
TTL_RE = re.compile(r"^[1-9][0-9]*[smhd]$") TTL_RE = re.compile(r"^[1-9][0-9]*[smhd]$")
@@ -177,6 +186,19 @@ def validate_workload_kv_read(ccr: dict[str, Any], errors: list[str], warnings:
frontdoor = require_object(ccr.get("access_frontdoor"), "access_frontdoor", errors) frontdoor = require_object(ccr.get("access_frontdoor"), "access_frontdoor", errors)
require_string(frontdoor.get("type"), "access_frontdoor.type", errors) require_string(frontdoor.get("type"), "access_frontdoor.type", errors)
require_string(frontdoor.get("catalog_id"), "access_frontdoor.catalog_id", errors) require_string(frontdoor.get("catalog_id"), "access_frontdoor.catalog_id", errors)
readiness = require_string(frontdoor.get("readiness"), "access_frontdoor.readiness", errors)
if readiness and readiness not in FRONTDOOR_READINESS:
errors.append(
f"access_frontdoor.readiness must be one of {sorted(FRONTDOOR_READINESS)}"
)
resolvable = frontdoor.get("resolvable")
if not isinstance(resolvable, bool):
errors.append("access_frontdoor.resolvable must be a boolean")
if resolvable is True and ccr.get("status") != "active":
errors.append("access_frontdoor.resolvable=true requires status active")
command = frontdoor.get("command")
if command is not None and not isinstance(command, str):
errors.append("access_frontdoor.command must be a string when present")
risk = require_object(ccr.get("risk"), "risk", errors) risk = require_object(ccr.get("risk"), "risk", errors)
require_string(risk.get("classification"), "risk.classification", errors) require_string(risk.get("classification"), "risk.classification", errors)
@@ -258,8 +280,11 @@ def render_summary(ccr: dict[str, Any], warnings: list[str]) -> str:
f" confirmed: {auth.get('bound_claims_confirmed') is True}", f" confirmed: {auth.get('bound_claims_confirmed') is True}",
"Access front door:", "Access front door:",
f" {frontdoor['type']} {frontdoor['catalog_id']}", f" {frontdoor['type']} {frontdoor['catalog_id']}",
f"Risk: {risk['classification']}", 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']}")
for note in risk.get("notes", []): for note in risk.get("notes", []):
lines.append(f" - {note}") lines.append(f" - {note}")
lines.append("Checks:") lines.append("Checks:")
@@ -298,8 +323,19 @@ def generated_policy_hcl(ccr: dict[str, Any]) -> str:
def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]: def auth_payload(ccr: dict[str, Any]) -> dict[str, Any]:
auth = ccr["openbao"]["auth"] auth = ccr["openbao"]["auth"]
if auth["method"] == "kubernetes":
claims = auth["bound_claims"]
return {
"bound_service_account_names": claims.get("service_account_names", []),
"bound_service_account_namespaces": claims.get(
"service_account_namespaces", []
),
"policies": ",".join(auth["policies"]),
"ttl": auth.get("ttl", "15m"),
}
payload: dict[str, Any] = { payload: dict[str, Any] = {
"role_type": "oidc" if auth["method"] == "oidc" else "jwt", "role_type": "oidc",
"user_claim": auth.get("user_claim", "sub"), "user_claim": auth.get("user_claim", "sub"),
"policies": ",".join(auth["policies"]), "policies": ",".join(auth["policies"]),
"ttl": auth.get("ttl", "15m"), "ttl": auth.get("ttl", "15m"),
@@ -347,6 +383,91 @@ def validate_or_exit(path: Path) -> tuple[dict[str, Any], list[str]]:
return ccr, warnings return ccr, warnings
def apply_blockers(ccr: dict[str, Any]) -> list[str]:
blockers: list[str] = []
if ccr.get("status") not in APPLY_ALLOWED_STATUSES:
blockers.append(f"apply requires status approved, got {ccr.get('status')}")
if ccr["openbao"]["auth"].get("bound_claims_confirmed") is not True:
blockers.append("apply requires confirmed OpenBao auth binding")
return blockers
def frontdoor_blockers(ccr: dict[str, Any]) -> list[str]:
frontdoor = ccr["access_frontdoor"]
blockers: list[str] = []
if ccr.get("status") != "active":
blockers.append(f"front door requires CCR status active, got {ccr.get('status')}")
if frontdoor.get("readiness") != "ready":
blockers.append(
f"front door readiness must be ready, got {frontdoor.get('readiness')}"
)
if frontdoor.get("resolvable") is not True:
blockers.append("front door is marked resolvable=false")
return blockers
def status_payload(ccr: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
apply_blocked_by = apply_blockers(ccr)
frontdoor_blocked_by = frontdoor_blockers(ccr)
frontdoor = ccr["access_frontdoor"]
openbao = ccr["openbao"]
auth = openbao["auth"]
return {
"id": ccr["id"],
"title": ccr["title"],
"status": ccr["status"],
"request_type": ccr["request_type"],
"apply_allowed": not apply_blocked_by,
"apply_blockers": apply_blocked_by,
"frontdoor_resolvable": not frontdoor_blocked_by,
"frontdoor_blockers": frontdoor_blocked_by,
"warnings": warnings,
"openbao": {
"mount": openbao["mount"],
"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"],
"bound_claims_confirmed": auth.get("bound_claims_confirmed") is True,
},
"access_frontdoor": {
"type": frontdoor["type"],
"catalog_id": frontdoor["catalog_id"],
"readiness": frontdoor.get("readiness"),
"resolvable": frontdoor.get("resolvable") is True,
"command": frontdoor.get("command"),
},
}
def render_status(payload: dict[str, Any]) -> str:
lines = [
f"CCR: {payload['id']} ({payload['status']})",
f"Catalog: {payload['access_frontdoor']['catalog_id']}",
f"Readiness: {payload['access_frontdoor']['readiness']}",
f"Resolvable: {payload['frontdoor_resolvable']}",
f"Apply allowed: {payload['apply_allowed']}",
]
command = payload["access_frontdoor"].get("command")
if command:
lines.append(f"Command: {command}")
if payload["apply_blockers"]:
lines.append("Apply blockers:")
for blocker in payload["apply_blockers"]:
lines.append(f" - {blocker}")
if payload["frontdoor_blockers"]:
lines.append("Front-door blockers:")
for blocker in payload["frontdoor_blockers"]:
lines.append(f" - {blocker}")
if payload["warnings"]:
lines.append("Warnings:")
for warning in payload["warnings"]:
lines.append(f" - {warning}")
return "\n".join(lines)
def append_decision(path: Path, status: str, reviewer: str, comment: str) -> None: def append_decision(path: Path, status: str, reviewer: str, comment: str) -> None:
ccr, _warnings = validate_or_exit(path) ccr, _warnings = validate_or_exit(path)
review = ccr.setdefault("review", {}) review = ccr.setdefault("review", {})
@@ -399,6 +520,21 @@ def command_plan(args: argparse.Namespace) -> int:
return 0 return 0
def command_status(args: argparse.Namespace) -> int:
path = resolve_ccr(args.ref)
ccr, errors, warnings = validate_ccr(path)
if errors:
for error in errors:
print(f"[FAIL] {path.name}: {error}", file=sys.stderr)
return 1
payload = status_payload(ccr, warnings)
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(render_status(payload))
return 0
def command_apply_plan(args: argparse.Namespace) -> int: def command_apply_plan(args: argparse.Namespace) -> int:
path = resolve_ccr(args.ref) path = resolve_ccr(args.ref)
ccr, _warnings = validate_or_exit(path) ccr, _warnings = validate_or_exit(path)
@@ -436,6 +572,11 @@ def build_parser() -> argparse.ArgumentParser:
plan.add_argument("ref") plan.add_argument("ref")
plan.set_defaults(func=command_plan) plan.set_defaults(func=command_plan)
status = sub.add_parser("status", help="Render machine-readable readiness status")
status.add_argument("ref")
status.add_argument("--json", action="store_true")
status.set_defaults(func=command_status)
apply_plan = sub.add_parser( apply_plan = sub.add_parser(
"apply-plan", help="Render an operator apply plan only for approved CCRs" "apply-plan", help="Render an operator apply plan only for approved CCRs"
) )

View File

@@ -22,7 +22,11 @@ class CredentialChangeTests(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.sample = ( self.sample = (
REPO_DIR REPO_DIR
/ "credential-change-requests/CCR-2026-0001-whynot-design-npm-token.yaml" / "credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml"
)
self.issue_core = (
REPO_DIR
/ "credential-change-requests/CCR-2026-0002-issue-core-ingestion-api-key.yaml"
) )
def test_sample_ccr_validates_with_bound_claim_warning(self) -> None: def test_sample_ccr_validates_with_bound_claim_warning(self) -> None:
@@ -30,13 +34,38 @@ class CredentialChangeTests(unittest.TestCase):
self.assertEqual(errors, []) self.assertEqual(errors, [])
self.assertIn("bound claim is not confirmed", warnings[0]) self.assertIn("bound claim is not confirmed", warnings[0])
def test_all_repo_ccrs_validate(self) -> None:
for path in sorted((REPO_DIR / "credential-change-requests").glob("*.yaml")):
with self.subTest(path=path.name):
_ccr, errors, _warnings = credential_change.validate_ccr(path)
self.assertEqual(errors, [])
def test_render_summary_contains_review_fields(self) -> None: def test_render_summary_contains_review_fields(self) -> None:
ccr, _errors, warnings = credential_change.validate_ccr(self.sample) ccr, _errors, warnings = credential_change.validate_ccr(self.sample)
rendered = credential_change.render_summary(ccr, warnings) rendered = credential_change.render_summary(ccr, warnings)
self.assertIn("whynot-design npm publish token lane", rendered) self.assertIn("whynot-design npm publish token lane", rendered)
self.assertIn("platform/workloads/whynot-design/whynot-design/npm-publish", rendered) self.assertIn("platform/workloads/whynot-design/whynot-design/npm-publish", rendered)
self.assertIn("whynot-design-npm-publish", rendered)
self.assertIn("readiness: template resolvable=False", rendered)
self.assertIn("approve | deny | needs_changes", rendered) self.assertIn("approve | deny | needs_changes", rendered)
def test_status_payload_marks_template_not_resolvable(self) -> None:
ccr, _errors, warnings = credential_change.validate_ccr(self.sample)
payload = credential_change.status_payload(ccr, warnings)
self.assertFalse(payload["apply_allowed"])
self.assertFalse(payload["frontdoor_resolvable"])
self.assertEqual(payload["access_frontdoor"]["readiness"], "template")
self.assertEqual(payload["access_frontdoor"]["catalog_id"], "whynot-design-npm-publish")
self.assertIn("front door is marked resolvable=false", payload["frontdoor_blockers"])
def test_kubernetes_auth_payload_uses_service_account_bounds(self) -> None:
ccr, errors, _warnings = credential_change.validate_ccr(self.issue_core)
self.assertEqual(errors, [])
payload = credential_change.auth_payload(ccr)
self.assertEqual(payload["bound_service_account_names"], ["issue-core"])
self.assertEqual(payload["bound_service_account_namespaces"], ["issue-core"])
self.assertNotIn("bound_claims", payload)
def test_apply_plan_refuses_unapproved_ccr(self) -> None: def test_apply_plan_refuses_unapproved_ccr(self) -> None:
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
credential_change.command_apply_plan(type("Args", (), {"ref": str(self.sample)})()) credential_change.command_apply_plan(type("Args", (), {"ref": str(self.sample)})())

View File

@@ -30,7 +30,7 @@ holding secret values itself.
The immediate request is for `whynot-design` to retrieve its npm publish token. The immediate request is for `whynot-design` to retrieve its npm publish token.
The path must be concrete, policy-scoped, and documented so the ops-warden The path must be concrete, policy-scoped, and documented so the ops-warden
catalog can replace the current unresolved template path with a live catalog can replace the current unresolved template path with a live
`whynot-design-npm-token` entry. `whynot-design-npm-publish` entry.
No task in this workplan may paste, commit, log, or send secret values through No task in this workplan may paste, commit, log, or send secret values through
Git, State Hub, chat, prompts, or workplan text. Git, State Hub, chat, prompts, or workplan text.
@@ -47,7 +47,7 @@ Ops-warden message `551031d1-335e-4db8-9535-820fea52d0a3` asks
- the flex-auth policy reference, if pre-approval is required. - the flex-auth policy reference, if pre-approval is required.
Once these pointers are live, ops-warden will add a dedicated Once these pointers are live, ops-warden will add a dedicated
`whynot-design-npm-token` access catalog entry and a playbook, then notify `whynot-design-npm-publish` access catalog entry and a playbook, then notify
whynot-design. whynot-design.
## Proposed Contract ## Proposed Contract
@@ -248,7 +248,7 @@ Acceptance:
- The State Hub reply to ops-warden includes only path, field, KV mount, - The State Hub reply to ops-warden includes only path, field, KV mount,
OIDC role, policy name/path, optional flex-auth ref, and runbook location. OIDC role, policy name/path, optional flex-auth ref, and runbook location.
- Ops-warden confirms the `whynot-design-npm-token` catalog entry no longer - Ops-warden confirms the `whynot-design-npm-publish` catalog entry no longer
contains unresolved placeholders. contains unresolved placeholders.
- `warden access "npm auth token" --fetch` or the agreed exact selector resolves - `warden access "npm auth token" --fetch` or the agreed exact selector resolves
to the whynot-design lane and proxies the read as the caller. to the whynot-design lane and proxies the read as the caller.
@@ -281,10 +281,11 @@ Acceptance:
- If batching is deferred, notify ops-warden that this workplan will deliver - If batching is deferred, notify ops-warden that this workplan will deliver
whynot-design first and leave the sibling entries for separate planning. whynot-design first and leave the sibling entries for separate planning.
**2026-06-27:** Deferred sibling lanes (`issue-core-ingestion-api-key` and **2026-06-27:** Initially deferred sibling lanes (`issue-core-ingestion-api-key`
`openrouter-llm-connect`) so the whynot-design npm token request can be serviced and `openrouter-llm-connect`) so the whynot-design npm token request could be
first. They should get concrete tasks or a follow-up workplan after this access serviced first. The later ops-warden batch follow-up is now represented as
lane pattern is validated. proposed CCRs in `RAILIANCE-WP-0007`, still unapproved and unresolvable until
human review and verification.
## Exit Criteria ## Exit Criteria
@@ -294,5 +295,5 @@ lane pattern is validated.
ops-warden without ops-warden storing the value. ops-warden without ops-warden storing the value.
- Unauthorized reads are denied. - Unauthorized reads are denied.
- ops-warden has enough non-secret pointers to activate - ops-warden has enough non-secret pointers to activate
`whynot-design-npm-token`. `whynot-design-npm-publish`.
- No secret values appear in Git, State Hub, chat, prompts, logs, or workplans. - No secret values appear in Git, State Hub, chat, prompts, logs, or workplans.

View File

@@ -119,7 +119,7 @@ Acceptance:
**2026-06-27:** Added `schemas/credential-change-request.schema.yaml`, the **2026-06-27:** Added `schemas/credential-change-request.schema.yaml`, the
`credential-change-requests/` storage directory, and `credential-change-requests/` storage directory, and
`credential-change-requests/CCR-2026-0001-whynot-design-npm-token.yaml` as the `credential-change-requests/CCR-2026-0001-whynot-design-npm-publish.yaml` as the
first non-secret CCR fixture. The whynot CCR is intentionally `proposed` and first non-secret CCR fixture. The whynot CCR is intentionally `proposed` and
marks the bound claim as unconfirmed, so apply is blocked until review. marks the bound claim as unconfirmed, so apply is blocked until review.
@@ -148,7 +148,9 @@ Acceptance:
plus Make targets `credential-change-validate` and `credential-change-render`. plus Make targets `credential-change-validate` and `credential-change-render`.
Validation rejects secret-looking markers and broad/unsafe request shapes; render Validation rejects secret-looking markers and broad/unsafe request shapes; render
produces the chat/State Hub review summary and highlights unconfirmed bound produces the chat/State Hub review summary and highlights unconfirmed bound
claims. Unit coverage lives in `tests/test_credential_change.py`. claims. CCRs now also carry machine-readable front-door readiness fields:
`access_frontdoor.readiness` and `access_frontdoor.resolvable`. Unit coverage
lives in `tests/test_credential_change.py`.
## T04 - Generate OpenBao apply plans from approved CCRs ## T04 - Generate OpenBao apply plans from approved CCRs
@@ -201,8 +203,10 @@ Acceptance:
**2026-06-27:** Added file-backed `approve`, `deny`, and `needs-changes` **2026-06-27:** Added file-backed `approve`, `deny`, and `needs-changes`
commands that require reviewer and comment text and append non-secret review commands that require reviewer and comment text and append non-secret review
comments to the CCR. Remaining T05 work is State Hub decision-event emission and comments to the CCR. Added `status` plus Make targets
tighter chat integration. `credential-change-status` and `credential-change-status-json` so ops-warden can
consume `readiness`/`resolvable` without scraping prose. Remaining T05 work is
State Hub decision-event emission and tighter chat integration.
## T06 - Build an interactive runbook for apply and verify ## T06 - Build an interactive runbook for apply and verify
@@ -248,6 +252,14 @@ Acceptance:
can be rendered for review. It remains proposed/unapproved with unconfirmed can be rendered for review. It remains proposed/unapproved with unconfirmed
bound claims, so live apply and ops-warden activation are correctly blocked. bound claims, so live apply and ops-warden activation are correctly blocked.
**2026-06-27:** Converted the ops-warden batch follow-up
`fe5b1696-8956-4bd5-9d6f-dbde1901a076` into three proposed CCRs:
`CCR-2026-0001` for `whynot-design-npm-publish`, `CCR-2026-0002` for
`issue-core-ingestion-api-key`, and `CCR-2026-0003` for
`llm-connect-openrouter-api-key`. All three are explicitly `readiness: template`
and `resolvable: false` until owner confirmation, approval, OpenBao apply,
secret provisioning, and verification are complete.
## T08 - Add deactivation, rotation, and compromise flows ## T08 - Add deactivation, rotation, and compromise flows
```task ```task