claude-multiacc 2.0.39 → 2.0.41
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 +6 -1
- package/bin/codex-accounts +6 -1
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
- package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
- package/lib/__pycache__/mcp_registry.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/__pycache__/shim_path.cpython-312.pyc +0 -0
- package/lib/report.py +16 -1
- package/package.json +1 -1
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +17 -4
- package/tests/test_claude_reset.py +30 -0
package/bin/claude-accounts
CHANGED
|
@@ -3107,7 +3107,12 @@ EOF
|
|
|
3107
3107
|
if [ -n "$peers" ]; then
|
|
3108
3108
|
while IFS="$tab" read -r pt pr pp; do
|
|
3109
3109
|
[ -n "$pt" ] || continue
|
|
3110
|
-
|
|
3110
|
+
# </dev/null: the ssh/rsync calls inside read STDIN, and stdin here is the
|
|
3111
|
+
# peer list itself — the first peer's ssh swallowed every later line, so sync
|
|
3112
|
+
# reached gas-mini (first in the list) and never mini-3..mini-8 (peers since
|
|
3113
|
+
# 2026-09-04; the limits fan-out, which reads its list through process
|
|
3114
|
+
# substitution, was the only thing that ever reached them).
|
|
3115
|
+
sync_push_target "$pt" "$pr" "$pp" </dev/null
|
|
3111
3116
|
npeers=$((npeers + 1))
|
|
3112
3117
|
done <<EOF
|
|
3113
3118
|
$peers
|
package/bin/codex-accounts
CHANGED
|
@@ -2208,7 +2208,12 @@ EOF
|
|
|
2208
2208
|
if [ -n "$peers" ]; then
|
|
2209
2209
|
while IFS="$tab" read -r pt pr pp; do
|
|
2210
2210
|
[ -n "$pt" ] || continue
|
|
2211
|
-
|
|
2211
|
+
# </dev/null: the ssh/rsync calls inside read STDIN, and stdin here is the
|
|
2212
|
+
# peer list itself — the first peer's ssh swallowed every later line, so sync
|
|
2213
|
+
# reached gas-mini (first in the list) and never mini-3..mini-8 (peers since
|
|
2214
|
+
# 2026-09-04; the limits fan-out, which reads its list through process
|
|
2215
|
+
# substitution, was the only thing that ever reached them).
|
|
2216
|
+
sync_push_target "$pt" "$pr" "$pp" </dev/null
|
|
2212
2217
|
npeers=$((npeers + 1))
|
|
2213
2218
|
done <<EOF
|
|
2214
2219
|
$peers
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/report.py
CHANGED
|
@@ -46,7 +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
|
+
from audit import creds_doc_state, token_verified # noqa: E402
|
|
50
50
|
from claude_reset import marker_superseded, record_from # noqa: E402
|
|
51
51
|
|
|
52
52
|
SCHEMA = 'claude-multiacc/pool.v1'
|
|
@@ -194,6 +194,21 @@ def _claude_credentials(d):
|
|
|
194
194
|
if isinstance(o, dict):
|
|
195
195
|
detail['oauth_expires_at'] = _iso_ms(o.get('expiresAt'))
|
|
196
196
|
detail['oauth_refresh_expires_at'] = _iso_ms(o.get('refreshTokenExpiresAt'))
|
|
197
|
+
# `oauth` says a sign-in EXISTS here; `oauth_alive` says whether it can still work —
|
|
198
|
+
# the limits writer's own rule (audit.creds_doc_state). A dead one (refresh token
|
|
199
|
+
# expired) keeps inference running on the portable token but can never read usage
|
|
200
|
+
# or limit resets again, so the panel must ask for a sign-in (2026-09-22: acct-02 and
|
|
201
|
+
# acct-07 sat dark for days behind `oauth: true`). None = could not be read here (a
|
|
202
|
+
# Keychain this session cannot open) or no sign-in at all.
|
|
203
|
+
detail['oauth_alive'] = None
|
|
204
|
+
if has_oauth and isinstance(o, dict):
|
|
205
|
+
try:
|
|
206
|
+
state, reason = creds_doc_state({'claudeAiOauth': o}, time.time())
|
|
207
|
+
detail['oauth_alive'] = state == 'ok'
|
|
208
|
+
if state != 'ok':
|
|
209
|
+
detail['oauth_problem'] = reason
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
197
212
|
if has_token:
|
|
198
213
|
mt = _mtime(tpath)
|
|
199
214
|
detail['token_minted_at'] = _iso(mt)
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/run-tests.sh
CHANGED
|
@@ -3908,15 +3908,20 @@ EOF
|
|
|
3908
3908
|
SSHFAKE="$WORK/sshfake"
|
|
3909
3909
|
SSHLOG="$WORK/sshfake.log"
|
|
3910
3910
|
mkdir -p "$SSHFAKE"
|
|
3911
|
+
# Both fakes DRAIN stdin like the real tools do: ssh forwards it to the remote
|
|
3912
|
+
# command and rsync's transport reads it. A fake that left stdin alone hid the
|
|
3913
|
+
# bug where the peer loop fed them its own peer list.
|
|
3911
3914
|
cat > "$SSHFAKE/ssh" <<'EOF'
|
|
3912
3915
|
#!/usr/bin/env bash
|
|
3913
3916
|
printf 'ssh %s\n' "$*" >> "${SSHLOG:?}"
|
|
3917
|
+
[ -t 0 ] || cat >/dev/null
|
|
3914
3918
|
case "$*" in *"${SSH_FAIL_HOST:-@@none@@}"*) exit 1 ;; esac
|
|
3915
3919
|
exit 0
|
|
3916
3920
|
EOF
|
|
3917
3921
|
cat > "$SSHFAKE/rsync" <<'EOF'
|
|
3918
3922
|
#!/usr/bin/env bash
|
|
3919
3923
|
printf 'rsync %s\n' "$*" >> "${SSHLOG:?}"
|
|
3924
|
+
[ -t 0 ] || cat >/dev/null
|
|
3920
3925
|
exit 0
|
|
3921
3926
|
EOF
|
|
3922
3927
|
chmod +x "$SSHFAKE/ssh" "$SSHFAKE/rsync"
|
|
@@ -3966,14 +3971,22 @@ EOF
|
|
|
3966
3971
|
python3 - "$ACC/accounts.json" <<'EOF'
|
|
3967
3972
|
import json, os, sys
|
|
3968
3973
|
d = json.load(open(sys.argv[1]))
|
|
3969
|
-
d['peers'] = [{'target': 'gas@peer1', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'}
|
|
3974
|
+
d['peers'] = [{'target': 'gas@peer1', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'},
|
|
3975
|
+
{'target': 'gas@peer2', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'},
|
|
3976
|
+
{'target': 'gas@peer3', 'root': '/Users/gas/.claude-accounts', 'repo': '/Users/gas/claude-multiacc'}]
|
|
3970
3977
|
json.dump(d, open(sys.argv[1] + '.tmp', 'w'), indent=2); os.replace(sys.argv[1] + '.tmp', sys.argv[1])
|
|
3971
3978
|
EOF
|
|
3972
3979
|
: > "$SSHLOG"
|
|
3973
3980
|
out="$(PATH="$SSHFAKE:$PATH" CLAUDE_MULTIACC_NO_SYNC=0 claude-accounts sync 2>&1)"
|
|
3974
3981
|
rc=$?
|
|
3975
3982
|
[ "$rc" = "0" ] && t_ok "multi-target sync succeeds" || t_fail "multi-target sync" "rc=$rc: $out"
|
|
3976
|
-
check "multi-target sync reports
|
|
3983
|
+
check "multi-target sync reports every peer" "+ 3 peer(s)" "$out"
|
|
3984
|
+
# EVERY peer is pushed, not just the first: the ssh/rsync inside the loop must not
|
|
3985
|
+
# be allowed to eat the peer list off stdin (mini-3..8 got nothing for 18 days).
|
|
3986
|
+
grep -q "rsync.*gas@peer2:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
|
|
3987
|
+
&& grep -q "rsync.*gas@peer3:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
|
|
3988
|
+
&& t_ok "the second and third peers are pushed too (stdin not eaten by the first)" \
|
|
3989
|
+
|| t_fail "later peers skipped" "$(grep -c 'accounts.json' "$SSHLOG") manifest pushes: $(grep accounts.json "$SSHLOG" | tr '\n' '|')"
|
|
3977
3990
|
grep -q "rsync.*root@203.0.113.1:/root/.claude-accounts/accounts.json" "$SSHLOG" \
|
|
3978
3991
|
&& grep -q "rsync.*gas@peer1:/Users/gas/.claude-accounts/accounts.json" "$SSHLOG" \
|
|
3979
3992
|
&& t_ok "manifest pushed to BOTH targets" \
|
|
@@ -3984,8 +3997,8 @@ EOF
|
|
|
3984
3997
|
&& t_ok "primary target pushed before the peer" \
|
|
3985
3998
|
|| t_fail "target order" "primary=$first_primary peer=$first_peer"
|
|
3986
3999
|
n_remov="$(grep -c "for dd in acct-" "$SSHLOG")"
|
|
3987
|
-
[ "$n_remov" = "
|
|
3988
|
-
|| t_fail "per-target removal" "expected
|
|
4000
|
+
[ "$n_remov" = "4" ] && t_ok "removal propagation ran on each target" \
|
|
4001
|
+
|| t_fail "per-target removal" "expected 4 removal sweeps, saw $n_remov"
|
|
3989
4002
|
grep -q "post-sync" "$SSHLOG" && t_ok "post-sync hooks invoked" || t_fail "post-sync hooks" "none recorded"
|
|
3990
4003
|
# The MCP registry rides with the manifest to EVERY target; the machine-local overlay
|
|
3991
4004
|
# (a runner daemon's view of THIS Mac) never leaves. Without a registry file nothing
|
|
@@ -660,6 +660,36 @@ class ClaudeResetFleetRecordTest(ClaudePoolSandbox, unittest.TestCase):
|
|
|
660
660
|
self.assertIn("written before a confirmed limit reset", self.log())
|
|
661
661
|
|
|
662
662
|
|
|
663
|
+
class ClaudeDeadSignInReportTest(ClaudePoolSandbox, unittest.TestCase):
|
|
664
|
+
"""A sign-in that EXISTS but can no longer work must say so (2026-09-22: acct-02 and
|
|
665
|
+
acct-07 read `oauth: true` with a refresh token expired days before, so the panel
|
|
666
|
+
never offered the sign-in that restores their telemetry and limit resets)."""
|
|
667
|
+
|
|
668
|
+
def listed(self) -> dict:
|
|
669
|
+
result = subprocess.run([REPO / "bin/claude-accounts", "list", "--json"],
|
|
670
|
+
capture_output=True, text=True, env=self.env(), timeout=60,
|
|
671
|
+
check=False)
|
|
672
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
673
|
+
return json.loads(result.stdout)["accounts"][0]["credentials"]
|
|
674
|
+
|
|
675
|
+
def test_a_live_sign_in_is_alive(self) -> None:
|
|
676
|
+
creds = self.listed()
|
|
677
|
+
self.assertTrue(creds["oauth"])
|
|
678
|
+
self.assertTrue(creds["oauth_alive"])
|
|
679
|
+
self.assertNotIn("oauth_problem", creds)
|
|
680
|
+
|
|
681
|
+
def test_an_expired_refresh_token_is_a_dead_sign_in(self) -> None:
|
|
682
|
+
(self.account / "server.token").write_text("sk-ant-oat01-portable\n", encoding="utf-8")
|
|
683
|
+
dead = {"claudeAiOauth": {"accessToken": "old", "refreshToken": "r",
|
|
684
|
+
"expiresAt": int(time.time() - 7 * 86400) * 1000,
|
|
685
|
+
"refreshTokenExpiresAt": int(time.time() - 3 * 86400) * 1000}}
|
|
686
|
+
(self.account / ".credentials.json").write_text(json.dumps(dead), encoding="utf-8")
|
|
687
|
+
creds = self.listed()
|
|
688
|
+
self.assertTrue(creds["oauth"]) # it exists...
|
|
689
|
+
self.assertFalse(creds["oauth_alive"]) # ...but cannot work
|
|
690
|
+
self.assertIn("refresh token expired", creds["oauth_problem"])
|
|
691
|
+
|
|
692
|
+
|
|
663
693
|
class ClaudeResetReportingTest(ClaudePoolSandbox, unittest.TestCase):
|
|
664
694
|
def report(self, **overrides) -> dict:
|
|
665
695
|
result = self.run_limits("--json", **overrides)
|