claude-multiacc 2.0.10 → 2.0.11

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,137 @@ 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
+ if [ "$rc" -eq 1 ]; then
913
+ if [ "${#tok}" -lt "$SETUP_TOKEN_FULL_LEN" ]; then
914
+ die "Claude rejected the captured token (${CEREMONY_CHECK_DETAIL:-401}) — it is ${#tok} characters and a complete setup-token is $SETUP_TOKEN_FULL_LEN: 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."
915
+ fi
916
+ die "Claude rejected the captured token (${CEREMONY_CHECK_DETAIL:-401}) — nothing saved for $id; sign in again as the right account and retry"
917
+ fi
918
+ ( umask 077; printf '%s' "$tok" > "$d/server.token" )
919
+ chmod 600 "$d/server.token"
920
+ if [ "$rc" -eq 0 ]; then
921
+ record_token_verified "$d" "$tok"
922
+ echo "Token verified by a real inference."
923
+ else
924
+ rm -f "$d/.server-token-verified" 2>/dev/null || true
925
+ 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"
926
+ fi
927
+ }
928
+
797
929
  CEREMONY_TOKEN=""
798
930
  run_token_ceremony() { # $1 = config dir
799
931
  CEREMONY_TOKEN=""
@@ -817,11 +949,16 @@ If you do see "Sign in again to continue", that is Claude's security step, not a
817
949
  error — just sign in to that account and approve; the code still appears.
818
950
  TIP
819
951
  if [ -t 0 ]; then
952
+ # The client's TUI hard-wraps at the pty's width, and the token is scraped from
953
+ # the transcript below — see SETUP_TOKEN_FULL_LEN for the 79-character tokens
954
+ # this produced. `script` copies the (widened) window size to the client's pty.
955
+ widen_ceremony_tty
820
956
  if [ "$(machine_kind)" = "mac" ]; then
821
957
  script -q "$cap" env CLAUDE_CONFIG_DIR="$d" CLAUDE_SHIM_ACTIVE=1 "$real" setup-token
822
958
  else
823
959
  script -q -c "CLAUDE_CONFIG_DIR='$d' CLAUDE_SHIM_ACTIVE=1 '$real' setup-token" "$cap"
824
960
  fi
961
+ restore_ceremony_tty
825
962
  else
826
963
  # Headless (tests / piped code): capture into the 0600 file only. Never tee the
827
964
  # raw token to stdout — a redirected run would write the secret to a plain log.
@@ -829,7 +966,10 @@ TIP
829
966
  sed -E 's/sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]*/sk-ant-oat**-<redacted>/g' "$cap"
830
967
  fi
831
968
  umask "$old_umask"
832
- CEREMONY_TOKEN="$(grep -aoE 'sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{40,}' "$cap" | tail -1)"
969
+ # The LONGEST match, not the last: a TUI repaints its frame many times, and a
970
+ # frame rendered while the terminal was still narrow holds a wrapped fragment.
971
+ CEREMONY_TOKEN="$(grep -aoE 'sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{40,}' "$cap" \
972
+ | awk '{ if (length($0) > length(best)) best = $0 } END { if (best != "") print best }')"
833
973
  rm -f "$cap"
834
974
  [ -n "$CEREMONY_TOKEN" ] || return 1
835
975
  valid_subscription_token "$CEREMONY_TOKEN" || {
@@ -941,6 +1081,8 @@ cmd_mint() {
941
1081
  local email tok="" got
942
1082
  email="$(account_email "$id")"
943
1083
  [ -n "$email" ] || die "$id has no email in the manifest — refusing to mint a token nobody could attribute"
1084
+ trap 'restore_ceremony_tty; exit 130' INT TERM
1085
+ trap 'restore_ceremony_tty' EXIT
944
1086
  if [ "$paste" = "1" ]; then
945
1087
  printf 'Paste the sk-ant-oat... token for %s (%s): ' "$id" "${email:-unknown email}"
946
1088
  read -r tok
@@ -957,8 +1099,7 @@ cmd_mint() {
957
1099
  if [ -n "$got" ] && [ -n "$email" ] && [ "$got" != "$email" ]; then
958
1100
  die "that token authenticates as $got but $id is $email — nothing saved"
959
1101
  fi
960
- ( umask 077; printf '%s' "$tok" > "$d/server.token" )
961
- chmod 600 "$d/server.token"
1102
+ commit_ceremony_token "$d" "$tok" "$id"
962
1103
  clear_auth_markers "$d"
963
1104
  log_to ops.log "mint $id"
964
1105
  echo "Token saved to $d/server.token"
@@ -988,13 +1129,14 @@ cmd_login() {
988
1129
  email="$(account_email "$id")"
989
1130
  echo "Sign in as $email for $id."
990
1131
  if [ "$token" = "1" ]; then
1132
+ trap 'restore_ceremony_tty; exit 130' INT TERM
1133
+ trap 'restore_ceremony_tty' EXIT
991
1134
  run_token_ceremony "$d" || die "sign-in failed or aborted — nothing changed"
992
1135
  got="$(token_email "$CEREMONY_TOKEN")"
993
1136
  if [ -n "$got" ] && [ "$got" != "$email" ] && [ "$force" != "1" ]; then
994
1137
  die "you signed in as $got but $id is $email — nothing saved (use --force to override)"
995
1138
  fi
996
- ( umask 077; printf '%s' "$CEREMONY_TOKEN" > "$d/server.token" )
997
- chmod 600 "$d/server.token"
1139
+ commit_ceremony_token "$d" "$CEREMONY_TOKEN" "$id"
998
1140
  clear_auth_markers "$d"
999
1141
  [ -n "$got" ] || warn "a setup token carries no identity — $id is trusted to hold $email because that is who you approved as"
1000
1142
  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.11",
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,13 @@ 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
+ case "$CLAUDE_CODE_OAUTH_TOKEN" in
108
+ *TAILOK) : ;;
109
+ *) echo "Failed to authenticate. API Error: 401 OAuth access token is invalid." >&2; exit 1 ;;
110
+ esac ;;
91
111
  esac
92
112
  case " $* " in
93
113
  *" -p --output-format text --max-turns 1 "*) echo "OK"; exit 0 ;;
@@ -1263,6 +1283,86 @@ esac
1263
1283
  [ -s "$ACC/acct-04/server.token" ] && t_ok "add --token writes server.token" || t_fail "add --token" "missing"
1264
1284
  out="$(CLAUDE_ACCOUNT=acct-04 claude 2>&1)"
1265
1285
  check "token account exports CLAUDE_CODE_OAUTH_TOKEN" "TOK=sk-ant-oat01-FAKE" "$out"
1286
+
1287
+ # ---- 15c1a. a ceremony's token is PROVEN before it is saved --------------------------
1288
+ # add --token above ran the inference probe: the exact token is on record as verified
1289
+ # on this machine, so the audit never calls a fresh mint UNVERIFIED — and the panel
1290
+ # can tell a proven token from one a Mac merely holds.
1291
+ [ -f "$ACC/acct-04/.server-token-verified" ] \
1292
+ && t_ok "add --token proves the token with a real inference before saving" \
1293
+ || t_fail "add --token verification marker" ".server-token-verified missing"
1294
+ out="$(claude-accounts list --json 2>/dev/null | python3 -c '
1295
+ import json, sys
1296
+ rows = {a["id"]: a for a in json.load(sys.stdin)["accounts"]}
1297
+ print(rows["acct-04"].get("token_verified"))')"
1298
+ [ "$out" = "True" ] && t_ok "list --json reports the token as verified here" \
1299
+ || t_fail "list --json token_verified" "expected True, got: $out"
1300
+ # The 2026-08-28 shape: the client's TUI wrapped the 108-character token at 80 columns
1301
+ # and only its 79-character first line was captured. Claude rejects the fragment; the
1302
+ # mint must refuse to save it and say WHY (seven fleet-wide 401s were the old answer).
1303
+ orig="$(cat "$ACC/acct-04/server.token")"
1304
+ out="$(FAKE_TOKEN_FULL=1 FAKE_TOKEN_WRAP=1 claude-accounts mint acct-04 2>&1 </dev/null)"
1305
+ rc=$?
1306
+ [ "$rc" != "0" ] && t_ok "a wrapped (79-char) token capture is refused" || t_fail "wrapped token rc" "rc=0"
1307
+ check "the refusal names the truncation" "79 characters" "$out"
1308
+ check "the refusal names the cause" "terminal wrapped it" "$out"
1309
+ [ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "a refused capture leaves the old token in place" \
1310
+ || t_fail "refused capture" "server.token was overwritten"
1311
+ # A pasted token gets the same proof: a revoked one is refused, not saved.
1312
+ out="$(printf 'sk-ant-oat01-REVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKEDREVOKED' | claude-accounts mint acct-04 --paste 2>&1)"
1313
+ rc=$?
1314
+ [ "$rc" != "0" ] && t_ok "mint --paste refuses a token Claude rejects" || t_fail "paste rejected rc" "rc=0"
1315
+ check "the paste refusal says Claude rejected it" "Claude rejected the captured token" "$out"
1316
+ [ "$(cat "$ACC/acct-04/server.token")" = "$orig" ] && t_ok "a rejected paste saves nothing" \
1317
+ || t_fail "rejected paste" "server.token was overwritten"
1318
+
1319
+ # ---- 15c1b. the ceremony widens an unsized pty, so the whole token is captured ------
1320
+ # The panel drives the ceremony over a pty it never sized (0x0 -> the TUI renders 80
1321
+ # columns wide). Run the mint under exactly such a pty: the fake wraps its token
1322
+ # whenever the terminal is narrower than the token, so only a widened terminal yields
1323
+ # all 108 characters — and the capture must then pass the inference probe.
1324
+ cat > "$WORK/pty-run.py" <<'EOF'
1325
+ import os, pty, select, sys
1326
+ pid, fd = pty.fork() # a fresh pty: 0 rows, 0 columns, like the panel's
1327
+ if pid == 0:
1328
+ os.execvp(sys.argv[1], sys.argv[1:])
1329
+ buf = b''
1330
+ while True:
1331
+ try:
1332
+ ready, _, _ = select.select([fd], [], [], 60)
1333
+ if not ready:
1334
+ break
1335
+ chunk = os.read(fd, 4096)
1336
+ except OSError:
1337
+ break
1338
+ if not chunk:
1339
+ break
1340
+ buf += chunk
1341
+ _, status = os.waitpid(pid, 0)
1342
+ sys.stdout.write(buf.decode('utf-8', 'ignore'))
1343
+ sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
1344
+ EOF
1345
+ size="$(python3 "$WORK/pty-run.py" stty size | tr -d '\r' | tail -1)"
1346
+ [ "$size" = "0 0" ] && t_ok "the test pty really is unsized (the panel's shape)" \
1347
+ || t_fail "test pty size" "expected '0 0', got '$size'"
1348
+ if command -v script >/dev/null 2>&1; then
1349
+ out="$(FAKE_TOKEN_FULL=1 python3 "$WORK/pty-run.py" claude-accounts mint acct-04 2>&1)"
1350
+ rc=$?
1351
+ [ "$rc" = "0" ] && t_ok "mint under an unsized pty succeeds" \
1352
+ || t_fail "unsized-pty mint rc" "rc=$rc: $(printf '%s' "$out" | tail -c 300)"
1353
+ n="$(tr -d '[:space:]' < "$ACC/acct-04/server.token" | wc -c | tr -d ' ')"
1354
+ [ "$n" = "108" ] && t_ok "the unsized pty is widened: all 108 characters captured" \
1355
+ || t_fail "unsized-pty capture" "saved $n characters"
1356
+ check "the widened-pty mint verifies its token" "Token verified by a real inference" "$out"
1357
+ case "$(tr -d '[:space:]' < "$ACC/acct-04/server.token")" in
1358
+ *TAILOK) t_ok "the saved token is the complete one" ;;
1359
+ *) t_fail "saved token" "tail missing" ;;
1360
+ esac
1361
+ [ -f "$ACC/acct-04/.server-token-verified" ] && t_ok "the widened-pty mint records its proof" \
1362
+ || t_fail "widened-pty proof" "marker missing"
1363
+ else
1364
+ printf 'skip unsized-pty mint (no script(1) here)\n'
1365
+ fi
1266
1366
  claude-accounts remove acct-04 --yes >/dev/null 2>&1
1267
1367
 
1268
1368
  # ---- 15c0. add with NO email: derives it from the verified sign-in -------------------