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.
- package/bin/claude-accounts +153 -8
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
- package/lib/audit.py +6 -0
- package/lib/report.py +8 -1
- package/package.json +1 -1
- package/tests/run-tests.sh +127 -0
package/bin/claude-accounts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)."
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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
package/tests/run-tests.sh
CHANGED
|
@@ -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 -------------------
|