claude-multiacc 2.0.50 → 2.0.51
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/codex +55 -11
- package/bin/codex-accounts +11 -4
- package/bin/multiacc-select +6 -1
- package/docs/CODEX.md +90 -0
- package/lib/__pycache__/audit.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
- package/lib/__pycache__/autoresume_probe.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_credit_local_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
- package/lib/__pycache__/codex_credits.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__/report.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
- package/lib/__pycache__/selector_funding.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/autoresume.py +7 -2
- package/lib/autoresume_probe.py +3 -3
- package/lib/codex_credit_local_scope.py +35 -0
- package/lib/codex_credit_selection.py +166 -0
- package/lib/codex_credits.py +162 -0
- package/lib/report.py +6 -0
- package/lib/selector_credit_scope.py +55 -0
- package/lib/selector_credits.py +99 -0
- package/lib/selector_funding.py +111 -0
- package/lib/selector_policy.py +20 -2
- package/lib/selector_primitives.py +20 -2
- 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_autoresume_fallback.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_autoresume_probe.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_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_codex_reset_credits_report.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_selector.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_selector_spread.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/test_codex_reset_credits.py +112 -0
- package/tests/test_codex_reset_credits_polling.py +94 -0
- package/tests/test_codex_reset_credits_report.py +85 -0
- package/tests/test_selector_credit_scope.py +118 -0
- package/tests/test_selector_credit_shim.py +139 -0
- package/tests/test_selector_credits.py +139 -0
package/lib/selector_policy.py
CHANGED
|
@@ -5,6 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from decimal import Decimal, localcontext
|
|
7
7
|
|
|
8
|
+
from selector_funding import (
|
|
9
|
+
apply_funding, best_funding_tier, credit_rank, funding_proof, funding_score,
|
|
10
|
+
)
|
|
8
11
|
from selector_primitives import (
|
|
9
12
|
DEFAULT_HEADROOM_BAND,
|
|
10
13
|
DEFAULT_SESSION_GATE,
|
|
@@ -207,25 +210,35 @@ def select(request: object) -> dict:
|
|
|
207
210
|
duplicate = next((items for items in identities.values() if len(items) > 1), None)
|
|
208
211
|
if duplicate:
|
|
209
212
|
return _error("duplicate_candidate_identity", {"input_ordinals": duplicate})
|
|
213
|
+
credit_mode = request.get("credit_fallback") is True
|
|
214
|
+
if credit_mode:
|
|
215
|
+
apply_funding(rows, request["candidates"], now, request)
|
|
210
216
|
producer, alternatives = _apply_policy(request, rows)
|
|
211
217
|
eligible = [row for row in rows if row["eligible"]]
|
|
212
218
|
if not eligible:
|
|
213
219
|
return _error("no_candidate", {"eligible_count": 0})
|
|
220
|
+
funded = best_funding_tier(eligible) if credit_mode else eligible
|
|
214
221
|
band, band_text = decimal_value(request.get("headroom_band", DEFAULT_HEADROOM_BAND))
|
|
215
222
|
gate, gate_text = decimal_value(request.get("session_gate", DEFAULT_SESSION_GATE))
|
|
216
223
|
# Two cuts, in this order (operator's 2026-09-03 decision): the session gate says
|
|
217
224
|
# WHO may be considered, the weekly band says which of those count as equally
|
|
218
225
|
# good. "healthy or known" is the gate stepping aside when nobody clears it.
|
|
219
|
-
known = [row for row in
|
|
226
|
+
known = [row for row in funded if row["quota_known"]]
|
|
220
227
|
healthy = _session_gate(known, gate)
|
|
221
228
|
ranked = healthy or known
|
|
222
229
|
floor = _band_floor(ranked, band) if band is not None else None
|
|
223
230
|
if floor is None:
|
|
224
231
|
# No usable quota anywhere: every eligible row is an unknown peer.
|
|
225
232
|
# Keep LRU and the request spread instead of preferring a fixed identity.
|
|
226
|
-
banded =
|
|
233
|
+
banded = funded
|
|
227
234
|
else:
|
|
228
235
|
banded = [row for row in ranked if Decimal(row["weekly_remaining"]) >= floor]
|
|
236
|
+
if credit_mode and funded[0]["funding_tier"] == "credits":
|
|
237
|
+
# Subscription windows cannot rank credit-funded work. Credit capacity per
|
|
238
|
+
# identity holder decides, with the same deterministic LRU/spread for ties.
|
|
239
|
+
best_credit = min(credit_rank(row) for row in funded)
|
|
240
|
+
banded = [row for row in funded if credit_rank(row) == best_credit]
|
|
241
|
+
floor, healthy = None, []
|
|
229
242
|
winner = min(banded, key=_band_key(request["reservation_key"]))
|
|
230
243
|
rows.sort(key=_snapshot_key)
|
|
231
244
|
snapshot = canonical_sha256(rows)
|
|
@@ -239,6 +252,8 @@ def select(request: object) -> dict:
|
|
|
239
252
|
"headroom_band": band_text,
|
|
240
253
|
"session_gate": gate_text,
|
|
241
254
|
"candidate_snapshot_digest": snapshot, "selected_identity": chosen}
|
|
255
|
+
if credit_mode:
|
|
256
|
+
digest_input.update(funding_proof(request, winner))
|
|
242
257
|
score_fields = ("quota_known", "weekly_pct", "session_pct", "weekly_remaining",
|
|
243
258
|
"session_remaining", "effective_headroom", "last_selected_at")
|
|
244
259
|
response = {"schema": SCHEMA, "selector_version": SELECTOR_VERSION, "ok": True, **chosen,
|
|
@@ -256,6 +271,9 @@ def select(request: object) -> dict:
|
|
|
256
271
|
"session_ok_count": len(healthy),
|
|
257
272
|
"candidate_snapshot_digest": snapshot,
|
|
258
273
|
"selection_digest": canonical_sha256(digest_input)}
|
|
274
|
+
if credit_mode:
|
|
275
|
+
response.update(funding_proof(request, winner))
|
|
276
|
+
response["score_basis"].update(funding_score(winner))
|
|
259
277
|
if request["policy"] == "reviewer" and not alternatives and _row_identity(winner) == producer:
|
|
260
278
|
response["fallback_reason"] = "sole_eligible_account"
|
|
261
279
|
return response
|
|
@@ -39,7 +39,8 @@ REQUEST_KEYS = {
|
|
|
39
39
|
# headroom_band from its own setting and never sends session_gate, so the gate default
|
|
40
40
|
# below is what every panel launch gets (operator's 2026-09-03 decision: session
|
|
41
41
|
# headroom gates FIRST, then the weekly band picks among the survivors).
|
|
42
|
-
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate"
|
|
42
|
+
OPTIONAL_REQUEST_KEYS = {"headroom_band", "session_gate", "credit_fallback",
|
|
43
|
+
"required_funding_tier", "credit_max_age_seconds"}
|
|
43
44
|
DEFAULT_HEADROOM_BAND = "30"
|
|
44
45
|
DEFAULT_SESSION_GATE = "50"
|
|
45
46
|
POLICIES = {
|
|
@@ -144,8 +145,12 @@ def _identity_error(value: object, field: str) -> str | None:
|
|
|
144
145
|
|
|
145
146
|
def _candidate_error(value: object, index: int) -> str | None:
|
|
146
147
|
field = f"candidates[{index}]"
|
|
147
|
-
|
|
148
|
+
from selector_credits import OPTIONAL_CANDIDATE_FIELDS, credit_candidate_error
|
|
149
|
+
if not isinstance(value, dict) or not CANDIDATE_KEYS <= set(value) \
|
|
150
|
+
or set(value) - CANDIDATE_KEYS - OPTIONAL_CANDIDATE_FIELDS:
|
|
148
151
|
return field
|
|
152
|
+
if error := credit_candidate_error(value, field):
|
|
153
|
+
return error
|
|
149
154
|
for name in ("runner_id", "runner_generation"):
|
|
150
155
|
item = value[name]
|
|
151
156
|
if not isinstance(item, int) or isinstance(item, bool) or not 0 < item <= SAFE_INTEGER:
|
|
@@ -186,6 +191,16 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
186
191
|
keys = set(request)
|
|
187
192
|
if not REQUEST_KEYS <= keys or keys - REQUEST_KEYS - OPTIONAL_REQUEST_KEYS:
|
|
188
193
|
return "invalid_request", {"field": "$"}
|
|
194
|
+
if "credit_fallback" in request and not isinstance(request["credit_fallback"], bool):
|
|
195
|
+
return "invalid_request", {"field": "credit_fallback"}
|
|
196
|
+
if "required_funding_tier" in request and (not isinstance(request["required_funding_tier"], str)
|
|
197
|
+
or request["required_funding_tier"] not in {"included", "reserve", "credits"}):
|
|
198
|
+
return "invalid_request", {"field": "required_funding_tier"}
|
|
199
|
+
if "required_funding_tier" in request and request.get("credit_fallback") is not True:
|
|
200
|
+
return "invalid_request", {"field": "required_funding_tier"}
|
|
201
|
+
age = request.get("credit_max_age_seconds", 900)
|
|
202
|
+
if isinstance(age, bool) or not isinstance(age, int) or not 30 <= age <= 3600:
|
|
203
|
+
return "invalid_request", {"field": "credit_max_age_seconds"}
|
|
189
204
|
for knob in ("headroom_band", "session_gate"):
|
|
190
205
|
if knob not in request:
|
|
191
206
|
continue
|
|
@@ -224,6 +239,9 @@ def request_error(request: object) -> tuple[str, dict] | None:
|
|
|
224
239
|
if not isinstance(request["candidates"], list):
|
|
225
240
|
return "invalid_request", {"field": "candidates"}
|
|
226
241
|
for index, value in enumerate(request["candidates"]):
|
|
242
|
+
if isinstance(value, dict) and set(value) - CANDIDATE_KEYS \
|
|
243
|
+
and request.get("credit_fallback") is not True:
|
|
244
|
+
return "invalid_request", {"field": f"candidates[{index}]"}
|
|
227
245
|
if error := _candidate_error(value, index):
|
|
228
246
|
return "invalid_request", {"field": error}
|
|
229
247
|
return None
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Pure contract checks for spendable credit telemetry, separate from earned resets."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "lib"))
|
|
8
|
+
from codex_credits import collect_credits, credit_view, failed_credits, snapshot_from_usage
|
|
9
|
+
|
|
10
|
+
NOW = 1791115200
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def payload(**updates):
|
|
14
|
+
return {"account_id": "account-test", "credits": {
|
|
15
|
+
"has_credits": True, "unlimited": False, "balance": "1234.5000"},
|
|
16
|
+
"rate_limit": {"allowed": False}, "spend_control_reached": False, **updates}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpendableCreditsTest(unittest.TestCase):
|
|
20
|
+
def test_provider_balance_and_identity_are_preserved_without_float_rounding(self):
|
|
21
|
+
raw = payload()
|
|
22
|
+
raw["credits"]["balance"] = "12345678901234567890.123456789"
|
|
23
|
+
result = snapshot_from_usage(raw, "account-test", NOW)
|
|
24
|
+
self.assertEqual(result["balance"], "12345678901234567890.123456789")
|
|
25
|
+
self.assertEqual(result["account_id"], "account-test")
|
|
26
|
+
self.assertEqual(result["fetched_at"], "2026-10-04T12:00:00Z")
|
|
27
|
+
self.assertIs(result["ordinary_usage_allowed"], False)
|
|
28
|
+
self.assertIs(result["spend_control_reached"], False)
|
|
29
|
+
|
|
30
|
+
def test_measured_zero_unlimited_and_unknown_balance_remain_distinct(self):
|
|
31
|
+
for raw, expected in (({"has_credits": False, "unlimited": False, "balance": "0.00"}, "0"),
|
|
32
|
+
({"has_credits": True, "unlimited": True, "balance": None}, None),
|
|
33
|
+
({"has_credits": True, "unlimited": False}, None)):
|
|
34
|
+
with self.subTest(raw=raw):
|
|
35
|
+
result = snapshot_from_usage(payload(credits=raw), "account-test", NOW)
|
|
36
|
+
self.assertEqual(result["balance"], expected)
|
|
37
|
+
self.assertEqual(result["unlimited"], raw["unlimited"])
|
|
38
|
+
|
|
39
|
+
def test_earned_resets_never_become_a_spendable_balance(self):
|
|
40
|
+
self.assertIsNone(collect_credits({"rate_limit_reset_credits": {"available_count": 3}},
|
|
41
|
+
"account-test", NOW))
|
|
42
|
+
self.assertIsNone(collect_credits({"credits": None}, "account-test", NOW))
|
|
43
|
+
|
|
44
|
+
def test_malformed_balances_and_truthy_flags_do_not_claim_available_credit(self):
|
|
45
|
+
for invalid in (True, 12.34, -1, "-1", "NaN", "Infinity", "1e3", "1,000", "9" * 129):
|
|
46
|
+
with self.subTest(invalid=invalid):
|
|
47
|
+
raw = payload()
|
|
48
|
+
raw["credits"]["balance"] = invalid
|
|
49
|
+
self.assertIsNone(collect_credits(raw, "account-test", NOW))
|
|
50
|
+
raw = payload()
|
|
51
|
+
raw["credits"]["has_credits"] = "true"
|
|
52
|
+
self.assertIsNone(collect_credits(raw, "account-test", NOW))
|
|
53
|
+
|
|
54
|
+
def test_response_identity_must_agree_with_authenticated_account(self):
|
|
55
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
56
|
+
bad = collect_credits(payload(account_id="other-account"), "account-test", NOW + 10, previous)
|
|
57
|
+
self.assertEqual(bad["account_id"], "account-test")
|
|
58
|
+
self.assertEqual(bad["last_error"], "account_mismatch")
|
|
59
|
+
self.assertEqual(bad["fetched_at"], previous["fetched_at"])
|
|
60
|
+
no_response_id = snapshot_from_usage(payload(account_id=None), "account-test", NOW)
|
|
61
|
+
self.assertEqual(no_response_id["account_id"], "account-test")
|
|
62
|
+
|
|
63
|
+
def test_failure_preserves_last_observation_then_success_clears_it(self):
|
|
64
|
+
previous = snapshot_from_usage(payload(), "account-test", NOW)
|
|
65
|
+
failed = failed_credits(previous, "http_429", NOW + 10)
|
|
66
|
+
self.assertEqual(failed["balance"], "1234.5")
|
|
67
|
+
self.assertEqual(failed["fetched_at"], previous["fetched_at"])
|
|
68
|
+
self.assertTrue(failed["stale"])
|
|
69
|
+
self.assertEqual(failed["last_error"], "http_429")
|
|
70
|
+
missing = collect_credits({}, "account-test", NOW + 20, previous)
|
|
71
|
+
self.assertEqual(missing["last_error"], "missing_credits")
|
|
72
|
+
recovered = collect_credits(payload(), "account-test", NOW + 30, failed)
|
|
73
|
+
self.assertNotIn("last_error", recovered)
|
|
74
|
+
self.assertFalse(credit_view(recovered, NOW + 30)["stale"])
|
|
75
|
+
|
|
76
|
+
def test_staleness_uses_credit_timestamp_and_rejects_future_or_invalid_time(self):
|
|
77
|
+
value = snapshot_from_usage(payload(), "account-test", NOW)
|
|
78
|
+
self.assertFalse(credit_view(value, NOW + 900)["stale"])
|
|
79
|
+
self.assertTrue(credit_view(value, NOW + 901)["stale"])
|
|
80
|
+
self.assertTrue(credit_view(value, NOW - 31)["stale"])
|
|
81
|
+
self.assertIsNone(credit_view({**value, "fetched_at": "2026-10-04T12:00:00"}, NOW))
|
|
82
|
+
self.assertIsNone(credit_view({**value, "fetched_at": {}}, NOW))
|
|
83
|
+
|
|
84
|
+
def test_admission_facts_are_never_inferred_from_percentages(self):
|
|
85
|
+
raw = payload(rate_limit={"primary_window": {"used_percent": 0}})
|
|
86
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
87
|
+
self.assertIsNone(snapshot["ordinary_usage_allowed"])
|
|
88
|
+
raw["spend_control_reached"] = "false"
|
|
89
|
+
raw["rate_limit_reached_type"] = {"unexpected": "shape"}
|
|
90
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
91
|
+
self.assertIsNone(snapshot["spend_control_reached"])
|
|
92
|
+
self.assertEqual(snapshot["limit_reached_type"], "unknown")
|
|
93
|
+
|
|
94
|
+
def test_documented_raw_and_app_server_nested_admission_fields(self):
|
|
95
|
+
raw = payload(spend_control={"reached": True},
|
|
96
|
+
rate_limit_reached_type={"type": "workspace_member_usage_limit_reached"})
|
|
97
|
+
raw.pop("spend_control_reached")
|
|
98
|
+
snapshot = snapshot_from_usage(raw, "account-test", NOW)
|
|
99
|
+
self.assertTrue(snapshot["spend_control_reached"])
|
|
100
|
+
self.assertEqual(snapshot["limit_reached_type"], "workspace_member_usage_limit_reached")
|
|
101
|
+
app_server = {"accountId": "account-test", "ordinaryUsageAllowed": False, "rateLimits": {
|
|
102
|
+
"credits": {"hasCredits": True, "unlimited": False, "balance": "12.34"},
|
|
103
|
+
"rateLimitReachedType": "rate_limit_reached", "spendControlReached": False}}
|
|
104
|
+
snapshot = snapshot_from_usage(app_server, "account-test", NOW)
|
|
105
|
+
self.assertEqual(snapshot["limit_reached_type"], "rate_limit_reached")
|
|
106
|
+
self.assertEqual(snapshot["balance"], "12.34")
|
|
107
|
+
self.assertIs(snapshot["ordinary_usage_allowed"], False)
|
|
108
|
+
self.assertIs(snapshot["spend_control_reached"], False)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Existing Codex usage polling carries paid balances without extra provider requests."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
import test_codex_reset as fixtures
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CreditPollingTest(fixtures.CodexPoolSandbox, unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
super().setUp()
|
|
15
|
+
now = int(time.time())
|
|
16
|
+
self.server.state["usage"] = {
|
|
17
|
+
"account_id": "account-test", "plan_type": "pro", "rate_limit": {
|
|
18
|
+
"allowed": False, "limit_reached": True, "primary_window": {
|
|
19
|
+
"used_percent": 100, "limit_window_seconds": 604800, "reset_at": now + 86400}},
|
|
20
|
+
"credits": {"has_credits": True, "unlimited": False, "balance": "61812.4520750000"},
|
|
21
|
+
"rate_limit_reached_type": {"type": "rate_limit_reached"}, "spend_control": {"reached": False},
|
|
22
|
+
}
|
|
23
|
+
self.server.state["credits"] = {"available_count": 2, "credits": []}
|
|
24
|
+
|
|
25
|
+
def command(self, *args, auto_reset="0"):
|
|
26
|
+
base = f"http://127.0.0.1:{self.server.server_port}/wham"
|
|
27
|
+
env = dict(os.environ, CODEX_ACCOUNTS_ROOT=str(self.pool), CODEX_MULTIACC_NO_SYNC="1",
|
|
28
|
+
CODEX_MULTIACC_AUTO_RESET=auto_reset, CODEX_MULTIACC_USAGE_URL=f"{base}/usage",
|
|
29
|
+
CLAUDE_MULTIACC_PATH_PROBE="0", PYTHONDONTWRITEBYTECODE="1")
|
|
30
|
+
for key in ("CODEX_MULTIACC_RESET_CREDITS_URL", "CODEX_MULTIACC_RESET_CONSUME_URL"):
|
|
31
|
+
env.pop(key, None)
|
|
32
|
+
result = subprocess.run([fixtures.REPO / "bin/codex-accounts", *args], env=env,
|
|
33
|
+
capture_output=True, text=True, timeout=20, check=False)
|
|
34
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def test_existing_usage_read_reports_both_credit_types_without_more_calls(self):
|
|
38
|
+
answer = json.loads(self.command("limits", "--force", "--json").stdout)
|
|
39
|
+
usage = answer["accounts"][0]["usage"]
|
|
40
|
+
self.assertEqual(usage["credits"]["balance"], "61812.452075")
|
|
41
|
+
self.assertEqual(usage["credits"]["account_id"], "account-test")
|
|
42
|
+
self.assertEqual(usage["credits"]["limit_reached_type"], "rate_limit_reached")
|
|
43
|
+
self.assertIs(usage["credits"]["ordinary_usage_allowed"], False)
|
|
44
|
+
self.assertEqual(usage["reset_credits_available"], 2)
|
|
45
|
+
self.assertEqual(self.server.state["usage_gets"], 1)
|
|
46
|
+
self.assertEqual(self.server.state["credit_gets"], 1)
|
|
47
|
+
self.assertEqual(self.server.state["posts"], [])
|
|
48
|
+
self.assertTrue((self.pool / "acct-01/.limited").exists())
|
|
49
|
+
listed = json.loads(self.command("list", "--json").stdout)
|
|
50
|
+
self.assertEqual(listed["accounts"][0]["usage"]["credits"]["balance"], "61812.452075")
|
|
51
|
+
self.assertEqual(self.server.state["usage_gets"], 1)
|
|
52
|
+
|
|
53
|
+
def test_missing_credit_read_preserves_previous_amount_as_unusable(self):
|
|
54
|
+
self.command("limits", "--force", "--quiet")
|
|
55
|
+
path = self.pool / "acct-01/limits.json"
|
|
56
|
+
before = json.loads(path.read_text())["credits"]
|
|
57
|
+
self.server.state["usage"].pop("credits")
|
|
58
|
+
self.command("limits", "--force", "--quiet")
|
|
59
|
+
after = json.loads(path.read_text())["credits"]
|
|
60
|
+
self.assertEqual(after["balance"], before["balance"])
|
|
61
|
+
self.assertEqual(after["fetched_at"], before["fetched_at"])
|
|
62
|
+
self.assertEqual(after["last_error"], "missing_credits")
|
|
63
|
+
self.assertTrue(after["stale"])
|
|
64
|
+
|
|
65
|
+
def test_bad_usage_does_not_silently_keep_paid_admission_fresh(self):
|
|
66
|
+
self.command("limits", "--force", "--quiet")
|
|
67
|
+
self.server.state["usage"] = []
|
|
68
|
+
self.command("limits", "--force", "--quiet")
|
|
69
|
+
stored = json.loads((self.pool / "acct-01/limits.json").read_text())
|
|
70
|
+
self.assertEqual(stored["credits"]["last_error"], "invalid_usage")
|
|
71
|
+
self.assertTrue(stored["credits"]["stale"])
|
|
72
|
+
self.assertEqual(stored["weekly_percent"], 100)
|
|
73
|
+
|
|
74
|
+
def test_credit_snapshot_survives_unknown_quota_without_inventing_usage(self):
|
|
75
|
+
self.server.state["usage"].pop("rate_limit")
|
|
76
|
+
self.command("limits", "--force", "--quiet")
|
|
77
|
+
stored = json.loads((self.pool / "acct-01/limits.json").read_text())
|
|
78
|
+
self.assertTrue(stored["no_data"])
|
|
79
|
+
self.assertNotIn("weekly_percent", stored)
|
|
80
|
+
self.assertEqual(stored["credits"]["balance"], "61812.452075")
|
|
81
|
+
self.assertIsNone(stored["credits"]["ordinary_usage_allowed"])
|
|
82
|
+
|
|
83
|
+
def test_earned_reset_requires_new_usage_before_authorizing_paid_fallback(self):
|
|
84
|
+
self.server.state["responses"] = [(200, {"code": "reset", "windows_reset": 1})]
|
|
85
|
+
self.command("limits", "--force", "--quiet", auto_reset="1")
|
|
86
|
+
stored = json.loads((self.pool / "acct-01/limits.json").read_text())
|
|
87
|
+
self.assertEqual(stored["fetched_at"], 0)
|
|
88
|
+
self.assertEqual(stored["credits"]["balance"], "61812.452075")
|
|
89
|
+
self.assertEqual(stored["credits"]["last_error"], "usage_reset_pending")
|
|
90
|
+
self.assertFalse((self.pool / "acct-01/.limited").exists())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
unittest.main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Credit report freshness, failure persistence and safe output use isolated files only."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "lib"))
|
|
11
|
+
import report
|
|
12
|
+
from codex_credits import record_read_failure, snapshot_from_usage
|
|
13
|
+
from test_codex_reset_credits import NOW, payload
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CreditReportTest(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
scratch = tempfile.TemporaryDirectory(prefix="multiacc-credit-report-")
|
|
19
|
+
self.addCleanup(scratch.cleanup)
|
|
20
|
+
self.account = Path(scratch.name)
|
|
21
|
+
self.path = self.account / "limits.json"
|
|
22
|
+
self.credits = snapshot_from_usage(payload(), "account-test", NOW)
|
|
23
|
+
|
|
24
|
+
def test_report_keeps_earned_resets_and_spendable_credits_separate(self):
|
|
25
|
+
self.path.write_text(json.dumps({"fetched_at": NOW, "credits": self.credits,
|
|
26
|
+
"reset_credits_available": 2, "reset_credits_fetched_at": NOW, "weekly_percent": 100}))
|
|
27
|
+
with patch("report.time.time", return_value=NOW):
|
|
28
|
+
usage = report._usage(str(self.account), 900)
|
|
29
|
+
self.assertEqual(usage["reset_credits_available"], 2)
|
|
30
|
+
self.assertEqual(usage["credits"]["balance"], "1234.5")
|
|
31
|
+
self.assertFalse(usage["credits"]["stale"])
|
|
32
|
+
self.assertEqual(usage["weekly_percent"], 100)
|
|
33
|
+
|
|
34
|
+
def test_fresh_windows_do_not_refresh_an_old_wallet(self):
|
|
35
|
+
self.path.write_text(json.dumps({"fetched_at": NOW + 901, "credits": self.credits}))
|
|
36
|
+
with patch("report.time.time", return_value=NOW + 901):
|
|
37
|
+
usage = report._usage(str(self.account), 900)
|
|
38
|
+
self.assertFalse(usage["stale"])
|
|
39
|
+
self.assertTrue(usage["credits"]["stale"])
|
|
40
|
+
self.assertEqual(usage["credits"]["age_seconds"], 901)
|
|
41
|
+
|
|
42
|
+
def test_failed_read_keeps_windows_and_backoff_without_refreshing_balance(self):
|
|
43
|
+
previous = {"fetched_at": NOW, "credits": self.credits, "weekly_percent": 100,
|
|
44
|
+
"retry_after": NOW + 300, "reset_credits_available": 2}
|
|
45
|
+
record_read_failure(str(self.path), previous, "http_429", NOW + 10)
|
|
46
|
+
stored = json.loads(self.path.read_text())
|
|
47
|
+
self.assertEqual(stored["fetched_at"], NOW)
|
|
48
|
+
self.assertEqual(stored["retry_after"], NOW + 300)
|
|
49
|
+
self.assertEqual(stored["credits"]["fetched_at"], self.credits["fetched_at"])
|
|
50
|
+
with patch("report.time.time", return_value=NOW + 10):
|
|
51
|
+
usage = report._usage(str(self.account), 900)
|
|
52
|
+
self.assertEqual(usage["credits"]["last_error"], "http_429")
|
|
53
|
+
self.assertTrue(usage["credits"]["stale"])
|
|
54
|
+
|
|
55
|
+
def test_first_failed_read_records_unknown_and_never_zero(self):
|
|
56
|
+
record_read_failure(str(self.path), {}, "unauthenticated", NOW)
|
|
57
|
+
usage = report._usage(str(self.account), 900)
|
|
58
|
+
self.assertNotIn("credits", usage)
|
|
59
|
+
self.assertEqual(usage["last_error"], "unauthenticated")
|
|
60
|
+
|
|
61
|
+
def test_report_never_passes_arbitrary_wallet_fields_or_error_details(self):
|
|
62
|
+
credits = {**self.credits, "access_token": "must-not-leak", "last_error": "secret-detail"}
|
|
63
|
+
self.path.write_text(json.dumps({"fetched_at": NOW, "credits": credits}))
|
|
64
|
+
usage = report._usage(str(self.account), 900)
|
|
65
|
+
rendered = json.dumps(usage)
|
|
66
|
+
self.assertNotIn("must-not-leak", rendered)
|
|
67
|
+
self.assertNotIn("secret-detail", rendered)
|
|
68
|
+
self.assertEqual(usage["credits"]["last_error"], "usage_read_failed")
|
|
69
|
+
|
|
70
|
+
def test_refusal_evidence_belongs_to_the_observed_wallet_identity(self):
|
|
71
|
+
self.path.write_text(json.dumps({"fetched_at": NOW, "credits": self.credits}))
|
|
72
|
+
receipt = self.account / ".credit-refused.json"
|
|
73
|
+
refused = {"credit_blocked_at": self.credits["fetched_at"], "credit_blocked_balance": "1234.5",
|
|
74
|
+
"limited_reason": "credits", "account_id": "account-test"}
|
|
75
|
+
receipt.write_text(json.dumps(refused))
|
|
76
|
+
usage = report._usage(str(self.account), 900)
|
|
77
|
+
self.assertEqual(usage["credit_blocked_balance"], "1234.5")
|
|
78
|
+
self.assertEqual(usage["limited_reason"], "credits")
|
|
79
|
+
receipt.write_text(json.dumps({**refused, "account_id": "another-account"}))
|
|
80
|
+
usage = report._usage(str(self.account), 900)
|
|
81
|
+
self.assertNotIn("credit_blocked_at", usage)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
unittest.main()
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Shared wallets never erase actor permissions, authentication or paid refusals."""
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from test_selector import ROOT, _request
|
|
10
|
+
from test_selector_credits import _paid
|
|
11
|
+
from selector_policy import select
|
|
12
|
+
import test_selector_credit_shim as shim_fixtures
|
|
13
|
+
from codex_credit_selection import record_launch, record_refusal
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CreditScopeTests(unittest.TestCase):
|
|
17
|
+
def test_refusal_follows_same_actor_across_slots_but_not_other_members(self):
|
|
18
|
+
blocked, peer = _paid(account_id="blocked"), _paid(account_id="peer")
|
|
19
|
+
blocked.update(credit_actor_email=" Person@Example.COM ", limited_reason="credits",
|
|
20
|
+
credit_blocked_at="2026-08-25T08:59:59Z", credit_blocked_balance="61812.452075")
|
|
21
|
+
peer["credit_actor_email"] = "person@example.com"
|
|
22
|
+
self.assertFalse(select(_request([blocked, peer], credit_fallback=True))["ok"])
|
|
23
|
+
peer["credit_actor_email"] = "other@example.com"
|
|
24
|
+
response = select(_request([blocked, peer], credit_fallback=True))
|
|
25
|
+
self.assertEqual(response["account_id"], "peer")
|
|
26
|
+
self.assertEqual(response["score_basis"]["credit_actor_email"], "other@example.com")
|
|
27
|
+
|
|
28
|
+
def test_unknown_actor_refusal_remains_with_its_runner_and_slot(self):
|
|
29
|
+
blocked, peer = _paid(account_id="blocked"), _paid(account_id="peer")
|
|
30
|
+
blocked.update(limited_reason="credits", credit_blocked_at="2026-08-25T08:59:59Z",
|
|
31
|
+
credit_blocked_balance="61812.452075")
|
|
32
|
+
self.assertEqual(select(_request([blocked, peer], credit_fallback=True))["account_id"], "peer")
|
|
33
|
+
peer.update(runner_id=4, account_id="blocked")
|
|
34
|
+
self.assertEqual(select(_request([blocked, peer], credit_fallback=True))["runner_id"], 4)
|
|
35
|
+
|
|
36
|
+
def test_peer_wallet_cannot_erase_local_auth_identity_or_read_failure(self):
|
|
37
|
+
failed, busy = _paid(account_id="failed"), _paid(account_id="busy")
|
|
38
|
+
busy["reservation_history"]["active_expires_at"] = "2026-08-25T09:05:00Z"
|
|
39
|
+
for error in ("account_mismatch", "unauthenticated", "http_401", "usage_read_failed"):
|
|
40
|
+
with self.subTest(error=error):
|
|
41
|
+
failed["credits"].update(last_error=error, fetched_at="2026-08-25T08:59:00Z")
|
|
42
|
+
self.assertFalse(select(_request([failed, busy], credit_fallback=True))["ok"])
|
|
43
|
+
|
|
44
|
+
def test_peer_wallet_cannot_erase_member_spend_controls_or_share_them(self):
|
|
45
|
+
blocked, healthy = _paid(account_id="blocked"), _paid(account_id="healthy")
|
|
46
|
+
blocked["credits"]["spend_control_reached"] = True
|
|
47
|
+
blocked["credit_actor_email"], healthy["credit_actor_email"] = "a@example.com", "b@example.com"
|
|
48
|
+
response = select(_request([blocked, healthy], credit_fallback=True))
|
|
49
|
+
self.assertEqual(response["account_id"], "healthy")
|
|
50
|
+
healthy["reservation_history"]["active_expires_at"] = "2026-08-25T09:05:00Z"
|
|
51
|
+
self.assertFalse(select(_request([blocked, healthy], credit_fallback=True))["ok"])
|
|
52
|
+
|
|
53
|
+
def test_peer_observation_cannot_end_actor_cooldown_without_new_actor_read(self):
|
|
54
|
+
blocked, busy = _paid(account_id="blocked"), _paid(account_id="busy")
|
|
55
|
+
blocked.update(credit_actor_email="a@example.com", limited_reason="credits",
|
|
56
|
+
credit_blocked_at="2026-08-25T08:49:00Z", credit_blocked_balance="61812.452075")
|
|
57
|
+
blocked["credits"]["fetched_at"] = "2026-08-25T08:48:00Z"
|
|
58
|
+
busy["credit_actor_email"] = "b@example.com"
|
|
59
|
+
busy["reservation_history"]["active_expires_at"] = "2026-08-25T09:05:00Z"
|
|
60
|
+
self.assertFalse(select(_request([blocked, busy], credit_fallback=True))["ok"])
|
|
61
|
+
|
|
62
|
+
def test_latest_failed_financial_observation_does_not_revive_old_positive_balance(self):
|
|
63
|
+
old, failed = _paid(account_id="old"), _paid(account_id="failed")
|
|
64
|
+
old["credits"]["fetched_at"] = "2026-08-25T08:59:00Z"
|
|
65
|
+
failed["credits"].update(fetched_at="2026-08-25T08:58:00Z", last_error="usage_read_failed",
|
|
66
|
+
last_error_at="2026-08-25T09:00:00Z")
|
|
67
|
+
self.assertFalse(select(_request([old, failed], credit_fallback=True))["ok"])
|
|
68
|
+
old["credits"]["fetched_at"] = "2026-08-25T09:00:01Z"
|
|
69
|
+
self.assertTrue(select(_request([old, failed], credit_fallback=True))["ok"])
|
|
70
|
+
|
|
71
|
+
def test_standalone_refusal_is_shared_only_for_same_manifest_actor(self):
|
|
72
|
+
case = shim_fixtures.CreditShimTests()
|
|
73
|
+
case.setUp()
|
|
74
|
+
self.addCleanup(case.doCleanups)
|
|
75
|
+
first = case.account("acct-01", account_id="workspace-a")
|
|
76
|
+
case.account("acct-02", account_id="workspace-a")
|
|
77
|
+
accounts = [{"id": name, "email": "person@example.com"} for name in ("acct-01", "acct-02")]
|
|
78
|
+
(case.pool / "accounts.json").write_text(json.dumps({"accounts": accounts}))
|
|
79
|
+
record_launch(first, os.getpid(), time.time())
|
|
80
|
+
record_refusal(first, os.getpid(), time.time())
|
|
81
|
+
self.assertIn("tier=hard", case.run_shim().stdout)
|
|
82
|
+
accounts[1]["email"] = "other@example.com"
|
|
83
|
+
(case.pool / "accounts.json").write_text(json.dumps({"accounts": accounts}))
|
|
84
|
+
self.assertIn("pick=acct-02 tier=credits", case.run_shim().stdout)
|
|
85
|
+
(case.pool / "accounts.json").write_text('{"accounts":[]}')
|
|
86
|
+
record_launch(first, os.getpid(), time.time())
|
|
87
|
+
record_refusal(first, os.getpid(), time.time())
|
|
88
|
+
self.assertIn("pick=acct-02 tier=credits", case.run_shim().stdout)
|
|
89
|
+
|
|
90
|
+
def test_ordinary_gate_log_retains_legacy_fields_and_paid_only_appends_funding(self):
|
|
91
|
+
case = shim_fixtures.CreditShimTests()
|
|
92
|
+
case.setUp()
|
|
93
|
+
self.addCleanup(case.doCleanups)
|
|
94
|
+
case.account("acct-01", weekly=20)
|
|
95
|
+
result = case.run_shim(False)
|
|
96
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
97
|
+
line = (case.pool / "selection.log").read_text().splitlines()[-1].split(" ", 1)[1]
|
|
98
|
+
self.assertEqual(line, "acct-01 weekly=20% session=10% band=30 band-count=1 "
|
|
99
|
+
f"session-gate=50 session-ok=1 pwd={os.getcwd()}")
|
|
100
|
+
case.account("acct-01")
|
|
101
|
+
result = case.run_shim(False)
|
|
102
|
+
self.assertEqual(result.returncode, 0, result.stderr)
|
|
103
|
+
line = (case.pool / "selection.log").read_text().splitlines()[-1].split(" ", 1)[1]
|
|
104
|
+
self.assertEqual(line, "acct-01 weekly=100% session=10% band=30 band-count=0 "
|
|
105
|
+
f"session-gate=50 session-ok=0 pwd={os.getcwd()} funding=credits")
|
|
106
|
+
|
|
107
|
+
def test_new_modules_parse_as_python39_and_defer_union_annotations(self):
|
|
108
|
+
for name in ("selector_credits", "selector_funding", "selector_credit_scope", "codex_credit_selection",
|
|
109
|
+
"codex_credit_local_scope"):
|
|
110
|
+
source = (ROOT / "lib" / f"{name}.py").read_text()
|
|
111
|
+
tree = ast.parse(source, feature_version=(3, 9))
|
|
112
|
+
self.assertTrue(any(isinstance(node, ast.ImportFrom) and node.module == "__future__"
|
|
113
|
+
and any(alias.name == "annotations" for alias in node.names)
|
|
114
|
+
for node in tree.body), name)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
unittest.main()
|