claude-multiacc 2.0.10 → 2.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -508,8 +508,7 @@ cmd_add() {
508
508
  # Portable setup-token variant (works on Mac AND server; needs a recent sign-in).
509
509
  echo "Preparing $id for $elabel (portable token) — NOTHING is registered until sign-in completes."
510
510
  run_token_ceremony "$d" || die "sign-in failed or aborted — nothing was created"
511
- ( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
512
- chmod 600 "$d/server.token"
511
+ commit_ceremony_token "$d" "$CEREMONY_TOKEN" "$id"
513
512
  got="$(token_email "$CEREMONY_TOKEN")"
514
513
  else
515
514
  # Default: full Claude Code login (full scopes, no long-lived-token step-up).
@@ -571,7 +570,9 @@ EOF
571
570
 
572
571
  add_cleanup_reserved() {
573
572
  # EXIT trap for cmd_add: remove a reserved-but-uncommitted account dir (and, in case
574
- # we died mid-critical-section, release the lock).
573
+ # we died mid-critical-section, release the lock). A ceremony interrupted mid-way
574
+ # must also hand the terminal back at its own width.
575
+ restore_ceremony_tty 2>/dev/null || true
575
576
  [ -n "${RESERVED_DIR:-}" ] && rm -rf "$RESERVED_DIR" 2>/dev/null
576
577
  mutate_unlock 2>/dev/null || true
577
578
  }
@@ -794,6 +795,140 @@ valid_subscription_token() {
794
795
  esac
795
796
  }
796
797
 
798
+ # A complete subscription setup-token is 108 characters (sk-ant-oat01- + 95). The
799
+ # ceremony scrapes it from a terminal transcript, and the client's TUI HARD-WRAPS at
800
+ # the pty's width — an unsized pty (the panel's) renders as 80 columns. Every
801
+ # panel-driven mint of 2026-08-28 therefore saved the first 79 characters of a
802
+ # 108-character token; the fleet rejected all seven with 401 while the minting Mac,
803
+ # still holding its OAuth login, looked healthy. Two guards, belt and braces: the
804
+ # terminal is widened for the ceremony, and the capture must pass a real inference
805
+ # before it is saved anywhere.
806
+ SETUP_TOKEN_FULL_LEN=108
807
+ CEREMONY_TTY_MIN_COLS=160
808
+ CEREMONY_TTY_COLS=400
809
+ CEREMONY_TTY_ORIG=""
810
+
811
+ widen_ceremony_tty() {
812
+ CEREMONY_TTY_ORIG=""
813
+ [ -t 0 ] || return 0
814
+ local size rows cols
815
+ size="$(stty size 2>/dev/null)" || return 0
816
+ rows="${size%% *}"; cols="${size##* }"
817
+ case "$cols" in ''|*[!0-9]*) return 0 ;; esac
818
+ case "$rows" in ''|*[!0-9]*) rows=0 ;; esac
819
+ [ "$cols" -ge "$CEREMONY_TTY_MIN_COLS" ] && return 0
820
+ CEREMONY_TTY_ORIG="$rows $cols"
821
+ # `script` copies stdin's window size to the pty it opens for the client, so
822
+ # widening here is what the client sees. A zero row count is an unsized pty too.
823
+ stty cols "$CEREMONY_TTY_COLS" rows "$([ "$rows" -gt 0 ] && echo "$rows" || echo 50)" 2>/dev/null \
824
+ || CEREMONY_TTY_ORIG=""
825
+ }
826
+
827
+ restore_ceremony_tty() {
828
+ [ -n "$CEREMONY_TTY_ORIG" ] || return 0
829
+ stty rows "${CEREMONY_TTY_ORIG%% *}" cols "${CEREMONY_TTY_ORIG##* }" 2>/dev/null || true
830
+ CEREMONY_TTY_ORIG=""
831
+ }
832
+
833
+ token_digest() { # $1 token; prints a non-secret sha256 digest (same as the shim's)
834
+ local h=""
835
+ if command -v shasum >/dev/null 2>&1; then
836
+ h="$(printf '%s' "$1" | shasum -a 256 2>/dev/null)"
837
+ elif command -v sha256sum >/dev/null 2>&1; then
838
+ h="$(printf '%s' "$1" | sha256sum 2>/dev/null)"
839
+ fi
840
+ printf '%s' "$h" | cut -d ' ' -f1
841
+ }
842
+
843
+ record_token_verified() { # $1 acct dir, $2 token — the shim's own proof marker (token_preflight)
844
+ local digest
845
+ digest="$(token_digest "$2")"
846
+ [ -n "$digest" ] || return 0
847
+ { umask 077; printf '%s\n' "$digest" > "$1/.server-token-verified.$$"; } 2>/dev/null \
848
+ && mv -f "$1/.server-token-verified.$$" "$1/.server-token-verified" 2>/dev/null \
849
+ || rm -f "$1/.server-token-verified.$$" 2>/dev/null || true
850
+ }
851
+
852
+ # Prove a captured token with ONE real inference before it is saved. rc 0 = Claude
853
+ # answered with it; rc 1 = Claude REJECTED it (401: revoked, wrong account's grant,
854
+ # or captured incomplete); rc 2 = inconclusive (network, 429, timeout). The probe runs
855
+ # in an EMPTY config dir: the account dir may hold an OAuth login the client would
856
+ # silently prefer, and this must exercise the captured token and nothing else. The
857
+ # token travels by environment, never argv.
858
+ CEREMONY_CHECK_DETAIL=""
859
+ ceremony_probe() { # $1 = real claude, $2 = empty config dir; token in CEREMONY_TOKEN_UNDER_TEST
860
+ "$PYBIN" - "$1" "$2" "$ACC_ROOT" <<'PYEOF'
861
+ import os, re, subprocess, sys
862
+ real, cfg, root = sys.argv[1], sys.argv[2], sys.argv[3]
863
+ env = {k: v for k, v in os.environ.items()
864
+ if k not in ('ANTHROPIC_API_KEY', 'CLAUDE_ACCOUNT', 'CEREMONY_TOKEN_UNDER_TEST')}
865
+ env['CLAUDE_CONFIG_DIR'] = cfg
866
+ env['CLAUDE_CODE_OAUTH_TOKEN'] = os.environ['CEREMONY_TOKEN_UNDER_TEST']
867
+ env['CLAUDE_SHIM_ACTIVE'] = '1'
868
+ # The shim's own vocabulary for a rejected token (bin/claude token_preflight).
869
+ AUTH = re.compile(r'failed to authenticate|oauth (access )?token is invalid|oauth session expired'
870
+ r'|please run /login|invalid bearer token|authentication_error|\b401\b', re.I)
871
+ try:
872
+ r = subprocess.run([real, '-p', '--output-format', 'text', '--max-turns', '1'],
873
+ env=env, capture_output=True, text=True, timeout=180,
874
+ input='Reply with exactly: OK\n', cwd=root)
875
+ except subprocess.TimeoutExpired:
876
+ print('inconclusive\ttimed out after 180s'); sys.exit(0)
877
+ except OSError as exc:
878
+ print(f'inconclusive\t{exc}'); sys.exit(0)
879
+ out, err = (r.stdout or '').strip(), (r.stderr or '').strip()
880
+ if r.returncode == 0 and 'ok' in out.lower():
881
+ print('ok\t'); sys.exit(0)
882
+ if AUTH.search(out) or AUTH.search(err):
883
+ print('rejected\t' + (err or out)[:160].replace('\n', ' ')); sys.exit(0)
884
+ print('inconclusive\t' + f'rc={r.returncode} ' + (err or out)[:160].replace('\n', ' '))
885
+ PYEOF
886
+ }
887
+
888
+ ceremony_token_check() { # $1 = token
889
+ local tok="$1" real tmpd verdict
890
+ CEREMONY_CHECK_DETAIL=""
891
+ real="$(find_real_claude "$_self")" || { CEREMONY_CHECK_DETAIL="real claude binary not found"; return 2; }
892
+ mkdir -p "$ACC_ROOT/tmp" 2>/dev/null || true
893
+ tmpd="$(mktemp -d "$ACC_ROOT/tmp/mint-check.XXXXXX" 2>/dev/null)" \
894
+ || { CEREMONY_CHECK_DETAIL="cannot create a scratch config dir"; return 2; }
895
+ chmod 700 "$tmpd" 2>/dev/null || true
896
+ verdict="$(CEREMONY_TOKEN_UNDER_TEST="$tok" ceremony_probe "$real" "$tmpd")"
897
+ rm -rf "$tmpd" 2>/dev/null || true
898
+ case "$verdict" in
899
+ ok*) return 0 ;;
900
+ rejected*) CEREMONY_CHECK_DETAIL="${verdict#rejected }"; return 1 ;;
901
+ inconclusive*) CEREMONY_CHECK_DETAIL="${verdict#inconclusive }"; return 2 ;;
902
+ *) CEREMONY_CHECK_DETAIL="no verdict"; return 2 ;;
903
+ esac
904
+ }
905
+
906
+ # The ONE place a ceremony's token is written: only after ceremony_token_check, so a
907
+ # token Claude rejects is never saved, uploaded, or distributed. $1 = acct dir,
908
+ # $2 = token, $3 = acct id (messages only).
909
+ commit_ceremony_token() {
910
+ local d="$1" tok="$2" id="$3" rc=0
911
+ ceremony_token_check "$tok" || rc=$?
912
+ # A short capture is a wrapped one unless Claude itself has just answered with it:
913
+ # the length alone convicts it, so an inconclusive probe (429, network) must not let
914
+ # it through as merely "unverified" — that is the exact token the fleet then rejects.
915
+ if [ "$rc" -ne 0 ] && [ "${#tok}" -lt "$SETUP_TOKEN_FULL_LEN" ]; then
916
+ die "the captured token is ${#tok} characters and a complete setup-token is $SETUP_TOKEN_FULL_LEN (${CEREMONY_CHECK_DETAIL:-not proven by a real call}): the sign-in terminal wrapped it and only its first line was captured. Nothing saved for $id. Mint again from a terminal at least $CEREMONY_TTY_MIN_COLS columns wide, or from an updated panel."
917
+ fi
918
+ if [ "$rc" -eq 1 ]; then
919
+ die "Claude rejected the captured token (${CEREMONY_CHECK_DETAIL:-401}) — nothing saved for $id; sign in again as the right account and retry"
920
+ fi
921
+ ( umask 077; printf '%s' "$tok" > "$d/server.token" )
922
+ chmod 600 "$d/server.token"
923
+ if [ "$rc" -eq 0 ]; then
924
+ record_token_verified "$d" "$tok"
925
+ echo "Token verified by a real inference."
926
+ else
927
+ rm -f "$d/.server-token-verified" 2>/dev/null || true
928
+ warn "the token could not be verified right now (${CEREMONY_CHECK_DETAIL:-no answer}) — saved as UNVERIFIED; the shim proves it on first use, or run: claude-accounts verify"
929
+ fi
930
+ }
931
+
797
932
  CEREMONY_TOKEN=""
798
933
  run_token_ceremony() { # $1 = config dir
799
934
  CEREMONY_TOKEN=""
@@ -817,11 +952,16 @@ If you do see "Sign in again to continue", that is Claude's security step, not a
817
952
  error — just sign in to that account and approve; the code still appears.
818
953
  TIP
819
954
  if [ -t 0 ]; then
955
+ # The client's TUI hard-wraps at the pty's width, and the token is scraped from
956
+ # the transcript below — see SETUP_TOKEN_FULL_LEN for the 79-character tokens
957
+ # this produced. `script` copies the (widened) window size to the client's pty.
958
+ widen_ceremony_tty
820
959
  if [ "$(machine_kind)" = "mac" ]; then
821
960
  script -q "$cap" env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" setup-token
822
961
  else
823
962
  script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' setup-token" "$cap"
824
963
  fi
964
+ restore_ceremony_tty
825
965
  else
826
966
  # Headless (tests / piped code): capture into the 0600 file only. Never tee the
827
967
  # raw token to stdout — a redirected run would write the secret to a plain log.
@@ -829,7 +969,10 @@ TIP
829
969
  sed -E 's/sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]*/sk-ant-oat**-<redacted>/g' "$cap"
830
970
  fi
831
971
  umask "$old_umask"
832
- CEREMONY_TOKEN="$(grep -aoE 'sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{40,}' "$cap" | tail -1)"
972
+ # The LONGEST match, not the last: a TUI repaints its frame many times, and a
973
+ # frame rendered while the terminal was still narrow holds a wrapped fragment.
974
+ CEREMONY_TOKEN="$(grep -aoE 'sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{40,}' "$cap" \
975
+ | awk '{ if (length($0) > length(best)) best = $0 } END { if (best != "") print best }')"
833
976
  rm -f "$cap"
834
977
  [ -n "$CEREMONY_TOKEN" ] || return 1
835
978
  valid_subscription_token "$CEREMONY_TOKEN" || {
@@ -941,6 +1084,8 @@ cmd_mint() {
941
1084
  local email tok="" got
942
1085
  email="$(account_email "$id")"
943
1086
  [ -n "$email" ] || die "$id has no email in the manifest — refusing to mint a token nobody could attribute"
1087
+ trap 'restore_ceremony_tty; exit 130' INT TERM
1088
+ trap 'restore_ceremony_tty' EXIT
944
1089
  if [ "$paste" = "1" ]; then
945
1090
  printf 'Paste the sk-ant-oat... token for %s (%s): ' "$id" "${email:-unknown email}"
946
1091
  read -r tok
@@ -957,8 +1102,7 @@ cmd_mint() {
957
1102
  if [ -n "$got" ] && [ -n "$email" ] && [ "$got" != "$email" ]; then
958
1103
  die "that token authenticates as $got but $id is $email — nothing saved"
959
1104
  fi
960
- ( umask 077; printf '%s' "$tok" > "$d/server.token" )
961
- chmod 600 "$d/server.token"
1105
+ commit_ceremony_token "$d" "$tok" "$id"
962
1106
  clear_auth_markers "$d"
963
1107
  log_to ops.log "mint $id"
964
1108
  echo "Token saved to $d/server.token"
@@ -988,13 +1132,14 @@ cmd_login() {
988
1132
  email="$(account_email "$id")"
989
1133
  echo "Sign in as $email for $id."
990
1134
  if [ "$token" = "1" ]; then
1135
+ trap 'restore_ceremony_tty; exit 130' INT TERM
1136
+ trap 'restore_ceremony_tty' EXIT
991
1137
  run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
992
1138
  got="$(token_email "$CEREMONY_TOKEN")"
993
1139
  if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
994
1140
  die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
995
1141
  fi
996
- ( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
997
- chmod 600 "$d/server.token"
1142
+ commit_ceremony_token "$d" "$CEREMONY_TOKEN" "$id"
998
1143
  clear_auth_markers "$d"
999
1144
  [ -n "$got" ] || warn "a setup token carries no identity — $id is trusted to hold $email because that is who you approved as"
1000
1145
  echo "$id token saved (portable — works on Mac and server)."
package/lib/audit.py CHANGED
@@ -248,6 +248,12 @@ def _token_verified(d, tpath):
248
248
  return False
249
249
 
250
250
 
251
+ # The report (`list --json`) tells the panel the same thing per Mac: a Mac reads
252
+ # "active" for any token FILE it can see, and only this says whether a real call
253
+ # ever succeeded with it here.
254
+ token_verified = _token_verified
255
+
256
+
251
257
  def audit_account(root, acct, now=None, machine=None, require_verified_token=False):
252
258
  now = time.time() if now is None else now
253
259
  aid = acct.get('id', '')
package/lib/report.py CHANGED
@@ -46,6 +46,7 @@ import time
46
46
 
47
47
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
48
48
  import keychain # noqa: E402
49
+ from audit import token_verified # noqa: E402
49
50
 
50
51
  SCHEMA = 'claude-multiacc/pool.v1'
51
52
 
@@ -172,7 +173,10 @@ def _claude_credentials(d):
172
173
  has_token = _size(tpath) > 0
173
174
  detail = {'oauth': has_oauth, 'token': has_token, 'oauth_store': None, 'keychain': None,
174
175
  'oauth_expires_at': None, 'oauth_refresh_expires_at': None,
175
- 'token_minted_at': None, 'token_age_days': None}
176
+ 'token_minted_at': None, 'token_age_days': None,
177
+ # True once THIS exact token passed a real inference on this machine
178
+ # (.server-token-verified); False = present but never proven here.
179
+ 'token_verified': token_verified(d, tpath) if has_token else None}
176
180
  o = None
177
181
  if has_oauth:
178
182
  detail['oauth_store'] = 'file'
@@ -347,6 +351,9 @@ def _account_row(root, a, aid, d, provider, jwt_claims, audit_account, now, kind
347
351
  'credential_class': cclass,
348
352
  'portable': cclass == 'portable',
349
353
  'credentials': cdetail,
354
+ # Flat copy for heartbeat consumers: a portable token this Mac has proven
355
+ # (True), holds unproven (False), or does not hold (None).
356
+ 'token_verified': cdetail.get('token_verified') if provider == 'claude' else None,
350
357
  'limited': bool(limited),
351
358
  'limit_reset_at': _iso(reset_epoch) if limited and reset_epoch else None,
352
359
  'limit_reset_epoch': reset_epoch if limited and reset_epoch else None,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,6 +74,19 @@ fi
74
74
  if [ "${1:-}" = "setup-token" ]; then
75
75
  echo "Open this sign-in link: https://claude.ai/oauth/authorize?fake=1"
76
76
  [ -n "${FAKE_TOKEN_FAIL:-}" ] && { echo "sign-in aborted" >&2; exit 1; }
77
+ if [ -n "${FAKE_TOKEN_FULL:-}" ]; then
78
+ # The real client's shape: a 108-character token that the TUI HARD-WRAPS at the
79
+ # terminal's width — an unsized pty (0 columns) renders as 80, and a 79-character
80
+ # first line is exactly what every panel mint of 2026-08-28 saved.
81
+ tok="sk-ant-oat01-$(printf '%66s' '' | tr ' ' W)$(printf '%23s' '' | tr ' ' T)TAILOK"
82
+ cols="$(stty size 2>/dev/null | awk '{print $2}')"
83
+ if [ -n "${FAKE_TOKEN_WRAP:-}" ] || { [ -n "$cols" ] && [ "$cols" -lt 108 ]; }; then
84
+ printf ' %s\n%s\n' "${tok:0:79}" "${tok:79}"
85
+ else
86
+ printf ' %s\n' "$tok"
87
+ fi
88
+ exit 0
89
+ fi
77
90
  echo "Your token: sk-ant-oat01-FAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKEFAKE"
78
91
  exit 0
79
92
  fi
@@ -88,6 +101,17 @@ case "${CLAUDE_CODE_OAUTH_TOKEN:-}" in
88
101
  *REVOKED*)
89
102
  echo "Please run /login · API Error: 401 OAuth access token is invalid." >&2
90
103
  exit 1 ;;
104
+ sk-ant-oat01-WWW*)
105
+ # The wrapped fixture above: only the COMPLETE token (with its tail) authenticates;
106
+ # its 79-character first line is what Claude answered 401 to, fleet-wide.
107
+ if [ -n "${FAKE_PROBE_FLAKY:-}" ]; then
108
+ # Not an auth verdict at all — the API is busy. The probe is inconclusive.
109
+ echo "API Error: 529 Overloaded" >&2; exit 1
110
+ fi
111
+ case "$CLAUDE_CODE_OAUTH_TOKEN" in
112
+ *TAILOK) : ;;
113
+ *) echo "Failed to authenticate. API Error: 401 OAuth access token is invalid." >&2; exit 1 ;;
114
+ esac ;;
91
115
  esac
92
116
  case " $* " in
93
117
  *" -p --output-format text --max-turns 1 "*) echo "OK"; exit 0 ;;
@@ -1263,6 +1287,109 @@ esac
1263
1287
  [ -s "$ACC/acct-04/server.token" ] && t_ok "add --token writes server.token" || t_fail "add --token" "missing"
1264
1288
  out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
1265
1289
  check "token account exports CLAUDE_CODE_OAUTH_TOKEN" "TOK=sk-ant-oat01-FAKE" "$out"
1290
+
1291
+ # ---- 15c1a. a ceremony's token is PROVEN before it is saved --------------------------
1292
+ # add --token above ran the inference probe: the exact token is on record as verified
1293
+ # on this machine, so the audit never calls a fresh mint UNVERIFIED — and the panel
1294
+ # can tell a proven token from one a Mac merely holds.
1295
+ [ -f "$ACC/acct-04/.server-token-verified" ] \
1296
+ && t_ok "add --token proves the token with a real inference before saving" \
1297
+ || t_fail "add --token verification marker" ".server-token-verified missing"
1298
+ out="$(claude-accounts list --json 2>/dev/null | python3 -c '
1299
+ import json, sys
1300
+ rows = {a["id"]: a for a in json.load(sys.stdin)["accounts"]}
1301
+ print(rows["acct-04"].get("token_verified"))')"
1302
+ [ "$out" = "True" ] && t_ok "list --json reports the token as verified here" \
1303
+ || t_fail "list --json token_verified" "expected True, got: $out"
1304
+ # The 2026-08-28 shape: the client's TUI wrapped the 108-character token at 80 columns
1305
+ # and only its 79-character first line was captured. Claude rejects the fragment; the
1306
+ # mint must refuse to save it and say WHY (seven fleet-wide 401s were the old answer).
1307
+ orig="$(cat "$ACC/acct-04/server.token")"
1308
+ out="$(FAKE_TOKEN_FULL=1 FAKE_TOKEN_WRAP=1 claude-accounts mint acct-04 2>&1 </dev/null)"
1309
+ rc=$?
1310
+ [ "$rc" != "0" ] && t_ok "a wrapped (79-char) token capture is refused" || t_fail "wrapped token rc" "rc=0"
1311
+ check "the refusal names the truncation" "79 characters" "$out"
1312
+ check "the refusal names the cause" "terminal wrapped it" "$out"
1313
+ [ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "a refused capture leaves the old token in place" \
1314
+ || t_fail "refused capture" "server.token was overwritten"
1315
+ # An INCONCLUSIVE probe (the API is busy) must not let a wrapped capture through as
1316
+ # merely "unverified": its length alone convicts it, and that is the exact token the
1317
+ # fleet then rejects on every Mac.
1318
+ out="$(FAKE_TOKEN_FULL=1 FAKE_TOKEN_WRAP=1 FAKE_PROBE_FLAKY=1 claude-accounts mint acct-04 2>&1 </dev/null)"
1319
+ rc=$?
1320
+ [ "$rc" != "0" ] && t_ok "a wrapped capture is refused even when the probe is inconclusive" \
1321
+ || t_fail "wrapped+inconclusive rc" "rc=0"
1322
+ check "the inconclusive refusal still names the cause" "terminal wrapped it" "$out"
1323
+ [ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "an unproven fragment saves nothing" \
1324
+ || t_fail "unproven fragment" "server.token was overwritten"
1325
+ # …while a COMPLETE token the probe cannot reach right now is saved as UNVERIFIED — the
1326
+ # shim proves it on first use — rather than blocking the operator on a busy API.
1327
+ out="$(FAKE_TOKEN_FULL=1 FAKE_PROBE_FLAKY=1 claude-accounts mint acct-04 2>&1 </dev/null)"
1328
+ rc=$?
1329
+ [ "$rc" = "0" ] && t_ok "a complete token survives an inconclusive probe" || t_fail "complete+inconclusive rc" "rc=$rc: $(printf '%s' "$out" | tail -c 200)"
1330
+ check "the inconclusive save says so" "saved as UNVERIFIED" "$out"
1331
+ [ "$(tr -d '[:space:]' < "$ACC/acct-04/server.token" | wc -c | tr -d ' ')" = "108" ] \
1332
+ && t_ok "the complete token was saved" || t_fail "complete token save" "wrong length"
1333
+ [ ! -f "$ACC/acct-04/.server-token-verified" ] && t_ok "an unproven save records no proof" \
1334
+ || t_fail "unproven proof marker" "marker present"
1335
+ printf '%s' "$orig" > "$ACC/acct-04/server.token"
1336
+ # A pasted token gets the same proof: a revoked one is refused, not saved.
1337
+ # (A complete-length token, so the verdict is the rejection itself and not the
1338
+ # length rule that catches wrapped fragments first.)
1339
+ out="$(printf 'sk-ant-oat01-REVOKED%s' "$(printf '%88s' '' | tr ' ' R)" | claude-accounts mint acct-04 --paste 2>&1)"
1340
+ rc=$?
1341
+ [ "$rc" != "0" ] && t_ok "mint --paste refuses a token Claude rejects" || t_fail "paste rejected rc" "rc=0"
1342
+ check "the paste refusal says Claude rejected it" "Claude rejected the captured token" "$out"
1343
+ [ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "a rejected paste saves nothing" \
1344
+ || t_fail "rejected paste" "server.token was overwritten"
1345
+
1346
+ # ---- 15c1b. the ceremony widens an unsized pty, so the whole token is captured ------
1347
+ # The panel drives the ceremony over a pty it never sized (0x0 -> the TUI renders 80
1348
+ # columns wide). Run the mint under exactly such a pty: the fake wraps its token
1349
+ # whenever the terminal is narrower than the token, so only a widened terminal yields
1350
+ # all 108 characters — and the capture must then pass the inference probe.
1351
+ cat > "$WORK/pty-run.py" <<'EOF'
1352
+ import os, pty, select, sys
1353
+ pid, fd = pty.fork() # a fresh pty: 0 rows, 0 columns, like the panel's
1354
+ if pid == 0:
1355
+ os.execvp(sys.argv[1], sys.argv[1:])
1356
+ buf = b''
1357
+ while True:
1358
+ try:
1359
+ ready, _, _ = select.select([fd], [], [], 60)
1360
+ if not ready:
1361
+ break
1362
+ chunk = os.read(fd, 4096)
1363
+ except OSError:
1364
+ break
1365
+ if not chunk:
1366
+ break
1367
+ buf += chunk
1368
+ _, status = os.waitpid(pid, 0)
1369
+ sys.stdout.write(buf.decode('utf-8', 'ignore'))
1370
+ sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
1371
+ EOF
1372
+ size="$(python3 "$WORK/pty-run.py" stty size | tr -d '\r' | tail -1)"
1373
+ [ "$size" = "0 0" ] && t_ok "the test pty really is unsized (the panel's shape)" \
1374
+ || t_fail "test pty size" "expected '0 0', got '$size'"
1375
+ if command -v script >/dev/null 2>&1; then
1376
+ out="$(FAKE_TOKEN_FULL=1 python3 "$WORK/pty-run.py" claude-accounts mint acct-04 2>&1)"
1377
+ rc=$?
1378
+ [ "$rc" = "0" ] && t_ok "mint under an unsized pty succeeds" \
1379
+ || t_fail "unsized-pty mint rc" "rc=$rc: $(printf '%s' "$out" | tail -c 300)"
1380
+ n="$(tr -d '[:space:]' < "$ACC/acct-04/server.token" | wc -c | tr -d ' ')"
1381
+ [ "$n" = "108" ] && t_ok "the unsized pty is widened: all 108 characters captured" \
1382
+ || t_fail "unsized-pty capture" "saved $n characters"
1383
+ check "the widened-pty mint verifies its token" "Token verified by a real inference" "$out"
1384
+ case "$(tr -d '[:space:]' < "$ACC/acct-04/server.token")" in
1385
+ *TAILOK) t_ok "the saved token is the complete one" ;;
1386
+ *) t_fail "saved token" "tail missing" ;;
1387
+ esac
1388
+ [ -f "$ACC/acct-04/.server-token-verified" ] && t_ok "the widened-pty mint records its proof" \
1389
+ || t_fail "widened-pty proof" "marker missing"
1390
+ else
1391
+ printf 'skip unsized-pty mint (no script(1) here)\n'
1392
+ fi
1266
1393
  claude-accounts remove acct-04 --yes >/dev/null 2>&1
1267
1394
 
1268
1395
  # ---- 15c0. add with NO email: derives it from the verified sign-in -------------------