160 lines
7.2 KiB
Bash
Executable File
160 lines
7.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# The opencode config, generated rather than linked.
|
|
#
|
|
# Every other personal destination is a symlink or a one-time copy, so getting
|
|
# one wrong is visible and cheap to undo. This one is written whole by
|
|
# setup/scripts/link-mcp, and it is the only file in the set that carries
|
|
# secrets -- which is the reason it is generated at all. Three properties are
|
|
# worth pinning:
|
|
#
|
|
# 1. The tracked template is the source of truth and holds no secret. It is
|
|
# readable by anyone who finds the repository, so a token in it is a
|
|
# disclosure rather than a mistake.
|
|
# 2. The generation is faithful: the template's keys survive, the MCP block
|
|
# appears, and a leading ~/ is resolved so the output is readable.
|
|
# 3. Nothing is destroyed and nothing churns. A file somebody wrote by hand
|
|
# goes to config/old/, and running the stage again does not fill it up.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
linker="$repo_dir/setup/scripts/link-mcp"
|
|
template="$repo_dir/user/agents/opencode/config.json"
|
|
servers="$repo_dir/user/agents/mcp/servers"
|
|
|
|
findings=()
|
|
note() { findings+=("$1"); }
|
|
|
|
[[ -x "$linker" ]] || { printf 'opencode config contract: %s is not executable\n' "$linker" >&2; exit 1; }
|
|
|
|
# ── 1. The template holds no secret ──────────────────────────────────────────
|
|
|
|
if [[ ! -r "$template" ]]; then
|
|
note 'user/agents/opencode/config.json is missing, so nothing generates an opencode config'
|
|
else
|
|
jq -e . "$template" >/dev/null 2>&1 \
|
|
|| note 'user/agents/opencode/config.json is not valid JSON'
|
|
|
|
if grep -qE 'BEGIN [A-Z ]*PRIVATE KEY|sk-ant-[A-Za-z0-9]|ghp_[A-Za-z0-9]{20}|xox[baprs]-[A-Za-z0-9]' "$template" 2>/dev/null; then
|
|
note 'user/agents/opencode/config.json carries something that looks like a token'
|
|
fi
|
|
fi
|
|
|
|
[[ -r "$servers" ]] || note 'user/agents/mcp/servers is missing, so no MCP servers can be registered'
|
|
|
|
# ── 2 and 3, against a throwaway home ────────────────────────────────────────
|
|
|
|
work="$(mktemp -d)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
checkout="$work/Panama"
|
|
mkdir -p "$checkout/setup/scripts" "$checkout/user/agents/mcp/bin" "$checkout/user/agents/opencode"
|
|
cp "$linker" "$checkout/setup/scripts/link-mcp"
|
|
cp "$template" "$checkout/user/agents/opencode/config.json"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$checkout/user/agents/mcp/bin/gamma"
|
|
chmod +x "$checkout/user/agents/mcp/bin/gamma"
|
|
cat >"$checkout/user/agents/mcp/servers" <<'FIXTURE'
|
|
# a comment, then a row that needs a token, one that needs none, and a stdio one
|
|
|
|
alpha http https://alpha.example/mcp ALPHA_TOKEN
|
|
beta http https://beta.example/mcp
|
|
gamma stdio bin/gamma
|
|
FIXTURE
|
|
printf 'ALPHA_TOKEN=sekrit-token-value\n' >"$checkout/user/agents/mcp/env"
|
|
|
|
# link-mcp registers through whichever runtime it finds, and gives up entirely
|
|
# when it finds neither. A stub is enough to prove the config generation.
|
|
mkdir -p "$work/bin"
|
|
printf '#!/usr/bin/env bash\nexit 0\n' >"$work/bin/opencode"
|
|
chmod +x "$work/bin/opencode"
|
|
|
|
home="$work/home"
|
|
state="$work/state"
|
|
old="$checkout/config/old"
|
|
# Every XDG directory is pinned to the throwaway home. XDG_CONFIG_HOME is
|
|
# usually set in the environment this runs from, and leaving it alone would send
|
|
# the generated config at the real ~/.config/opencode rather than at the fixture.
|
|
mkdir -p "$home/.config/opencode" "$state/panama"
|
|
|
|
run() {
|
|
HOME="$home" XDG_CONFIG_HOME="$home/.config" XDG_STATE_HOME="$state" \
|
|
PANAMA_PATH="$checkout" PATH="$work/bin:$PATH" \
|
|
"$checkout/setup/scripts/link-mcp" >"$work/log" 2>&1
|
|
}
|
|
|
|
generated="$home/.config/opencode/opencode.jsonc"
|
|
|
|
# A file somebody wrote by hand is there before this stage ever runs.
|
|
printf '{"model": "someone-elses-hand-edit"}\n' >"$generated"
|
|
|
|
printf 'yes\n' >"$state/panama/user-content"
|
|
if ! run; then
|
|
note "link-mcp failed against a throwaway home: $(tail -1 "$work/log")"
|
|
fi
|
|
|
|
# ── The hand-edit is preserved, not deleted ──────────────────────────────────
|
|
|
|
if ! grep -rq 'someone-elses-hand-edit' "$old" 2>/dev/null; then
|
|
note 'a config written by hand was not preserved in config/old/'
|
|
fi
|
|
|
|
# ── The generation is faithful ───────────────────────────────────────────────
|
|
|
|
body="$(sed '/^\/\//d' "$generated" 2>/dev/null)"
|
|
|
|
jq -e . <<<"$body" >/dev/null 2>&1 \
|
|
|| note 'the generated opencode config is not valid JSON once its header comment is stripped'
|
|
grep -qF 'generated by setup/scripts/link-mcp' "$generated" 2>/dev/null \
|
|
|| note 'the generated opencode config does not say who wrote it or where to edit instead'
|
|
|
|
if ! jq -e --arg home "$home" '.instructions | index($home + "/.claude/rules/*.md")' <<<"$body" >/dev/null 2>&1; then
|
|
note 'the generated opencode config did not resolve the ~/ rules path to this home'
|
|
fi
|
|
jq -e '.permissions | length > 0' <<<"$body" >/dev/null 2>&1 \
|
|
|| note 'the template permissions did not reach the generated opencode config'
|
|
|
|
jq -e '.mcp.alpha.headers.Authorization == "sekrit-token-value"' <<<"$body" >/dev/null 2>&1 \
|
|
|| note 'a token-carrying server was registered without its Authorization header'
|
|
jq -e '.mcp.beta | has("headers") | not' <<<"$body" >/dev/null 2>&1 \
|
|
|| note 'a server that needs no token was given a header anyway'
|
|
jq -e --arg launcher "$checkout/user/agents/mcp/bin/gamma" '.mcp.gamma.command == [$launcher]' <<<"$body" >/dev/null 2>&1 \
|
|
|| note 'a stdio server was not registered against its launcher in this checkout'
|
|
|
|
# The file holds tokens, so it is private. Checked on the file rather than on
|
|
# the umask, because the umask is not what a later reader sees.
|
|
mode="$(stat -c '%a' "$generated" 2>/dev/null)"
|
|
[[ "$mode" == 600 ]] || note "the generated opencode config is mode $mode, not 600"
|
|
|
|
# The template is readable by anybody who finds the repository. The tokens live
|
|
# in the generated file and in user/agents/mcp/env, never here.
|
|
if grep -qF 'sekrit-token-value' "$template" 2>/dev/null; then
|
|
note 'the tracked opencode template carries an MCP token'
|
|
fi
|
|
|
|
# ── Re-running replaces, and does not churn ──────────────────────────────────
|
|
|
|
before="$(find "$old" -type f 2>/dev/null | wc -l)"
|
|
run
|
|
after="$(find "$old" -type f 2>/dev/null | wc -l)"
|
|
[[ "$before" == "$after" ]] \
|
|
|| note 're-running link-mcp buried its own output in config/old/, so it grows every upgrade'
|
|
grep -qF 'generated by setup/scripts/link-mcp' "$generated" 2>/dev/null \
|
|
|| note 're-running link-mcp did not rewrite the config it generates'
|
|
|
|
# A machine that never said yes is left alone, the same gate every other
|
|
# personal destination obeys.
|
|
rm -f "$generated"
|
|
printf 'no\n' >"$state/panama/user-content"
|
|
run
|
|
[[ ! -e "$generated" ]] \
|
|
|| note 'an opencode config was generated on a machine that never said yes'
|
|
|
|
if (( ${#findings[@]} > 0 )); then
|
|
printf 'opencode config contract: %d finding(s)\n' "${#findings[@]}" >&2
|
|
printf ' - %s\n' "${findings[@]}" >&2
|
|
exit 1
|
|
fi
|
|
|
|
printf 'opencode config contract: PASS\n'
|