subs-pool 0.1.0__py3-none-any.whl

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.
subs_pool/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """General-purpose subscription pool with explicit built-in modules."""
2
+
3
+ __version__ = "0.1.0"
subs_pool/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
subs_pool/agent_cli.py ADDED
@@ -0,0 +1,69 @@
1
+ """Machine-only ``subspool-cli`` entry point."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from typing import Any
8
+
9
+ from . import __version__
10
+ from .output import envelope, error_object
11
+ from .registry import BUILTIN_MODULES
12
+
13
+
14
+ def _emit(command: str, rc: int, data: Any = None, *, error: dict[str, Any] | None = None) -> int:
15
+ if rc != 0 and error is None:
16
+ refresh_error = data.get("refresh", {}).get("error") if isinstance(data, dict) else None
17
+ if isinstance(refresh_error, dict):
18
+ error = error_object(
19
+ str(refresh_error.get("code", "operation_unavailable")),
20
+ str(refresh_error.get("message", "operation did not complete")),
21
+ )
22
+ else:
23
+ error = error_object("operation_unavailable", "operation did not complete")
24
+ print(json.dumps(envelope(command, ok=rc == 0, data=data, error=error), ensure_ascii=False, separators=(",", ":"), allow_nan=False))
25
+ return rc
26
+
27
+
28
+ def main(argv: list[str] | None = None) -> int:
29
+ args = list(argv) if argv is not None else sys.argv[1:]
30
+ if not args:
31
+ return _emit("help", 0, {"usage": "subspool-cli [--help|--version] codex COMMAND"})
32
+ if args == ["--help"]:
33
+ return _emit("help", 0, {"usage": "subspool-cli codex account|status|quota"})
34
+ if args == ["--version"]:
35
+ return _emit("version", 0, {"version": __version__})
36
+ if args[0] == "modules":
37
+ if args[1:] == ["--help"]:
38
+ return _emit("modules", 0, {"usage": "subspool-cli modules [list]"})
39
+ if args[1:] not in ([], ["list"]):
40
+ return _emit("modules", 2, None, error=error_object("invalid_syntax", "usage: subspool-cli modules [list]"))
41
+ return _emit("modules", 0, {"modules": [{"id": m.id, "display_name": m.display_name} for m in BUILTIN_MODULES]})
42
+ if args[0] == "tui":
43
+ return _emit("tui", 2, None, error=error_object("unsupported_command", "the machine executable never opens the TUI"))
44
+ if args[0] != "codex":
45
+ return _emit("help", 2, None, error=error_object("unknown_module", "unknown subscription module"))
46
+ command = "codex"
47
+ if len(args) > 1:
48
+ if args[1] != "--help":
49
+ command = "codex." + args[1]
50
+ if args[1] == "account" and len(args) > 2 and args[2] != "--help":
51
+ command += "." + args[2]
52
+ try:
53
+ from .modules.codex.cli import machine_operation
54
+
55
+ rc, data = machine_operation(args)
56
+ if isinstance(data, dict) and isinstance(data.get("error"), dict):
57
+ return _emit(command, rc, None, error=data["error"])
58
+ return _emit(command, rc, data)
59
+ except KeyboardInterrupt:
60
+ return _emit(command, 130, None, error=error_object("interrupted", "operation interrupted"))
61
+ except Exception:
62
+ return _emit(command, 5, None, error=error_object("internal_error", "unexpected internal failure"))
63
+
64
+
65
+ if __name__ == "__main__":
66
+ raise SystemExit(main())
67
+
68
+
69
+ __all__ = ["main"]
subs_pool/cli.py ADDED
@@ -0,0 +1,77 @@
1
+ """Human ``subspool`` dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from . import __version__
8
+ from .registry import BUILTIN_MODULES, DEFAULT_MODULE_ID, UnknownModuleError, get_module
9
+
10
+ _USAGE = """usage: subspool [tui | modules [list] | codex COMMAND ...]
11
+
12
+ With no arguments, open the Codex account TUI.
13
+ tui open the TUI
14
+ modules [list] list static built-in modules
15
+ codex COMMAND run a human Codex operation
16
+ --version print the version
17
+ """
18
+
19
+
20
+ def _error(message: str, code: int = 2) -> int:
21
+ print(f"error: {message}", file=sys.stderr)
22
+ return code
23
+
24
+
25
+ def _run_tui(module_id: str) -> int:
26
+ try:
27
+ module = get_module(module_id)
28
+ except UnknownModuleError as exc:
29
+ return _error(str(exc))
30
+ from .tui import run_tui
31
+
32
+ run_tui(module.id)
33
+ return 0
34
+
35
+
36
+ def _list_modules() -> int:
37
+ for module in BUILTIN_MODULES:
38
+ marker = " (default)" if module.id == DEFAULT_MODULE_ID else ""
39
+ print(f"{module.id:<16} {module.display_name}{marker}")
40
+ return 0
41
+
42
+
43
+ def main(argv: list[str] | None = None) -> int:
44
+ args = list(argv) if argv is not None else sys.argv[1:]
45
+ if not args:
46
+ return _run_tui(DEFAULT_MODULE_ID)
47
+ if args == ["--help"] or args == ["-h"]:
48
+ print(_USAGE, end="")
49
+ return 0
50
+ if args == ["--version"]:
51
+ print(f"subspool {__version__}")
52
+ return 0
53
+ if args[0] == "modules":
54
+ rest = args[1:]
55
+ if rest in ([], ["list"]):
56
+ return _list_modules()
57
+ return _error("usage: subspool modules [list]")
58
+ if args[0] == "tui":
59
+ if args[1:] in ([], ["codex"]):
60
+ return _run_tui(DEFAULT_MODULE_ID)
61
+ if args[1:] in (["--help"], ["-h"]):
62
+ print("usage: subspool tui [codex]")
63
+ return 0
64
+ return _error("usage: subspool tui [codex]")
65
+ try:
66
+ module = get_module(args[0])
67
+ except UnknownModuleError as exc:
68
+ print(f"error: {exc}", file=sys.stderr)
69
+ return 2
70
+ return module.command_main(args[1:])
71
+
72
+
73
+ if __name__ == "__main__":
74
+ raise SystemExit(main())
75
+
76
+
77
+ __all__ = ["main"]
@@ -0,0 +1,365 @@
1
+ """Thin async subprocess wrapper around a subs-pool module CLI contract.
2
+
3
+ This module owns no account/token/auth state and makes no provider HTTP calls.
4
+ Machine-safe operations use the JSON-only ``subspool-cli`` module entrypoint;
5
+ the human TUI's device-login stream uses the human dispatcher with a private
6
+ JSONL adapter. The subprocess spawn function is injectable so tests can supply
7
+ a fake process without starting a real one.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import re
15
+ import sys
16
+ from collections.abc import Awaitable, Callable, Sequence
17
+ from typing import Any, Protocol
18
+
19
+
20
+ class CLIError(Exception):
21
+ """Raised when a subs-pool module CLI exits non-zero or emits bad output."""
22
+
23
+ def __init__(self, message: str, *, returncode: int | None = None, data: dict | None = None) -> None:
24
+ super().__init__(message)
25
+ self.message = message
26
+ self.returncode = returncode
27
+ self.data = data
28
+
29
+
30
+ class Process(Protocol):
31
+ """The minimal subset of :class:`asyncio.subprocess.Process` used here."""
32
+
33
+ returncode: int | None
34
+ stdout: Any
35
+ stderr: Any
36
+
37
+ async def communicate(self) -> tuple[bytes, bytes]: ...
38
+
39
+ async def wait(self) -> int: ...
40
+
41
+ def terminate(self) -> None: ...
42
+
43
+ def kill(self) -> None: ...
44
+
45
+
46
+ Spawner = Callable[[Sequence[str]], Awaitable[Process]]
47
+
48
+
49
+ async def _default_spawn(args: Sequence[str]) -> Process:
50
+ return await asyncio.create_subprocess_exec(
51
+ sys.executable,
52
+ "-m",
53
+ "subs_pool.agent_cli",
54
+ *args,
55
+ stdout=asyncio.subprocess.PIPE,
56
+ stderr=asyncio.subprocess.PIPE,
57
+ )
58
+
59
+
60
+ async def _default_human_spawn(args: Sequence[str]) -> Process:
61
+ """Run the human dispatcher for the TUI-only login event stream."""
62
+ return await asyncio.create_subprocess_exec(
63
+ sys.executable,
64
+ "-m",
65
+ "subs_pool.cli",
66
+ *args,
67
+ stdout=asyncio.subprocess.PIPE,
68
+ stderr=asyncio.subprocess.PIPE,
69
+ )
70
+
71
+
72
+ _MAX_RAW_ERROR_CHARS = 2000
73
+ _LOGIN_EXIT_TIMEOUT_SECONDS = 5.0
74
+ _SENSITIVE_ASSIGNMENT = re.compile(
75
+ r"(?i)\b(access[_-]?token|refresh[_-]?token|id[_-]?token|password|secret|api[_-]?key|authorization)"
76
+ r"(\s*[:=]\s*)([\"']?)([^\"'\s,}]+)"
77
+ )
78
+ _SENSITIVE_BEARER = re.compile(r"(?i)\bBearer\s+[^\s,}]+")
79
+ _SENSITIVE_SECRET_PREFIX = re.compile(r"\b(?:sk|sess|rt|at)-[A-Za-z0-9_-]{8,}\b")
80
+
81
+
82
+ def _safe_text(text: str) -> str:
83
+ """Bound and redact common credential forms in fallback diagnostics."""
84
+ text = _SENSITIVE_ASSIGNMENT.sub(r"\1\2<redacted>", text)
85
+ text = _SENSITIVE_BEARER.sub("Bearer <redacted>", text)
86
+ text = _SENSITIVE_SECRET_PREFIX.sub("<redacted>", text)
87
+ return text if len(text) <= _MAX_RAW_ERROR_CHARS else text[:_MAX_RAW_ERROR_CHARS] + "… (truncated)"
88
+
89
+
90
+ def _parse_error(stderr: bytes) -> str:
91
+ text = stderr.decode(errors="replace").strip()
92
+ if not text:
93
+ return "subs-pool module CLI exited with an error (no message)"
94
+ # Contract errors are one JSON object on the last non-empty stderr line.
95
+ # A broken installation may instead produce a traceback; retain a bounded,
96
+ # redacted diagnostic rather than dumping unbounded/provider-tainted text.
97
+ try:
98
+ payload = json.loads(text.splitlines()[-1])
99
+ except json.JSONDecodeError:
100
+ return _safe_text(text)
101
+ if isinstance(payload, dict):
102
+ if isinstance(payload.get("error"), str):
103
+ return _safe_text(payload["error"])
104
+ if isinstance(payload.get("error"), dict):
105
+ return _safe_text(str(payload["error"].get("message", "module CLI failed")))
106
+ return "subs-pool module CLI returned a malformed error response"
107
+ return _safe_text(text)
108
+
109
+
110
+ def _safe_login_event(event: dict) -> dict:
111
+ """Project JSONL events onto the small non-secret frontend contract."""
112
+ kind = event.get("event")
113
+ if kind == "authorization_required":
114
+ return {
115
+ "event": "authorization_required",
116
+ "verification_uri": event.get("verification_uri", ""),
117
+ "user_code": event.get("user_code", ""),
118
+ "expires_in": event.get("expires_in"),
119
+ "interval": event.get("interval"),
120
+ }
121
+ if kind == "completed":
122
+ account = event.get("account")
123
+ if not isinstance(account, dict):
124
+ raise CLIError("malformed login event: completed event lacks account")
125
+ return {
126
+ "event": "completed",
127
+ "account": {
128
+ key: account[key]
129
+ for key in ("ref", "enabled", "weight", "auth_present", "quota")
130
+ if key in account
131
+ },
132
+ }
133
+ # Unknown events are not part of the contract. Preserve only their type so
134
+ # a broken child cannot smuggle arbitrary provider payloads into the UI.
135
+ return {"event": str(kind) if kind is not None else "unknown"}
136
+
137
+
138
+ async def _spawn_or_raise(spawn: Spawner, args: Sequence[str]) -> Process:
139
+ try:
140
+ return await spawn(args)
141
+ except FileNotFoundError as exc:
142
+ raise CLIError(
143
+ "subs-pool CLI not found: is subs-pool installed for this interpreter?"
144
+ ) from exc
145
+
146
+
147
+ async def _terminate_process(proc: Process) -> None:
148
+ """Best-effort terminate and reap a local child process this wrapper owns.
149
+
150
+ This only touches our local CLI subprocess; it never claims to cancel a
151
+ request already submitted upstream to a provider.
152
+ """
153
+ if proc.returncode is not None:
154
+ return
155
+ try:
156
+ proc.terminate()
157
+ except (OSError, ProcessLookupError):
158
+ return
159
+ try:
160
+ await asyncio.wait_for(proc.wait(), timeout=5)
161
+ except asyncio.TimeoutError:
162
+ try:
163
+ proc.kill()
164
+ except (OSError, ProcessLookupError):
165
+ return
166
+ await proc.wait()
167
+
168
+
169
+ class LoginStream:
170
+ """One in-flight device-login event stream for the human frontend.
171
+
172
+ Async-iterate this to receive parsed JSONL events as they arrive. A
173
+ ``completed`` line is held until the child has drained its pipes and exited
174
+ with code zero; a line alone is never treated as success. :meth:`cancel`
175
+ stops only the local CLI subprocess.
176
+ """
177
+
178
+ def __init__(self, spawn: Spawner, args: Sequence[str]) -> None:
179
+ self._spawn = spawn
180
+ self._args = list(args)
181
+ self._proc: Process | None = None
182
+ self._done = False
183
+ self._cancel_requested = False
184
+
185
+ def __aiter__(self) -> "LoginStream":
186
+ return self
187
+
188
+ async def __anext__(self) -> dict:
189
+ if self._done:
190
+ raise StopAsyncIteration
191
+ if self._proc is None:
192
+ proc = await _spawn_or_raise(self._spawn, self._args)
193
+ self._proc = proc
194
+ if self._cancel_requested:
195
+ await _terminate_process(proc)
196
+ raise StopAsyncIteration
197
+ assert self._proc.stdout is not None
198
+ raw = await self._proc.stdout.readline()
199
+ if not raw:
200
+ try:
201
+ returncode = await asyncio.wait_for(self._proc.wait(), timeout=_LOGIN_EXIT_TIMEOUT_SECONDS)
202
+ except asyncio.TimeoutError:
203
+ await _terminate_process(self._proc)
204
+ self._done = True
205
+ raise CLIError("login CLI did not exit after closing stdout")
206
+ self._done = True
207
+ if returncode != 0:
208
+ stderr = await self._proc.stderr.read() if self._proc.stderr is not None else b""
209
+ raise CLIError(_parse_error(stderr), returncode=returncode)
210
+ raise StopAsyncIteration
211
+
212
+ line = raw.decode(errors="replace").strip()
213
+ if not line:
214
+ return await self.__anext__()
215
+ try:
216
+ event = json.loads(line)
217
+ except json.JSONDecodeError as exc:
218
+ self._done = True
219
+ await _terminate_process(self._proc)
220
+ raise CLIError("malformed login event: could not parse JSON") from exc
221
+ if not isinstance(event, dict):
222
+ self._done = True
223
+ await _terminate_process(self._proc)
224
+ raise CLIError(
225
+ f"malformed login event: expected a JSON object, got {type(event).__name__}"
226
+ )
227
+ if event.get("event") == "completed":
228
+ try:
229
+ safe_event = _safe_login_event(event)
230
+ except CLIError:
231
+ self._done = True
232
+ await _terminate_process(self._proc)
233
+ raise
234
+ return await self._finish_completed(safe_event)
235
+ return _safe_login_event(event)
236
+
237
+ async def _finish_completed(self, event: dict) -> dict:
238
+ """Trust ``completed`` only after clean child termination.
239
+
240
+ ``communicate`` drains both pipes while waiting. The bounded wait also
241
+ handles a provider-poll process that emits a completion line and then
242
+ hangs, without leaving a child behind or claiming successful login.
243
+ """
244
+ assert self._proc is not None
245
+ try:
246
+ _stdout, stderr = await asyncio.wait_for(
247
+ self._proc.communicate(), timeout=_LOGIN_EXIT_TIMEOUT_SECONDS
248
+ )
249
+ except asyncio.TimeoutError as exc:
250
+ await _terminate_process(self._proc)
251
+ self._done = True
252
+ raise CLIError("login CLI did not exit cleanly after completion") from exc
253
+ except asyncio.CancelledError:
254
+ await _terminate_process(self._proc)
255
+ self._done = True
256
+ raise
257
+ self._done = True
258
+ returncode = self._proc.returncode
259
+ if returncode is None:
260
+ returncode = await self._proc.wait()
261
+ if returncode != 0:
262
+ raise CLIError(_parse_error(stderr), returncode=returncode)
263
+ return event
264
+
265
+ async def cancel(self) -> None:
266
+ """Terminate the in-flight login subprocess, if any (idempotent)."""
267
+ if self._done:
268
+ return
269
+ self._done = True
270
+ proc = self._proc
271
+ if proc is None:
272
+ self._cancel_requested = True
273
+ return
274
+ await _terminate_process(proc)
275
+
276
+
277
+ class CLIClient:
278
+ """Async wrapper for one module's machine command surface."""
279
+
280
+ def __init__(
281
+ self,
282
+ module_id: str,
283
+ spawn: Spawner | None = None,
284
+ *,
285
+ timeout: float = 30.0,
286
+ ) -> None:
287
+ self.module_id = module_id
288
+ self._spawn = spawn or _default_spawn
289
+ # Bounds one-shot commands (status/quota/pool/import/etc.), not the
290
+ # device-login provider poll window owned by the CLI subprocess.
291
+ self._timeout = timeout
292
+
293
+ async def _run_json(self, args: Sequence[str]) -> dict:
294
+ proc = await _spawn_or_raise(
295
+ self._spawn, [self.module_id, *args]
296
+ )
297
+ try:
298
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=self._timeout)
299
+ except asyncio.TimeoutError as exc:
300
+ await _terminate_process(proc)
301
+ raise CLIError(f"subs-pool module CLI timed out after {self._timeout:g}s") from exc
302
+ except asyncio.CancelledError:
303
+ await _terminate_process(proc)
304
+ raise
305
+ if proc.returncode != 0:
306
+ try:
307
+ payload = json.loads(stdout.decode(errors="replace"))
308
+ error = payload.get("error") if isinstance(payload, dict) else None
309
+ if isinstance(error, dict):
310
+ raise CLIError(str(error.get("message", "module CLI failed")), returncode=proc.returncode, data=payload.get("data") if isinstance(payload.get("data"), dict) else None)
311
+ except json.JSONDecodeError:
312
+ pass
313
+ raise CLIError(_parse_error(stderr), returncode=proc.returncode)
314
+ try:
315
+ payload = json.loads(stdout.decode(errors="replace"))
316
+ except json.JSONDecodeError as exc:
317
+ raise CLIError(f"malformed CLI output: could not parse JSON ({len(stdout)} bytes)") from exc
318
+ if not isinstance(payload, dict):
319
+ raise CLIError(
320
+ f"malformed CLI output: expected a JSON object, got {type(payload).__name__}"
321
+ )
322
+ if payload.get("schema_version") == 1 and "ok" in payload:
323
+ if not payload.get("ok"):
324
+ if isinstance(payload.get("data"), dict):
325
+ return payload["data"]
326
+ error = payload.get("error")
327
+ message = error.get("message", "module CLI failed") if isinstance(error, dict) else "module CLI failed"
328
+ raise CLIError(str(message), returncode=proc.returncode)
329
+ data = payload.get("data")
330
+ return data if isinstance(data, dict) else {}
331
+ return payload
332
+
333
+ async def accounts_list(self) -> dict:
334
+ return await self._run_json(["account", "list"])
335
+
336
+ async def accounts_import(self, ref: str, path: str, *, weight: int = 1) -> dict:
337
+ return await self._run_json(
338
+ ["account", "import", ref, "--path", path, "--weight", str(weight)]
339
+ )
340
+
341
+ async def pool_enable(self, ref: str) -> dict:
342
+ return await self._run_json(["account", "enable", ref])
343
+
344
+ async def pool_disable(self, ref: str) -> dict:
345
+ return await self._run_json(["account", "disable", ref])
346
+
347
+ async def pool_weight(self, ref: str, weight: int) -> dict:
348
+ return await self._run_json(["account", "weight", ref, str(weight)])
349
+
350
+ async def status(self) -> dict:
351
+ return await self._run_json(["status"])
352
+
353
+ async def quota(self) -> dict:
354
+ return await self._run_json(["quota"])
355
+
356
+ def login(self, ref: str) -> LoginStream:
357
+ """Start login lazily, on first iteration."""
358
+ spawn = _default_human_spawn if self._spawn is _default_spawn else self._spawn
359
+ return LoginStream(
360
+ spawn,
361
+ [self.module_id, "account", "login", ref, "--device", "--events-jsonl"],
362
+ )
363
+
364
+
365
+ __all__ = ["CLIClient", "CLIError", "LoginStream", "Process", "Spawner"]
subs_pool/home.py ADDED
@@ -0,0 +1,41 @@
1
+ """Canonical data-root resolution owned by the subscription-pool core."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ DEFAULT_HOME = "~/.subs-pool"
9
+
10
+
11
+ class HomeError(ValueError):
12
+ """The selected subscription-pool root is not usable."""
13
+
14
+
15
+ def _resolve_env_path(name: str, default: str) -> Path:
16
+ value = os.environ.get(name, default)
17
+ if value == "":
18
+ raise HomeError(f"{name} must not be empty")
19
+ path = Path(value).expanduser()
20
+ if not path.is_absolute():
21
+ path = Path.cwd() / path
22
+ return path.resolve()
23
+
24
+
25
+ def data_home(*, create: bool = True) -> Path:
26
+ """Return the canonical generic root.
27
+
28
+ The core never searches for old roots. Creation is explicit so machine
29
+ help/version paths can remain state-free while stateful operations create
30
+ only their selected root.
31
+ """
32
+ home = _resolve_env_path("SUBS_POOL_HOME", DEFAULT_HOME)
33
+ if create:
34
+ existed = home.exists()
35
+ home.mkdir(parents=True, exist_ok=True)
36
+ if not existed and os.name != "nt":
37
+ home.chmod(0o700)
38
+ return home
39
+
40
+
41
+ __all__ = ["DEFAULT_HOME", "HomeError", "data_home"]
@@ -0,0 +1 @@
1
+ """Built-in subscription modules."""
@@ -0,0 +1,44 @@
1
+ # Codex CLI contract
2
+
3
+ Human operations are selected by `subspool codex`. Agent operations are
4
+ selected by `subspool-cli codex` and are wrapped by the root JSON envelope.
5
+
6
+ ```text
7
+ account import ID --path AUTH.json [--weight N]
8
+ account list
9
+ account login ID --device # human only
10
+ account enable ID
11
+ account disable ID
12
+ account weight ID N
13
+ status
14
+ quota
15
+ serve --listen 127.0.0.1:8765 # human only
16
+ ```
17
+
18
+ Account list is metadata-only and never reports quota readiness. Account IDs
19
+ are any non-empty path-safe strings; `/`, `\`, `.`, and `..` are rejected,
20
+ with no additional ASCII or length restriction. Existing IDs remain readable.
21
+ Auth paths are explicit references; credentials
22
+ are not copied into a generic home. There is no logout/remove command in this
23
+ release.
24
+
25
+ `quota` always requests the shared bounded pool-wide refresh, whether or not a
26
+ proxy is running. Its data includes `module`, resolved `codex_root`,
27
+ `generated_at`, committed `snapshot_revision`, refresh outcome, account rows,
28
+ current sample, historical `last_success`, timestamps, freshness/age,
29
+ eligibility, exclusion reason, and sanitized error. Missing/failed/checking or
30
+ stale samples are not current values. A failed required refresh returns exit
31
+ 3 while retaining the readable partial snapshot in Agent `data`.
32
+
33
+ The WHAM adapter makes one direct request per account, retains the baseline
34
+ nullable secondary-window parsing, calculates remaining as `100 - used` only
35
+ for valid finite percentages, and never fabricates zero for unknown. Provider
36
+ bodies, auth headers, tokens, device codes, and credential paths never enter
37
+ machine output or errors.
38
+
39
+ Agent `login` and `serve` return stable `prompt_required`/`unsupported_command`
40
+ errors; they never prompt, open a browser, bind a port, or write human text.
41
+ Agent parser/usage errors likewise produce one JSON object plus newline. Exit
42
+ codes are 0 success, 2 syntax/unsupported, 3 unavailable/incomplete quota or
43
+ auth/network work, 4 local state/configuration, 5 unexpected internal failure,
44
+ and 130 interruption.
@@ -0,0 +1,52 @@
1
+ # Codex module contract
2
+
3
+ Codex is the only built-in provider module. It owns auth, account
4
+ configuration, WHAM parsing, quota sidecar/freshness/refresh, eligibility,
5
+ routing, affinity, upstream wire behavior, SSE, and the local Responses
6
+ server. Generic `subs_pool` code does not inspect any of these facts.
7
+
8
+ The module keeps the baseline two-rule scheduler: a full input-prefix and
9
+ effective-config match stays on its current in-memory chain account; a miss is
10
+ a weighted choice. Full-prefix hashing, UUID session labels, chain capacity,
11
+ successful-only commits, and response normalization remain in the relocated
12
+ baseline files. A failed, partial, cancelled, or incomplete generation never
13
+ commits and is never retried or replayed.
14
+
15
+ Before selection, the module reads account configuration and
16
+ `quota-v1.json` together under `state.lock`. A candidate must be enabled,
17
+ locally authenticated, on the current quota
18
+ epoch, `status=ok`, fresh under the fixed 60-second source-time policy, and
19
+ have known positive remaining capacity. A nullable secondary window remains
20
+ nullable according to the baseline WHAM parser; if it is present and
21
+ exhausted it excludes the account. Unknown quota is never turned into zero or
22
+ green capacity. No candidate means a local HTTP 503 with code
23
+ `quota_unavailable`, before any upstream generation request.
24
+
25
+ The sidecar is the sole quota authority. Its top-level schema is version 1,
26
+ module `codex`, monotonic revision, replacement timestamp, and account-keyed
27
+ records containing epoch, status (`never`/`checking`/`ok`/`failed`), attempt
28
+ metadata, refresh marker, last successful sample, and sanitized error. It
29
+ contains no tokens or raw provider bodies. Failed refreshes preserve the
30
+ historical `last_success` but never expose it as current.
31
+
32
+ Sidecar writes use same-directory exclusive temporary files, flush/fsync,
33
+ atomic replace, directory fsync where supported, and mode 0600 state files.
34
+ `filelock` backs the persistent `state.lock` and
35
+ `quota-v1.refresh.lock` inodes. State locking is short; network work never
36
+ holds it. One pool-wide owner performs at most two account checks concurrently
37
+ and foreground coordination is bounded. Followers join completed overlapping
38
+ waves or fail closed; they do not steal a live kernel lock.
39
+
40
+ The direct Codex root is explicit `CODEX_POOL_HOME`, otherwise
41
+ `${SUBS_POOL_HOME:-~/.subs-pool}/codex`. The root itself contains `pool.json`,
42
+ `quota-v1.json`, `state.lock`, and `quota-v1.refresh.lock`; there is no
43
+ `accounts/` relocation or hidden migration. Existing `pool.json` entries
44
+ without `quota_epoch` mean `legacy-v1` for compatibility. Mutations that
45
+ change enablement, auth, or identity advance the epoch and remove its quota
46
+ record.
47
+
48
+ The foreground server remains `--listen 127.0.0.1:8765`, loopback-only, and
49
+ user-managed. It refreshes on startup, at the 30-second target while running,
50
+ and just-in-time when no current candidate exists. It does not install a
51
+ service, auto-start from the TUI, persist affinity, or perform generation
52
+ failover.
@@ -0,0 +1 @@
1
+ """Codex subscription module: accounts, auth, quota, routing, and protocol."""