Replace ops-mason-build with read-only ops-mason-survey grant (MASON-IN-0004)
Founder decision 2026-10-07, option (c)+(a): ops-mason surveys and verifies only. Policy/role writes, secret-id minting and token creation go through the founder-attended railiance-platform delegated applier. ops-mason-build retired to policies/retired/ (never installed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 132360@bnt-lap001 Assistant-Session: 769b1afd-4330-4c86-96b2-e510e26db42b
This commit is contained in:
parent
47171385ce
commit
6b177270d3
10 changed files with 261 additions and 40 deletions
63
AGENTS.md
63
AGENTS.md
|
|
@ -261,21 +261,47 @@ it as `BAO_TOKEN=$(cat ~/.claude-bao-token) bao <cmd>`.
|
||||||
|
|
||||||
### What the grant authorises
|
### What the grant authorises
|
||||||
|
|
||||||
Policy `ops-mason-build` (`policies/ops-mason-build.hcl`), 45 minutes, max one
|
Policy `ops-mason-survey` (`policies/ops-mason-survey.hcl`), 45 minutes, max
|
||||||
hour, named `claude-<task>`:
|
one hour, named `claude-<task>`. Read-only, by founder decision 2026-10-07
|
||||||
|
(MASON-IN-0004, option (c)+(a)); it replaces `ops-mason-build`, now in
|
||||||
|
`policies/retired/` and never to be installed.
|
||||||
|
|
||||||
| Allowed | Denied |
|
| Allowed | Denied / not granted |
|
||||||
|---|---|
|
|---|---|
|
||||||
| read `sys/mounts`, `sys/auth`, policy list — the phase-2 survey | **every read of `*/data/*`** on `platform`, `operators`, `secret` |
|
| read/list `sys/mounts`, `sys/auth`; list `sys/policies/acl`, read `sys/policies/acl/*` — the phase-2 survey | **any policy write** (no create/update/delete on `sys/policies/acl/*`; explicit deny on its own policy) |
|
||||||
| create/update policies under `sys/policies/acl/*` | enabling or tuning mounts (`sys/mounts/*`) — a railiance-platform act |
|
| read/list role definitions on `auth/kubernetes/role/*`, `auth/netkingdom/role/*`, `auth/jwt/role/*`; list `auth/approle/role`, read `auth/approle/role/<name>` | **any auth-role write**; AppRole `role-id`, `secret-id`, `secret-id/*`, `secret-id-accessor/*`, `custom-secret-id` (explicit deny) |
|
||||||
| create/update `auth/kubernetes/role/*`, `auth/approle/role/*` | anything not listed |
|
| read/list KV **metadata** on `platform/`, `operators/` — enough to confirm a delivery landed | **every `*/data/*`** on `platform`, `operators`, `secret` (explicit deny); KV metadata writes |
|
||||||
| read KV **metadata** — versions and timestamps, enough to confirm a delivery landed | KV **values** |
|
| `sys/capabilities`, `sys/capabilities-self` (query only), `auth/token/lookup-self`, `auth/token/revoke-self` | **token creation of any kind**; revoking other tokens; `sys/mounts/*` |
|
||||||
| mint and revoke short-lived test tokens, check capabilities | |
|
|
||||||
|
|
||||||
The denial is the point. `SCOPE.md` says ops-mason never touches secret values;
|
The grant can show what exists and prove what a token can reach; it cannot
|
||||||
this makes OpenBao enforce it rather than leaving it to whoever is driving. An
|
change anything. That removes the escalation MASON-IN-0004 described — write a
|
||||||
explicit `deny` outranks any grant, including one added to this policy later by
|
policy without the denies, bind it to an AppRole, read the role-id, mint a
|
||||||
mistake.
|
secret-id, log in — because none of those steps is granted.
|
||||||
|
|
||||||
|
### Installing the survey policy (founder act)
|
||||||
|
|
||||||
|
ops-mason cannot install its own grant. The founder runs, silently and with an
|
||||||
|
absolute path (the warden wrapper refuses output and runs with a temporary
|
||||||
|
`HOME`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
warden access openbao-platform-admin-login --exec -- sh -c \
|
||||||
|
'bao policy write ops-mason-survey /home/worsch/ops-mason/policies/ops-mason-survey.hcl >/dev/null 2>&1'
|
||||||
|
```
|
||||||
|
|
||||||
|
Silent check — the installed policy's hash into a 0600 receipt, compared with
|
||||||
|
the repo file's hash outside the wrapper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
warden access openbao-platform-admin-login --exec -- sh -c \
|
||||||
|
'umask 077; mkdir -p /home/worsch/.local/state/railiance/receipts; bao policy read ops-mason-survey 2>/dev/null | sha256sum > /home/worsch/.local/state/railiance/receipts/ops-mason-survey.sha256'
|
||||||
|
cat /home/worsch/.local/state/railiance/receipts/ops-mason-survey.sha256
|
||||||
|
sha256sum < /home/worsch/ops-mason/policies/ops-mason-survey.hcl
|
||||||
|
```
|
||||||
|
|
||||||
|
The two hashes match only if OpenBao returns the file byte-for-byte; if they
|
||||||
|
differ, compare the stored text against the file before concluding the install
|
||||||
|
is wrong (OpenBao may normalise a trailing newline).
|
||||||
|
|
||||||
### Choosing the task name
|
### Choosing the task name
|
||||||
|
|
||||||
|
|
@ -286,8 +312,13 @@ recognise later.
|
||||||
|
|
||||||
### When a broader grant is genuinely needed
|
### When a broader grant is genuinely needed
|
||||||
|
|
||||||
Enabling a mount, reading a value, or touching another subsystem's paths is out
|
Writing a policy or auth role, minting a secret-id or a token, enabling a
|
||||||
of `ops-mason-build` on purpose. Do not widen the policy to get past a refusal.
|
mount, reading a value, or touching another subsystem's paths is out of
|
||||||
Either the act belongs to another repo, or it needs its own named policy and its
|
`ops-mason-survey` on purpose. Do not widen the policy to get past a refusal.
|
||||||
own decision — the way `operators/` did.
|
Builds that need writes are applied by the founder through the
|
||||||
|
railiance-platform delegated applier (`scripts/credential-change.py
|
||||||
|
applier-apply` under `warden access openbao-platform-admin-login --exec`), as
|
||||||
|
for CCR-2026-0033/0034; ops-mason supplies the plan and verifies afterwards.
|
||||||
|
Anything else belongs to another repo or needs its own named policy and its
|
||||||
|
own founder decision.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,11 @@ tooling_contacts:
|
||||||
capability: "Create policies and auth roles for an approved construction plan (structure only, never secret values)"
|
capability: "Create policies and auth roles for an approved construction plan (structure only, never secret values)"
|
||||||
intended_owner: secrets-engine
|
intended_owner: secrets-engine
|
||||||
blocked_on: >-
|
blocked_on: >-
|
||||||
No engine exposes a policy / auth-role construction surface; ops-mason
|
No engine exposes a policy / auth-role construction surface. Since
|
||||||
runs the bao CLI under a scoped, time-boxed ops-mason-build token.
|
MASON-IN-0004 (founder, 2026-10-07) ops-mason holds no build grant:
|
||||||
|
these writes are applied by the founder through the railiance-platform
|
||||||
|
delegated applier; ops-mason surveys and verifies under the read-only
|
||||||
|
ops-mason-survey token.
|
||||||
review: "2026-12-21"
|
review: "2026-12-21"
|
||||||
gap_state: accepted
|
gap_state: accepted
|
||||||
accepted_exception: *plan-approval-gate
|
accepted_exception: *plan-approval-gate
|
||||||
|
|
@ -164,7 +167,7 @@ tooling_contacts:
|
||||||
tooling: OpenBao
|
tooling: OpenBao
|
||||||
operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session"
|
operation: "Operator OIDC login; mint a scoped, bounded token under the operator's own session"
|
||||||
write: true
|
write: true
|
||||||
capability: "Hand an agent session a task-scoped ops-mason-build token instead of the operator's session"
|
capability: "Hand an agent session a task-scoped, read-only ops-mason-survey token instead of the operator's session"
|
||||||
supplied_authority: none
|
supplied_authority: none
|
||||||
note: >-
|
note: >-
|
||||||
Runs under the operator's own authority and narrows it; ops-mason holds
|
Runs under the operator's own authority and narrows it; ops-mason holds
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ Generate the section rather than gathering it by hand (MASON-WP-0008). The
|
||||||
founder mints the scoped grant; the agent then runs the survey with it:
|
founder mints the scoped grant; the agent then runs the survey with it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/bao-session.sh grant <task> # founder: ops-mason-build, 45m, claude-<task>
|
./scripts/bao-session.sh grant <task> # founder: ops-mason-survey (read-only), 45m, claude-<task>
|
||||||
./scripts/plan-survey.py \
|
./scripts/plan-survey.py \
|
||||||
--kv-path platform/workloads/<consumer>/<name> \
|
--kv-path platform/workloads/<consumer>/<name> \
|
||||||
--policy workload-kv-read-<consumer>-<name> \
|
--policy workload-kv-read-<consumer>-<name> \
|
||||||
|
|
@ -83,10 +83,10 @@ matching ops-warden catalog entries, a UTC timestamp, `BAO_ADDR`, the token
|
||||||
draft answer into a decision; it is generated evidence, not a review.
|
draft answer into a decision; it is generated evidence, not a review.
|
||||||
|
|
||||||
The survey refuses to run without a grant, refuses a token whose policies
|
The survey refuses to run without a grant, refuses a token whose policies
|
||||||
are anything but `ops-mason-build` (plus `default`), and never prints the
|
are anything but `ops-mason-survey` (plus `default`), and never prints the
|
||||||
token. Every bao call passes an allowlist that rejects `kv get` and any
|
token. Every bao call passes an allowlist that rejects `kv get` and any
|
||||||
`*/data/*` path before a process starts. A query the grant does not permit
|
`*/data/*` path before a process starts. A query the grant does not permit
|
||||||
is reported as `not permitted by ops-mason-build` and listed under
|
is reported as `not permitted by ops-mason-survey` and listed under
|
||||||
"incomplete" rather than read as an empty answer. Policies with names that
|
"incomplete" rather than read as an empty answer. Policies with names that
|
||||||
do not match the demand are not read, so a broadly named policy covering
|
do not match the demand are not read, so a broadly named policy covering
|
||||||
the path is not detected; the output says so.
|
the path is not detected; the output says so.
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ state_hub_intake_id: "01a06e68-8c29-7230-b0dc-88f1e374afa4"
|
||||||
id: MASON-IN-0004
|
id: MASON-IN-0004
|
||||||
kind: intake
|
kind: intake
|
||||||
title: Close the policy-plus-AppRole escalation path in ops-mason-build
|
title: Close the policy-plus-AppRole escalation path in ops-mason-build
|
||||||
status: open
|
status: closed
|
||||||
|
outcome: resolved
|
||||||
origin: review
|
origin: review
|
||||||
origin_ref: inbound-library ILIB-WP-0003 (finding raised 2026-10-05 while preparing the first grant)
|
origin_ref: inbound-library ILIB-WP-0003 (finding raised 2026-10-05 while preparing the first grant)
|
||||||
priority: high
|
priority: high
|
||||||
|
|
@ -152,5 +153,23 @@ notes: >-
|
||||||
A survey needs only (c)'s read-only grant. Today MASON-WP-0008 plan-survey runs
|
A survey needs only (c)'s read-only grant. Today MASON-WP-0008 plan-survey runs
|
||||||
under the full build grant, so separating them also reduces what a survey
|
under the full build grant, so separating them also reduces what a survey
|
||||||
session can do.
|
session can do.
|
||||||
|
resolution: >-
|
||||||
|
Founder decision (Bernd Worsch, 2026-10-07, given in the key-cape session):
|
||||||
|
(c)+(a), a read-only grant only. ops-mason gets the read-only
|
||||||
|
ops-mason-survey policy (policies/ops-mason-survey.hcl: read/list on mounts,
|
||||||
|
auth methods, policies, auth role definitions and KV metadata; capability
|
||||||
|
queries; lookup-self and revoke-self; explicit denies on */data/*, its own
|
||||||
|
policy, and AppRole role-id / secret-id / secret-id-accessor /
|
||||||
|
custom-secret-id). No policy writes, no auth-role writes, no secret-id
|
||||||
|
minting, no token creation. Those acts go through the founder-attended
|
||||||
|
railiance-platform delegated applier (scripts/credential-change.py
|
||||||
|
applier-apply under warden access openbao-platform-admin-login --exec), as
|
||||||
|
for CCR-2026-0033/0034 on 2026-10-07. ops-mason-build moved to
|
||||||
|
policies/retired/ with a do-not-install header; it was not present in
|
||||||
|
OpenBao at the time. bao-session.sh, plan-survey.py, custody-inventory.py,
|
||||||
|
tests, AGENTS.md, INTENT.md and docs/construction-plan-format.md switched to
|
||||||
|
ops-mason-survey. Installing the policy is a founder act (AGENTS.md,
|
||||||
|
"Installing the survey policy"). Commit: COMMIT_PENDING.
|
||||||
|
updated: '2026-10-07T00:00:00Z'
|
||||||
state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f"
|
state_hub_intake_id: "01a11118-1f35-7ad5-ad51-65dd785c932f"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
150
policies/ops-mason-survey.hcl
Normal file
150
policies/ops-mason-survey.hcl
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
# ops-mason-survey — read-only survey and verification. Nothing else.
|
||||||
|
#
|
||||||
|
# Founder decision 2026-10-07 under MASON-IN-0004, option (c)+(a): ops-mason
|
||||||
|
# designs, surveys and verifies; it does not build. Policy writes, auth-role
|
||||||
|
# writes, AppRole secret-id minting and token creation are applied by the
|
||||||
|
# founder through the railiance-platform delegated applier
|
||||||
|
# (scripts/credential-change.py applier-apply under
|
||||||
|
# `warden access openbao-platform-admin-login --exec`). This replaces
|
||||||
|
# ops-mason-build (policies/retired/), whose policy-write + AppRole + token
|
||||||
|
# authority could be chained into a token carrying none of its denies.
|
||||||
|
#
|
||||||
|
# There is no write capability anywhere in this file except the two
|
||||||
|
# query-shaped endpoints (sys/capabilities*, revoke-self) that grant nothing.
|
||||||
|
# Granted through scripts/bao-session.sh as a 45-minute, named token.
|
||||||
|
#
|
||||||
|
# Path priority note: OpenBao uses the single most specific matching path
|
||||||
|
# within a policy, and a path with a `+` segment loses to one without. A deny
|
||||||
|
# on `auth/approle/role/+/secret-id/*` would therefore LOSE to a broad
|
||||||
|
# `auth/approle/role/*` grant. So AppRole is granted narrowly (list the roles,
|
||||||
|
# read `role/<name>` only) and the sub-endpoints are never granted at all; the
|
||||||
|
# explicit denies below are a second line, not the only one.
|
||||||
|
|
||||||
|
# --- survey: mounts, auth methods, policies ---------------------------------
|
||||||
|
# Phase 2 of the construction process. `bao secrets list` / `bao auth list`.
|
||||||
|
path "sys/mounts" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "sys/auth" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# `bao policy list`.
|
||||||
|
path "sys/policies/acl" {
|
||||||
|
capabilities = ["list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# `bao policy read <name>` — policy text, never a secret value. read only:
|
||||||
|
# no create/update/delete, so this grant cannot write or remove any policy.
|
||||||
|
path "sys/policies/acl/*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- survey: auth role definitions (read only) ------------------------------
|
||||||
|
# Kubernetes roles hold bindings and policy names, no credential material.
|
||||||
|
path "auth/kubernetes/role/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# AppRole: list role names, read a role's definition. Deliberately not
|
||||||
|
# `auth/approle/role/*` — see the priority note above.
|
||||||
|
path "auth/approle/role" {
|
||||||
|
capabilities = ["list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/approle/role/+" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# OIDC/JWT roles used in this estate (netkingdom OIDC mount, jwt mount):
|
||||||
|
# bound claims, redirect URIs and policy names, no credential material.
|
||||||
|
path "auth/netkingdom/role/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/jwt/role/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- survey: KV structure, never KV values ----------------------------------
|
||||||
|
# Metadata carries versions, timestamps and custom_metadata — enough to confirm
|
||||||
|
# a path exists and that a paste-once delivery landed. Read/list only: this
|
||||||
|
# grant cannot describe, create or delete a path.
|
||||||
|
path "platform/metadata/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "operators/metadata/*" {
|
||||||
|
capabilities = ["read", "list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- verification -----------------------------------------------------------
|
||||||
|
# `bao token capabilities <path>` asks what a token can reach. The endpoints
|
||||||
|
# take a POST, hence update; they answer a question and grant nothing.
|
||||||
|
path "sys/capabilities" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "sys/capabilities-self" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# The survey checks its own token's policies before running.
|
||||||
|
path "auth/token/lookup-self" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# End the grant early when the task is done.
|
||||||
|
path "auth/token/revoke-self" {
|
||||||
|
capabilities = ["update"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Not granted, on purpose: auth/token/create (no token minting of any kind),
|
||||||
|
# auth/token/revoke (other tokens), any auth/*/role write, sys/mounts/*.
|
||||||
|
|
||||||
|
# --- the grant cannot read or rewrite its own scope -------------------------
|
||||||
|
# It has no policy write at all, so this is belt and braces: if a write on
|
||||||
|
# sys/policies/acl/* is ever added here by mistake, this exact path still
|
||||||
|
# outranks the glob and the grant cannot edit its own denials.
|
||||||
|
path "sys/policies/acl/ops-mason-survey" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the AppRole escalation, stated as denials ------------------------------
|
||||||
|
# Reading a role-id or minting a secret-id is exactly how a role becomes a
|
||||||
|
# login. None of these is granted above; deny makes that explicit and also
|
||||||
|
# overrides any other policy attached to the same token.
|
||||||
|
path "auth/approle/role/+/role-id" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/approle/role/+/secret-id" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/approle/role/+/secret-id/*" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/approle/role/+/secret-id-accessor/*" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "auth/approle/role/+/custom-secret-id" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- the line on secret values, stated as a denial --------------------------
|
||||||
|
# Explicit deny outranks any grant, including one added here later by mistake.
|
||||||
|
path "platform/data/*" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "operators/data/*" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "secret/data/*" {
|
||||||
|
capabilities = ["deny"]
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,19 @@
|
||||||
|
# RETIRED — DO NOT INSTALL.
|
||||||
|
#
|
||||||
|
# Retired 2026-10-07 by founder decision (Bernd Worsch) under MASON-IN-0004,
|
||||||
|
# option (c)+(a): ops-mason holds only the read-only ops-mason-survey grant
|
||||||
|
# (policies/ops-mason-survey.hcl). This policy could write ACL policies and
|
||||||
|
# AppRole roles and mint tokens; together those let a holder create a policy
|
||||||
|
# without its denies, bind it to an AppRole, read the role-id, mint a secret-id
|
||||||
|
# and log in as a token that carries none of the denies below. Policy writes,
|
||||||
|
# auth-role writes, secret-id minting and token creation now go through the
|
||||||
|
# founder-attended railiance-platform delegated applier
|
||||||
|
# (scripts/credential-change.py applier-apply under
|
||||||
|
# `warden access openbao-platform-admin-login --exec`).
|
||||||
|
#
|
||||||
|
# Kept for the record only. It was never present in OpenBao at retirement
|
||||||
|
# (bao-session.sh grant reported "does not exist yet" on 2026-10-07).
|
||||||
|
#
|
||||||
# ops-mason-build — the authority ops-mason actually needs, and no more.
|
# ops-mason-build — the authority ops-mason actually needs, and no more.
|
||||||
#
|
#
|
||||||
# SCOPE.md says ops-mason "never touches secret values, even transiently". Until
|
# SCOPE.md says ops-mason "never touches secret values, even transiently". Until
|
||||||
|
|
@ -12,10 +12,12 @@
|
||||||
# for as long as you have it, indistinguishable from you in the audit log. The
|
# for as long as you have it, indistinguishable from you in the audit log. The
|
||||||
# grant path instead mints a separate token that is:
|
# grant path instead mints a separate token that is:
|
||||||
#
|
#
|
||||||
# * scoped — carries the ops-mason-build policy, which can create policies,
|
# * scoped — carries the read-only ops-mason-survey policy: it can list
|
||||||
# auth roles and KV *structure* but cannot read secret values.
|
# and read mounts, auth methods, policies, auth role definitions
|
||||||
# SCOPE.md's "never touch secret values" stops being a promise in
|
# and KV *metadata*, and query capabilities. It cannot write a
|
||||||
# a document and becomes something OpenBao enforces.
|
# policy or role, mint a secret-id or a token, or read a secret
|
||||||
|
# value (MASON-IN-0004, founder decision 2026-10-07). Builds go
|
||||||
|
# through the founder-attended railiance-platform applier.
|
||||||
# * bounded — 45 minutes, no renewal past one hour.
|
# * bounded — 45 minutes, no renewal past one hour.
|
||||||
# * named — display_name claude-<task>, so the audit log says which task an
|
# * named — display_name claude-<task>, so the audit log says which task an
|
||||||
# action belonged to, not just that platform-root did it.
|
# action belonged to, not just that platform-root did it.
|
||||||
|
|
@ -30,7 +32,7 @@ set -euo pipefail
|
||||||
|
|
||||||
export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}"
|
export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}"
|
||||||
GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}"
|
GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}"
|
||||||
GRANT_POLICY="${GRANT_POLICY:-ops-mason-build}"
|
GRANT_POLICY="${GRANT_POLICY:-ops-mason-survey}"
|
||||||
GRANT_TTL="${GRANT_TTL:-45m}"
|
GRANT_TTL="${GRANT_TTL:-45m}"
|
||||||
GRANT_MAX_TTL="${GRANT_MAX_TTL:-1h}"
|
GRANT_MAX_TTL="${GRANT_MAX_TTL:-1h}"
|
||||||
OIDC_PATH="${OIDC_PATH:-netkingdom}"
|
OIDC_PATH="${OIDC_PATH:-netkingdom}"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
./scripts/custody-inventory.py operators one mount or prefix
|
./scripts/custody-inventory.py operators one mount or prefix
|
||||||
./scripts/custody-inventory.py --undescribed only paths missing metadata
|
./scripts/custody-inventory.py --undescribed only paths missing metadata
|
||||||
|
|
||||||
Reads metadata only — never a value — so it runs under ops-mason-build and can
|
Reads metadata only — never a value — so it runs under ops-mason-survey and can
|
||||||
be handed to anyone orienting themselves without granting them a single secret.
|
be handed to anyone orienting themselves without granting them a single secret.
|
||||||
|
|
||||||
A path with no description is a finding, not a formatting problem: it is a
|
A path with no description is a finding, not a formatting problem: it is a
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ auth-role listings, KV metadata existence for the target path, matching
|
||||||
ops-warden routing catalog entries, and a draft answer to "does an existing lane
|
ops-warden routing catalog entries, and a draft answer to "does an existing lane
|
||||||
already satisfy this demand?".
|
already satisfy this demand?".
|
||||||
|
|
||||||
Metadata only. It runs under the ops-mason-build grant (~/.claude-bao-token),
|
Metadata only. It runs under the ops-mason-survey grant (~/.claude-bao-token),
|
||||||
refuses any other token, and every bao invocation passes `allowed_bao_call`, an
|
refuses any other token, and every bao invocation passes `allowed_bao_call`, an
|
||||||
allowlist that rejects `kv get` and every `*/data/*` path before a process starts.
|
allowlist that rejects `kv get` and every `*/data/*` path before a process starts.
|
||||||
Policy bodies are read with `bao policy read` (policy text, never a secret value)
|
Policy bodies are read with `bao policy read` (policy text, never a secret value)
|
||||||
|
|
@ -33,11 +33,11 @@ from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
GRANT_FILE = "~/.claude-bao-token"
|
GRANT_FILE = "~/.claude-bao-token"
|
||||||
GRANT_POLICY = "ops-mason-build"
|
GRANT_POLICY = "ops-mason-survey"
|
||||||
DEFAULT_ADDR = "http://127.0.0.1:18200"
|
DEFAULT_ADDR = "http://127.0.0.1:18200"
|
||||||
DEFAULT_CATALOG = "~/ops-warden/registry/routing/catalog.yaml"
|
DEFAULT_CATALOG = "~/ops-warden/registry/routing/catalog.yaml"
|
||||||
MAX_POLICY_READS = 15
|
MAX_POLICY_READS = 15
|
||||||
NOT_PERMITTED = "not permitted by ops-mason-build"
|
NOT_PERMITTED = "not permitted by ops-mason-survey"
|
||||||
# Path segments too generic to identify a consumer.
|
# Path segments too generic to identify a consumer.
|
||||||
GENERIC = {"platform", "workloads", "operators", "secret", "data", "metadata",
|
GENERIC = {"platform", "workloads", "operators", "secret", "data", "metadata",
|
||||||
"kv", "read", "workload", "role", "token", "auth", "policy"}
|
"kv", "read", "workload", "role", "token", "auth", "policy"}
|
||||||
|
|
@ -133,7 +133,7 @@ def grant_env(environ: dict | None = None) -> dict:
|
||||||
with open(grant) as f:
|
with open(grant) as f:
|
||||||
env["BAO_TOKEN"] = f.read().strip()
|
env["BAO_TOKEN"] = f.read().strip()
|
||||||
if not env.get("BAO_TOKEN"):
|
if not env.get("BAO_TOKEN"):
|
||||||
raise SurveyError("no ops-mason-build grant (~/.claude-bao-token absent and BAO_TOKEN unset); "
|
raise SurveyError("no ops-mason-survey grant (~/.claude-bao-token absent and BAO_TOKEN unset); "
|
||||||
"ask the founder to run ./scripts/bao-session.sh grant <task>")
|
"ask the founder to run ./scripts/bao-session.sh grant <task>")
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ path "platform/metadata/workloads/inbound-library/imap" {
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
def lookup(policies=("default", "ops-mason-build")):
|
def lookup(policies=("default", "ops-mason-survey")):
|
||||||
return {"data": {"id": TOKEN, "accessor": "acc-SECRET", "display_name": "token-claude-mason-wp-0008",
|
return {"data": {"id": TOKEN, "accessor": "acc-SECRET", "display_name": "token-claude-mason-wp-0008",
|
||||||
"policies": list(policies), "ttl": 2000}}
|
"policies": list(policies), "ttl": 2000}}
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ class FakeBao:
|
||||||
"sys/": {"type": "system"}},
|
"sys/": {"type": "system"}},
|
||||||
("auth", "list"): {"approle/": {"type": "approle"}, "kubernetes/": {"type": "kubernetes"},
|
("auth", "list"): {"approle/": {"type": "approle"}, "kubernetes/": {"type": "kubernetes"},
|
||||||
"token/": {"type": "token"}},
|
"token/": {"type": "token"}},
|
||||||
("policy", "list"): ["default", "root", "ops-mason-build", "workload-kv-read-inbound-library-imap",
|
("policy", "list"): ["default", "root", "ops-mason-survey", "workload-kv-read-inbound-library-imap",
|
||||||
"workload-kv-read-other"],
|
"workload-kv-read-other"],
|
||||||
("list", "auth/approle/role"): ["inbound-library", "rein-openweights"],
|
("list", "auth/approle/role"): ["inbound-library", "rein-openweights"],
|
||||||
("list", "auth/kubernetes/role"): ["external-secrets-activity-core"],
|
("list", "auth/kubernetes/role"): ["external-secrets-activity-core"],
|
||||||
|
|
@ -149,20 +149,20 @@ def test_missing_capability_degrades_and_is_reported(catalog, capsys):
|
||||||
("policy", "read", "workload-kv-read-inbound-library-imap")})
|
("policy", "read", "workload-kv-read-inbound-library-imap")})
|
||||||
rc, out = run(fake, catalog, capsys)
|
rc, out = run(fake, catalog, capsys)
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
assert "not permitted by ops-mason-build" in out
|
assert "not permitted by ops-mason-survey" in out
|
||||||
assert "incomplete:" in out
|
assert "incomplete:" in out
|
||||||
|
|
||||||
|
|
||||||
def test_no_grant_fails_loudly(monkeypatch, tmp_path):
|
def test_no_grant_fails_loudly(monkeypatch, tmp_path):
|
||||||
monkeypatch.setattr(survey.os.path, "expanduser", lambda p: str(tmp_path / "absent"))
|
monkeypatch.setattr(survey.os.path, "expanduser", lambda p: str(tmp_path / "absent"))
|
||||||
with pytest.raises(survey.SurveyError, match="no ops-mason-build grant"):
|
with pytest.raises(survey.SurveyError, match="no ops-mason-survey grant"):
|
||||||
survey.main(ARGS, runner=FakeBao(), environ={})
|
survey.main(ARGS, runner=FakeBao(), environ={})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("policies", [("root",), ("default", "ops-mason-build", "platform-admin"), ("default",)])
|
@pytest.mark.parametrize("policies", [("root",), ("default", "ops-mason-survey", "platform-admin"), ("default",)])
|
||||||
def test_non_grant_token_is_refused(catalog, policies):
|
def test_non_grant_token_is_refused(catalog, policies):
|
||||||
fake = FakeBao(table={("token", "lookup"): lookup(policies)})
|
fake = FakeBao(table={("token", "lookup"): lookup(policies)})
|
||||||
with pytest.raises(survey.SurveyError, match="not an ops-mason-build grant") as exc:
|
with pytest.raises(survey.SurveyError, match="not an ops-mason-survey grant") as exc:
|
||||||
survey.main([*ARGS, "--catalog", str(catalog)], runner=fake, environ={"BAO_TOKEN": TOKEN})
|
survey.main([*ARGS, "--catalog", str(catalog)], runner=fake, environ={"BAO_TOKEN": TOKEN})
|
||||||
assert TOKEN not in str(exc.value)
|
assert TOKEN not in str(exc.value)
|
||||||
assert len(fake.calls) == 1
|
assert len(fake.calls) == 1
|
||||||
|
|
@ -182,7 +182,7 @@ def test_script_exits_nonzero_without_grant(tmp_path):
|
||||||
p = subprocess.run([sys.executable, str(Path(__file__).parents[1] / "scripts/plan-survey.py"), *ARGS],
|
p = subprocess.run([sys.executable, str(Path(__file__).parents[1] / "scripts/plan-survey.py"), *ARGS],
|
||||||
capture_output=True, text=True, env=env)
|
capture_output=True, text=True, env=env)
|
||||||
assert p.returncode == 2
|
assert p.returncode == 2
|
||||||
assert "ERROR: no ops-mason-build grant" in p.stderr
|
assert "ERROR: no ops-mason-survey grant" in p.stderr
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("args", [
|
@pytest.mark.parametrize("args", [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue