Skip to content

Commit caeb14e

Browse files
feat(foundry): add the missing Configure stage; make the wizard fail closed (#102)
## The gap `SPEC.adoc` specifies `mint → provision → configure → harness`, but stage 3 of 4 in `foundry.sh` was a single `say` line — **`stages/configure.sh` did not exist**. Several other paths could report success having done nothing. ## What this adds ### `stages/configure.sh` (new) Applies endpoint and auth settings under the two rules `Foundry.idr` types as `configure : Artifact ds caps -> Artifact ds caps`: | Rule | Meaning | |---|---| | **PRESERVATION** | the `capabilities` block is compared before/after and must be byte-identical — configure may never edit the grant | | **NO ESCALATION** | a setting requiring a `locked_down` capability is refused | | Setting | Requires | |---|---| | `--api-base` loopback (`local://`, `127.0.0.1`, `localhost`, `[::1]`) | — | | `--api-base` remote | `Net` | | `--auth` ≠ `none` | `Cred` | | `--auth-env` / `--auth-source` | `Cred`, **unconditionally** | That last row is deliberate. Deciding it from the manifest's *current* `auth.method` — which is what I wrote first — lets credential wiring land in a freshly-minted cartridge (where `method` is `none`) and be activated by a later `--auth`. A two-step escalation. The test suite caught it. ### Fail-closed wizard — `--strict` / `FOUNDRY_STRICT=1` Missing `deno`/`idris2`/`jq` is now a failure, not a skip. Previously, with `deno` absent, the wizard scaffolded nothing, skipped provision/configure/harness, printed **"Foundry · done."** and **exited 0**. ### Four further defects found while wiring it up 1. **`foundry.sh` bypassed `stages/mint.sh`** — it called `mint.ts` directly then re-derived the path with `find -name`, a second copy of the logic. 2. **`mint.sh` corrupted its own output contract** — `deno run … 2>&1` folded Deno's diagnostics into stdout, the channel carrying the cartridge path. Now `>&2`. 3. **The interactive wizard could never mint.** The minter requires `protocols`; the wizard never prompted for it or wrote it, so every interactively-produced descriptor was rejected at Mint. 4. **`typecheck-proofs.sh` could pass vacuously** — `PASS=0 FAIL=0` exited 0, a green run having verified nothing. ### Honest toolchain diagnostics `harness.sh` reported a bare `zig build failed` where the real cause was a version mismatch. It now compares `zig version` against the repo pin: ``` FAIL zig build failed under zig 0.16.0, but this repo pins zig 0.15.1 TOOLCHAIN MISMATCH — this is very likely not a cartridge defect. first error: build.zig:35:8: error: no field or member function named 'linkLibC' ``` ## Verification (run locally, toolchains present) - `test-foundry.sh` — **25/25 pass** (was 12; 13 new Configure cases). Still pure bash+jq, so it runs anywhere CI has `jq`. - `typecheck-proofs.sh` — **116 proofs typecheck**, `FAIL=0`, under real Idris2. - Vacuous-pass guard exits `1` on an empty tree. - Full `foundry.sh --strict --from <toml>` run executes all four stages end-to-end. - The escalation path was exercised for real: a descriptor granting only `Net` while requesting `api-key` auth was correctly **refused** at Configure. ## Note `.tool-versions` pins zig `0.15.1` while `zig-test.yml` uses `0.15.2`. Not addressed here, but worth reconciling. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 197185e commit caeb14e

7 files changed

Lines changed: 354 additions & 35 deletions

File tree

scripts/typecheck-proofs.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,17 @@ fi
5757
echo "────────────────────────────────────────"
5858
echo "Proof type-check: PASS=${pass} FAIL=${fail}"
5959
[ "$fail" -eq 0 ] || { echo "PROOF TYPECHECK FAILED"; exit 1; }
60+
61+
# Vacuous-pass guard. PASS=0/FAIL=0 means the `find` matched nothing — a moved
62+
# directory, a bad checkout, or a future refactor that renames `abi/`. Without
63+
# this the gate reports success having verified NOTHING, which is precisely the
64+
# failure mode this script was written to prevent. A repo with zero proofs is
65+
# not a passing repo; it is a broken gate.
66+
if [ "$pass" -eq 0 ]; then
67+
echo "PROOF TYPECHECK FAILED: no proofs were found to check." >&2
68+
echo " Expected .idr files under cartridges/**/abi and/or tools/foundry/proof." >&2
69+
echo " A green run with zero proofs verified would be a false assurance." >&2
70+
exit 1
71+
fi
72+
6073
echo "All proofs type-check under the pinned toolchain."

tools/foundry/SPEC.adoc

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,36 @@ Three principles make that possible:
5353
against the shared shim, the truthfulness probe returns non-stub. | `Truthful`
5454
|===
5555

56+
=== Configure — what a setting is allowed to do
57+
58+
`stages/configure.sh` applies settings under two mechanically-checked rules,
59+
the operational counterpart of `configure : Artifact ds caps -> Artifact ds caps`:
60+
61+
PRESERVATION::
62+
The `capabilities` block written by Provision is compared before and after and
63+
must be byte-identical. Configure may never edit the grant.
64+
65+
NO ESCALATION::
66+
A setting whose use would require a `locked_down` capability is refused.
67+
Configuration cannot smuggle in authority that provisioning denied.
68+
69+
[cols="2,1,3",options="header"]
70+
|===
71+
| Setting | Requires | Rule
72+
| `--api-base` (loopback) | — | `local://<name>`, `127.0.0.1`, `localhost` and
73+
`[::1]` forms address a loopback backend, which needs no network authority.
74+
| `--api-base` (remote) | `Net` | Any other address reaches the network.
75+
| `--auth` (≠ `none`) | `Cred` | Presenting a credential is exactly `Cred`.
76+
| `--auth-env`, `--auth-source` | `Cred` | *Unconditionally* — naming a secret is
77+
itself credential authority. Deciding this from the manifest's current
78+
`auth.method` would let credential wiring be written into a freshly-minted
79+
cartridge (where `method` is `none`) and activated by a later `--auth`.
80+
|===
81+
82+
Configure also refuses a cartridge with no `capabilities` block: without a
83+
provisioned grant there is no authority to check against, so it fails closed
84+
rather than configure something whose authority is unknown.
85+
5686
[#assurance]
5787
== Assurance (proof of design)
5888

tools/foundry/foundry.sh

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,67 @@
1010
# The user's only real effort is choosing settings; everything else is derived
1111
# from the proven pre-mint framework. See SPEC.adoc.
1212
#
13+
# FAIL-CLOSED (--strict). The wizard's value is the assurance it carries, so a
14+
# missing toolchain must not silently degrade it. In strict mode an absent
15+
# deno/idris2/jq is a FAILURE, not a skip. Without --strict the wizard is
16+
# permissive for local exploration, but it will still never claim success for a
17+
# cartridge it did not actually produce.
18+
#
1319
# Usage:
1420
# foundry.sh # interactive wizard
1521
# foundry.sh --from minter.toml # non-interactive (CI / scripted)
22+
# foundry.sh --strict --from f.toml# fail on any missing toolchain (use in CI)
1623
# foundry.sh --harness <dir> # just re-run the assurance gate on a dir
24+
#
25+
# Env: FOUNDRY_STRICT=1 is equivalent to passing --strict.
1726
set -euo pipefail
1827

1928
HERE="$(cd "$(dirname "$0")" && pwd)"
20-
REPO="$(cd "$HERE/../.." && pwd)"
21-
MINTER="$REPO/tools/cartridge-minter/mint.ts"
2229
KINDS="domain coordination agentic nesy"
2330
CAPS="Net Fs Cred Clock Rand"
2431

2532
say() { printf '\033[1m%s\033[0m\n' "$*"; }
2633
note(){ printf ' %s\n' "$*"; }
2734

35+
STRICT="${FOUNDRY_STRICT:-0}"
36+
37+
# need <tool> <why> — in strict mode a missing tool aborts; otherwise it reports
38+
# the degradation and returns 1 so the caller can branch.
39+
need() {
40+
if command -v "$1" >/dev/null 2>&1; then return 0; fi
41+
if [ "$STRICT" = 1 ]; then
42+
echo "ABORT (--strict): '$1' is required — $2" >&2
43+
exit 1
44+
fi
45+
note "$1 absent — $2 (re-run with --strict to make this an error)"
46+
return 1
47+
}
48+
49+
# ── argument pre-pass: --strict may appear anywhere ────────────────────────
50+
ARGS=()
51+
for a in "$@"; do
52+
case "$a" in
53+
--strict) STRICT=1 ;;
54+
*) ARGS+=("$a") ;;
55+
esac
56+
done
57+
set -- ${ARGS[@]+"${ARGS[@]}"}
58+
2859
# ── re-run the harness only ────────────────────────────────────────────────
2960
if [ "${1:-}" = "--harness" ]; then
3061
exec "$HERE/stages/harness.sh" "${2:?usage: --harness <cartridge-dir>}" "${@:3}"
3162
fi
3263

3364
# ── 0 · confirm the design proof still holds before minting anything ────────
3465
say "Foundry · verifying the design proof (no dropped proofs, least authority)"
35-
if command -v idris2 >/dev/null 2>&1; then
66+
if need idris2 "the Foundry design proof cannot be checked locally"; then
3667
"$HERE/proof/check.sh" >/dev/null && note "proof/Foundry.idr ✓ typechecks" \
3768
|| { echo "ABORT: the Foundry design proof does not hold — refusing to mint." >&2; exit 1; }
38-
else
39-
note "idris2 absent — skipping local proof check (CI enforces it)"
4069
fi
4170

4271
# ── gather settings ────────────────────────────────────────────────────────
4372
TOML=""
73+
API_BASE=""; AUTH_METHOD=""; AUTH_ENV=""
4474
if [ "${1:-}" = "--from" ]; then
4575
TOML="${2:?--from needs a path}"
4676
[ -f "$TOML" ] || { echo "no such file: $TOML" >&2; exit 2; }
@@ -50,61 +80,82 @@ else
5080
read -rp " one-line description: " DESC
5181
read -rp " domain (e.g. Archive, Comms, Cloud): " DOMAIN
5282
read -rp " tier [Teranga|Shield|Ayo] (Ayo): " TIER; TIER="${TIER:-Ayo}"
83+
# `protocols` is REQUIRED by the minter's validator. The wizard used to omit
84+
# it, so every interactively-produced descriptor was rejected at Mint.
85+
read -rp " protocols, comma-sep [mcp|rest|grpc|graphql|lsp|dap|bsp] (mcp): " PROTOCOLS
86+
PROTOCOLS="${PROTOCOLS:-mcp}"
5387
read -rp " kind [$KINDS] (domain): " KIND; KIND="${KIND:-domain}"
5488
read -rp " granted capabilities, comma-sep from [$CAPS] (blank = none, maximally inert): " GRANT
5589
note "fork-per-request (ADR-0005) ⇒ every grant is ephemeral; ungranted caps are locked-down/denied"
90+
read -rp " backend base_url (blank = local://<name> loopback): " API_BASE
91+
read -rp " auth method [none|api-key|oauth2|vault] (none): " AUTH_METHOD
92+
AUTH_METHOD="${AUTH_METHOD:-none}"
93+
[ "$AUTH_METHOD" = none ] || read -rp " auth env var (e.g. EXAMPLE_TOKEN): " AUTH_ENV
5694
TOML="$(mktemp --suffix=.minter.toml)"
5795
{
5896
echo "name = \"$NAME\""
5997
echo "description = \"$DESC\""
6098
echo "version = \"0.1.0\""
6199
echo "domain = \"$DOMAIN\""
62100
echo "tier = \"$TIER\""
101+
# CSV -> TOML array of strings, e.g. `mcp,rest` -> ["mcp", "rest"].
102+
echo "protocols = [$(printf '%s' "$PROTOCOLS" | tr ',' '\n' \
103+
| sed -E 's/^[[:space:]]+|[[:space:]]+$//g; /^$/d; s/^/"/; s/$/"/' \
104+
| paste -sd, -)]"
63105
[ "$KIND" = domain ] || { echo "category = \"cross-cutting\""; echo "cross_cutting_category = \"$KIND\""; }
64106
# Capability plan consumed by the Provision stage (provision.sh --granted).
65107
echo "granted = \"$GRANT\""
108+
# Settings consumed by the Configure stage (configure.sh).
109+
[ -n "$API_BASE" ] && echo "api_base = \"$API_BASE\""
110+
[ "$AUTH_METHOD" != none ] && echo "auth_method = \"$AUTH_METHOD\""
111+
[ -n "$AUTH_ENV" ] && echo "auth_env = \"$AUTH_ENV\""
66112
} > "$TOML"
67113
note "wrote $TOML"
68114
fi
69115

70-
# In --from mode the grant lives in the descriptor; read it (default: none).
116+
# Read any settings that live in the descriptor (both --from and interactive).
117+
toml_get() { sed -nE "s/^[[:space:]]*$1[[:space:]]*=[[:space:]]*\"([^\"]*)\".*/\1/p" "$TOML" | head -1; }
71118
: "${GRANT:=}"
72-
if [ -z "$GRANT" ] && grep -qE '^[[:space:]]*granted[[:space:]]*=' "$TOML"; then
73-
GRANT="$(sed -nE 's/^[[:space:]]*granted[[:space:]]*=[[:space:]]*"([^"]*)".*/\1/p' "$TOML" | head -1)"
74-
fi
119+
[ -n "$GRANT" ] || GRANT="$(toml_get granted)"
120+
[ -n "$API_BASE" ] || API_BASE="$(toml_get api_base)"
121+
[ -n "$AUTH_METHOD" ] || AUTH_METHOD="$(toml_get auth_method)"
122+
[ -n "$AUTH_ENV" ] || AUTH_ENV="$(toml_get auth_env)"
75123

76-
DEST_NAME="$(grep -E '^name' "$TOML" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
77-
78-
# ── 1 · MINT (delegate to the existing minter) ─────────────────────────────
124+
# ── 1 · MINT (delegate to the Mint stage — single source of path logic) ────
79125
say "1/4 · mint — scaffold from the proven template"
80-
if command -v deno >/dev/null 2>&1; then
81-
deno run --allow-read --allow-write "$MINTER" "$TOML"
82-
note "minted via cartridge-minter"
83-
else
84-
note "deno not installed here — the Mint stage shells to $MINTER"
85-
note "(install deno to run end-to-end; the rest of the flow is toolchain-local)"
126+
CART_DIR=""
127+
if need deno "the Mint stage cannot scaffold a cartridge"; then
128+
# mint.sh's stdout is the scaffolded path; its chatter goes to stderr.
129+
CART_DIR="$("$HERE/stages/mint.sh" "$TOML")"
130+
[ -n "$CART_DIR" ] && [ -d "$CART_DIR" ] \
131+
|| { echo "ABORT: Mint reported '$CART_DIR' but no such directory exists." >&2; exit 1; }
132+
note "minted -> $CART_DIR"
86133
fi
87134

88-
CART_DIR="$(find "$REPO/cartridges" -type d -name "$DEST_NAME" 2>/dev/null | head -1)"
135+
# Without a minted directory the remaining stages have nothing to act on. Say so
136+
# plainly and exit non-zero: the wizard must never report success for a
137+
# cartridge it did not produce.
138+
if [ -z "$CART_DIR" ]; then
139+
echo "Foundry: INCOMPLETE — no cartridge was minted, so provision/configure/harness did not run." >&2
140+
echo " Install deno and re-run, or gate an existing cartridge with: foundry.sh --harness <dir>" >&2
141+
exit 1
142+
fi
89143

90144
# ── 2 · PROVISION (grant exactly the chosen caps; record the partition) ────
91145
say "2/4 · provision — grant exactly the capabilities chosen, nothing more"
92-
if [ -n "$CART_DIR" ]; then
93-
"$HERE/stages/provision.sh" "$CART_DIR" --granted "$GRANT"
94-
note "least authority: ungranted capabilities are locked-down (provably absent — see proof)"
95-
else
96-
note "cartridge dir not found (Mint did not run — deno absent). Provision recorded plan: granted=[$GRANT]"
97-
fi
146+
"$HERE/stages/provision.sh" "$CART_DIR" --granted "$GRANT"
147+
note "least authority: ungranted capabilities are locked-down (provably absent — see proof)"
98148

99-
# ── 3 · CONFIGURE (settings already captured in the descriptor) ────────────
149+
# ── 3 · CONFIGURE (apply settings; cannot drop a proof or widen the grant) ──
100150
say "3/4 · configure — apply settings (preserves every proof + the grant)"
151+
CFG_ARGS=()
152+
[ -n "$API_BASE" ] && CFG_ARGS+=(--api-base "$API_BASE")
153+
[ -n "$AUTH_METHOD" ] && [ "$AUTH_METHOD" != none ] && CFG_ARGS+=(--auth "$AUTH_METHOD")
154+
[ -n "$AUTH_ENV" ] && CFG_ARGS+=(--auth-env "$AUTH_ENV")
155+
"$HERE/stages/configure.sh" "$CART_DIR" ${CFG_ARGS[@]+"${CFG_ARGS[@]}"}
101156

102157
# ── 4 · HARNESS (the one standard assurance gate) ──────────────────────────
103158
say "4/4 · harness — run the standard assurance gate"
104-
if [ -n "$CART_DIR" ]; then
105-
"$HERE/stages/harness.sh" "$CART_DIR" --granted "$GRANT"
106-
else
107-
note "harness skipped (no minted dir). To gate an existing cartridge: foundry.sh --harness <dir>"
108-
fi
159+
"$HERE/stages/harness.sh" "$CART_DIR" --granted "$GRANT"
109160

110-
say "Foundry · done."
161+
say "Foundry · done$CART_DIR"

tools/foundry/stages/configure.sh

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: MPL-2.0
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# The Configure stage — apply the chosen settings (endpoint, auth, tools) to a
6+
# minted cartridge WITHOUT dropping a proof or widening the capability grant.
7+
#
8+
# This is the operational counterpart of `configure` in proof/Foundry.idr, where
9+
# the stage is typed so that it preserves both indices:
10+
#
11+
# configure : Artifact ds caps -> Artifact ds caps
12+
#
13+
# i.e. the discharged-obligation set `ds` and the capability set `caps` come out
14+
# EXACTLY as they went in. Here that becomes two mechanically-checked rules:
15+
#
16+
# PRESERVATION the `capabilities` block written by the Provision stage is
17+
# byte-identical before and after. Configure may never edit it.
18+
#
19+
# NO ESCALATION a setting whose use would REQUIRE a capability that is
20+
# `locked_down` is refused. Configuration cannot smuggle in
21+
# authority that provisioning denied.
22+
#
23+
# The capability a setting requires (universe: Net, Fs, Cred, Clock, Rand):
24+
#
25+
# .api.base_url -> `Net`, but ONLY when the URL is non-loopback. The schema
26+
# defines base_url as a loopback address (`local://<name>`
27+
# or `http://127.0.0.1:<port>`); a loopback backend needs no
28+
# network authority, a remote one does.
29+
# .auth.method -> `Cred` for anything other than `none`. Presenting a
30+
# credential is exactly the authority `Cred` denotes.
31+
#
32+
# Usage:
33+
# configure.sh <cartridge-dir> [--api-base URL] [--content-type CT]
34+
# [--auth METHOD] [--auth-env VAR]
35+
# [--auth-source SRC]
36+
#
37+
# configure.sh <dir> # no settings: a valid no-op
38+
# configure.sh <dir> --auth api-key --auth-env GITHUB_TOKEN
39+
#
40+
# Exit codes: 2 usage/missing cartridge · 3 missing jq · 1 escalation or invalid
41+
# setting · 4 INTERNAL (preservation violated — should be unreachable).
42+
set -euo pipefail
43+
44+
CART="${1:-}"
45+
[ -n "$CART" ] || { echo "usage: configure.sh <cartridge-dir> [--api-base URL] [--auth METHOD] ..." >&2; exit 2; }
46+
shift
47+
48+
[ -d "$CART" ] || { echo "configure: no such cartridge: $CART" >&2; exit 2; }
49+
command -v jq >/dev/null 2>&1 || { echo "configure: jq is required" >&2; exit 3; }
50+
CJ="$CART/cartridge.json"
51+
[ -f "$CJ" ] || { echo "configure: no cartridge.json at $CART" >&2; exit 2; }
52+
53+
API_BASE=""; CONTENT_TYPE=""; AUTH_METHOD=""; AUTH_ENV=""; AUTH_SRC=""
54+
SET_API=0; SET_CT=0; SET_AUTH=0; SET_ENV=0; SET_SRC=0
55+
while [ $# -gt 0 ]; do
56+
case "$1" in
57+
--api-base) API_BASE="${2-}"; SET_API=1; shift 2 ;;
58+
--content-type) CONTENT_TYPE="${2-}"; SET_CT=1; shift 2 ;;
59+
--auth) AUTH_METHOD="${2-}"; SET_AUTH=1; shift 2 ;;
60+
--auth-env) AUTH_ENV="${2-}"; SET_ENV=1; shift 2 ;;
61+
--auth-source) AUTH_SRC="${2-}"; SET_SRC=1; shift 2 ;;
62+
*) echo "configure: unknown arg: $1" >&2; exit 2 ;;
63+
esac
64+
done
65+
66+
name=$(jq -r '.name // "?"' "$CJ")
67+
echo "configure: $name"
68+
69+
# ── the grant we must not exceed ───────────────────────────────────────────
70+
# Absent a capabilities block the cartridge has not been provisioned; refuse
71+
# rather than silently configure something whose authority is unknown.
72+
if ! jq -e '(.capabilities? | type) == "object"' "$CJ" >/dev/null 2>&1; then
73+
echo "configure: no capabilities block — run the Provision stage first" >&2
74+
exit 1
75+
fi
76+
CAPS_BEFORE=$(jq -cS '.capabilities' "$CJ")
77+
granted() { jq -e --arg c "$1" '.capabilities.ephemeral | index($c) != null' "$CJ" >/dev/null 2>&1; }
78+
79+
deny() { # capability setting detail
80+
echo "configure: REFUSED — '$2' requires the '$1' capability, which is locked-down." >&2
81+
echo " $3" >&2
82+
echo " Re-provision with --granted \"...,$1\" if this authority is intended." >&2
83+
exit 1
84+
}
85+
86+
# ── validate + capability-check each requested setting ─────────────────────
87+
if [ "$SET_API" = 1 ]; then
88+
[ -n "$API_BASE" ] || { echo "configure: --api-base needs a value" >&2; exit 1; }
89+
# Loopback forms need no network authority; anything else does.
90+
case "$API_BASE" in
91+
local://*|http://127.0.0.1|http://127.0.0.1:*|http://localhost|http://localhost:*|http://\[::1\]|http://\[::1\]:*)
92+
: ;;
93+
*)
94+
granted Net || deny Net --api-base "'$API_BASE' is not a loopback address."
95+
;;
96+
esac
97+
fi
98+
99+
if [ "$SET_AUTH" = 1 ]; then
100+
case "$AUTH_METHOD" in
101+
none|api-key|oauth2|vault) : ;;
102+
*) echo "configure: --auth must be one of: none, api-key, oauth2, vault (got '$AUTH_METHOD')" >&2; exit 1 ;;
103+
esac
104+
if [ "$AUTH_METHOD" != none ]; then
105+
granted Cred || deny Cred --auth "auth method '$AUTH_METHOD' presents a credential."
106+
fi
107+
fi
108+
109+
# `env_var` and `credential_source` exist for one purpose: to name a secret. That
110+
# is the authority `Cred` denotes, so they require it UNCONDITIONALLY — we do not
111+
# consult the manifest's current `auth.method` to decide.
112+
#
113+
# Deciding from the manifest was the earlier behaviour and it was wrong: on a
114+
# freshly-minted cartridge `auth.method` is `none`, so credential wiring could be
115+
# written into a cartridge whose Cred was locked-down, and a later `--auth` would
116+
# then activate it. Requiring Cred for the act of naming a secret closes that
117+
# two-step path and is the simpler rule to reason about.
118+
if [ "$SET_ENV" = 1 ] || [ "$SET_SRC" = 1 ]; then
119+
granted Cred || deny Cred --auth-env "naming a credential source is itself credential authority."
120+
fi
121+
122+
# ── apply ──────────────────────────────────────────────────────────────────
123+
# One jq pass, so the manifest is never left half-written. `.capabilities` is
124+
# not referenced: preservation is by construction, then verified below.
125+
tmp=$(mktemp)
126+
jq \
127+
--arg api "$API_BASE" --argjson set_api "$SET_API" \
128+
--arg ct "$CONTENT_TYPE" --argjson set_ct "$SET_CT" \
129+
--arg am "$AUTH_METHOD" --argjson set_auth "$SET_AUTH" \
130+
--arg ae "$AUTH_ENV" --argjson set_env "$SET_ENV" \
131+
--arg as "$AUTH_SRC" --argjson set_src "$SET_SRC" '
132+
(if $set_api == 1 then .api.base_url = $api else . end)
133+
| (if $set_ct == 1 then .api.content_type = $ct else . end)
134+
| (if $set_auth == 1 then .auth.method = $am else . end)
135+
| (if $set_env == 1 then .auth.env_var = (if $ae == "" then null else $ae end) else . end)
136+
| (if $set_src == 1 then .auth.credential_source = (if $as == "" then null else $as end) else . end)
137+
' "$CJ" > "$tmp" && mv "$tmp" "$CJ"
138+
139+
# ── PRESERVATION: the grant must be untouched ──────────────────────────────
140+
CAPS_AFTER=$(jq -cS '.capabilities' "$CJ")
141+
if [ "$CAPS_BEFORE" != "$CAPS_AFTER" ]; then
142+
echo "configure: INTERNAL — the capabilities block changed; this must never happen." >&2
143+
echo " before: $CAPS_BEFORE" >&2
144+
echo " after : $CAPS_AFTER" >&2
145+
exit 4
146+
fi
147+
148+
changed=0
149+
report() { printf ' %-20s %s\n' "$1" "$2"; changed=1; }
150+
[ "$SET_API" = 1 ] && report "api.base_url" "$(jq -r '.api.base_url' "$CJ")"
151+
[ "$SET_CT" = 1 ] && report "api.content_type" "$(jq -r '.api.content_type' "$CJ")"
152+
[ "$SET_AUTH" = 1 ] && report "auth.method" "$(jq -r '.auth.method' "$CJ")"
153+
[ "$SET_ENV" = 1 ] && report "auth.env_var" "$(jq -r '.auth.env_var // "null"' "$CJ")"
154+
[ "$SET_SRC" = 1 ] && report "auth.credential_source" "$(jq -r '.auth.credential_source // "null"' "$CJ")"
155+
[ "$changed" = 1 ] || echo " (no settings supplied — nothing to apply)"
156+
157+
eph=$(jq -r '.capabilities.ephemeral | join(", ") | if . == "" then "(none)" else . end' "$CJ")
158+
echo " grant preserved : [$eph]"

0 commit comments

Comments
 (0)