Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307#!/bin/bash# ac-inscribe — bind an authenticated session to a flashable AC OS image## Pipelined flash flow:# ac-os pull — download + verify kernel artifacts# ac-inscribe — gather + validate credentials, write inscription.json# ac-os flash — read inscription.json + write USB## Usage:# ac-inscribe # interactive: validate, prompt, write# ac-inscribe --yes # non-interactive, no confirmation prompt# ac-inscribe --anon # anonymous USB (no session, no tokens)# ac-inscribe --out <path> # override output (default: /tmp/ac-os-pull/inscription.json)# ac-inscribe --no-claude # skip baking local Claude session# ac-inscribe --inspect <path> # print summary of an existing inscription# ac-inscribe --for-handle <h> # admin: inscribe for another user# ac-inscribe --mood <text> # override boot splash subtitle (else uses the handle's AC mood)# ac-inscribe --city <city> # preset first-boot greeting city until the device geolocates (e.g. "Ridgewood")## Exit codes:# 0 inscription written and valid# 1 generic failure (auth, network, IO)# 2 user declined the validation prompt## This script never writes to USB. ac-os flash consumes the artifact and# is the only path that touches block devices.## --for-handle <handle>: when an admin (per backend hasAdmin) flashes a# USB on behalf of another user, this passes ?handle=<h> to the# claude-token API to pull that handle's claude/github tokens. The# resulting inscription has empty access_token (admins cannot sign for# other users), so on-device tape uploads will be silently skipped# until the recipient runs ac-login on the device once. The local# Claude session bake is also skipped — that's the operator's, not# the recipient's.set -uSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"LIB="${SCRIPT_DIR}/scripts/inscribe-lib.sh"[ -f "${LIB}" ] || { echo "ac-inscribe: missing ${LIB}" >&2; exit 1; }# shellcheck disable=SC1090source "${LIB}"# ─── parse flags ──────────────────────────────────────────────────ANON=0YES=0NO_CLAUDE=0OUT="/tmp/ac-os-pull/inscription.json"INSPECT=""FOR_HANDLE=""MOOD_OVERRIDE=""MOOD_OVERRIDE_SET=0CITY_OVERRIDE=""CITY_OVERRIDE_SET=0ACI_CITY="" # config "city"; only set via --city (no account fetch), so default empty under `set -u`while [ $# -gt 0 ]; do case "$1" in --anon) ANON=1 ;; --yes|-y) YES=1 ;; --no-claude) NO_CLAUDE=1 ;; --out) OUT="$2"; shift ;; --inspect) INSPECT="$2"; shift ;; --for-handle) FOR_HANDLE="${2#@}"; shift ;; --mood) MOOD_OVERRIDE="$2"; MOOD_OVERRIDE_SET=1; shift ;; --city) CITY_OVERRIDE="$2"; CITY_OVERRIDE_SET=1; shift ;; --help|-h) sed -n '2,28p' "$0" | sed 's/^# *//' exit 0 ;; *) aci_die "unknown flag: $1 (try --help)" ;; esac shiftdone# ─── inspect mode — read an existing inscription, print summary, exit ─if [ -n "${INSPECT}" ]; then [ -f "${INSPECT}" ] || aci_die "no inscription at ${INSPECT}" printf '%s[ac-inscribe]%s inspecting %s%s%s\n\n' \ "${ACI_C_CYAN}" "${ACI_C_RESET}" \ "${ACI_C_BOLD}" "${INSPECT}" "${ACI_C_RESET}" node -e " const d = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8')); const c = d.usbConfig || {}; const yn = (v) => v ? 'yes' : 'no'; process.stdout.write( \` version : \${d.version || '?'}\n\` + \` createdAt : \${d.createdAt || '?'}\n\` + \` createdBy : \${d.createdBy || '?'}\n\` + \` handle : \${c.handle || '?'}\n\` + \` email : \${c.email || '-'}\n\` + \` ac-token : \${yn(c.token)}\n\` + \` claudeToken: \${yn(c.claudeToken)}\n\` + \` githubPat : \${yn(c.githubPat)}\n\` + \` claudeCreds: \${yn(c.claudeCreds)}\n\` + \` claudeState: \${yn(c.claudeState)}\n\` ); " "${INSPECT}" exit 0fi# ─── anonymous shortcut ───────────────────────────────────────────if [ "${ANON}" -eq 1 ]; then aci_step 1 2 "Building anonymous inscription" aci_ok "no token, no session, no claude/github creds" aci_ok "USB will boot into notepat as anonymous" aci_step 2 2 "Writing ${OUT}" mkdir -p "$(dirname "${OUT}")" || aci_die "could not create output dir" aci_build_anon_inscription_json > "${OUT}" || aci_die "write failed" chmod 600 "${OUT}" 2>/dev/null || true aci_ok "wrote $(wc -c < "${OUT}" | tr -d ' ') bytes (mode 0600)" printf '\n%s[ac-inscribe]%s ready. run: %sac-os flash%s\n' \ "${ACI_C_CYAN}" "${ACI_C_RESET}" "${ACI_C_BOLD}" "${ACI_C_RESET}" exit 0fi# ─── for-handle (admin-on-behalf) path ────────────────────────────# Admin inscribes for another user. Pulls the target's claudeToken +# githubPat from the backend; access_token stays empty (admins cannot# sign for other users — the device's tape uploader requires the# recipient's own access_token, so the recipient runs ac-login once# on the device after first boot to populate it).if [ -n "${FOR_HANDLE}" ]; then aci_step 1 4 "Admin auth — operator must be logged in" if aci_require_login; then aci_ok "operator: ${ACI_C_BOLD}@${ACI_HANDLE}${ACI_C_RESET}" aci_ok "operator sub: ${ACI_SUB}" OPERATOR_HANDLE="${ACI_HANDLE}" else aci_die "operator auth failed" fi aci_step 2 4 "Pulling tokens for ${ACI_C_BOLD}@${FOR_HANDLE}${ACI_C_RESET} (admin path)" if aci_fetch_handle_tokens "${FOR_HANDLE}"; then if [ -z "${ACI_HANDLE_API}" ] || [ -z "${ACI_TARGET_SUB}" ]; then aci_die "@${FOR_HANDLE} not found, or not authorized to pull (need admin)" fi aci_ok "target handle: @${ACI_HANDLE_API}" aci_ok "target sub: ${ACI_TARGET_SUB}" [ -n "${ACI_CLAUDE_TOKEN}" ] && aci_ok "claudeToken: present (${#ACI_CLAUDE_TOKEN} bytes)" || aci_warn "claudeToken: none stored for @${FOR_HANDLE}" [ -n "${ACI_GITHUB_PAT}" ] && aci_ok "githubPat: present (${#ACI_GITHUB_PAT} bytes)" || aci_warn "githubPat: none stored for @${FOR_HANDLE}" # Override identity so the bundle is for the target, not the operator. ACI_HANDLE="${ACI_HANDLE_API}" ACI_SUB="${ACI_TARGET_SUB}" ACI_EMAIL="" # not stored in @handles; recipient ac-logins on device ACI_ACCESS_TOKEN="" # admin cannot sign for the target ACI_CLAUDE_CREDS="" # operator's local Claude session is irrelevant here ACI_CLAUDE_STATE="" else aci_die "claude-token API failed (network? not admin? @${FOR_HANDLE} doesn't exist?)" fi # Boot personalization for the target — handle-colors + initial mood. # No auth required for these endpoints (mood read is public, colors # too). C boot renderer reads these from config.json on next boot. aci_fetch_handle_colors "${ACI_HANDLE}" aci_fetch_mood "${ACI_HANDLE}" if [ "${MOOD_OVERRIDE_SET}" -eq 1 ]; then ACI_MOOD="${MOOD_OVERRIDE}" aci_ok "mood override: ${ACI_C_BOLD}${ACI_MOOD:-(cleared)}${ACI_C_RESET}" fi if [ "${CITY_OVERRIDE_SET}" -eq 1 ]; then ACI_CITY="${CITY_OVERRIDE}" aci_ok "city override: ${ACI_C_BOLD}${ACI_CITY:-(cleared)}${ACI_C_RESET}" fi aci_step 3 4 "Assembling inscription bundle for @${ACI_HANDLE}" BUNDLE=$(aci_build_inscription_json) || aci_die "bundle assembly failed" BUNDLE_BYTES=$(printf '%s' "${BUNDLE}" | wc -c | tr -d ' ') aci_ok "version 1 envelope, ${BUNDLE_BYTES} bytes" aci_ok "fields: handle, sub, email(empty), token(empty)$([ -n "${ACI_CLAUDE_TOKEN}" ] && printf ', claudeToken')$([ -n "${ACI_GITHUB_PAT}" ] && printf ', githubPat')$([ -n "${ACI_HANDLE_COLORS_JSON}" ] && printf ', colors')$([ -n "${ACI_MOOD}" ] && printf ', mood')$([ -n "${ACI_CITY}" ] && printf ', city')" [ -n "${ACI_HANDLE_COLORS_JSON}" ] && aci_ok "handle-colors: ${ACI_C_BOLD}per-character palette baked${ACI_C_RESET}" || aci_info "handle-colors: none stored — boot uses theme color" [ -n "${ACI_MOOD}" ] && aci_ok "mood: ${ACI_C_BOLD}${ACI_MOOD}${ACI_C_RESET}" || aci_info "mood: none yet — boot subtitle uses default" [ -n "${ACI_CITY}" ] && aci_ok "city: ${ACI_C_BOLD}${ACI_CITY}${ACI_C_RESET} (greeting until geolocated)" || aci_info "city: none — first-boot greeting uses default" aci_warn "access_token is empty — recipient runs ac-login on device once for tape uploads" if [ "${YES}" -eq 0 ] && [ -t 0 ]; then printf '\n%sproceed? this writes credentials for @%s to %s%s%s [y/N]: ' \ "${ACI_C_YELLOW}" "${ACI_HANDLE}" "${ACI_C_BOLD}" "${OUT}" "${ACI_C_RESET}" read -r CONFIRM case "${CONFIRM}" in y|Y|yes|YES) ;; *) printf '%s[ac-inscribe]%s declined\n' "${ACI_C_CYAN}" "${ACI_C_RESET}"; exit 2 ;; esac fi aci_step 4 4 "Writing ${OUT}" mkdir -p "$(dirname "${OUT}")" || aci_die "could not create output dir" printf '%s' "${BUNDLE}" > "${OUT}" || aci_die "write failed" chmod 600 "${OUT}" 2>/dev/null || true aci_ok "wrote ${BUNDLE_BYTES} bytes (mode 0600)" printf '\n%s[ac-inscribe]%s %sinscribed%s @%s on behalf of admin @%s — run: %sac-os flash%s\n' \ "${ACI_C_CYAN}" "${ACI_C_RESET}" \ "${ACI_C_GREEN}" "${ACI_C_RESET}" \ "${ACI_HANDLE}" \ "${OPERATOR_HANDLE:-???}" \ "${ACI_C_BOLD}" "${ACI_C_RESET}" exit 0fi# ─── authenticated path ───────────────────────────────────────────# Step 1 — auth tokenaci_step 1 5 "Validating ~/.ac-token"if aci_require_login; then aci_ok "token present and parseable" aci_ok "handle: ${ACI_C_BOLD}@${ACI_HANDLE}${ACI_C_RESET}" aci_ok "email: ${ACI_EMAIL:-(none)}" aci_ok "sub: ${ACI_SUB}" [ -n "${ACI_ACCESS_TOKEN}" ] || aci_warn "access_token is empty"else aci_die "auth check failed"fi# Step 2 — handle-token APIaci_step 2 5 "Fetching handle tokens (claude-token API)"if aci_fetch_handle_tokens; then aci_ok "API responded" if [ -n "${ACI_HANDLE_API}" ] && [ "${ACI_HANDLE_API}" != "${ACI_HANDLE}" ]; then aci_warn "API handle (@${ACI_HANDLE_API}) ≠ token handle (@${ACI_HANDLE}) — using token handle" fi [ -n "${ACI_CLAUDE_TOKEN}" ] && aci_ok "claudeToken: present" || aci_warn "claudeToken: empty" [ -n "${ACI_GITHUB_PAT}" ] && aci_ok "githubPat: present" || aci_warn "githubPat: empty"else aci_warn "handle-token API unavailable — bundle will lack claude/github tokens" ACI_CLAUDE_TOKEN="" ACI_GITHUB_PAT=""fi# Step 3 — local Claude sessionaci_step 3 5 "Local Claude session"if [ "${NO_CLAUDE}" -eq 1 ]; then aci_info "skipped (--no-claude)" ACI_CLAUDE_CREDS="" ACI_CLAUDE_STATE=""else aci_collect_local_claude if [ -n "${ACI_CLAUDE_STATE}" ]; then aci_ok "~/.claude.json found, email matches — will bake" if [ -n "${ACI_CLAUDE_CREDS}" ]; then aci_ok "~/.claude/.credentials.json bundled" else aci_warn "~/.claude/.credentials.json missing — state without creds" fi elif [ -f "${HOME}/.claude.json" ]; then if [ -n "${ACI_CLAUDE_LOCAL_EMAIL:-}" ]; then aci_warn "skipped — local claude email (${ACI_CLAUDE_LOCAL_EMAIL}) ≠ AC email (${ACI_EMAIL})" else aci_warn "skipped — could not read local claude email" fi else aci_info "no ~/.claude.json found — nothing to bake" fifi# Boot personalization — handle-colors + most recent mood. Public APIs;# no auth needed. C boot renderer reads them from config.json.aci_fetch_handle_colors "${ACI_HANDLE}"aci_fetch_mood "${ACI_HANDLE}"if [ "${MOOD_OVERRIDE_SET}" -eq 1 ]; then ACI_MOOD="${MOOD_OVERRIDE}" aci_ok "mood override: ${ACI_C_BOLD}${ACI_MOOD:-(cleared)}${ACI_C_RESET}"fiif [ "${CITY_OVERRIDE_SET}" -eq 1 ]; then ACI_CITY="${CITY_OVERRIDE}" aci_ok "city override: ${ACI_C_BOLD}${ACI_CITY:-(cleared)}${ACI_C_RESET}"fi# Step 4 — assemble bundleaci_step 4 5 "Assembling inscription bundle"BUNDLE=$(aci_build_inscription_json) || aci_die "bundle assembly failed"BUNDLE_BYTES=$(printf '%s' "${BUNDLE}" | wc -c | tr -d ' ')aci_ok "version 1 envelope, ${BUNDLE_BYTES} bytes"aci_ok "fields: handle, sub, email, token$([ -n "${ACI_CLAUDE_TOKEN}" ] && printf ', claudeToken')$([ -n "${ACI_GITHUB_PAT}" ] && printf ', githubPat')$([ -n "${ACI_CLAUDE_CREDS}" ] && printf ', claudeCreds')$([ -n "${ACI_CLAUDE_STATE}" ] && printf ', claudeState')$([ -n "${ACI_HANDLE_COLORS_JSON}" ] && printf ', colors')$([ -n "${ACI_MOOD}" ] && printf ', mood')$([ -n "${ACI_CITY}" ] && printf ', city')"[ -n "${ACI_HANDLE_COLORS_JSON}" ] && aci_ok "handle-colors: ${ACI_C_BOLD}per-character palette baked${ACI_C_RESET}" || aci_info "handle-colors: none stored — boot uses theme color"[ -n "${ACI_MOOD}" ] && aci_ok "mood: ${ACI_C_BOLD}${ACI_MOOD}${ACI_C_RESET}" || aci_info "mood: none yet — boot subtitle uses default"# Confirmation gate (interactive only)if [ "${YES}" -eq 0 ] && [ -t 0 ]; then printf '\n%sproceed? this writes credentials to %s%s%s [y/N]: ' \ "${ACI_C_YELLOW}" "${ACI_C_BOLD}" "${OUT}" "${ACI_C_RESET}" read -r CONFIRM case "${CONFIRM}" in y|Y|yes|YES) ;; *) printf '%s[ac-inscribe]%s declined\n' "${ACI_C_CYAN}" "${ACI_C_RESET}"; exit 2 ;; esacfi# Step 5 — write artifactaci_step 5 5 "Writing ${OUT}"mkdir -p "$(dirname "${OUT}")" || aci_die "could not create output dir"printf '%s' "${BUNDLE}" > "${OUT}" || aci_die "write failed"chmod 600 "${OUT}" 2>/dev/null || trueaci_ok "wrote ${BUNDLE_BYTES} bytes (mode 0600)"aci_ok "$(stat -f '%Sp %Su:%Sg' "${OUT}" 2>/dev/null || stat -c '%a %U:%G' "${OUT}" 2>/dev/null || echo permissions checked)"printf '\n%s[ac-inscribe]%s %sinscribed%s @%s — run: %sac-os flash%s\n' \ "${ACI_C_CYAN}" "${ACI_C_RESET}" \ "${ACI_C_GREEN}" "${ACI_C_RESET}" \ "${ACI_HANDLE}" \ "${ACI_C_BOLD}" "${ACI_C_RESET}"