Files
insights/bin/setup-gateway-e2e
zeekay 19fdf55ceb feat(auth): one key scheme, minted and read in one place
A key now opens with what it is allowed to touch. `pk-` is publishable and
safe in a browser; `sk-` is secret and never leaves a server. The five marks
it replaces all began `ph` -- the upstream's initials -- and they were the
last of its brand a customer read back to us, in every SDK snippet. No word
rule could reach them, because `ph` is not a word.

The marks were also not telling the truth. The constant said `phc_` while
every live project token said `hi_`, so the middleware that routes on the
constant matched no real token at all. Two spellings existed and one was
fiction. Minting now happens in exactly one function and reading in exactly
one, so a second spelling has nowhere to live.

`phx_` (personal) and `phs_` (project secret) both become `sk-`. The mark
now says only that a value is secret; which store holds it is settled by
asking. That was already how personal keys worked -- keys minted before the
marks carry none, so the store always decided -- and both secret resolvers
already returned None on a miss and fell through, so one shared extractor
replaces the two that could drift apart.

OAuth's access and refresh tokens stay distinguishable as `at-` and `rt-`,
because the parser tells them apart before either is looked up, and the
domain-verification token becomes `vt-` rather than keeping a sixth mint of
its own.

`bin/debrand --check` now refuses the marks as a shape, so a seventh cannot
be coined. Two rules that were about to rename things that are not ours to
spell are bounded: `Lemon` no longer eats `LemonSqueezy`, and the `hog_flow`
rules no longer reach a Postgres table, a Datastore table or a route.

Keys are stored hashed, so a mark cannot be rewritten in place: the three
live project tokens have to be reminted.
2026-08-08 19:42:44 -07:00

140 lines
6.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# Provision a real local ai-gateway e2e (resolver mode, not open/anonymous).
#
# Wires the full credential path end to end:
# 1. enable the gateway on the main local team
# 2. mint a deterministic sk- project-secret key (llm_gateway:read)
# 3. publish its credential blob to the SAME Valkey the Go gateway reads
# (localhost:6381) — Django's hypercache and the gateway must share one
# Redis or the resolver 401s
# 4. fund that team's ledger in the gateway's Postgres (admission needs it)
# 5. set the sibling gateway to AI_GATEWAY_AUTH_MODE=resolver
#
# Django defaults AI_GATEWAY_REDIS_URL to :6381 in dev — so no .env.local is
# needed.
# Idempotent. The gateway itself runs from the sibling repo:
# cd ~/Development/ai-gateway && bin/start gateway (or: just dev)
#
# Usage: bin/setup-gateway-e2e
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
AI_GATEWAY_REPO="${AI_GATEWAY_REPO:-$HOME/Development/ai-gateway}"
# The Go gateway reads credentials from its Valkey (sibling compose, host
# port 6381). Django must publish there too — same instance, or no match.
GATEWAY_REDIS_URL="${AI_GATEWAY_REDIS_URL:-redis://localhost:6381}"
# Deterministic so reruns + DB resets stay idempotent and the published blob's
# hash matches the runner's dev-default INSIGHTS_AI_GATEWAY_KEY (config.ts).
DEV_PHS="${DEV_GATEWAY_PHS:-sk-localgatewaye2elocalgatewaye2e0001}"
TOPUP_USD="${AI_GATEWAY_DEV_TOPUP_USD:-100.00}"
# Pin the sibling gateway's compose project to its own name. Insights's dev env
# exports COMPOSE_PROJECT_NAME=insights, which outranks the sibling compose's
# `name: ai-gateway` and would scope the gateway's postgres/valkey under the
# insights project (wrong, and collides with the insights stack).
export COMPOSE_PROJECT_NAME=ai-gateway
log() { printf '[setup-gateway-e2e] %s\n' "$*"; }
if [ ! -d "$AI_GATEWAY_REPO" ]; then
cat >&2 <<EOF
[setup-gateway-e2e] No sibling clone at $AI_GATEWAY_REPO.
git clone [email protected]:Insights/ai-gateway.git "$AI_GATEWAY_REPO"
(or set AI_GATEWAY_REPO=/path/to/clone)
EOF
exit 1
fi
if ! docker version --format '{{.Server.Version}}' >/dev/null 2>&1; then
echo "[setup-gateway-e2e] Docker daemon is not running. Start Docker Desktop and retry." >&2
exit 1
fi
# Upsert KEY=VALUE into a dotenv-style file (replace any existing line).
upsert_env() {
local file="$1" key="$2" value="$3"
touch "$file"
# Drop any existing assignment, then append the fresh one. grep -v exits 0
# (lines kept) or 1 (none kept — file empty or only this key); both are fine.
# Exit >=2 is a real grep error — don't overwrite the original with a possibly
# truncated temp; leave the file intact and just append below.
local rc=0
grep -vE "^${key}=" "$file" >"${file}.tmp" 2>/dev/null || rc=$?
if [ "$rc" -le 1 ]; then
mv "${file}.tmp" "$file"
else
rm -f "${file}.tmp"
fi
printf '%s=%s\n' "$key" "$value" >>"$file"
}
# 1. Sibling deps (postgres :5435 + valkey :6381). --remove-orphans clears the
# stale gateway/billing containers from the old `--profile full` era (the
# gateway runs on the host now, not as a container).
log "bringing up sibling gateway deps (postgres + valkey)"
(cd "$AI_GATEWAY_REPO" && docker compose up -d --wait --remove-orphans >/dev/null)
# 2-3. Enable the team, mint the sk-, publish the blob to the gateway's Valkey
# — all in the testable `setup_local_gateway_credential` management command
# (was an inline `manage.py shell` heredoc). AI_GATEWAY_REDIS_URL points the
# dedicated hypercache alias at :6381 so the write lands where the gateway
# reads (insights:1:cache/...).
log "provisioning sk- + publishing credential blob to $GATEWAY_REDIS_URL"
provision_out="$(
DEBUG="${DEBUG:-1}" AI_GATEWAY_REDIS_URL="$GATEWAY_REDIS_URL" \
python "$REPO_ROOT/manage.py" setup_local_gateway_credential --phs "$DEV_PHS" 2>&1
)" || true
TEAM_ID="$(printf '%s\n' "$provision_out" | sed -n 's/^__GATEWAY_E2E_TEAM_ID__=//p')"
if [ -z "${TEAM_ID:-}" ]; then
echo "[setup-gateway-e2e] failed to provision the credential. Django output:" >&2
printf '%s\n' "$provision_out" >&2
exit 1
fi
log "team $TEAM_ID enabled; sk- minted + blob published"
# 4. Fund that team's ledger in the gateway's Postgres (admission gate). The
# gateway creates the ledger schema on its first boot, so this is best-effort
# on a fresh stack — if `ledger_entries` doesn't exist yet, fund on the next
# run (after the gateway has come up once). The INSERT is guarded so reruns
# (start-ai-gateway re-invokes this script on every gateway boot) don't keep
# stacking +$100 topups onto the same team.
log "funding team $TEAM_ID gateway ledger (+${TOPUP_USD} USD if not already funded)"
# Capture stderr (discard stdout) so a real SQL error — e.g. a sibling renames a
# ledger_entries column — is surfaced instead of being silently indistinguishable
# from the expected "schema not created yet" on a fresh stack.
fund_rc=0
fund_err="$(
cd "$AI_GATEWAY_REPO" && docker compose exec -T postgres psql -U postgres -d ai_gateway -v ON_ERROR_STOP=1 -c \
"INSERT INTO ledger_entries (team_id, transaction_type, source, destination, amount_usd) SELECT ${TEAM_ID}, 'topup', 'funding', 'prepaid', ${TOPUP_USD} WHERE NOT EXISTS (SELECT 1 FROM ledger_entries WHERE team_id = ${TEAM_ID} AND transaction_type = 'topup' AND source = 'funding');" 2>&1 >/dev/null
)" || fund_rc=$?
if [ "$fund_rc" -eq 0 ]; then
log "ledger funded (or already funded — no-op)"
elif printf '%s' "$fund_err" | grep -qiE "does not exist"; then
log "ledger not funded — gateway hasn't created the schema yet; start the gateway, then re-run this to fund"
else
log "ledger funding failed unexpectedly (not a missing-schema case) — psql said:"
printf '%s\n' "$fund_err" >&2
fi
# 5. Switch the sibling gateway to resolver mode (validate sk- vs the blob).
SIBLING_ENV="$AI_GATEWAY_REPO/.env"
if [ ! -f "$SIBLING_ENV" ] && [ -f "$AI_GATEWAY_REPO/.env.example" ]; then
cp "$AI_GATEWAY_REPO/.env.example" "$SIBLING_ENV"
fi
upsert_env "$SIBLING_ENV" "AI_GATEWAY_AUTH_MODE" "resolver"
log "set AI_GATEWAY_AUTH_MODE=resolver in $SIBLING_ENV"
cat <<EOF
[setup-gateway-e2e] done. Next:
1. Run the gateway (resolver mode picked up from its .env):
cd $AI_GATEWAY_REPO && bin/start gateway
Make sure its .env also has provider keys:
AI_GATEWAY_ANTHROPIC_API_KEY=sk-ant-... AI_GATEWAY_OPENAI_API_KEY=sk-proj-...
2. Smoke it:
curl -sS http://localhost:8080/v1/messages -H "Authorization: Bearer $DEV_PHS" \\
-H 'content-type: application/json' \\
-d '{"model":"claude-sonnet-4-5","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}'
EOF