claude-multiacc 2.0.47 → 2.0.48
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/docs/ACCOUNT_OPERATIONS.md +7 -1
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
- package/lib/__pycache__/claude_token_identity.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/claude_client_error.py +18 -0
- package/lib/report.py +4 -0
- package/package.json +1 -1
- package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_claude_token_identity.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/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
- package/tests/run-tests.sh +6 -0
- package/tests/test_claude_client_error.py +28 -0
- package/tests/test_claude_org_report.py +68 -0
|
@@ -279,7 +279,7 @@ providers (`lib/report.py`), so a consumer writes one parser:
|
|
|
279
279
|
"portable": true,
|
|
280
280
|
"credentials": {"oauth": true, "token": true, "oauth_expires_at": "…Z",
|
|
281
281
|
"oauth_refresh_expires_at": "…Z", "token_minted_at": "…Z",
|
|
282
|
-
"token_age_days": 39},
|
|
282
|
+
"token_age_days": 39, "org_access_verified_at": 1790765000.125},
|
|
283
283
|
"limited": false, "limit_reset_at": null, "limit_detail": null,
|
|
284
284
|
"usage": {"fetched_at": "…Z", "age_seconds": 41, "source": "oauth",
|
|
285
285
|
// a null percent = that signal had no informative bucket; all three
|
|
@@ -306,6 +306,12 @@ A consumer that only distinguishes *usable / parked / needs-a-human* can read
|
|
|
306
306
|
without the limit overlay. `limited` follows the shim's marker rule exactly — a marker
|
|
307
307
|
whose reset time has passed does not count, an unreadable one does.
|
|
308
308
|
|
|
309
|
+
Claude's optional `credentials.org_access_verified_at` is a Unix timestamp from the
|
|
310
|
+
last real verification that passed the organisation access gate (a successful inference
|
|
311
|
+
or a genuine usage-limit refusal). Missing or invalid proof is omitted. Consumers can
|
|
312
|
+
use newer proof to lift an older organisation hold; a later refusal still keeps the
|
|
313
|
+
account blocked. Usage telemetry, login and token verification alone are not recovery proof.
|
|
314
|
+
|
|
309
315
|
A percent field is **null** when that signal had no informative bucket to aggregate. The
|
|
310
316
|
writers keep the signals separate: `weekly_percent` comes from informative weekly buckets,
|
|
311
317
|
`session_percent` from informative session ones, and each is omitted on its own when its
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,11 +11,29 @@ import argparse
|
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
import json
|
|
13
13
|
import math
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
import sys
|
|
16
|
+
import time
|
|
15
17
|
|
|
16
18
|
ERRORS = {"authentication_failed": "auth", "oauth_org_not_allowed": "org-blocked"}
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
def org_access_verified_at(account_dir, now=None):
|
|
22
|
+
"""Read a bounded proof that this account's organization gate allowed inference."""
|
|
23
|
+
try:
|
|
24
|
+
with (Path(account_dir) / ".client-org-cleared").open("rb") as handle:
|
|
25
|
+
raw = handle.read(65)
|
|
26
|
+
if len(raw) > 64:
|
|
27
|
+
return None
|
|
28
|
+
stamp = float(raw.strip())
|
|
29
|
+
reference = time.time() if now is None else float(now)
|
|
30
|
+
except (OSError, ValueError, TypeError, OverflowError):
|
|
31
|
+
return None
|
|
32
|
+
if not math.isfinite(stamp) or not math.isfinite(reference) or not 0 < stamp <= reference + 300:
|
|
33
|
+
return None
|
|
34
|
+
return stamp
|
|
35
|
+
|
|
36
|
+
|
|
19
37
|
def iso_epoch(value):
|
|
20
38
|
"""Return an aware ISO timestamp's epoch, or None when it cannot prove timing."""
|
|
21
39
|
if not isinstance(value, str) or "T" not in value or any(c.isspace() for c in value):
|
package/lib/report.py
CHANGED
|
@@ -47,6 +47,7 @@ import time
|
|
|
47
47
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
48
48
|
import keychain # noqa: E402
|
|
49
49
|
from audit import creds_doc_state, token_verified # noqa: E402
|
|
50
|
+
from claude_client_error import org_access_verified_at # noqa: E402
|
|
50
51
|
from claude_reset import marker_superseded, record_from # noqa: E402
|
|
51
52
|
|
|
52
53
|
SCHEMA = 'claude-multiacc/pool.v1'
|
|
@@ -178,6 +179,9 @@ def _claude_credentials(d):
|
|
|
178
179
|
# True once THIS exact token passed a real inference on this machine
|
|
179
180
|
# (.server-token-verified); False = present but never proven here.
|
|
180
181
|
'token_verified': token_verified(d, tpath) if has_token else None}
|
|
182
|
+
proof = org_access_verified_at(d)
|
|
183
|
+
if proof is not None:
|
|
184
|
+
detail['org_access_verified_at'] = proof
|
|
181
185
|
o = None
|
|
182
186
|
if has_oauth:
|
|
183
187
|
detail['oauth_store'] = 'file'
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/tests/run-tests.sh
CHANGED
|
@@ -9303,6 +9303,12 @@ else
|
|
|
9303
9303
|
t_fail "claude persistent organization-block suite" "see unittest output above"
|
|
9304
9304
|
fi
|
|
9305
9305
|
|
|
9306
|
+
if python3 "$REPO_DIR/tests/test_claude_org_report.py"; then
|
|
9307
|
+
t_ok "claude: organization recovery report contract suite"
|
|
9308
|
+
else
|
|
9309
|
+
t_fail "claude organization recovery report suite" "see unittest output above"
|
|
9310
|
+
fi
|
|
9311
|
+
|
|
9306
9312
|
# ---- summary ---------------------------------------------------------------------
|
|
9307
9313
|
echo
|
|
9308
9314
|
echo "passed: $PASS failed: $FAIL"
|
|
@@ -7,6 +7,7 @@ import json
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
import subprocess
|
|
9
9
|
import sys
|
|
10
|
+
import tempfile
|
|
10
11
|
import unittest
|
|
11
12
|
|
|
12
13
|
REPO = Path(__file__).resolve().parents[1]
|
|
@@ -114,6 +115,33 @@ class ClaudeClientErrorTests(unittest.TestCase):
|
|
|
114
115
|
result = subprocess.run(command + args, input=json.dumps(record), text=True, capture_output=True)
|
|
115
116
|
self.assertEqual((result.returncode, result.stdout, result.stderr), (1, "", ""))
|
|
116
117
|
|
|
118
|
+
def test_org_access_proof_returns_exact_stamp_with_clock_slack(self):
|
|
119
|
+
now = 1800000000.25
|
|
120
|
+
with tempfile.TemporaryDirectory(prefix="multiacc-client-org-proof-") as directory:
|
|
121
|
+
marker = Path(directory) / ".client-org-cleared"
|
|
122
|
+
for stamp in (now - 123.125, now, now + 0.125, now + 300):
|
|
123
|
+
with self.subTest(stamp=stamp):
|
|
124
|
+
marker.write_text(f" {stamp}\n")
|
|
125
|
+
self.assertEqual(errors.org_access_verified_at(directory, now), stamp)
|
|
126
|
+
marker.write_text(str(now).ljust(64))
|
|
127
|
+
self.assertEqual(errors.org_access_verified_at(directory, now), now)
|
|
128
|
+
stamp = errors.time.time() - 10
|
|
129
|
+
marker.write_text(repr(stamp))
|
|
130
|
+
self.assertEqual(errors.org_access_verified_at(directory), stamp)
|
|
131
|
+
|
|
132
|
+
def test_org_access_proof_ignores_missing_invalid_or_oversized_values(self):
|
|
133
|
+
now = 1800000000.25
|
|
134
|
+
with tempfile.TemporaryDirectory(prefix="multiacc-client-org-proof-") as directory:
|
|
135
|
+
marker = Path(directory) / ".client-org-cleared"
|
|
136
|
+
self.assertIsNone(errors.org_access_verified_at(directory, now))
|
|
137
|
+
invalid = [b"", b" ", b"NaN", b"Infinity", b"-Infinity", b"-10", b"0", b"true", b"false",
|
|
138
|
+
b"True", b"null", b"{}", b"[]", b'"1800000000"', b"\xff", b"1" + b" " * 64,
|
|
139
|
+
str(now + 300.001).encode()]
|
|
140
|
+
for raw in invalid:
|
|
141
|
+
with self.subTest(raw=raw):
|
|
142
|
+
marker.write_bytes(raw)
|
|
143
|
+
self.assertIsNone(errors.org_access_verified_at(directory, now))
|
|
144
|
+
|
|
117
145
|
|
|
118
146
|
if __name__ == "__main__":
|
|
119
147
|
unittest.main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The report publishes recovery proof without overriding a newer refusal."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import unittest
|
|
12
|
+
|
|
13
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OrgReportTests(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.work = tempfile.TemporaryDirectory(prefix="multiacc-org-report-")
|
|
19
|
+
self.addCleanup(self.work.cleanup)
|
|
20
|
+
self.root = Path(self.work.name)
|
|
21
|
+
self.account = self.root / "acct-01"
|
|
22
|
+
self.account.mkdir()
|
|
23
|
+
self.now = time.time()
|
|
24
|
+
(self.root / "accounts.json").write_text(json.dumps({"version": 1, "server": "none",
|
|
25
|
+
"accounts": [{"id": "acct-01", "email": "fixture@example.invalid", "home": "mac"}]}))
|
|
26
|
+
(self.account / ".credentials.json").write_text(json.dumps({"claudeAiOauth": {
|
|
27
|
+
"accessToken": "synthetic", "refreshToken": "synthetic",
|
|
28
|
+
"expiresAt": (self.now + 86400) * 1000,
|
|
29
|
+
"refreshTokenExpiresAt": (self.now + 864000) * 1000}}))
|
|
30
|
+
|
|
31
|
+
def row(self, kind, provider="claude"):
|
|
32
|
+
result = subprocess.run([sys.executable, str(REPO / "lib/report.py"),
|
|
33
|
+
str(self.root), provider, "mac", kind], capture_output=True, text=True,
|
|
34
|
+
env={**os.environ, "CLAUDE_MULTIACC_KEYCHAIN": "0", "PYTHONDONTWRITEBYTECODE": "1"},
|
|
35
|
+
timeout=10, check=True)
|
|
36
|
+
self.assertEqual(result.stderr, "")
|
|
37
|
+
document = json.loads(result.stdout)
|
|
38
|
+
self.assertEqual(document["schema"], "claude-multiacc/pool.v1")
|
|
39
|
+
return document["accounts"][0]
|
|
40
|
+
|
|
41
|
+
def test_all_report_verbs_publish_proof_and_preserve_later_org_block(self):
|
|
42
|
+
stamp = self.now - 5.125
|
|
43
|
+
(self.account / ".client-org-cleared").write_text(repr(stamp))
|
|
44
|
+
for kind in ("list", "status", "limits"):
|
|
45
|
+
with self.subTest(kind=kind):
|
|
46
|
+
row = self.row(kind)
|
|
47
|
+
self.assertEqual(row["credentials"]["org_access_verified_at"], stamp)
|
|
48
|
+
self.assertEqual(row["status"], "active")
|
|
49
|
+
(self.account / ".expired").write_text(f"{int(self.now)}\nreason=org-blocked\n")
|
|
50
|
+
row = self.row(kind)
|
|
51
|
+
self.assertEqual(row["credentials"]["org_access_verified_at"], stamp)
|
|
52
|
+
self.assertEqual(row["status"], "blocked")
|
|
53
|
+
self.assertFalse(row["selectable"])
|
|
54
|
+
(self.account / ".expired").unlink()
|
|
55
|
+
|
|
56
|
+
def test_invalid_missing_and_other_provider_proofs_are_omitted(self):
|
|
57
|
+
marker = self.account / ".client-org-cleared"
|
|
58
|
+
self.assertNotIn("org_access_verified_at", self.row("list")["credentials"])
|
|
59
|
+
for text in ("NaN", "-1", str(self.now + 3600), "1" * 65):
|
|
60
|
+
with self.subTest(text=text):
|
|
61
|
+
marker.write_text(text)
|
|
62
|
+
self.assertNotIn("org_access_verified_at", self.row("status")["credentials"])
|
|
63
|
+
marker.write_text(repr(self.now))
|
|
64
|
+
self.assertNotIn("org_access_verified_at", self.row("list", "codex")["credentials"])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
unittest.main()
|