Finish coding-agent high-risk boundary coverage
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02669-87ee-7a31-b111-edc95a16e0fa
This commit is contained in:
codex 2026-08-22 10:03:54 +02:00
parent 7a1dcb8a52
commit 429cc912ed
12 changed files with 720 additions and 54 deletions

View file

@ -340,6 +340,9 @@ credential-change-lifecycle-plan: ## Render deactivation/rotation/compromise lif
credential-change-lifecycle-event: ## Record lifecycle event; pass CREDENTIAL_CHANGE_LIFECYCLE_ARGS
scripts/credential-change.py lifecycle-event $(CREDENTIAL_CHANGE) --action $(CREDENTIAL_CHANGE_LIFECYCLE_ACTION) $(CREDENTIAL_CHANGE_LIFECYCLE_ARGS)
agent-high-risk-boundary-check: ## Check policy against the versioned ops-warden high-risk path input
python3 scripts/agent_high_risk_boundary.py --json
credential-change-import-inventory: ## Import existing lane as non-secret CCR; pass CREDENTIAL_CHANGE_IMPORT_ARGS
scripts/credential-change.py import-inventory $(CREDENTIAL_CHANGE_IMPORT_ARGS)
@ -464,4 +467,4 @@ help: ## Show this help
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
.PHONY: apps-pg-isolation-probe
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory agent-high-risk-boundary-check openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help

View file

@ -20,7 +20,7 @@
| 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 | RAILIANCE-WP-0022 | finished | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
| workplan | RAILIANCE-WP-0023 | finished | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.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 |
@ -91,7 +91,8 @@
| 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 | RAILIANCE-WP-0022-T05 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
| task | RAILIANCE-WP-0022-T06 | done | — | workplans/RAILIANCE-WP-0022-agent-high-risk-boundary-coverage.md |
| task | RAILIANCE-WP-0023-T01 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
| task | RAILIANCE-WP-0023-T02 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |
| task | RAILIANCE-WP-0023-T03 | done | — | workplans/RAILIANCE-WP-0023-hub-core-candidate-credential-lanes.md |

View file

@ -0,0 +1,117 @@
# Coding-agent OpenBao identity
`RAILIANCE-WP-0022` requires an OpenBao identity that is distinct from the
attended human `platform-admin` role. The identity is split across its proper
owners:
- KeyCape owns a confidential `client_credentials` service principal and its
signed short-lived JWT.
- railiance-platform owns the exact-bound OpenBao JWT role and the policies on
the resulting OpenBao token.
- ops-warden remains an interested consumer and SSH-certificate issuer; it does
not issue this identity or its credentials.
The operational identity is the platform-owned AppRole
`coding-agent-railiance-platform`. The exact-bound KeyCape JWT role is the
issuer-backed migration target; it does not block the deny-wins control while
the KeyCape client is being registered.
## Operational AppRole
The reviewed role body is `openbao/auth/coding-agent-approle.json`. Apply it
with attended platform authority:
```bash
bao write auth/approle/role/coding-agent-railiance-platform \
@openbao/auth/coding-agent-approle.json
```
This is a standing machine role, not a manually minted token. Every SecretID
expires after five minutes and is single-use; resulting tokens last at most 15
minutes, have eight uses, omit the default policy, and always carry
`agent-high-risk-boundary`. The role also carries the otherwise-readable
issue-core policy solely to make the deny-wins invariant directly testable.
The boundary permits token metadata lookup, capabilities inspection, and
self-revocation; it does not grant token creation or management of any other
token.
Run the non-disclosing verifier while holding attended authority to create one
ephemeral SecretID:
```bash
python3 scripts/verify_coding_agent_approle.py
```
The verifier holds the SecretID and token only in memory, calls
`sys/capabilities-self` rather than a KV read, and revokes the test token before
exit. It prints only policy names, TTL, capabilities, and pass/fail state.
## KeyCape contract
The accepted service token must contain exactly the following identity
coordinates:
| Claim | Required value |
| --- | --- |
| `aud` / client id | `codex-railiance-platform` |
| `sub` | `service:codex:railiance-platform` |
| `principal_type` | `service` |
| `tenant` | `tenant:coulomb` |
| `roles` | contains `coding-agent` |
| requested scope | `openbao:login` |
The KeyCape access token lifetime is 15 minutes. Its confidential client secret
must be generated and delivered through an approved out-of-repository custody
path. It must never appear in Git, State Hub, command arguments, logs, or chat.
## KeyCape-backed OpenBao role
The migration-target role body is
`openbao/auth/coding-agent-jwt-role.json`. Apply it only with attended platform
authority:
```bash
bao write auth/netkingdom/role/coding-agent \
@openbao/auth/coding-agent-jwt-role.json
```
The role binds the exact service audience, subject, principal type, tenant, and
role. It produces a 15-minute, eight-use token without the default policy. The
token deliberately combines `agent-high-risk-boundary` with
`workload-kv-read-issue-core-runtime`: the latter would otherwise read the
issue-core bundle, while the former must win with `deny`.
## Value-safe verification
Do not verify by attempting `bao kv get`; an unexpected policy error could
print a protected value. After KeyCape exchanges the client credential and the
JWT login produces an OpenBao token through a mode-0600 temporary file, use only
capabilities and token metadata:
```bash
BAO_TOKEN_FILE=/run/user/$(id -u)/coding-agent.openbao-token
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") \
bao token capabilities \
platform/data/workloads/issue-core/issue-core/issue-core-runtime
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") \
bao token capabilities \
platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
BAO_TOKEN=$(head -n 1 "$BAO_TOKEN_FILE") bao token lookup -format=json \
| jq '{display_name:.data.display_name,policies:.data.policies,ttl:.data.ttl}'
```
Expected results are `deny` for the data path and `read` for the metadata path.
The lookup must list both policies, omit `default`, and report a TTL no greater
than 15 minutes. Shred the temporary JWT and OpenBao token files after the
check. Revoke the issued OpenBao token if it remains live.
## Abort and rollback
- Abort if any bound claim differs, the JWT issuer/audience is not the live
KeyCape contract, or a token would be printed.
- Delete `auth/netkingdom/role/coding-agent` if deny-wins does not appear in
capabilities or the token includes an unexpected policy.
- Delete `auth/approle/role/coding-agent-railiance-platform` if its metadata
differs from the reviewed body or its verifier fails.
- Revoke the test OpenBao token, disable the KeyCape client, and rotate its
confidential secret after suspected disclosure.

View file

@ -22,6 +22,23 @@ modified.
catalog entries share the Binky IMAP path and Core Hub is an additional
reviewed deny without a catalog lane.
### 2026-08-22 generated-input reconciliation
- Ops-warden delivered generated artifact revision
`55f0f47a021375b8b25c924953d1b49a24e002c5`: 19 high-risk lanes, 14
concrete entries (13 unique paths), and five pattern/non-KV lanes.
- The corrected grades add issue-core runtime (whose bundle also contains
`GITEA_BACKEND_TOKEN`) and reuse-surface runtime (whose bundle also contains
`REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET`). Both data paths now have explicit
deny blocks and read-only metadata blocks.
- `scripts/agent_high_risk_boundary.py` verified that the vendored input equals
the clean upstream artifact aside from generation time and that every entry
is covered. Forty-nine focused tests passed.
- Under attended `platform-admin` OIDC, the policy was uploaded and read back.
OpenBao added only its normal trailing newline. The server-backed ops-warden
audit reported 19 high-risk lanes, 14 covered, zero uncovered, and five with
no concrete address.
## Attachment audit and residual blocker
A metadata-only scan listed and read role configuration under netkingdom OIDC,
@ -30,14 +47,33 @@ 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 2026-08-21 scan accurately found no attachment at that time. On 2026-08-22,
State Hub decision `f0955252-7b20-4c80-86e9-f8080ec60793` selected the
platform-owned `coding-agent-railiance-platform` AppRole as the operational
machine identity while retaining the KeyCape JWT role as an issuer-backed
migration target.
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.
Live AppRole metadata matched the reviewed source:
- `bind_secret_id=true`, `secret_id_ttl=300`, and
`secret_id_num_uses=1`;
- `token_ttl=900`, `token_max_ttl=900`, `token_num_uses=8`, and no default
policy;
- policies exactly `agent-high-risk-boundary` and
`workload-kv-read-issue-core-runtime`.
The non-disclosing verifier minted one ephemeral SecretID, logged in, and used
only token metadata plus `sys/capabilities-self`. The data path resolved to
`deny`; its metadata path resolved to `read`; the token TTL was 900 seconds.
The first verification exposed that a no-default-policy token could not revoke
itself. That bounded test token was located and revoked by accessor under
attended authority, `auth/token/revoke-self:update` was added to the boundary,
and the repeat verification passed and self-revoked without warning. No KV read
was attempted and no SecretID, token, or protected value was printed or logged.
The exact-bound JWT role `auth/netkingdom/role/coding-agent` was also applied
with audience `codex-railiance-platform`, subject
`service:codex:railiance-platform`, `principal_type=service`,
`tenant=tenant:coulomb`, and role `coding-agent`. KeyCape client registration
is a future identity-hardening handoff; it is not required for the live AppRole
attachment proven here.

View file

@ -0,0 +1,13 @@
{
"bind_secret_id": true,
"secret_id_ttl": "5m",
"secret_id_num_uses": 1,
"token_policies": [
"agent-high-risk-boundary",
"workload-kv-read-issue-core-runtime"
],
"token_ttl": "15m",
"token_max_ttl": "15m",
"token_num_uses": 8,
"token_no_default_policy": true
}

View file

@ -0,0 +1,23 @@
{
"role_type": "jwt",
"bound_audiences": [
"codex-railiance-platform"
],
"user_claim": "sub",
"bound_claims_type": "string",
"bound_claims": {
"sub": "service:codex:railiance-platform",
"principal_type": "service",
"tenant": "tenant:coulomb",
"roles": "coding-agent"
},
"token_policies": [
"agent-high-risk-boundary",
"workload-kv-read-issue-core-runtime"
],
"token_ttl": "15m",
"token_max_ttl": "15m",
"token_num_uses": 8,
"token_no_default_policy": true,
"verbose_oidc_logging": false
}

View file

@ -22,6 +22,18 @@ path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-sec
path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["read"]
}
path "platform/data/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["deny"]
}
path "platform/metadata/workloads/issue-core/issue-core/issue-core-runtime" {
capabilities = ["read"]
}
path "platform/data/workloads/reuse/reuse-surface/runtime-secrets" {
capabilities = ["deny"]
}
path "platform/metadata/workloads/reuse/reuse-surface/runtime-secrets" {
capabilities = ["read"]
}
path "platform/data/workloads/core-hub/runtime" {
capabilities = ["deny"]
}
@ -85,3 +97,6 @@ path "sys/capabilities-self" {
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}

View file

@ -0,0 +1,90 @@
# GENERATED by ops-warden/scripts/emit_high_risk_paths.py -- do not edit by hand.
# Vendored policy input. ops-warden owns the risk grade; railiance-platform owns
# whether and how agent-high-risk-boundary denies each path.
generated_at: "2026-08-21T11:28:33Z"
source: ops-warden/registry/routing/catalog.yaml
catalog_revision: "55f0f47a021375b8b25c924953d1b49a24e002c5"
catalog_revision_date: "2026-08-21T13:26:10+02:00"
catalog_dirty: false
high_risk_lane_count: 19
concrete_path_count: 14
no_concrete_path:
- database-dynamic-credentials
- inter-hub-bootstrap-ssh
- object-storage-sts
- openbao-api-key
- ops-warden-warden-sign-token
paths:
- id: agent-harness-binky-mail-approle
data_path: tenants/data/binky/company-email/imap
metadata_path: tenants/metadata/binky/company-email/imap
owner_repo: railiance-platform
fields: null
- id: agent-harness-forgejo-deploy
data_path: platform/data/workloads/agent-harness/forgejo-deploy-key
metadata_path: platform/metadata/workloads/agent-harness/forgejo-deploy-key
owner_repo: railiance-platform
fields: null
- id: audit-core-senders
data_path: platform/data/workloads/audit-core/senders
metadata_path: platform/metadata/workloads/audit-core/senders
owner_repo: ops-mason
fields: null
- id: binky-company-email-imap
data_path: tenants/data/binky/company-email/imap
metadata_path: tenants/metadata/binky/company-email/imap
owner_repo: railiance-platform
fields: null
- id: binky-qonto-api
data_path: tenants/data/binky/qonto-api
metadata_path: tenants/metadata/binky/qonto-api
owner_repo: railiance-platform
fields: null
- id: email-connect-transactional
data_path: platform/data/workloads/email-connect/transactional
metadata_path: platform/metadata/workloads/email-connect/transactional
owner_repo: railiance-platform
fields: null
- id: forgejo-admin-api-token
data_path: platform/data/workloads/forgejo/forgejo-admin
metadata_path: platform/metadata/workloads/forgejo/forgejo-admin
owner_repo: railiance-platform
fields: null
- id: issue-core-ingestion-api-key
data_path: platform/data/workloads/issue-core/issue-core/issue-core-runtime
metadata_path: platform/metadata/workloads/issue-core/issue-core/issue-core-runtime
owner_repo: railiance-platform
fields: [ISSUE_CORE_API_KEY, GITEA_BACKEND_TOKEN]
- id: openrouter-llm-connect
data_path: platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets
metadata_path: platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider-secrets
owner_repo: railiance-platform
fields: null
- id: railiance-backup-offsite-lane
data_path: platform/data/workloads/railiance/backup/offsite-lane
metadata_path: platform/metadata/workloads/railiance/backup/offsite-lane
owner_repo: railiance-platform
fields: null
- id: rapp-qonto-keycape-client
data_path: platform/data/workloads/rapp-qonto/keycape-client
metadata_path: platform/metadata/workloads/rapp-qonto/keycape-client
owner_repo: key-cape
fields: null
- id: reuse-surface-hub-write-token
data_path: platform/data/workloads/reuse/reuse-surface/runtime-secrets
metadata_path: platform/metadata/workloads/reuse/reuse-surface/runtime-secrets
owner_repo: railiance-platform
fields: [REUSE_SURFACE_TOKEN, REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET]
- id: scaleway-bootstrap
data_path: platform/data/workloads/railiance/scaleway/bootstrap
metadata_path: platform/metadata/workloads/railiance/scaleway/bootstrap
owner_repo: railiance-platform
fields: null
- id: whynot-design-npm-publish
data_path: platform/data/workloads/coulomb/whynot-design/npm-publish
metadata_path: platform/metadata/workloads/coulomb/whynot-design/npm-publish
owner_repo: railiance-platform
fields: null

View file

@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Check the platform-owned agent deny policy against ops-warden's risk input."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any
import yaml
REPO_DIR = Path(__file__).resolve().parents[1]
DEFAULT_ARTIFACT = (
REPO_DIR / "openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml"
)
DEFAULT_POLICY = REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl"
def load_artifact(path: Path) -> dict[str, Any]:
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"artifact must be a mapping: {path}")
return data
def parse_policy(text: str) -> dict[str, set[str]]:
paths: dict[str, set[str]] = {}
for match in re.finditer(
r'path\s+"([^"]+)"\s*\{[^}]*?capabilities\s*=\s*\[([^\]]*)\]',
text,
re.DOTALL,
):
paths[match.group(1)] = {
value.strip().strip('"\'')
for value in match.group(2).split(",")
if value.strip()
}
return paths
def check_boundary(
artifact: dict[str, Any], policy: dict[str, set[str]]
) -> dict[str, Any]:
errors: list[str] = []
rows = artifact.get("paths")
no_concrete = artifact.get("no_concrete_path")
if not isinstance(rows, list):
rows = []
errors.append("artifact paths must be a list")
if not isinstance(no_concrete, list):
no_concrete = []
errors.append("artifact no_concrete_path must be a list")
if artifact.get("catalog_dirty") is not False:
errors.append("artifact catalog_dirty must be false")
if artifact.get("concrete_path_count") != len(rows):
errors.append("artifact concrete_path_count does not match paths")
if artifact.get("high_risk_lane_count") != len(rows) + len(no_concrete):
errors.append("artifact high_risk_lane_count does not match its entries")
seen_ids: set[str] = set()
unique_paths: set[str] = set()
for row in rows:
if not isinstance(row, dict):
errors.append("artifact path entry must be a mapping")
continue
lane_id = row.get("id")
data_path = row.get("data_path")
metadata_path = row.get("metadata_path")
if not isinstance(lane_id, str) or not lane_id:
errors.append("artifact path entry has no id")
continue
if lane_id in seen_ids:
errors.append(f"duplicate lane id: {lane_id}")
seen_ids.add(lane_id)
if not isinstance(data_path, str) or "/data/" not in data_path:
errors.append(f"{lane_id}: invalid data_path")
continue
expected_metadata = data_path.replace("/data/", "/metadata/", 1)
if metadata_path != expected_metadata:
errors.append(f"{lane_id}: metadata_path does not match data_path")
continue
unique_paths.add(data_path)
if "deny" not in policy.get(data_path, set()):
errors.append(f"{lane_id}: data path is not denied: {data_path}")
if "read" not in policy.get(metadata_path, set()):
errors.append(f"{lane_id}: metadata path is not readable: {metadata_path}")
return {
"catalog_revision": artifact.get("catalog_revision"),
"high_risk_lanes": artifact.get("high_risk_lane_count"),
"concrete_entries": len(rows),
"unique_concrete_paths": len(unique_paths),
"no_concrete_paths": len(no_concrete),
"errors": errors,
"ok": not errors,
}
def comparable_artifact(data: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in data.items() if key != "generated_at"}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--artifact", type=Path, default=DEFAULT_ARTIFACT)
parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
parser.add_argument(
"--upstream",
type=Path,
help="optionally require the vendored artifact to match this upstream copy",
)
parser.add_argument("--json", action="store_true")
args = parser.parse_args()
try:
artifact = load_artifact(args.artifact)
policy = parse_policy(args.policy.read_text(encoding="utf-8"))
report = check_boundary(artifact, policy)
if args.upstream:
upstream = load_artifact(args.upstream)
if comparable_artifact(artifact) != comparable_artifact(upstream):
report["errors"].append("vendored artifact differs from upstream")
report["ok"] = False
except (OSError, ValueError, yaml.YAMLError) as exc:
report = {"ok": False, "errors": [str(exc)]}
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
elif report["ok"]:
print(
"PASS: agent boundary covers "
f"{report['concrete_entries']} high-risk catalog entries at "
f"{report['catalog_revision']}"
)
else:
for error in report["errors"]:
print(f"FAIL: {error}", file=sys.stderr)
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Mint, verify, and revoke a coding-agent AppRole token without printing it."""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import urllib.error
import urllib.request
from typing import Any
DEFAULT_ADDR = "https://bao.coulomb.social"
DATA_PATH = "platform/data/workloads/issue-core/issue-core/issue-core-runtime"
METADATA_PATH = (
"platform/metadata/workloads/issue-core/issue-core/issue-core-runtime"
)
EXPECTED_POLICIES = {
"agent-high-risk-boundary",
"workload-kv-read-issue-core-runtime",
}
def bao_json(args: list[str]) -> dict[str, Any]:
completed = subprocess.run(
["bao", *args],
check=True,
capture_output=True,
text=True,
timeout=30,
)
return json.loads(completed.stdout)
def api_request(
addr: str,
method: str,
path: str,
*,
token: str | None = None,
payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if token:
headers["X-Vault-Token"] = token
body = json.dumps(payload).encode() if payload is not None else None
request = urllib.request.Request(
f"{addr.rstrip('/')}/v1/{path}", data=body, headers=headers, method=method
)
with urllib.request.urlopen(request, timeout=30) as response:
content = response.read()
return json.loads(content) if content else {}
def verify(role: str, addr: str) -> dict[str, Any]:
role_payload = bao_json(
["read", "-format=json", f"auth/approle/role/{role}/role-id"]
)
role_id = role_payload["data"]["role_id"]
secret_payload = bao_json(
["write", "-format=json", "-f", f"auth/approle/role/{role}/secret-id"]
)
secret_id = secret_payload["data"]["secret_id"]
agent_token: str | None = None
report: dict[str, Any] = {}
try:
login = api_request(
addr,
"POST",
"auth/approle/login",
payload={"role_id": role_id, "secret_id": secret_id},
)
agent_token = login["auth"]["client_token"]
lookup = api_request(addr, "GET", "auth/token/lookup-self", token=agent_token)
capabilities = api_request(
addr,
"POST",
"sys/capabilities-self",
token=agent_token,
payload={"paths": [DATA_PATH, METADATA_PATH]},
)
policies = set(lookup["data"].get("policies", []))
ttl = int(lookup["data"].get("ttl", 0))
data_caps = capabilities.get(DATA_PATH, [])
metadata_caps = capabilities.get(METADATA_PATH, [])
errors: list[str] = []
if policies != EXPECTED_POLICIES:
errors.append(f"unexpected policies: {sorted(policies)}")
if ttl <= 0 or ttl > 900:
errors.append(f"TTL outside 1..900 seconds: {ttl}")
if data_caps != ["deny"]:
errors.append(f"data capabilities are not deny: {data_caps}")
if "read" not in metadata_caps or "deny" in metadata_caps:
errors.append(f"metadata capabilities are not read-only: {metadata_caps}")
report = {
"role": role,
"policies": sorted(policies),
"ttl": ttl,
"data_capabilities": data_caps,
"metadata_capabilities": metadata_caps,
"deny_wins": data_caps == ["deny"],
"errors": errors,
"ok": not errors,
}
finally:
if agent_token:
try:
api_request(addr, "POST", "auth/token/revoke-self", token=agent_token)
except (OSError, urllib.error.HTTPError):
report.setdefault("errors", []).append("test token self-revocation failed")
report["ok"] = False
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--role", default="coding-agent-railiance-platform")
parser.add_argument("--addr", default=os.environ.get("BAO_ADDR", DEFAULT_ADDR))
args = parser.parse_args()
try:
report = verify(args.role, args.addr)
except (
KeyError,
json.JSONDecodeError,
OSError,
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
urllib.error.HTTPError,
) as exc:
report = {"role": args.role, "ok": False, "errors": [str(exc)]}
print(json.dumps(report, indent=2, sort_keys=True))
return 0 if report["ok"] else 1
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import importlib.util
import json
import os
import shutil
import sys
@ -17,6 +18,14 @@ assert SPEC.loader is not None
sys.modules[SPEC.name] = credential_change
SPEC.loader.exec_module(credential_change)
BOUNDARY_SPEC = importlib.util.spec_from_file_location(
"agent_high_risk_boundary", REPO_DIR / "scripts/agent_high_risk_boundary.py"
)
agent_high_risk_boundary = importlib.util.module_from_spec(BOUNDARY_SPEC)
assert BOUNDARY_SPEC.loader is not None
sys.modules[BOUNDARY_SPEC.name] = agent_high_risk_boundary
BOUNDARY_SPEC.loader.exec_module(agent_high_risk_boundary)
class CredentialChangeTests(unittest.TestCase):
def setUp(self) -> None:
@ -155,29 +164,69 @@ class CredentialChangeTests(unittest.TestCase):
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"\]',
)
artifact = agent_high_risk_boundary.load_artifact(
REPO_DIR
/ "openbao/policies/inputs/ops-warden-high-risk-data-paths.yaml"
)
boundary = agent_high_risk_boundary.parse_policy(
(REPO_DIR / "openbao/policies/agent-high-risk-boundary.hcl").read_text()
)
report = agent_high_risk_boundary.check_boundary(artifact, boundary)
self.assertEqual(report["errors"], [])
self.assertEqual(report["high_risk_lanes"], 19)
self.assertEqual(report["concrete_entries"], 14)
self.assertEqual(report["unique_concrete_paths"], 13)
self.assertEqual(report["no_concrete_paths"], 5)
self.assertEqual(boundary["auth/token/revoke-self"], {"update"})
def test_coding_agent_jwt_role_is_exact_bound_and_deny_wins(self) -> None:
role = json.loads(
(REPO_DIR / "openbao/auth/coding-agent-jwt-role.json").read_text()
)
self.assertEqual(role["role_type"], "jwt")
self.assertEqual(role["bound_audiences"], ["codex-railiance-platform"])
self.assertEqual(role["user_claim"], "sub")
self.assertEqual(
role["bound_claims"],
{
"sub": "service:codex:railiance-platform",
"principal_type": "service",
"tenant": "tenant:coulomb",
"roles": "coding-agent",
},
)
self.assertEqual(
set(role["token_policies"]),
{
"agent-high-risk-boundary",
"workload-kv-read-issue-core-runtime",
},
)
self.assertTrue(role["token_no_default_policy"])
self.assertEqual(role["token_ttl"], "15m")
self.assertEqual(role["token_max_ttl"], "15m")
self.assertEqual(role["token_num_uses"], 8)
def test_coding_agent_approle_is_bounded_and_deny_wins(self) -> None:
role = json.loads(
(REPO_DIR / "openbao/auth/coding-agent-approle.json").read_text()
)
self.assertTrue(role["bind_secret_id"])
self.assertEqual(role["secret_id_ttl"], "5m")
self.assertEqual(role["secret_id_num_uses"], 1)
self.assertEqual(
set(role["token_policies"]),
{
"agent-high-risk-boundary",
"workload-kv-read-issue-core-runtime",
},
)
self.assertTrue(role["token_no_default_policy"])
self.assertEqual(role["token_ttl"], "15m")
self.assertEqual(role["token_max_ttl"], "15m")
self.assertEqual(role["token_num_uses"], 8)
def test_core_hub_database_lanes_are_split_and_exact_scope(self) -> None:
database_policy = (
REPO_DIR / "openbao/policies/external-secrets-core-hub-database.hcl"
).read_text()

View file

@ -4,11 +4,11 @@ type: workplan
title: "Close agent high-risk OpenBao boundary coverage"
domain: financials
repo: railiance-platform
status: blocked
status: finished
owner: codex
topic_slug: railiance
created: "2026-08-21"
updated: "2026-08-21"
updated: "2026-08-22"
related:
- WARDEN-WP-0032
- RISK-F-0009
@ -43,9 +43,11 @@ priority: high
state_hub_task_id: "44d1a4bf-70bb-4d50-a40a-2158acc96b36"
```
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.
Run the capabilities-only ops-warden audit against the policy. The initial
2026-08-21 reconciliation found 17 high-risk lanes: six covered, six concrete
uncovered, and five without a concrete KV address. The final generated input
contains 19 high-risk lanes, 14 concrete entries, and five without an address.
No credential value was read.
## T02 — Close the concrete deny gaps
@ -61,9 +63,10 @@ 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.
Completed 2026-08-22. After ops-warden corrected two multi-field grades, the
source policy covers all 13 unique concrete catalog paths plus the additional
reviewed Core Hub path. The generated-input check reports all 14 concrete
catalog entries covered with none uncovered.
## T03 — Apply and verify live
@ -79,13 +82,11 @@ 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.
Completed initially on 2026-08-21 and reconciled again on 2026-08-22 under
attended `platform-admin` OIDC. OpenBao accepted the policy; normalized
readback matched source, and the final server-backed audit reported 19
high-risk lanes, 14 covered entries, zero uncovered, and five pattern/non-KV
lanes without a concrete address. No Secret value was read.
## T04 — Route the result
@ -105,11 +106,15 @@ Completed 2026-08-21 via State Hub message
attachment finding, the distinct-agent-identity blocker, and the requested
versioned generated artifact shape.
Ops-warden delivered revision `55f0f47a021375b8b25c924953d1b49a24e002c5`
on 2026-08-21. The platform now vendors it as a checked policy input and has a
repeatable upstream-equality plus policy-coverage command.
## T05 — Establish a distinct coding-agent identity
```task
id: RAILIANCE-WP-0022-T05
status: wait
status: done
priority: high
state_hub_task_id: "47aa5ed9-95c5-4a23-a460-e4bbd3ed6f65"
```
@ -119,12 +124,40 @@ The policy is live but no role attaches it. Do not add the boundary to
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.
Completed 2026-08-22. State Hub decision
`f0955252-7b20-4c80-86e9-f8080ec60793` establishes the platform-owned AppRole
`coding-agent-railiance-platform` as the operational machine identity. Its
single-use SecretIDs expire after five minutes; its tokens last at most 15
minutes, have eight uses, omit the default policy, and attach both
`agent-high-risk-boundary` and the otherwise-readable issue-core workload
policy. A live ephemeral login proved data capabilities resolve to `deny` and
metadata to `read`; the token was revoked without reading the KV value.
The exact-bound KeyCape JWT role is also live as the issuer-backed migration
target. KeyCape principal registration remains an identity hardening follow-up,
not a blocker to the active AppRole boundary.
## T06 — Consume the generated catalog artifact
```task
id: RAILIANCE-WP-0022-T06
status: done
priority: high
```
Consume ops-warden's versioned `high-risk-data-paths.yaml` as a policy-check
input, add the two newly regraded issue-core and reuse-surface paths, and rerun
source plus live coverage. The input remains ops-warden's risk statement, not
the policy authority; railiance-platform continues to own every deny decision.
Completed 2026-08-22. The vendored input matches the clean upstream artifact,
the repository check covers all 14 entries, and the server-backed audit reports
zero uncovered. Evidence: `docs/evidence/agent-high-risk-boundary-2026-08-21.md`.
## 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] A distinct AppRole attachment is established from live metadata.
- [x] Result and generated-artifact follow-up are routed to ops-warden.
- [ ] A distinct coding-agent identity actually attaches the boundary.
- [x] A distinct coding-agent identity actually attaches the boundary.