Founder decision 2026-10-18, "build, but never log in": a plan-scoped grant
that writes workload-kv-read-* policies, external-secrets-* and AppRole role
definitions, and create-only placeholder KV under platform/{workloads,suppliers},
and denies role-id, secret-id, token creation and all value reads, so the
MASON-IN-0004 chain stays broken. Adds bao-session.sh build, policy tests,
INTENT/AGENTS alignment and MASON-WP-0015. Not installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 278674@bnt-lap001
Assistant-Session: 64827407-e196-4ce3-b70c-76e7b537493f
141 lines
5.9 KiB
Bash
Executable file
141 lines
5.9 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# bao-session.sh — check, or grant, an OpenBao session an agent session can use.
|
|
#
|
|
# ./scripts/bao-session.sh status what session exists, if any
|
|
# ./scripts/bao-session.sh login refresh YOUR operator session (OIDC)
|
|
# ./scripts/bao-session.sh grant <task> mint a scoped, time-boxed token for Claude
|
|
# ./scripts/bao-session.sh build <task> same, with ops-mason-construct (approved plan only)
|
|
# ./scripts/bao-session.sh revoke end the granted token immediately
|
|
#
|
|
# Why `grant` exists
|
|
# ------------------
|
|
# Handing an agent your own platform-admin session gives it everything you have,
|
|
# for as long as you have it, indistinguishable from you in the audit log. The
|
|
# grant path instead mints a separate token that is:
|
|
#
|
|
# * scoped — carries the read-only ops-mason-survey policy: it can list
|
|
# and read mounts, auth methods, policies, auth role definitions
|
|
# and KV *metadata*, and query capabilities. It cannot write a
|
|
# policy or role, mint a secret-id or a token, or read a secret
|
|
# value (MASON-IN-0004, founder decision 2026-10-07).
|
|
# `build` carries ops-mason-construct instead (MASON-IN-0005,
|
|
# 2026-10-18): it writes workload-kv-read-* policies, ESO/AppRole
|
|
# role definitions and placeholder KV structure, and still cannot
|
|
# read a role-id, mint a secret-id or token, or read a value.
|
|
# Mint it only after the plan is approved.
|
|
# * bounded — 45 minutes, no renewal past one hour.
|
|
# * named — display_name claude-<task>, so the audit log says which task an
|
|
# action belonged to, not just that platform-root did it.
|
|
#
|
|
# It is written to ~/.claude-bao-token, NOT ~/.vault-token, so your own operator
|
|
# session is untouched and the two can be revoked independently.
|
|
#
|
|
# Under WSL2 the OIDC browser launch fails (gio cannot open a browser). This
|
|
# script prints the URL plainly and waits, rather than appearing to hang.
|
|
|
|
set -euo pipefail
|
|
|
|
export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}"
|
|
GRANT_FILE="${GRANT_FILE:-$HOME/.claude-bao-token}"
|
|
GRANT_POLICY="${GRANT_POLICY:-ops-mason-survey}"
|
|
GRANT_TTL="${GRANT_TTL:-45m}"
|
|
GRANT_MAX_TTL="${GRANT_MAX_TTL:-1h}"
|
|
OIDC_PATH="${OIDC_PATH:-netkingdom}"
|
|
|
|
green() { printf '\033[32m%s\033[0m\n' "$1"; }
|
|
red() { printf '\033[31m%s\033[0m\n' "$1"; }
|
|
|
|
describe() {
|
|
# Prints session metadata only. Never the token.
|
|
local token="${1:-}" label="$2"
|
|
local out
|
|
if ! out="$(BAO_TOKEN="$token" bao token lookup -format=json 2>/dev/null)"; then
|
|
red " $label: none or expired"
|
|
return 1
|
|
fi
|
|
printf '%s' "$out" | python3 -c "
|
|
import sys, json
|
|
d = json.load(sys.stdin)['data']
|
|
print(' $label: ttl=%s policies=%s name=%s' % (
|
|
d.get('ttl'), ','.join(d.get('policies', [])), d.get('display_name') or '-'))
|
|
"
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "BAO_ADDR=$BAO_ADDR"
|
|
describe "$(cat "$HOME/.vault-token" 2>/dev/null || true)" "your operator session" || true
|
|
if [[ -f "$GRANT_FILE" ]]; then
|
|
describe "$(cat "$GRANT_FILE")" "granted to Claude " || true
|
|
else
|
|
echo " granted to Claude : none ($GRANT_FILE absent)"
|
|
fi
|
|
}
|
|
|
|
cmd_login() {
|
|
# No TTY guard: the OIDC flow reads nothing from stdin. It starts a local
|
|
# callback listener, prints a URL, and waits for the browser to complete it,
|
|
# so this works fine from a non-interactive invocation.
|
|
echo "Opening OIDC login. Under WSL2 no browser will launch —"
|
|
echo "copy the URL below into your Windows browser, then come back here."
|
|
echo
|
|
# -no-print: the token goes only to the token helper, never to the
|
|
# terminal (it once landed in an agent transcript, 2026-10-08).
|
|
bao login -no-print -method=oidc -path="$OIDC_PATH"
|
|
echo
|
|
cmd_status
|
|
}
|
|
|
|
cmd_grant() {
|
|
local task="${1:-}"
|
|
if [[ -z "$task" ]]; then
|
|
red "usage: $0 grant <task-name> e.g. 'operators-mount'"; exit 2
|
|
fi
|
|
local probe_err
|
|
if ! probe_err="$(bao policy read "$GRANT_POLICY" 2>&1 >/dev/null)"; then
|
|
# Only bao's own "No policy named" means absent. A 403 or a missing
|
|
# token means your session cannot read it (expired, or not permitted),
|
|
# which says nothing about whether the policy exists.
|
|
if [[ "$probe_err" == *"No policy named"* ]]; then
|
|
red "policy $GRANT_POLICY does not exist yet."
|
|
echo "Create it first — see docs, or ask Claude to draft it. Refusing to"
|
|
echo "fall back to a broader policy: an unscoped grant is the thing this"
|
|
echo "script exists to avoid."
|
|
else
|
|
red "cannot read policy $GRANT_POLICY (session expired or not permitted)."
|
|
echo "Check '$0 status'; refresh with '$0 login' and retry. This is not"
|
|
echo "evidence that the policy is missing."
|
|
fi
|
|
exit 1
|
|
fi
|
|
umask 077
|
|
bao token create \
|
|
-policy="$GRANT_POLICY" \
|
|
-ttl="$GRANT_TTL" \
|
|
-explicit-max-ttl="$GRANT_MAX_TTL" \
|
|
-display-name="claude-${task}" \
|
|
-field=token > "$GRANT_FILE"
|
|
green "granted: $GRANT_POLICY for '$task', ttl $GRANT_TTL (max $GRANT_MAX_TTL)"
|
|
echo " file: $GRANT_FILE"
|
|
describe "$(cat "$GRANT_FILE")" "granted to Claude " || true
|
|
echo
|
|
echo "Claude uses it as: BAO_TOKEN=\$(cat $GRANT_FILE) bao <cmd>"
|
|
echo "Revoke any time: $0 revoke"
|
|
}
|
|
|
|
cmd_revoke() {
|
|
if [[ ! -f "$GRANT_FILE" ]]; then
|
|
echo "nothing to revoke ($GRANT_FILE absent)"; return 0
|
|
fi
|
|
bao token revoke "$(cat "$GRANT_FILE")" >/dev/null 2>&1 || true
|
|
shred -u "$GRANT_FILE" 2>/dev/null || rm -f "$GRANT_FILE"
|
|
green "granted token revoked and removed"
|
|
}
|
|
|
|
case "${1:-status}" in
|
|
status) cmd_status ;;
|
|
login) cmd_login ;;
|
|
grant) shift; cmd_grant "${1:-}" ;;
|
|
build) shift; GRANT_POLICY=ops-mason-construct; cmd_grant "${1:-}" ;;
|
|
revoke) cmd_revoke ;;
|
|
*) echo "usage: $0 status|login|grant <task>|build <task>|revoke" >&2; exit 2 ;;
|
|
esac
|