claude-multiacc 1.0.21 → 2.0.0

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.
@@ -0,0 +1,201 @@
1
+ """Strict transport, normalization, and RFC 8785 primitives for pool-selection.v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import re
9
+ import unicodedata
10
+ from datetime import datetime, timezone
11
+ from decimal import Decimal, InvalidOperation
12
+
13
+ SCHEMA = "claude-multiacc/pool-selection.v2"
14
+ SELECTOR_VERSION = "2.0.0"
15
+ TIME_RE = re.compile(
16
+ r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-5][0-9]"
17
+ r"(\.[0-9]{1,6})?(Z|[+-][0-9]{2}:[0-9]{2})$")
18
+ DECIMAL_RE = re.compile(r"^-?(0|[1-9][0-9]*)(\.[0-9]+)?$")
19
+ SAFE_INTEGER = 9_007_199_254_740_991
20
+ MAX_PLAIN_DECIMAL_PLACES = 10_000
21
+ IDENTITY_KEYS = {"runner_id", "runner_generation", "engine", "account_id"}
22
+ CANDIDATE_KEYS = IDENTITY_KEYS | {
23
+ "status", "weekly_pct", "session_pct", "resets_at", "limited_until", "seen_at",
24
+ "provider_capable", "reservation_history"}
25
+ REQUEST_KEYS = {
26
+ "schema", "database_now", "policy", "required_engine", "producer_identity",
27
+ "excluded_identities", "candidates", "reservation_key"}
28
+ POLICIES = {
29
+ "default_claude", "default_codex", "default_both", "explicit", "producer_retry", "reviewer"}
30
+
31
+
32
+ def timestamp(value: object) -> tuple[datetime, str]:
33
+ if not isinstance(value, str) or not TIME_RE.fullmatch(value):
34
+ raise ValueError("invalid timestamp")
35
+ try:
36
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
37
+ utc = parsed.astimezone(timezone.utc).replace(tzinfo=None)
38
+ except (ValueError, OverflowError) as error:
39
+ raise ValueError("invalid timestamp") from error
40
+ return parsed, utc.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
41
+
42
+
43
+ def decimal_value(value: object) -> tuple[Decimal | None, str | None]:
44
+ if isinstance(value, bool) or value is None:
45
+ return None, None
46
+ if isinstance(value, str) and not DECIMAL_RE.fullmatch(value):
47
+ return None, None
48
+ try:
49
+ parsed = Decimal(str(value))
50
+ if not parsed.is_finite() or (isinstance(value, float) and not math.isfinite(value)):
51
+ return None, None
52
+ if parsed <= 0:
53
+ clamped = Decimal(0)
54
+ elif parsed >= 100:
55
+ clamped = Decimal(100)
56
+ elif parsed.as_tuple().exponent < -MAX_PLAIN_DECIMAL_PLACES:
57
+ return None, None
58
+ else:
59
+ clamped = parsed
60
+ result = format(clamped, "f") if clamped else "0"
61
+ except (InvalidOperation, ValueError, OverflowError):
62
+ return None, None
63
+ return clamped, result.rstrip("0").rstrip(".") if "." in result else result
64
+
65
+
66
+ def identity(raw: dict) -> tuple[int | None, int | None, str | None, str | None, bool]:
67
+ runner = raw.get("runner_id")
68
+ generation = raw.get("runner_generation")
69
+ engine_value = raw.get("engine")
70
+ account_value = raw.get("account_id")
71
+ engine = engine_value.strip().lower() if isinstance(engine_value, str) else None
72
+ account = unicodedata.normalize("NFC", account_value).strip() \
73
+ if isinstance(account_value, str) else None
74
+ runner = runner if isinstance(runner, int) and not isinstance(runner, bool) and runner > 0 else None
75
+ generation = generation if isinstance(generation, int) \
76
+ and not isinstance(generation, bool) and generation > 0 else None
77
+ engine = engine if engine in {"claude", "codex"} else None
78
+ account = account or None
79
+ return runner, generation, engine, account, all((runner, generation, engine, account))
80
+
81
+
82
+ def collision_key(parts: tuple) -> tuple:
83
+ runner, generation, engine, account = parts
84
+ folded = unicodedata.normalize("NFC", account.casefold()) if account else account
85
+ return runner, generation, engine, folded
86
+
87
+
88
+ def _encode_rfc8785(value: object) -> str:
89
+ if value is None:
90
+ return "null"
91
+ if value is True:
92
+ return "true"
93
+ if value is False:
94
+ return "false"
95
+ if isinstance(value, int):
96
+ if abs(value) > SAFE_INTEGER:
97
+ raise ValueError("integer outside RFC 8785 interoperable range")
98
+ return str(value)
99
+ if isinstance(value, str):
100
+ value.encode("utf-8")
101
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
102
+ if isinstance(value, list):
103
+ return "[" + ",".join(_encode_rfc8785(item) for item in value) + "]"
104
+ if isinstance(value, dict):
105
+ if any(not isinstance(key, str) for key in value):
106
+ raise ValueError("RFC 8785 object keys must be strings")
107
+ keys = sorted(value, key=lambda key: key.encode("utf-16-be", "surrogatepass"))
108
+ pairs = (_encode_rfc8785(key) + ":" + _encode_rfc8785(value[key]) for key in keys)
109
+ return "{" + ",".join(pairs) + "}"
110
+ raise ValueError(f"value outside frozen RFC 8785 digest domain: {type(value).__name__}")
111
+
112
+
113
+ def canonical_sha256(value: object) -> str:
114
+ return hashlib.sha256(_encode_rfc8785(value).encode("utf-8")).hexdigest()
115
+
116
+
117
+ def _identity_error(value: object, field: str) -> str | None:
118
+ if not isinstance(value, dict) or set(value) != IDENTITY_KEYS:
119
+ return field
120
+ runner, generation, _, account, valid = identity(value)
121
+ if not isinstance(value.get("engine"), str) or not valid:
122
+ return field
123
+ if abs(runner or 0) > SAFE_INTEGER or abs(generation or 0) > SAFE_INTEGER or account is None:
124
+ return field
125
+ return None
126
+
127
+
128
+ def _candidate_error(value: object, index: int) -> str | None:
129
+ field = f"candidates[{index}]"
130
+ if not isinstance(value, dict) or set(value) != CANDIDATE_KEYS:
131
+ return field
132
+ for name in ("runner_id", "runner_generation"):
133
+ item = value[name]
134
+ if not isinstance(item, int) or isinstance(item, bool) or not 0 < item <= SAFE_INTEGER:
135
+ return f"{field}.{name}"
136
+ if not all(isinstance(value[name], str) for name in ("engine", "account_id", "status", "seen_at")):
137
+ return field
138
+ if not isinstance(value["provider_capable"], bool):
139
+ return f"{field}.provider_capable"
140
+ scalars = (str, int, float, Decimal, bool, type(None))
141
+ if any(not isinstance(value[name], scalars) for name in ("weekly_pct", "session_pct")):
142
+ return field
143
+ if any(isinstance(value[name], float) and not math.isfinite(value[name])
144
+ for name in ("weekly_pct", "session_pct")):
145
+ return field
146
+ if any(item is not None and not isinstance(item, str)
147
+ for item in (value["resets_at"], value["limited_until"])):
148
+ return field
149
+ history = value["reservation_history"]
150
+ if not isinstance(history, dict) or set(history) != {"active_expires_at", "last_selected_at"}:
151
+ return f"{field}.reservation_history"
152
+ if any(item is not None and not isinstance(item, str) for item in history.values()):
153
+ return f"{field}.reservation_history"
154
+ if history["active_expires_at"] is not None:
155
+ try:
156
+ timestamp(history["active_expires_at"])
157
+ except ValueError:
158
+ return f"{field}.reservation_history.active_expires_at"
159
+ return None
160
+
161
+
162
+ def request_error(request: object) -> tuple[str, dict] | None:
163
+ if not isinstance(request, dict):
164
+ return "invalid_request", {"field": "$"}
165
+ if "schema" not in request:
166
+ return "invalid_request", {"field": "schema"}
167
+ if request.get("schema") != SCHEMA:
168
+ return "unsupported_schema", {"field": "schema"}
169
+ if set(request) != REQUEST_KEYS:
170
+ return "invalid_request", {"field": "$"}
171
+ try:
172
+ timestamp(request["database_now"])
173
+ except ValueError:
174
+ return "invalid_request", {"field": "database_now"}
175
+ policy, required = request["policy"], request["required_engine"]
176
+ if not isinstance(policy, str) or policy not in POLICIES:
177
+ return "invalid_request", {"field": "policy"}
178
+ if required is not None and (not isinstance(required, str) or required not in {"claude", "codex"}):
179
+ return "invalid_request", {"field": "required_engine"}
180
+ needs_engine = policy in {"explicit", "producer_retry", "reviewer"}
181
+ if (needs_engine and required is None) or (not needs_engine and required is not None):
182
+ return "invalid_request", {"field": "required_engine"}
183
+ producer = request["producer_identity"]
184
+ if (policy == "reviewer") != (producer is not None):
185
+ return "invalid_request", {"field": "producer_identity"}
186
+ if producer is not None and (error := _identity_error(producer, "producer_identity")):
187
+ return "invalid_request", {"field": error}
188
+ excluded = request["excluded_identities"]
189
+ if not isinstance(excluded, list):
190
+ return "invalid_request", {"field": "excluded_identities"}
191
+ for index, value in enumerate(excluded):
192
+ if error := _identity_error(value, f"excluded_identities[{index}]"):
193
+ return "invalid_request", {"field": error}
194
+ if not isinstance(request["reservation_key"], str) or not request["reservation_key"].strip():
195
+ return "invalid_request", {"field": "reservation_key"}
196
+ if not isinstance(request["candidates"], list):
197
+ return "invalid_request", {"field": "candidates"}
198
+ for index, value in enumerate(request["candidates"]):
199
+ if error := _candidate_error(value, index):
200
+ return "invalid_request", {"field": error}
201
+ return None
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "claude-multiacc",
3
- "version": "1.0.21",
4
- "description": "Multi-account addon for Claude Code and OpenAI Codex CLI: every claude / claude -p and every codex / codex exec runs under a randomly-picked subscription account with the most usage headroom. Mirrors to a deploy server. No API keys.",
3
+ "version": "2.0.0",
4
+ "description": "Unified Claude Code and OpenAI Codex subscription pooling with quota-aware selection.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-multiacc": "bin/cli.mjs"
7
+ "claude-multiacc": "bin/cli.mjs",
8
+ "multiacc-select": "bin/multiacc-select"
8
9
  },
9
10
  "files": [
10
11
  "bin/",
@@ -12,6 +13,7 @@
12
13
  "install.sh",
13
14
  "scripts/postinstall.mjs",
14
15
  "tests/",
16
+ "docs/",
15
17
  "README.md",
16
18
  "CLAUDE_ACCS_TASK.md"
17
19
  ],
@@ -51,7 +53,7 @@
51
53
  },
52
54
  "scripts": {
53
55
  "postinstall": "node scripts/postinstall.mjs",
54
- "test": "bash tests/run-tests.sh"
56
+ "test": "bash tests/run-tests.sh && python3 tests/test_selector.py"
55
57
  },
56
58
  "dependencies": {
57
59
  "update-notifier": "^7.3.1"
@@ -0,0 +1,209 @@
1
+ """Golden and transport tests for pool-selection.v2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ ROOT = Path(__file__).resolve().parents[1]
12
+ sys.path.insert(0, str(ROOT / "lib"))
13
+
14
+ from selector_policy import select # noqa: E402
15
+ from selector_primitives import canonical_sha256 # noqa: E402
16
+
17
+ SCHEMA = "claude-multiacc/pool-selection.v2"
18
+ NOW = "2026-08-25T09:00:00.000000Z"
19
+
20
+
21
+ def _candidate(**patch) -> dict:
22
+ row = {
23
+ "runner_id": 3, "runner_generation": 7, "engine": "claude", "account_id": "acct-a",
24
+ "status": "active", "weekly_pct": 20, "session_pct": 40,
25
+ "resets_at": "2026-08-26T09:00:00Z", "limited_until": None,
26
+ "seen_at": "2026-08-25T08:59:00Z", "provider_capable": True,
27
+ "reservation_history": {"active_expires_at": None, "last_selected_at": None}}
28
+ row.update(patch)
29
+ return row
30
+
31
+
32
+ def _request(candidates: list[dict], **patch) -> dict:
33
+ request = {
34
+ "schema": SCHEMA, "database_now": NOW, "policy": "default_both",
35
+ "required_engine": None, "producer_identity": None, "excluded_identities": [],
36
+ "candidates": candidates, "reservation_key": "proof"}
37
+ request.update(patch)
38
+ return request
39
+
40
+
41
+ class SelectorTests(unittest.TestCase):
42
+ def test_union_proof_vector_is_byte_stable(self):
43
+ request = _request([
44
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=10, session_pct=20,
45
+ seen_at="2026-08-25T08:54:59Z"),
46
+ _candidate(engine="codex", account_id="codex-b", weekly_pct=30, session_pct=40),
47
+ _candidate(engine="both", account_id=" ", weekly_pct=0, session_pct=0),
48
+ ], reservation_key="launch-1")
49
+ response = select(request)
50
+ self.assertEqual(response["engine"], "codex")
51
+ self.assertEqual(response["account_id"], "codex-b")
52
+ self.assertEqual(
53
+ response["candidate_snapshot_digest"],
54
+ "e8ef0b4bc7b95e76231ee6da79616d92bd9251c1c991845c8c0524fcbfb6593a")
55
+ self.assertEqual(
56
+ response["selection_digest"],
57
+ "66179083753e967bbe3914692103c1b4a30966a86ecf85cd62e14f3511c78cc3")
58
+
59
+ def test_unicode_and_active_reservation_proof(self):
60
+ busy = {"active_expires_at": "2026-08-25T09:00:00.000001Z", "last_selected_at": None}
61
+ response = select(_request([
62
+ _candidate(engine="claude", account_id="claude-active", reservation_history=busy),
63
+ _candidate(engine="codex", account_id="café-🚀", weekly_pct=25, session_pct=50),
64
+ ], policy="explicit", required_engine="codex", reservation_key="launch-unicode"))
65
+ self.assertEqual((response["engine"], response["account_id"]), ("codex", "café-🚀"))
66
+ self.assertEqual(
67
+ response["candidate_snapshot_digest"],
68
+ "29e8e7d1a611f38ee0be43622102b33875e7ff75ff9024aa43d16151575071d4")
69
+ self.assertEqual(
70
+ response["selection_digest"],
71
+ "eaea620082bb0882a12598b8cc46e79090da07be0b307f34f0a68f555a6c9d3e")
72
+
73
+ def test_both_chooses_each_provider_and_survives_provider_loss(self):
74
+ codex = select(_request([
75
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=70, session_pct=60),
76
+ _candidate(engine="codex", account_id="codex-a", weekly_pct=20, session_pct=30)]))
77
+ claude = select(_request([
78
+ _candidate(engine="claude", account_id="claude-a", weekly_pct=10, session_pct=30),
79
+ _candidate(engine="codex", account_id="codex-a", weekly_pct=20, session_pct=80)]))
80
+ only = select(_request([_candidate(engine="codex", account_id="codex-only")]))
81
+ self.assertEqual((codex["engine"], claude["engine"], only["engine"]),
82
+ ("codex", "claude", "codex"))
83
+
84
+ def test_known_quota_and_least_recent_selection_win(self):
85
+ known = select(_request([
86
+ _candidate(engine="claude", account_id="unknown", weekly_pct=None, session_pct=None),
87
+ _candidate(engine="codex", account_id="known", weekly_pct=99, session_pct=99)]))
88
+ old = {"active_expires_at": None, "last_selected_at": "2026-08-25T08:00:00Z"}
89
+ recent = {"active_expires_at": None, "last_selected_at": "2026-08-25T08:50:00Z"}
90
+ fair = select(_request([
91
+ _candidate(engine="claude", account_id="recent", weekly_pct=20, session_pct=20,
92
+ reservation_history=recent),
93
+ _candidate(engine="codex", account_id="old", weekly_pct=20, session_pct=20,
94
+ reservation_history=old)]))
95
+ self.assertEqual((known["account_id"], fair["account_id"]), ("known", "old"))
96
+ utc = select(_request([_candidate()]))
97
+ offset = select(_request(
98
+ [_candidate()], database_now="2026-08-25T12:00:00+03:00"))
99
+ self.assertEqual(utc["selection_digest"], offset["selection_digest"])
100
+
101
+ def test_explicit_and_reviewer_policies_never_cross_provider(self):
102
+ explicit = select(_request([
103
+ _candidate(engine="claude", account_id="free", weekly_pct=0, session_pct=0),
104
+ _candidate(engine="codex", account_id="busy", weekly_pct=90, session_pct=90),
105
+ ], policy="explicit", required_engine="codex"))
106
+ producer = {"runner_id": 3, "runner_generation": 7, "engine": "claude",
107
+ "account_id": "producer"}
108
+ review = select(_request([
109
+ _candidate(engine="claude", account_id="producer", weekly_pct=0, session_pct=0),
110
+ _candidate(engine="claude", account_id="reviewer", weekly_pct=90, session_pct=90),
111
+ ], policy="reviewer", required_engine="claude", producer_identity=producer,
112
+ excluded_identities=[producer]))
113
+ sole = select(_request([
114
+ _candidate(engine="claude", account_id="producer")], policy="reviewer",
115
+ required_engine="claude", producer_identity=producer))
116
+ alternative = {"runner_id": 4, "runner_generation": 7, "engine": "claude",
117
+ "account_id": "alternative"}
118
+ fallback = select(_request([
119
+ _candidate(account_id="producer"),
120
+ _candidate(runner_id=4, account_id="alternative")], policy="reviewer",
121
+ required_engine="claude", producer_identity=producer,
122
+ excluded_identities=[alternative]))
123
+ self.assertEqual((explicit["engine"], review["account_id"]), ("codex", "reviewer"))
124
+ self.assertEqual(sole["fallback_reason"], "sole_eligible_account")
125
+ self.assertEqual((fallback["account_id"], fallback["fallback_reason"]),
126
+ ("producer", "sole_eligible_account"))
127
+
128
+ def test_stable_errors_fail_closed(self):
129
+ collisions = [(" Acct-A ", "acct-a"), ("CAFÉ", "cafe\u0301")]
130
+ duplicates = [select(_request([
131
+ _candidate(engine=" CLAUDE ", account_id=left),
132
+ _candidate(engine="claude", account_id=right)])) for left, right in collisions]
133
+ no_candidate = select(_request([
134
+ _candidate(engine="claude")], policy="default_codex"))
135
+ invalid = select({**_request([]), "unexpected": True})
136
+ preserved = select(_request([_candidate(account_id=" Acct-A ")]))
137
+ overflow_now = select(_request(
138
+ [], database_now="9999-12-31T23:59:59-01:00"))
139
+ overflow_seen = select(_request([
140
+ _candidate(seen_at="0001-01-01T00:00:00+01:00")]))
141
+ overflow_history = select(_request([_candidate(reservation_history={
142
+ "active_expires_at": "9999-12-31T23:59:59-01:00", "last_selected_at": None})]))
143
+ self.assertTrue(all(item["error_code"] == "duplicate_candidate_identity"
144
+ for item in duplicates))
145
+ self.assertTrue(all(item["error_detail"] == {"input_ordinals": [0, 1]}
146
+ for item in duplicates))
147
+ self.assertEqual(no_candidate["error_code"], "no_candidate")
148
+ self.assertEqual(preserved["account_id"], "Acct-A")
149
+ self.assertEqual(overflow_now["error_detail"], {"field": "database_now"})
150
+ self.assertEqual(overflow_seen["error_code"], "no_candidate")
151
+ self.assertEqual(overflow_history["error_detail"], {
152
+ "field": "candidates[0].reservation_history.active_expires_at"})
153
+ self.assertEqual(invalid, {"schema": SCHEMA, "selector_version": "2.0.0", "ok": False,
154
+ "error_code": "invalid_request", "error_detail": {"field": "$"}})
155
+
156
+ def test_rfc8785_digest_vectors(self):
157
+ self.assertEqual(
158
+ canonical_sha256({"account_id": "café-🚀", "metadata": {"": "bmp", "😀": "astral"}}),
159
+ "2f33747d98d7c2f3cfc0a660eba492a7652a66bc46b147edeccf4cc941154981")
160
+ with self.assertRaises(ValueError):
161
+ canonical_sha256({"unsafe": 9_007_199_254_740_992})
162
+
163
+ def test_cli_transport_and_version_contract(self):
164
+ binary = ROOT / "bin" / "multiacc-select"
165
+ args = [str(binary), "--request-json", "-", "--response-json", "-"]
166
+ good_payload = json.dumps(_request([_candidate(engine="codex")]), separators=(",", ":"))
167
+ good = subprocess.run(args, input=good_payload, text=True, capture_output=True, check=True)
168
+ bad_payloads = [
169
+ good_payload.replace('"weekly_pct":20', '"weekly_pct":NaN', 1),
170
+ good_payload.replace(
171
+ '"weekly_pct":20', '"weekly_pct":1e999999999999999999999999999999', 1),
172
+ good_payload.replace('"policy":"default_both"',
173
+ '"policy":"explicit","policy":"default_both"', 1),
174
+ good_payload.replace('"active_expires_at":null',
175
+ '"active_expires_at":null,"active_expires_at":null', 1),
176
+ good_payload + "{}",
177
+ ]
178
+ bad = [subprocess.run(args, input=payload, text=True, capture_output=True, check=True)
179
+ for payload in bad_payloads]
180
+ decimal_request = _request([
181
+ _candidate(account_id="z-lower", weekly_pct="LOW"),
182
+ _candidate(runner_id=4, account_id="a-higher", weekly_pct="HIGH")])
183
+ decimal_payload = json.dumps(decimal_request, separators=(",", ":"))
184
+ decimal_payload = decimal_payload.replace(
185
+ '"LOW"', "50.00000000000000000000000000001")
186
+ decimal_payload = decimal_payload.replace(
187
+ '"HIGH"', "50.00000000000000000000000000002")
188
+ decimal_response = subprocess.run(
189
+ args, input=decimal_payload, text=True, capture_output=True, check=True)
190
+ version = subprocess.run([str(binary), "--version"], text=True,
191
+ capture_output=True, check=True).stdout.strip()
192
+ version_script = (
193
+ "import {resolveVersion} from './scripts/auto-version.mjs';"
194
+ "console.log(resolveVersion('2.0.0','1.0.21'));"
195
+ "console.log(resolveVersion('2.0.0','2.0.0'));"
196
+ )
197
+ versions = subprocess.run(
198
+ ["node", "--input-type=module", "-e", version_script], cwd=ROOT,
199
+ text=True, capture_output=True, check=True).stdout.splitlines()
200
+ self.assertTrue(json.loads(good.stdout)["ok"])
201
+ self.assertTrue(all(json.loads(item.stdout)["error_code"] == "invalid_request"
202
+ for item in bad))
203
+ self.assertEqual(json.loads(decimal_response.stdout)["account_id"], "z-lower")
204
+ self.assertEqual(version, "2.0.0")
205
+ self.assertEqual(versions, ["2.0.0", "2.0.1"])
206
+
207
+
208
+ if __name__ == "__main__":
209
+ unittest.main()