From cef1edae39d2bb4cc276a752bd1dc59b29e67e2d Mon Sep 17 00:00:00 2001 From: IsThatYou Date: Fri, 21 Aug 2026 16:42:22 -0700 Subject: [PATCH 1/2] feat: add interactive MCP no-log baseline --- research/arc-agi-3/README.md | 34 +- .../arc-agi-3/prolong_agent/agent/__init__.py | 3 + .../arc-agi-3/prolong_agent/agent/base.py | 46 +- .../prolong_agent/agent/claude_code_agent.py | 92 +++- .../prolong_agent/agent/codex_agent.py | 58 +- .../prolong_agent/agent/codex_events.py | 29 +- .../arc-agi-3/prolong_agent/agent/memory.py | 43 ++ .../arc-agi-3/prolong_agent/agent/prompts.py | 27 + .../arc-agi-3/prolong_agent/agent/swarm.py | 56 +- .../prolong_agent/environment/game_session.py | 379 +++++++++++++ .../prolong_agent/environment/mcp_game.py | 287 ++++++++++ .../prolong_agent/environment/runner.py | 406 +++++--------- .../prolong_agent/metrics/structures.py | 1 + research/arc-agi-3/pyproject.toml | 6 + research/arc-agi-3/tests/conftest.py | 62 +++ .../arc-agi-3/tests/test_container_mcp.py | 59 ++ .../tests/test_interactive_runner.py | 56 ++ research/arc-agi-3/tests/test_mcp_events.py | 50 ++ research/arc-agi-3/tests/test_mcp_game.py | 177 ++++++ research/arc-agi-3/tests/test_memory_modes.py | 98 ++++ .../arc-agi-3/tests/test_queue_regression.py | 57 ++ research/arc-agi-3/uv.lock | 503 +++++++++++++++++- 22 files changed, 2218 insertions(+), 311 deletions(-) create mode 100644 research/arc-agi-3/prolong_agent/agent/memory.py create mode 100644 research/arc-agi-3/prolong_agent/environment/game_session.py create mode 100644 research/arc-agi-3/prolong_agent/environment/mcp_game.py create mode 100644 research/arc-agi-3/tests/conftest.py create mode 100644 research/arc-agi-3/tests/test_container_mcp.py create mode 100644 research/arc-agi-3/tests/test_interactive_runner.py create mode 100644 research/arc-agi-3/tests/test_mcp_events.py create mode 100644 research/arc-agi-3/tests/test_mcp_game.py create mode 100644 research/arc-agi-3/tests/test_memory_modes.py create mode 100644 research/arc-agi-3/tests/test_queue_regression.py diff --git a/research/arc-agi-3/README.md b/research/arc-agi-3/README.md index 47cd098..e959eed 100644 --- a/research/arc-agi-3/README.md +++ b/research/arc-agi-3/README.md @@ -47,6 +47,7 @@ The agent container only mounts the game workspace and, by default, has no netwo prolong-swarm --suite all -m gpt-5.5 --max-actions 500 prolong-swarm --suite all --backend claude-code -m claude-opus-4-6 prolong-swarm --game ls20,ft09 -m gpt-5.5 +prolong-swarm --suite all --no-log ``` Results are written to `evaluation_results/`. @@ -64,16 +65,36 @@ Results are written to `evaluation_results/`. | `--effort` | `high` | Effort level (claude-code backend) | | `--reasoning-effort` | `none` | Reasoning effort (codex backend) | | `--operation-mode` | `online` | `online` / `offline` / `normal` | +| `--no-log` | off | Interactive MCP baseline with no game log in the agent workspace | +| `--in-prompt` | off | Existing current-board-in-prompt baseline | +| `--log-window N` | full log | Expose only the latest N action sections | ### Memory conditions -The agent's access to game history is controlled by `--log-window`. These are the ablation conditions from the paper: +The harness has four explicit memory conditions: | Condition | Flags | History available | |-----------|-------|-------------------| -| prolong | (default) | Full game log | -| lw25 | `--log-window 25` | Last 25 action sections of the log | -| no-log (in-prompt) | `--log-window -1` | No log file; the current board is added to the prompt | +| full-log (PRO-LONG) | (default) | Full durable game log | +| windowed-log | `--log-window 25` | Last 25 action sections of the durable log | +| in-prompt | `--in-prompt` | Current board serialized into each prompt; no agent-visible log | +| mcp-no-log | `--no-log` | Live state and actions available only through authenticated MCP tools | + +`--log-window -1` remains a deprecated alias for `--in-prompt`. The three +condition-selecting flags are mutually exclusive. + +In the MCP no-log baseline, each game gets a short-lived, bearer-authenticated +[Streamable HTTP MCP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) +endpoint with two tools: `current_board` and +`submit_actions`. Tool results enter the coding CLI's native resumed context, +but the runner does not expose an explicit durable game log, serialize an +observation into a prompt, or mount the private trace into the CLI workspace. +Normal coding tools and persistent helper files remain available. A private +host-side `logs.txt`, agent transcript, and usage record are retained for +evaluation and debugging. If a CLI process exits before the live game ends, +the runner resumes the same CLI session with a state-free prompt. In this +condition, `--retries` is the maximum number of consecutive resumed calls that +may execute zero actions before the run is marked `AGENT_STALLED`. ## Scorecards & logs @@ -88,12 +109,15 @@ prolong_agent/ │ ├── codex_agent.py # Codex CLI backend │ ├── claude_code_agent.py # Claude Code backend │ ├── swarm.py # CLI entry point +│ ├── memory.py # explicit memory-condition resolution │ ├── action_queue.py # action execution │ ├── game_state.py # board/log formatting │ └── prompts.py # prompts (~30 lines) ├── environment/ │ ├── arcagi3.py # ARC-AGI-3 API wrapper -│ ├── runner.py # per-game loop +│ ├── game_session.py # shared state, metrics, trace, and action execution +│ ├── mcp_game.py # authenticated per-game MCP tools +│ ├── runner.py # queued and interactive per-game loops │ └── config.py ├── metrics/ └── utils/ diff --git a/research/arc-agi-3/prolong_agent/agent/__init__.py b/research/arc-agi-3/prolong_agent/agent/__init__.py index e131db1..6e4e462 100644 --- a/research/arc-agi-3/prolong_agent/agent/__init__.py +++ b/research/arc-agi-3/prolong_agent/agent/__init__.py @@ -2,6 +2,7 @@ from prolong_agent.agent.claude_code_agent import ClaudeCodeAgent from prolong_agent.agent.action_queue import ActionQueue, QueueExhausted from prolong_agent.agent.game_state import GameState +from prolong_agent.agent.memory import MemoryMode, resolve_memory_mode __all__ = [ "CodexAgent", @@ -9,4 +10,6 @@ "ActionQueue", "QueueExhausted", "GameState", + "MemoryMode", + "resolve_memory_mode", ] diff --git a/research/arc-agi-3/prolong_agent/agent/base.py b/research/arc-agi-3/prolong_agent/agent/base.py index 15a8c99..e6e9df5 100644 --- a/research/arc-agi-3/prolong_agent/agent/base.py +++ b/research/arc-agi-3/prolong_agent/agent/base.py @@ -11,10 +11,14 @@ HEX_COLOR_MAP, INPROMPT_INITIAL_PROMPT, INPROMPT_RESUME_PROMPT, + MCP_NO_LOG_INITIAL_PROMPT, + MCP_NO_LOG_RESUME_PROMPT, + MCP_NO_LOG_SYSTEM_PROMPT, SYSTEM_PROMPT, SYSTEM_PROMPT_INPROMPT, format_actions_block, ) +from prolong_agent.agent.memory import MemoryMode log = logging.getLogger(__name__) @@ -27,9 +31,21 @@ class BaseAgent: _ACTION6_RE = re.compile(r'^ACTION6\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)$') - def __init__(self, grid_mode="hex", log_window=None, action_cap=20, workspace="/workspace"): + def __init__( + self, + grid_mode="hex", + log_window=None, + action_cap=20, + workspace="/workspace", + memory_mode: MemoryMode | str | None = None, + ): self._grid_mode = grid_mode self._log_window = log_window + self._memory_mode = MemoryMode(memory_mode) if memory_mode else ( + MemoryMode.IN_PROMPT if log_window == -1 + else MemoryMode.WINDOWED_LOG if log_window is not None + else MemoryMode.FULL_LOG + ) self._action_cap = max(1, int(action_cap)) self._workspace = workspace @@ -39,7 +55,12 @@ def _workspace_text(self, text): return text def _build_system_prompt(self, available_actions=None): - template = SYSTEM_PROMPT_INPROMPT if self._log_window == -1 else SYSTEM_PROMPT + if self._memory_mode == MemoryMode.MCP_NO_LOG: + return self._workspace_text( + MCP_NO_LOG_SYSTEM_PROMPT.format(action_cap=self._action_cap) + + (HEX_COLOR_MAP if self._grid_mode == "hex" else ASCII_COLOR_MAP) + ) + template = SYSTEM_PROMPT_INPROMPT if self._memory_mode == MemoryMode.IN_PROMPT else SYSTEM_PROMPT available_actions = available_actions or DEFAULT_ACTIONS if self._log_window is None: @@ -68,7 +89,11 @@ def _build_system_prompt(self, available_actions=None): return self._workspace_text(prompt) def _build_prompt(self, log_name, is_first, **values): - if self._log_window == -1: + if self._memory_mode == MemoryMode.MCP_NO_LOG: + return self._workspace_text( + MCP_NO_LOG_INITIAL_PROMPT if is_first else MCP_NO_LOG_RESUME_PROMPT + ) + if self._memory_mode == MemoryMode.IN_PROMPT: board = values.get("board_text", "") or "(board unavailable)" if is_first: prompt = INPROMPT_INITIAL_PROMPT.format(board=board) @@ -102,7 +127,7 @@ def _build_prompt(self, log_name, is_first, **values): return f"{description}\n\n{body}" def _sync_history(self, log_path, sandbox): - if self._log_window == -1: + if self._memory_mode in (MemoryMode.IN_PROMPT, MemoryMode.MCP_NO_LOG): return destination = sandbox / log_path.name if self._log_window is not None: @@ -114,7 +139,7 @@ def _sync_history(self, log_path, sandbox): shutil.copyfileobj(source, output) def _add_current_board(self, log_path, values): - if self._log_window != -1 or values.get("board_text"): + if self._memory_mode != MemoryMode.IN_PROMPT or values.get("board_text"): return board_file = log_path.parent / "current_board.txt" if not board_file.exists(): @@ -141,6 +166,17 @@ def _clear_files(sandbox, *names): except OSError: pass + @staticmethod + def _purge_interactive_game_artifacts(sandbox): + """Remove state-bearing artifacts before every MCP-backed CLI call.""" + for filename in ("logs.txt", "current_board.txt"): + for path in sandbox.rglob(filename): + try: + path.unlink() + except OSError: + pass + BaseAgent._clear_files(sandbox, "actions.json", "last_message.txt") + @staticmethod def _split_response(text): if "\n[PLAN]\n" not in text: diff --git a/research/arc-agi-3/prolong_agent/agent/claude_code_agent.py b/research/arc-agi-3/prolong_agent/agent/claude_code_agent.py index 7e4e379..b4587fb 100644 --- a/research/arc-agi-3/prolong_agent/agent/claude_code_agent.py +++ b/research/arc-agi-3/prolong_agent/agent/claude_code_agent.py @@ -24,6 +24,7 @@ from prolong_agent.agent.base import BaseAgent from prolong_agent.agent.claude_events import ClaudeEventParser +from prolong_agent.agent.memory import MemoryMode from prolong_agent.utils import sandbox_net log = logging.getLogger(__name__) @@ -40,7 +41,7 @@ def __init__(self, run_label: str = "", use_api_key: bool = False) -> None: self._containers: dict[str, dict] = {} self._lock = threading.Lock() - def get(self, key: str, workspace_dir: str) -> str: + def get(self, key: str, workspace_dir: str, *, allow_host: bool = False) -> str: with self._lock: if key in self._containers: info = self._containers[key] @@ -54,9 +55,9 @@ def get(self, key: str, workspace_dir: str) -> str: subprocess.run(["docker", "rm", "-f", info["name"]], capture_output=True, timeout=10) del self._containers[key] - return self._create(key, workspace_dir) + return self._create(key, workspace_dir, allow_host=allow_host) - def _create(self, key: str, workspace_dir: str) -> str: + def _create(self, key: str, workspace_dir: str, *, allow_host: bool = False) -> str: name = f"cc_{uuid.uuid4().hex[:12]}" env_flags: list[str] = [] @@ -101,12 +102,16 @@ def _create(self, key: str, workspace_dir: str) -> str: for _v in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"): net_flags += ["-e", f"{_v}={_proxy}"] - net_flags += ["-e", "NO_PROXY=localhost,127.0.0.1", - "-e", "no_proxy=localhost,127.0.0.1"] + bypass = "localhost,127.0.0.1" + if allow_host: + bypass += ",host.docker.internal" + net_flags += ["-e", f"NO_PROXY={bypass}", + "-e", f"no_proxy={bypass}"] cmd = [ "docker", "run", "-d", "--name", name, + *(["--add-host", "host.docker.internal:host-gateway"] if allow_host else []), "--entrypoint", "sleep", "--user", "1000:1000", "--cap-drop=ALL", @@ -162,8 +167,9 @@ def __init__( log_window: Optional[int] = None, effort: str = "high", action_cap: int = 20, + memory_mode: MemoryMode | str | None = None, ) -> None: - super().__init__(grid_mode, log_window, action_cap) + super().__init__(grid_mode, log_window, action_cap, memory_mode=memory_mode) self._model = model self._timeout = timeout or 2400 self._effort = effort @@ -190,6 +196,20 @@ def _session_args(self, path_key: str) -> tuple[list[str], str, bool]: re.IGNORECASE | re.DOTALL, ) + @staticmethod + def _mcp_config(url: str) -> dict[str, Any]: + return { + "mcpServers": { + "prolong_game": { + "type": "http", + "url": url, + "headers": { + "Authorization": "Bearer ${PROLONG_MCP_TOKEN}", + }, + }, + }, + } + @staticmethod def _parse_quota_reset(text): m = ClaudeCodeAgent._QUOTA_RE.search(text) @@ -221,13 +241,19 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", if not log_path.exists(): return None + mcp_url = kwargs.pop("_mcp_url", None) + mcp_token = kwargs.pop("_mcp_token", None) + interactive = bool(mcp_url and mcp_token) + path_key = str(log_path) is_first = path_key not in self._call_count self._call_count[path_key] = self._call_count.get(path_key, 0) + 1 sandbox = log_path.parent / "cc_sandbox" sandbox.mkdir(parents=True, exist_ok=True) - container = self._pool.get(path_key, str(sandbox.resolve())) + container = self._pool.get( + path_key, str(sandbox.resolve()), allow_host=interactive + ) self._sync_history(log_path, sandbox) self._add_current_board(log_path, kwargs) @@ -237,14 +263,21 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", claude_md.write_text(self._build_system_prompt(available_actions)) self._clear_files(sandbox, "actions.json") + if interactive: + self._purge_interactive_game_artifacts(sandbox) + (sandbox / ".mcp.json").write_text( + json.dumps(self._mcp_config(mcp_url), indent=2) + ) prompt = self._build_prompt(log_path.name, is_first, action_num=action_num, **kwargs) if retry_nudge: prompt += f"\n\n{retry_nudge}" - cmd = [ - "docker", "exec", "-i", + cmd = ["docker", "exec", "-i"] + if interactive: + cmd.extend(["-e", "PROLONG_MCP_TOKEN"]) + cmd += [ "-w", "/workspace", container, "claude", "-p", "-", @@ -254,8 +287,21 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", "--max-turns", "50", "--output-format", "stream-json", "--verbose", - "--disallowedTools", "Agent,Task,TodoWrite,ToolSearch,WebSearch,WebFetch,mcp__*,NotebookEdit,AskUserQuestion,Skill,ScheduleWakeup,CronCreate,CronDelete,CronList,EnterPlanMode,ExitPlanMode,EnterWorktree,ExitWorktree", + "--disallowedTools", ( + "Agent,Task,TodoWrite,ToolSearch,WebSearch,WebFetch,NotebookEdit," + "AskUserQuestion,Skill,ScheduleWakeup,CronCreate,CronDelete," + "CronList,EnterPlanMode,ExitPlanMode,EnterWorktree,ExitWorktree" + if interactive else + "Agent,Task,TodoWrite,ToolSearch,WebSearch,WebFetch,mcp__*,NotebookEdit," + "AskUserQuestion,Skill,ScheduleWakeup,CronCreate,CronDelete," + "CronList,EnterPlanMode,ExitPlanMode,EnterWorktree,ExitWorktree" + ), ] + if interactive: + cmd.extend([ + "--strict-mcp-config", + "--mcp-config", "/workspace/.mcp.json", + ]) session_args, session_id, resuming = self._session_args(path_key) cmd.extend(session_args) @@ -274,8 +320,12 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", call_started = time.monotonic() try: + process_env = os.environ.copy() + if interactive: + process_env["PROLONG_MCP_TOKEN"] = mcp_token proc = subprocess.Popen( cmd, + env=process_env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, @@ -414,6 +464,8 @@ def heartbeat() -> None: ) time.sleep(_quota_wait) log.info("quota sleep done -- retrying analyze() for action=%d", action_num) + if interactive: + kwargs.update(_mcp_url=mcp_url, _mcp_token=mcp_token) return self.analyze(log_path, action_num, retry_nudge=retry_nudge, **kwargs) actions = self._read_actions_json(container, action_num, log_path) @@ -454,6 +506,26 @@ def heartbeat() -> None: except Exception: pass + def interact( + self, + run_dir: Path, + action_num: int, + *, + mcp_url: str, + mcp_token: str, + retry_nudge: str = "", + ) -> Optional[dict[str, Any]]: + """Run or resume Claude Code against the live MCP game.""" + session_key = run_dir / ".claude-mcp-session" + session_key.touch(exist_ok=True) + return self.analyze( + session_key, + action_num, + retry_nudge=retry_nudge, + _mcp_url=mcp_url, + _mcp_token=mcp_token, + ) + def _read_actions_json(self, container: str, action_num: int, log_path: Path) -> list[dict]: try: diff --git a/research/arc-agi-3/prolong_agent/agent/codex_agent.py b/research/arc-agi-3/prolong_agent/agent/codex_agent.py index 3bb3d8e..5babb8e 100644 --- a/research/arc-agi-3/prolong_agent/agent/codex_agent.py +++ b/research/arc-agi-3/prolong_agent/agent/codex_agent.py @@ -14,6 +14,7 @@ from prolong_agent.agent.base import BaseAgent from prolong_agent.agent.codex_events import CodexEventParser +from prolong_agent.agent.memory import MemoryMode from prolong_agent.utils import sandbox_net log = logging.getLogger(__name__) @@ -41,8 +42,11 @@ def __init__( log_window: Optional[int] = None, codex_home: Optional[str] = None, action_cap: int = 20, + memory_mode: MemoryMode | str | None = None, ) -> None: - super().__init__(grid_mode, log_window, action_cap, workspace=".") + super().__init__( + grid_mode, log_window, action_cap, workspace=".", memory_mode=memory_mode + ) self._model = model self._reasoning_effort = reasoning_effort self._timeout = timeout @@ -84,7 +88,11 @@ def _session_exists_on_disk(self, session_id: str) -> bool: return self._find_session_file(session_id) is not None def _build_codex_args( - self, prompt: str, is_first: bool, session_id: Optional[str] + self, + prompt: str, + is_first: bool, + session_id: Optional[str], + mcp_url: str | None = None, ) -> list[str]: common_opts = [ "--json", @@ -96,6 +104,11 @@ def _build_codex_args( "-c", f'model_reasoning_effort="{self._reasoning_effort}"', "-c", "shell_environment_policy.ignore_default_excludes=false", ] + if mcp_url: + common_opts.extend([ + "-c", f'mcp_servers.prolong_game.url="{mcp_url}"', + "-c", 'mcp_servers.prolong_game.bearer_token_env_var="PROLONG_MCP_TOKEN"', + ]) if not is_first and session_id: return [ "exec", "resume", @@ -119,16 +132,22 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", is_first = path_key not in self._call_count self._call_count[path_key] = self._call_count.get(path_key, 0) + 1 + mcp_url = kwargs.pop("_mcp_url", None) + mcp_token = kwargs.pop("_mcp_token", None) + interactive = bool(mcp_url and mcp_token) + sandbox = self._get_sandbox(log_path) self._sync_history(log_path, sandbox) self._add_current_board(log_path, kwargs) available_actions = self._available_actions(kwargs) agents_md = sandbox / "AGENTS.md" - if not agents_md.exists(): + if interactive or not agents_md.exists(): agents_md.write_text(self._build_system_prompt(available_actions)) self._clear_files(sandbox, "actions.json", "last_message.txt") + if interactive: + self._purge_interactive_game_artifacts(sandbox) prompt = self._build_prompt(log_path.name, is_first, action_num=action_num, **kwargs) if retry_nudge: @@ -146,7 +165,7 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", self._session_ids.pop(path_key, None) session_id = None - codex_args = self._build_codex_args(prompt, is_first, session_id) + codex_args = self._build_codex_args(prompt, is_first, session_id, mcp_url=mcp_url) host_codex = self._codex_home # Secure by default: the agent runs on an --internal docker network @@ -166,11 +185,15 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", for _v in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"): net_flags += ["-e", f"{_v}={_proxy}"] - net_flags += ["-e", "NO_PROXY=localhost,127.0.0.1", - "-e", "no_proxy=localhost,127.0.0.1"] + bypass = "localhost,127.0.0.1" + if interactive: + bypass += ",host.docker.internal" + net_flags += ["-e", f"NO_PROXY={bypass}", + "-e", f"no_proxy={bypass}"] cmd = [ "docker", "run", "--rm", + *(["--add-host", "host.docker.internal:host-gateway"] if interactive else []), "--user", "1000:1000", "--cap-drop=ALL", "--security-opt=no-new-privileges:true", @@ -190,6 +213,9 @@ def analyze(self, log_path: Path, action_num: int, retry_nudge: str = "", log.error("Set CODEX_API_KEY or use a dedicated CODEX_HOME containing auth.json") return None docker_env = os.environ.copy() + if interactive: + docker_env["PROLONG_MCP_TOKEN"] = mcp_token + cmd += ["-e", "PROLONG_MCP_TOKEN"] if api_key: docker_env["CODEX_API_KEY"] = api_key cmd += ["-e", "CODEX_API_KEY"] @@ -368,6 +394,26 @@ def heartbeat() -> None: self._session_ids.pop(path_key, None) return None + def interact( + self, + run_dir: Path, + action_num: int, + *, + mcp_url: str, + mcp_token: str, + retry_nudge: str = "", + ) -> Optional[dict[str, Any]]: + """Run or resume Codex against the live MCP game without a trace path.""" + session_key = run_dir / ".codex-mcp-session" + session_key.touch(exist_ok=True) + return self.analyze( + session_key, + action_num, + retry_nudge=retry_nudge, + _mcp_url=mcp_url, + _mcp_token=mcp_token, + ) + def _read_actions_json(self, sandbox: Path, action_num: int, log_path: Path) -> list[dict]: actions_path = sandbox / "actions.json" diff --git a/research/arc-agi-3/prolong_agent/agent/codex_events.py b/research/arc-agi-3/prolong_agent/agent/codex_events.py index c5cb6d6..f4e1c88 100644 --- a/research/arc-agi-3/prolong_agent/agent/codex_events.py +++ b/research/arc-agi-3/prolong_agent/agent/codex_events.py @@ -79,7 +79,18 @@ def handle(self, event): elif event_type == "item.started": item = event.get("item", {}) or {} item_type = item.get("type", "") - if item_type == "command_execution": + if "mcp" in item_type: + server = item.get("server") or item.get("server_name") or "mcp" + tool = item.get("tool") or item.get("tool_name") or item.get("name") or "tool" + tool_name = f"{server}.{tool}" + with self.lock: + self.current_tool = tool_name + self.current_tool_started = time.monotonic() + self.phase = self.PHASE_TOOL_RUNNING + self._mark_event(self.PHASE_TOOL_RUNNING) + arguments = item.get("arguments") or item.get("input") or {} + self._write(f"TOOL USE: {tool_name}", json.dumps(arguments, indent=2)[:2000]) + elif item_type == "command_execution": command = item.get("command", "") with self.lock: self.current_tool = "bash" @@ -100,7 +111,21 @@ def handle(self, event): elif event_type == "item.completed": item = event.get("item", {}) or {} item_type = item.get("type", "") - if item_type == "agent_message": + if "mcp" in item_type: + result = item.get("result") or item.get("output") or item.get("error") or {} + self._write("TOOL RESULT", json.dumps(result, indent=2, default=str)[:4000]) + with self.lock: + duration = ( + time.monotonic() - self.current_tool_started + if self.current_tool_started else 0.0 + ) + tool_name = self.current_tool or "mcp" + self.current_tool = None + self.current_tool_started = None + self.phase = self.PHASE_POST_TOOL + self.tool_calls.append((tool_name, duration)) + self._mark_event(self.PHASE_POST_TOOL) + elif item_type == "agent_message": text = item.get("text", "") or "" if text: self.accumulated_text += text diff --git a/research/arc-agi-3/prolong_agent/agent/memory.py b/research/arc-agi-3/prolong_agent/agent/memory.py new file mode 100644 index 0000000..c158c15 --- /dev/null +++ b/research/arc-agi-3/prolong_agent/agent/memory.py @@ -0,0 +1,43 @@ +"""Memory-condition selection shared by the research runners and agents.""" + +from __future__ import annotations + +import warnings +from enum import Enum + + +class MemoryMode(str, Enum): + FULL_LOG = "full-log" + WINDOWED_LOG = "windowed-log" + IN_PROMPT = "in-prompt" + MCP_NO_LOG = "mcp-no-log" + + +def resolve_memory_mode( + *, + no_log: bool = False, + in_prompt: bool = False, + log_window: int | None = None, +) -> tuple[MemoryMode, int | None]: + """Resolve public flags to one unambiguous internal memory condition.""" + if log_window is not None and log_window != -1 and log_window < 1: + raise ValueError("--log-window must be -1 or a positive integer") + + selected = int(no_log) + int(in_prompt) + int(log_window is not None) + if selected > 1: + raise ValueError("--no-log, --in-prompt, and --log-window are mutually exclusive") + + if no_log: + return MemoryMode.MCP_NO_LOG, None + if in_prompt: + return MemoryMode.IN_PROMPT, None + if log_window == -1: + warnings.warn( + "--log-window -1 is deprecated; use --in-prompt instead", + FutureWarning, + stacklevel=2, + ) + return MemoryMode.IN_PROMPT, None + if log_window is not None: + return MemoryMode.WINDOWED_LOG, log_window + return MemoryMode.FULL_LOG, None diff --git a/research/arc-agi-3/prolong_agent/agent/prompts.py b/research/arc-agi-3/prolong_agent/agent/prompts.py index aa04e91..098807b 100644 --- a/research/arc-agi-3/prolong_agent/agent/prompts.py +++ b/research/arc-agi-3/prolong_agent/agent/prompts.py @@ -126,3 +126,30 @@ def format_actions_block(available_actions) -> str: What ran since the last call is listed above; check /workspace/ for anything you saved previously, then write a new /workspace/actions.json. """ + +MCP_NO_LOG_SYSTEM_PROMPT = """\ +You are a coding agent playing a grid-based puzzle game through two MCP tools. + +Your primary objective is to solve all levels in the game. Your secondary objective is to minimize total cumulative actions used. + +Use `current_board` to inspect the live environment. Use `submit_actions` to execute a batch of 1–{action_cap} actions. A batch stops early when the score changes, the game reaches a terminal state, or the action budget is exhausted, so inspect the returned observation before continuing. + +`submit_actions` accepts only an `actions` array. Each entry has an `action` name and, for ACTION6, integer `x` and `y` coordinates from 0 through 63. Do not add a plan or rationale field. + +**Workspace**: `/workspace/` persists across resumed calls. Normal coding tools are available, so you may save private notes, state, or helper programs there. + +**Game structure and strategy**: +- A score increase means that a level was cleared. +- Most games have a step budget or timer mechanism, which can cause a level reset if exceeded. +- Programmatic board analysis can identify connected components by color, position, size, and shape and help form testable hypotheses about the player, walls, goals, and UI. + +Continue using the MCP tools until the game is won or its action budget is exhausted. Do not merely describe actions you would take. +""" + +MCP_NO_LOG_INITIAL_PROMPT = """\ +Inspect the live game with `current_board`, then act through `submit_actions`. Continue until the game is won or the action budget is exhausted. +""" + +MCP_NO_LOG_RESUME_PROMPT = """\ +Continue the same live game through `current_board` and `submit_actions` until it is won or the action budget is exhausted. +""" diff --git a/research/arc-agi-3/prolong_agent/agent/swarm.py b/research/arc-agi-3/prolong_agent/agent/swarm.py index 4759366..3319a4d 100644 --- a/research/arc-agi-3/prolong_agent/agent/swarm.py +++ b/research/arc-agi-3/prolong_agent/agent/swarm.py @@ -24,7 +24,8 @@ from prolong_agent.environment import ArcAgi3Env from prolong_agent.environment.config import EVALUATION_GAMES -from prolong_agent.environment.runner import GameRunner +from prolong_agent.environment.runner import GameRunner, InteractiveMcpGameRunner +from prolong_agent.agent.memory import MemoryMode, resolve_memory_mode from prolong_agent.metrics.reporting import calculate_stats, generate_console_report, save_summary_report from prolong_agent.metrics.structures import GameMetrics, Status @@ -56,6 +57,7 @@ def __init__( prompts_log_dir: Path | None = None, log_post_board: bool = True, agent_retries: int = 5, + memory_mode: MemoryMode = MemoryMode.FULL_LOG, ) -> None: self.inner_agent_kwargs = inner_agent_kwargs self._arcade = arcade @@ -66,6 +68,7 @@ def __init__( self.prompts_log_dir = prompts_log_dir self.log_post_board = log_post_board self.agent_retries = agent_retries + self.memory_mode = memory_mode self.card_id: str | None = None self.scorecard: Any = None @@ -104,7 +107,12 @@ def _run_game(self, card_id: str, game_id: str) -> None: prompts_log_path = game_dir / "logs.txt" prompts_log_path.write_text("") - runner = GameRunner( + runner_class = ( + InteractiveMcpGameRunner + if self.memory_mode == MemoryMode.MCP_NO_LOG + else GameRunner + ) + runner = runner_class( env=env, game_id=game_id, agent_name=self.inner_agent_kwargs.get("name", "swarm_agent"), @@ -139,7 +147,7 @@ def _run_game(self, card_id: str, game_id: str) -> None: pass -def _parse_args(): +def _parse_args(argv: list[str] | None = None): parser = argparse.ArgumentParser(description="Run ARC-AGI-3 Swarm evaluation.") parser.add_argument("--agent", "-a", default="prolong_agent") parser.add_argument("--game", "-g", help="Comma-separated game names or IDs (e.g. ls20,ft09).") @@ -159,13 +167,27 @@ def _parse_args(): help="Codex reasoning effort") parser.add_argument("--grid-mode", default="hex", choices=["ascii", "hex", "num"]) parser.add_argument("--log-window", type=int, - help="History mode: full log by default, last N actions for N>0, or -1 for no log") + help="Expose only the last N log actions; -1 is a deprecated alias for --in-prompt") + parser.add_argument("--in-prompt", action="store_true", + help="Inject the current board directly into each agent prompt") + parser.add_argument("--no-log", action="store_true", + help="Expose the live game only through authenticated MCP tools") parser.add_argument("--action-cap", type=int, default=20, help="Max actions per agent plan (default 20)") parser.add_argument("--note", default="", help="Short run description saved to run_info.txt") - args = parser.parse_args() - if args.log_window is not None and args.log_window != -1 and args.log_window < 1: - parser.error("--log-window must be -1 or a positive integer") + args = parser.parse_args(argv) + if args.retries < 1: + parser.error("--retries must be at least 1") + if args.action_cap < 1: + parser.error("--action-cap must be at least 1") + try: + args.memory_mode, args.effective_log_window = resolve_memory_mode( + no_log=args.no_log, + in_prompt=args.in_prompt, + log_window=args.log_window, + ) + except ValueError as exc: + parser.error(str(exc)) return args @@ -190,7 +212,12 @@ def _resolve_games(args): def _create_agent(args, model): - history = "full" if args.log_window is None else "no log" if args.log_window == -1 else f"last {args.log_window}" + history = args.memory_mode.value + action_cap = ( + min(args.action_cap, 20) + if args.memory_mode == MemoryMode.MCP_NO_LOG + else args.action_cap + ) if args.backend == "codex": from prolong_agent.agent import CodexAgent agent = CodexAgent( @@ -198,8 +225,9 @@ def _create_agent(args, model): reasoning_effort=args.reasoning_effort, grid_mode=args.grid_mode, run_label=args.note, - log_window=args.log_window, - action_cap=args.action_cap, + log_window=args.effective_log_window, + memory_mode=args.memory_mode, + action_cap=action_cap, ) effort = args.reasoning_effort else: @@ -209,9 +237,10 @@ def _create_agent(args, model): use_api_key=args.use_api_key, grid_mode=args.grid_mode, run_label=args.note, - log_window=args.log_window, + log_window=args.effective_log_window, + memory_mode=args.memory_mode, effort=args.effort, - action_cap=args.action_cap, + action_cap=action_cap, ) effort = args.effort log.info("Agent (backend=%s, model=%s, effort=%s, history=%s)", @@ -260,10 +289,11 @@ def main() -> None: inner_agent_kwargs=inner_agent_kwargs, arcade=arcade, games=games, tags=tags, max_actions=args.max_actions, - agent=agent.analyze, + agent=(agent.interact if args.memory_mode == MemoryMode.MCP_NO_LOG else agent.analyze), prompts_log_dir=run_dir, log_post_board=True, agent_retries=args.retries, + memory_mode=args.memory_mode, ) runner = threading.Thread(target=swarm.run, daemon=True) diff --git a/research/arc-agi-3/prolong_agent/environment/game_session.py b/research/arc-agi-3/prolong_agent/environment/game_session.py new file mode 100644 index 0000000..108b6ee --- /dev/null +++ b/research/arc-agi-3/prolong_agent/environment/game_session.py @@ -0,0 +1,379 @@ +"""Shared ARC game-session state for queued and interactive runners.""" + +from __future__ import annotations + +import json +import logging +import os +import shlex +import sys +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Callable + +import requests +from arcengine import GameState as ArcGameState + +from prolong_agent.agent.game_state import GameState +from prolong_agent.environment import BaseEnv +from prolong_agent.metrics.structures import AttemptMetrics, GameMetrics, LevelMetrics, Status +from prolong_agent.utils import action_metadata + +log = logging.getLogger(__name__) + +ROOT_URL = os.environ.get("ROOT_URL", "https://three.arcprize.org") +MAX_RETRIES = 5 +INITIAL_BACKOFF = 1 + +ACTION_NAMES = { + 0: "RESET", 1: "ACTION1", 2: "ACTION2", 3: "ACTION3", + 4: "ACTION4", 5: "ACTION5", 6: "ACTION6", 7: "ACTION7", +} +_SECRET_OPTIONS = {"--claude-token"} + + +def _safe_command(argv: list[str]) -> str: + redacted: list[str] = [] + hide_next = False + for arg in argv: + if hide_next: + redacted.append("[REDACTED]") + hide_next = False + elif arg in _SECRET_OPTIONS: + redacted.append(arg) + hide_next = True + elif any(arg.startswith(f"{option}=") for option in _SECRET_OPTIONS): + redacted.append(f"{arg.split('=', 1)[0]}=[REDACTED]") + else: + redacted.append(arg) + return shlex.join(redacted) + + +def run_with_retries(func: Callable, *args: Any, **kwargs: Any) -> Any: + retries = 0 + backoff = INITIAL_BACKOFF + while True: + try: + return func(*args, **kwargs) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + if retries >= MAX_RETRIES: + log.error("Final attempt failed for %s after %d retries.", func.__name__, retries) + raise + log.warning("%s failed; retrying in %ds (%d/%d)", + func.__name__, backoff, retries + 1, MAX_RETRIES) + time.sleep(backoff) + retries += 1 + backoff *= 2 + + +@dataclass(frozen=True) +class ActionOutcome: + score_changed: bool + state: ArcGameState + exhausted: bool + + +class GameSessionController: + """Own the live environment, private trace, action budget, and metrics.""" + + def __init__( + self, + *, + env: BaseEnv, + game_id: str, + agent_name: str, + max_actions: int, + run_index: int = 1, + tags: list[str] | None = None, + trace_path: Path | None = None, + log_post_board: bool = False, + grid_mode: str = "hex", + ) -> None: + self.env = env + self.game_id = game_id + self.max_actions = max_actions + self.tags = tags + self.trace_path = trace_path + self.log_post_board = log_post_board + self.grid_mode = grid_mode + self.state = GameState(grid_mode=grid_mode) + self.metrics = GameMetrics( + game_id=game_id, + agent_name=agent_name, + run_index=run_index, + start_time=time.time(), + ) + self.metrics.status = Status.IN_PROGRESS + self.level_num = 1 + self.level_metrics = LevelMetrics(level_number=1) + self.attempt_num = 1 + self.attempt_metrics = AttemptMetrics(attempt_number=1) + self.attempt_start = self.metrics.start_time + self.total_actions = 0 + self.max_score = 0 + self.arc_score = 0 + self.arc_state = ArcGameState.NOT_PLAYED + self.available_action_names: list[str] = ["RESET"] + self.usage = {"calls": 0, "in": 0, "out": 0, "cache_read": 0, "cost": 0.0} + self._last_action: dict[str, Any] = {} + self._finalized = False + + @property + def observation(self) -> dict[str, Any]: + board = self.state.render_board(include_animation=False) or "" + return { + "game_id": self.game_id, + "state": "PLAYING" if self.arc_state == ArcGameState.NOT_FINISHED else self.arc_state.name, + "score": self.arc_score, + "level": self.level_num, + "action_num": self.total_actions, + "max_actions": self.max_actions, + "remaining_actions": max(0, self.max_actions - self.total_actions), + "available_actions": list(self.available_action_names), + "grid_mode": self.grid_mode, + "board": board.splitlines(), + } + + @property + def exhausted(self) -> bool: + return self.total_actions >= self.max_actions + + @property + def won(self) -> bool: + return self.arc_state == ArcGameState.WIN + + def start(self) -> dict[str, Any]: + self.state.reset() + if self.trace_path: + self.trace_path.parent.mkdir(parents=True, exist_ok=True) + observation = run_with_retries( + self.env.reset, + task={"game_id": self.game_id, "max_actions": self.max_actions, "tags": self.tags}, + ) + self._record_observation(observation) + self._record_run_info(observation) + if self.trace_path: + self.trace_path.write_text("") + grid = self.state.render_board() + if grid: + with self.trace_path.open("a", encoding="utf-8") as output: + output.write(f"{'=' * 80}\n") + output.write( + f"Action 0 | Level 1 | Attempt 1 | INITIAL STATE | Score: {self.arc_score}\n\n" + ) + output.write(f"[INITIAL BOARD STATE]\n{grid}\n\n") + return self.observation + + def record_usage(self, meta: dict[str, Any] | None) -> dict[str, Any] | None: + if not meta: + return None + meta = dict(meta) + input_tokens = meta.get("input_tokens", 0) or 0 + output_tokens = meta.get("output_tokens", 0) or 0 + cached_tokens = meta.get("cached_tokens", 0) or 0 + if meta.get("cumulative", False): + meta["input_tokens"] = max(0, input_tokens - self.usage["in"]) + meta["output_tokens"] = max(0, output_tokens - self.usage["out"]) + meta["cached_tokens"] = max(0, cached_tokens - self.usage["cache_read"]) + self.usage["in"] = input_tokens + self.usage["out"] = output_tokens + self.usage["cache_read"] = cached_tokens + else: + self.usage["in"] += input_tokens + self.usage["out"] += output_tokens + self.usage["cache_read"] += cached_tokens + self.usage["calls"] += 1 + self.usage["cost"] += meta.get("call_cost_usd", 0.0) or 0.0 + return meta + + def execute_action(self, action: dict[str, Any]) -> ActionOutcome: + if self.exhausted: + raise RuntimeError("action budget exhausted") + previous_score = self.arc_score + action_result = self.state.record_action(action) + self._last_action = action + observation, _, _ = run_with_retries(self.env.step, action_result) + self.total_actions += 1 + self.attempt_metrics.actions += 1 + self._record_observation(observation) + self.metrics.highest_level_reached = max( + self.metrics.highest_level_reached, self.level_num + ) + self._log_action() + + score_changed = self.arc_score != previous_score + if self.arc_score > self.max_score: + self.max_score = self.arc_score + + if score_changed and self.arc_state not in (ArcGameState.WIN, ArcGameState.GAME_OVER): + self._complete_level() + elif self.arc_state == ArcGameState.GAME_OVER: + self._complete_attempt_game_over() + elif self.arc_state == ArcGameState.WIN: + self._complete_win() + + return ActionOutcome(score_changed, self.arc_state, self.exhausted) + + def build_action_metadata( + self, + action: dict[str, Any], + *, + output: str, + step: int, + total: int, + model: str = "", + ) -> str: + name = action.get("name", "?") + data = action.get("data", {}) + action_text = ( + f"ACTION6({data.get('x', 0)},{data.get('y', 0)})" if name == "ACTION6" else name + ) + payload = action_metadata.build( + output=output, + plan={"step": f"{step}/{total}", "action": action_text}, + aggregate={ + "agent_calls": self.usage["calls"], + "actions": self.total_actions + 1, + "input_tokens": self.usage["in"], + "output_tokens": self.usage["out"], + "cache_read_tokens": self.usage["cache_read"], + "cost_usd": round(self.usage["cost"], 4), + }, + model=model, + ) + return json.dumps(payload, separators=(",", ":")) + + def set_stalled(self) -> None: + self.metrics.status = Status.AGENT_STALLED + + def finalize(self) -> GameMetrics: + if self._finalized: + return self.metrics + self._finalized = True + now = time.time() + self.metrics.end_time = now + self.metrics.run_duration_seconds = now - self.metrics.start_time + + if self.attempt_metrics.status == Status.IN_PROGRESS: + self.attempt_metrics.duration_seconds = now - self.attempt_start + if self.metrics.status == Status.ERROR: + self.attempt_metrics.status = Status.ERROR + elif self.arc_state == ArcGameState.WIN: + self.attempt_metrics.status = Status.COMPLETED + self.metrics.status = Status.COMPLETED_RUN + else: + self.attempt_metrics.status = Status.TIMEOUT + if self.metrics.status == Status.IN_PROGRESS: + self.metrics.status = Status.TIMEOUT + + if ( + not self.level_metrics.attempts + or self.level_metrics.attempts[-1].attempt_number != self.attempt_metrics.attempt_number + ): + self.level_metrics.attempts.append(self.attempt_metrics) + if self.level_metrics.status == Status.IN_PROGRESS: + self.level_metrics.status = self.attempt_metrics.status + self.metrics.level_metrics[self.level_num] = self.level_metrics + self.metrics.run_total_actions = sum( + level.total_actions for level in self.metrics.level_metrics.values() + ) + self.metrics.total_game_overs_across_run = sum( + level.total_game_overs for level in self.metrics.level_metrics.values() + ) + self.metrics.total_state_changes_across_run = sum( + level.total_state_changes for level in self.metrics.level_metrics.values() + ) + self.metrics.final_score = self.max_score + return self.metrics + + def _record_observation(self, observation: dict[str, Any]) -> None: + self.state.record_env_update(observation) + self.arc_state = ArcGameState[observation.get("state") or "NOT_PLAYED"] + self.arc_score = observation.get("score", 0) or 0 + raw_actions = observation.get("available_actions", []) + self.available_action_names = sorted( + {ACTION_NAMES.get(value, f"ACTION{value}") for value in raw_actions} | {"RESET"} + ) + + def _record_run_info(self, observation: dict[str, Any]) -> None: + guid = observation.get("guid") + if not guid: + return + self.metrics.guid = guid + self.metrics.replay_url = f"{ROOT_URL}/replay/{self.game_id}/{guid}" + if not self.trace_path: + return + note = "" + for index, arg in enumerate(sys.argv): + if arg == "--note" and index + 1 < len(sys.argv): + note = sys.argv[index + 1] + info_path = self.trace_path.parent / "run_info.txt" + info_path.write_text( + (f"note: {note}\n" if note else "") + + f"game_id: {self.game_id}\n" + f"guid: {guid}\n" + f"replay_url: {self.metrics.replay_url}\n" + f"scorecard_id: {getattr(self.env, '_scorecard_id', 'unknown')}\n" + f"command: {_safe_command([Path(sys.argv[0]).name, *sys.argv[1:]])}\n" + ) + + def _complete_level(self) -> None: + self.attempt_metrics.duration_seconds = time.time() - self.attempt_start + self.attempt_metrics.status = Status.COMPLETED + self.level_metrics.attempts.append(self.attempt_metrics) + self.level_metrics.status = Status.COMPLETED + self.metrics.level_metrics[self.level_num] = self.level_metrics + self.level_num += 1 + self.metrics.highest_level_reached = max( + self.metrics.highest_level_reached, self.level_num + ) + self.level_metrics = LevelMetrics(level_number=self.level_num) + self.attempt_num = 1 + self.attempt_metrics = AttemptMetrics(attempt_number=1) + self.attempt_start = time.time() + + def _complete_attempt_game_over(self) -> None: + self.attempt_metrics.duration_seconds = time.time() - self.attempt_start + self.attempt_metrics.status = Status.GAME_OVER + self.attempt_metrics.game_overs += 1 + self.level_metrics.attempts.append(self.attempt_metrics) + self.level_metrics.status = Status.GAME_OVER + self.metrics.level_metrics[self.level_num] = self.level_metrics + self.metrics.status = Status.TIMEOUT + self.attempt_num += 1 + self.attempt_metrics = AttemptMetrics(attempt_number=self.attempt_num) + self.attempt_start = time.time() + + def _complete_win(self) -> None: + self.attempt_metrics.duration_seconds = time.time() - self.attempt_start + self.attempt_metrics.status = Status.COMPLETED + self.level_metrics.attempts.append(self.attempt_metrics) + self.level_metrics.status = Status.COMPLETED + self.metrics.level_metrics[self.level_num] = self.level_metrics + self.metrics.status = Status.COMPLETED_RUN + + def _log_action(self) -> None: + if not self.trace_path: + return + action = self._last_action + with self.trace_path.open("a", encoding="utf-8") as output: + output.write(f"\n{'=' * 80}\n") + step = action.get("plan_step") + plan = f" | Plan Step {step}" if step else "" + output.write( + f"Action {self.total_actions} | Level {self.level_num} | " + f"Attempt {self.attempt_num}{plan} | Score: {self.arc_score}\n\n" + ) + hint = self.state.consume_hint_block() + if hint: + output.write(f"{hint}\n") + name = action.get("name", "?") + data = action.get("data", {}) + output.write( + f"Tool Call: {name}({json.dumps(data) if name == 'ACTION6' else '{}'})\n" + ) + if self.log_post_board: + grid = self.state.render_board() + if grid: + output.write(f"[POST-ACTION BOARD STATE]\n{grid}\n\n") diff --git a/research/arc-agi-3/prolong_agent/environment/mcp_game.py b/research/arc-agi-3/prolong_agent/environment/mcp_game.py new file mode 100644 index 0000000..1d99c34 --- /dev/null +++ b/research/arc-agi-3/prolong_agent/environment/mcp_game.py @@ -0,0 +1,287 @@ +"""Authenticated, per-game Streamable HTTP MCP server.""" + +from __future__ import annotations + +import logging +import secrets +import socket +import threading +import time +from typing import Annotated, Any, Literal + +import uvicorn +from mcp.server.mcpserver import MCPServer +from mcp.server.mcpserver.exceptions import ToolError +from mcp.server.transport_security import TransportSecuritySettings +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from prolong_agent.environment.game_session import GameSessionController + +log = logging.getLogger(__name__) + +MAX_BATCH_ACTIONS = 20 +VALID_ACTIONS = { + "ACTION1", "ACTION2", "ACTION3", "ACTION4", + "ACTION5", "ACTION6", "ACTION7", "RESET", +} + + +class SubmittedAction(BaseModel): + model_config = ConfigDict(extra="forbid") + + action: Literal[ + "ACTION1", "ACTION2", "ACTION3", "ACTION4", + "ACTION5", "ACTION6", "ACTION7", "RESET", + ] + x: Annotated[int, Field(strict=True, ge=0, le=63)] | None = None + y: Annotated[int, Field(strict=True, ge=0, le=63)] | None = None + + @model_validator(mode="after") + def validate_coordinates(self) -> SubmittedAction: + coordinate_fields = self.model_fields_set & {"x", "y"} + if self.action == "ACTION6" and coordinate_fields != {"x", "y"}: + raise ValueError("ACTION6 requires x and y") + if self.action != "ACTION6" and coordinate_fields: + raise ValueError(f"{self.action} does not accept coordinates") + return self + + +class BearerAuthMiddleware: + def __init__(self, app: Any, token: str) -> None: + self.app = app + self._expected = f"Bearer {token}".encode() + + async def __call__(self, scope: dict, receive: Any, send: Any) -> None: + if scope.get("type") == "http": + headers = dict(scope.get("headers", [])) + supplied = headers.get(b"authorization", b"") + if not secrets.compare_digest(supplied, self._expected): + await send({ + "type": "http.response.start", + "status": 401, + "headers": [(b"content-type", b"application/json")], + }) + await send({ + "type": "http.response.body", + "body": b'{"error":"unauthorized"}', + }) + return + await self.app(scope, receive, send) + + +class GameMcpServer: + """Expose one controller through exactly two MCP tools.""" + + def __init__(self, controller: GameSessionController) -> None: + self.controller = controller + self.token = secrets.token_urlsafe(32) + self._lock = threading.Lock() + self._mcp = MCPServer( + "prolong-game", + instructions="Inspect the current board and submit validated game actions.", + ) + self._register_tools() + self._socket: socket.socket | None = None + self._server: uvicorn.Server | None = None + self._thread: threading.Thread | None = None + self.port: int | None = None + + @property + def local_url(self) -> str: + if self.port is None: + raise RuntimeError("MCP server is not running") + return f"http://127.0.0.1:{self.port}/mcp" + + @property + def container_url(self) -> str: + if self.port is None: + raise RuntimeError("MCP server is not running") + return f"http://host.docker.internal:{self.port}/mcp" + + def _register_tools(self) -> None: + @self._mcp.tool(name="current_board", structured_output=True) + def current_board() -> dict[str, Any]: + """Return the live game observation and remaining action budget.""" + return self.current_board() + + @self._mcp.tool(name="submit_actions", structured_output=True) + def submit_actions( + actions: Annotated[ + list[SubmittedAction], + Field(min_length=1, max_length=MAX_BATCH_ACTIONS), + ], + ) -> dict[str, Any]: + """Validate and immediately execute a batch of one to twenty actions.""" + return self.submit_actions([ + action.model_dump(exclude_none=True) for action in actions + ]) + + def current_board(self) -> dict[str, Any]: + with self._lock: + return self.controller.observation + + def submit_actions(self, actions: list[dict[str, Any]]) -> dict[str, Any]: + with self._lock: + validated = self._validate_batch(actions) + submitted = len(validated) + executed = 0 + automatic_actions: list[str] = [] + stop_reason = "batch_complete" + + for index, action in enumerate(validated, start=1): + if self.controller.exhausted: + stop_reason = "action_budget_exhausted" + break + action["plan_step"] = f"{index}/{submitted}" + action["action_metadata"] = self.controller.build_action_metadata( + action, + output=f"MCP submit_actions batch of {submitted}", + step=index, + total=submitted, + ) + outcome = self.controller.execute_action(action) + executed += 1 + + if outcome.state.name == "WIN": + stop_reason = "win" + break + if outcome.state.name == "GAME_OVER": + stop_reason = "game_over" + if not self.controller.exhausted: + reset = { + "name": "RESET", + "data": {}, + "plan_step": "automatic", + } + reset["action_metadata"] = self.controller.build_action_metadata( + reset, + output="MCP automatic RESET after GAME_OVER", + step=1, + total=1, + ) + self.controller.execute_action(reset) + automatic_actions.append("RESET") + stop_reason = "game_over_reset" + break + if outcome.exhausted: + stop_reason = "action_budget_exhausted" + break + if outcome.score_changed: + stop_reason = "score_changed" + break + + return { + "submitted_count": submitted, + "executed_count": executed, + "automatic_actions": automatic_actions, + "stop_reason": stop_reason, + "observation": self.controller.observation, + } + + def _validate_batch(self, actions: Any) -> list[dict[str, Any]]: + if not isinstance(actions, list): + raise ToolError("actions must be an array") + if not 1 <= len(actions) <= MAX_BATCH_ACTIONS: + raise ToolError("actions must contain 1 to 20 entries") + + available = set(self.controller.available_action_names) + validated: list[dict[str, Any]] = [] + for index, entry in enumerate(actions): + if not isinstance(entry, dict): + raise ToolError(f"actions[{index}] must be an object") + extra = set(entry) - {"action", "x", "y"} + if extra: + raise ToolError( + f"actions[{index}] contains unsupported fields: {', '.join(sorted(extra))}" + ) + name = entry.get("action") + if not isinstance(name, str) or name not in VALID_ACTIONS: + raise ToolError(f"actions[{index}].action is invalid") + if name not in available: + raise ToolError(f"actions[{index}].action {name} is unavailable") + if name == "ACTION6": + if set(entry) != {"action", "x", "y"}: + raise ToolError(f"actions[{index}] ACTION6 requires x and y") + x, y = entry["x"], entry["y"] + if isinstance(x, bool) or isinstance(y, bool) or not isinstance(x, int) or not isinstance(y, int): + raise ToolError(f"actions[{index}] x and y must be integers") + if not 0 <= x <= 63 or not 0 <= y <= 63: + raise ToolError(f"actions[{index}] coordinates must be between 0 and 63") + validated.append({"name": name, "data": {"x": x, "y": y}}) + else: + if set(entry) != {"action"}: + raise ToolError(f"actions[{index}] {name} does not accept coordinates") + validated.append({"name": name, "data": {}}) + return validated + + def start(self) -> GameMcpServer: + if self._thread: + return self + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("0.0.0.0", 0)) + sock.listen(128) + self.port = sock.getsockname()[1] + self._socket = sock + + security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=["127.0.0.1:*", "localhost:*", "host.docker.internal:*"], + allowed_origins=[ + "http://127.0.0.1:*", + "http://localhost:*", + "http://host.docker.internal:*", + ], + ) + app = self._mcp.streamable_http_app( + streamable_http_path="/mcp", + json_response=True, + stateless_http=True, + transport_security=security, + host="0.0.0.0", + ) + config = uvicorn.Config( + BearerAuthMiddleware(app, self.token), + host="0.0.0.0", + port=self.port, + log_level="warning", + access_log=False, + ) + self._server = uvicorn.Server(config) + self._thread = threading.Thread( + target=self._server.run, + kwargs={"sockets": [sock]}, + name=f"mcp-{self.controller.game_id}", + daemon=True, + ) + self._thread.start() + deadline = time.monotonic() + 5 + while not self._server.started and self._thread.is_alive(): + if time.monotonic() >= deadline: + self.close() + raise RuntimeError("MCP server failed to start") + time.sleep(0.01) + if not self._server.started: + self.close() + raise RuntimeError("MCP server exited before startup completed") + return self + + def close(self) -> None: + if self._server: + self._server.should_exit = True + if self._thread: + self._thread.join(timeout=5) + if self._socket: + try: + self._socket.close() + except OSError: + pass + self._thread = None + self._server = None + self._socket = None + + def __enter__(self) -> GameMcpServer: + return self.start() + + def __exit__(self, *_: Any) -> None: + self.close() diff --git a/research/arc-agi-3/prolong_agent/environment/runner.py b/research/arc-agi-3/prolong_agent/environment/runner.py index cce04fa..38d5572 100644 --- a/research/arc-agi-3/prolong_agent/environment/runner.py +++ b/research/arc-agi-3/prolong_agent/environment/runner.py @@ -2,73 +2,26 @@ import json import logging -import os -import shlex -import sys import time from pathlib import Path -from typing import Any, Callable, Optional +from typing import Optional -import requests from arcengine import GameState as ArcGameState -from prolong_agent.agent import ActionQueue, GameState, QueueExhausted +from prolong_agent.agent import ActionQueue, QueueExhausted from prolong_agent.environment import ArcAgi3Env -from prolong_agent.metrics.structures import AttemptMetrics, GameMetrics, LevelMetrics, Status +from prolong_agent.environment.game_session import GameSessionController +from prolong_agent.environment.mcp_game import GameMcpServer +from prolong_agent.metrics.structures import GameMetrics, Status from prolong_agent.utils import action_metadata log = logging.getLogger(__name__) -ROOT_URL = os.environ.get("ROOT_URL", "https://three.arcprize.org") -MAX_RETRIES = 5 -INITIAL_BACKOFF = 1 - _RETRY_NUDGE = ( "Your previous response did not produce a valid /workspace/actions.json. " "Please write one with the shape {\"actions\": [...]}." ) -_SECRET_OPTIONS = {"--claude-token"} -ACTION_NAMES = { - 0: "RESET", 1: "ACTION1", 2: "ACTION2", 3: "ACTION3", - 4: "ACTION4", 5: "ACTION5", 6: "ACTION6", 7: "ACTION7", -} - - -def _safe_command(argv: list[str]) -> str: - """Format an invocation for logs without persisting CLI secrets.""" - redacted: list[str] = [] - hide_next = False - for arg in argv: - if hide_next: - redacted.append("[REDACTED]") - hide_next = False - elif arg in _SECRET_OPTIONS: - redacted.append(arg) - hide_next = True - elif any(arg.startswith(f"{option}=") for option in _SECRET_OPTIONS): - redacted.append(f"{arg.split('=', 1)[0]}=[REDACTED]") - else: - redacted.append(arg) - return shlex.join(redacted) - - -def _run_with_retries(func: Callable, *args: Any, **kwargs: Any) -> Any: - retries = 0 - backoff = INITIAL_BACKOFF - while True: - try: - return func(*args, **kwargs) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: - if retries >= MAX_RETRIES: - log.error("Final attempt failed for %s after %d retries.", func.__name__, retries) - raise - log.warning("%s: %s. Retrying in %ds (%d/%d)", - func.__name__, type(e).__name__, backoff, retries + 1, MAX_RETRIES) - time.sleep(backoff) - retries += 1 - backoff *= 2 - class GameRunner: """Runs a game using an agent and its queued actions.""" @@ -98,13 +51,23 @@ def __init__( self.agent = agent self.log_post_board = log_post_board self.agent_retries = agent_retries - self._state = GameState(**(agent_kwargs or {})) + self._session = GameSessionController( + env=env, + game_id=game_id, + agent_name=agent_name, + max_actions=max_actions_per_game, + run_index=run_index, + tags=tags, + trace_path=prompts_log_path, + log_post_board=log_post_board, + grid_mode=(agent_kwargs or {}).get("grid_mode", "hex"), + ) + self._state = self._session.state self._queue = ActionQueue() self._last_cost: float = 0.0 self._last_agent_duration: float = 0.0 - self._usage = {"calls": 0, "in": 0, "out": 0, "cache_read": 0, "cost": 0.0} + self._usage = self._session.usage self._recent_actions: list[str] = [] - self._last_logged_action: dict = {} def _next_action(self) -> dict: obs = self._state.last_observation or {} @@ -246,219 +209,54 @@ def _append_post_action_board(self): log.warning("failed to write board state after 3 retries (PermissionError)") def _record_usage(self, meta): - if not meta: - return None - - input_tokens = meta.get("input_tokens", 0) or 0 - output_tokens = meta.get("output_tokens", 0) or 0 - cached_tokens = meta.get("cached_tokens", 0) or 0 - if meta.get("cumulative", False): - current = self._usage - meta = dict( - meta, - input_tokens=max(0, input_tokens - current["in"]), - output_tokens=max(0, output_tokens - current["out"]), - cached_tokens=max(0, cached_tokens - current["cache_read"]), - ) - current["in"] = input_tokens - current["out"] = output_tokens - current["cache_read"] = cached_tokens - else: - self._usage["in"] += input_tokens - self._usage["out"] += output_tokens - self._usage["cache_read"] += cached_tokens - - self._usage["calls"] += 1 - self._usage["cost"] += meta.get("call_cost_usd", 0.0) or 0.0 - return meta + return self._session.record_usage(meta) def run(self) -> GameMetrics: - metrics = GameMetrics( - game_id=self.game_id, - agent_name=self.agent_name, - run_index=self.run_index, - start_time=time.time(), - ) - metrics.status = Status.IN_PROGRESS - - level_num = 1 - level_metrics = LevelMetrics(level_number=level_num) - attempt_num = 1 - attempt_metrics = AttemptMetrics(attempt_number=attempt_num) - attempt_start = metrics.start_time - - max_score = 0 - total_actions = 0 - arc_state: ArcGameState | None = None - arc_score = 0 - try: - self._state.reset() self._queue.reset() - - observation = _run_with_retries( - self.env.reset, - task={"game_id": self.game_id, "max_actions": self.max_actions_per_game, "tags": self.tags}, - ) - arc_state = ArcGameState[observation.get("state") or "NOT_PLAYED"] - arc_score = observation.get("score", 0) or 0 - - guid = observation.get("guid") - if guid and not metrics.guid: - metrics.guid = guid - metrics.replay_url = f"{ROOT_URL}/replay/{self.game_id}/{guid}" - log.info("[%s Run %d] Replay URL: %s", self.game_id, self.run_index, metrics.replay_url) - if self.prompts_log_path: - info_path = self.prompts_log_path.parent / "run_info.txt" - note = "" - for i, arg in enumerate(sys.argv): - if arg == "--note" and i + 1 < len(sys.argv): - note = sys.argv[i + 1] - info_path.write_text( - (f"note: {note}\n" if note else "") - + f"game_id: {self.game_id}\n" - f"guid: {guid}\n" - f"replay_url: {metrics.replay_url}\n" - f"scorecard_id: {getattr(self.env, '_scorecard_id', 'unknown')}\n" - f"command: {_safe_command([Path(sys.argv[0]).name, *sys.argv[1:]])}\n" - ) - - self._state.record_env_update(observation) - - raw_actions = observation.get("available_actions", []) - self._available_action_names = sorted( - {ACTION_NAMES.get(a, f"ACTION{a}") for a in raw_actions} | {"RESET"} - ) + self._session.start() + self._available_action_names = self._session.available_action_names log.info("[%s] Available actions: %s", self.game_id, ", ".join(self._available_action_names)) + self._write_current_board(self._session.arc_score, 0) - if self.prompts_log_path and self.prompts_log_path.stat().st_size == 0: - grid = self._state.render_board() - if grid: - with open(self.prompts_log_path, 'a', encoding='utf-8') as f: - f.write(f"{'='*80}\n") - f.write(f"Action 0 | Level {level_num} | Attempt {attempt_num} | INITIAL STATE | Score: {arc_score}\n\n") - f.write(f"[INITIAL BOARD STATE]\n{grid}\n\n") - - self._write_current_board(arc_score, 0) - - while total_actions < self.max_actions_per_game: + while not self._session.exhausted: try: action_dict = self._next_action() except QueueExhausted: - log.info("queue exhausted at action %d — calling agent", total_actions) - self._wait_for_plan(total_actions, arc_score, level_num) + log.info("queue exhausted at action %d — calling agent", self._session.total_actions) + self._wait_for_plan( + self._session.total_actions, + self._session.arc_score, + self._session.level_num, + ) action_dict = self._next_action() - action_dict["action_metadata"] = self._build_action_metadata(action_dict, total_actions) - action_result = self._state.record_action(action_dict) - self._last_logged_action = action_dict + action_dict["action_metadata"] = self._build_action_metadata( + action_dict, self._session.total_actions + ) + action_dict["plan_step"] = ( + f"{self._queue.plan_index}/{self._queue.plan_total}" + if self._queue.plan_total else None + ) self._remember_action(action_dict) - observation, _, _ = _run_with_retries(self.env.step, action_result) - - total_actions += 1 - attempt_metrics.actions += 1 - - prev_max_score = max_score - arc_state = ArcGameState[observation.get("state") or "NOT_PLAYED"] - arc_score = observation.get("score", 0) or 0 - max_score = max(max_score, arc_score) - metrics.highest_level_reached = max(metrics.highest_level_reached, level_num) - - self._state.record_env_update(observation) - self._queue.check_score(arc_score) - - self._log_action(total_actions, level_num, attempt_num, arc_score, arc_state) - - if self.log_post_board and self.prompts_log_path: - self._append_post_action_board() - - if arc_score > prev_max_score and arc_state not in (ArcGameState.WIN, ArcGameState.GAME_OVER): - attempt_metrics.duration_seconds = time.time() - attempt_start - attempt_metrics.status = Status.COMPLETED - level_metrics.attempts.append(attempt_metrics) - level_metrics.status = Status.COMPLETED - metrics.level_metrics[level_num] = level_metrics - - log.info("[%s Run %d] Level %d COMPLETED. Attempt %d actions: %d. Score: %d.", - self.game_id, self.run_index, level_num, attempt_num, attempt_metrics.actions, arc_score) - - level_num += 1 - metrics.highest_level_reached = max(metrics.highest_level_reached, level_num) - level_metrics = LevelMetrics(level_number=level_num) - attempt_num = 1 - attempt_metrics = AttemptMetrics(attempt_number=attempt_num) - attempt_start = time.time() - - continue - - if arc_state == ArcGameState.GAME_OVER: - attempt_metrics.duration_seconds = time.time() - attempt_start - attempt_metrics.status = Status.GAME_OVER - attempt_metrics.game_overs += 1 - level_metrics.attempts.append(attempt_metrics) - level_metrics.status = Status.GAME_OVER - metrics.level_metrics[level_num] = level_metrics - metrics.status = Status.TIMEOUT - log.warning("[%s Run %d] Game Over on Level %d, Attempt %d. Actions: %d.", - self.game_id, self.run_index, level_num, attempt_num, attempt_metrics.actions) - attempt_num += 1 - attempt_metrics = AttemptMetrics(attempt_number=attempt_num) - attempt_start = time.time() - - if arc_state == ArcGameState.WIN: - attempt_metrics.duration_seconds = time.time() - attempt_start - attempt_metrics.status = Status.COMPLETED - level_metrics.attempts.append(attempt_metrics) - level_metrics.status = Status.COMPLETED - metrics.level_metrics[level_num] = level_metrics - metrics.status = Status.COMPLETED_RUN - log.info("[%s Run %d] Game COMPLETED! Level %d actions: %d. Score: %d", - self.game_id, self.run_index, level_num, attempt_metrics.actions, arc_score) + outcome = self._session.execute_action(action_dict) + self._available_action_names = self._session.available_action_names + self._queue.check_score(self._session.arc_score) + + if outcome.state == ArcGameState.WIN: break except QueueExhausted as e: log.info("[%s Run %d] Episode ended (queue exhausted): %s", self.game_id, self.run_index, e) - metrics.status = Status.QUEUE_EXHAUSTED + self._session.metrics.status = Status.QUEUE_EXHAUSTED except Exception as e: - metrics.status = Status.ERROR - metrics.error_message = str(e) - attempt_metrics.status = Status.ERROR - level_metrics.status = Status.ERROR + self._session.metrics.status = Status.ERROR + self._session.metrics.error_message = str(e) + self._session.attempt_metrics.status = Status.ERROR + self._session.level_metrics.status = Status.ERROR log.error("[%s Run %d] Exception: %s", self.game_id, self.run_index, e, exc_info=True) - - finally: - metrics.end_time = time.time() - metrics.run_duration_seconds = metrics.end_time - metrics.start_time - - if attempt_metrics.status == Status.IN_PROGRESS: - attempt_metrics.duration_seconds = metrics.end_time - attempt_start - if metrics.status == Status.ERROR: - attempt_metrics.status = Status.ERROR - elif arc_state == ArcGameState.WIN: - attempt_metrics.status = Status.COMPLETED - metrics.status = Status.COMPLETED_RUN - else: - attempt_metrics.status = Status.TIMEOUT - if metrics.status == Status.IN_PROGRESS: - metrics.status = Status.TIMEOUT - - if (not level_metrics.attempts - or level_metrics.attempts[-1].attempt_number != attempt_metrics.attempt_number): - level_metrics.attempts.append(attempt_metrics) - if level_metrics.status == Status.IN_PROGRESS: - level_metrics.status = attempt_metrics.status - - metrics.level_metrics[level_num] = level_metrics - metrics.run_total_actions = sum(lm.total_actions for lm in metrics.level_metrics.values()) - metrics.total_game_overs_across_run = sum(lm.total_game_overs for lm in metrics.level_metrics.values()) - metrics.total_state_changes_across_run = sum(lm.total_state_changes for lm in metrics.level_metrics.values()) - metrics.final_score = max_score - - if metrics.guid and not metrics.replay_url: - metrics.replay_url = f"{ROOT_URL}/replay/{self.game_id}/{metrics.guid}" - - return metrics + return self._session.finalize() def _call_agent(self, action_num: int, arc_score: int, retry_nudge: str = "", level_num: int = 1) -> bool: @@ -500,21 +298,95 @@ def _call_agent(self, action_num: int, arc_score: int, retry_nudge: str = "", log.warning("agent at action %d: no actions from actions.json", action_num) return False - def _log_action(self, action_num: int, level: int, attempt: int, - arc_score: int, arc_state: ArcGameState) -> None: - if not self.prompts_log_path: - return - action_dict = self._last_logged_action or {} - with open(self.prompts_log_path, 'a', encoding='utf-8') as f: - f.write(f"\n{'='*80}\n") - plan_info = f" | Plan Step {self._queue.plan_index}/{self._queue.plan_total}" if self._queue.plan_total > 0 else "" - f.write(f"Action {action_num} | Level {level} | Attempt {attempt}{plan_info} | Score: {arc_score}\n\n") - hint = self._state.consume_hint_block() - if hint: - f.write(f"{hint}\n") - name = action_dict.get("name", "?") - data = action_dict.get("data", {}) - if name == "ACTION6": - f.write(f"Tool Call: {name}({json.dumps(data)})\n") - else: - f.write(f"Tool Call: {name}({{}})\n") +class InteractiveMcpGameRunner: + """Let a resumed coding-agent session operate the game through MCP.""" + + def __init__( + self, + *, + env: ArcAgi3Env, + game_id: str, + agent_name: str, + max_actions_per_game: int, + run_index: int = 1, + tags: Optional[list[str]] = None, + prompts_log_path: Path, + agent=None, + log_post_board: bool = True, + agent_retries: int = 5, + agent_kwargs: Optional[dict] = None, + ) -> None: + self.agent = agent + self.agent_retries = max(1, agent_retries) + self.run_dir = prompts_log_path.parent + self._session = GameSessionController( + env=env, + game_id=game_id, + agent_name=agent_name, + max_actions=max_actions_per_game, + run_index=run_index, + tags=tags, + trace_path=prompts_log_path, + log_post_board=log_post_board, + grid_mode=(agent_kwargs or {}).get("grid_mode", "hex"), + ) + + def run(self) -> GameMetrics: + server: GameMcpServer | None = None + try: + self._session.start() + if ( + self._session.arc_state in (ArcGameState.NOT_PLAYED, ArcGameState.GAME_OVER) + and not self._session.exhausted + ): + reset = {"name": "RESET", "data": {}, "plan_step": "automatic"} + reset["action_metadata"] = self._session.build_action_metadata( + reset, + output="MCP automatic initial RESET", + step=1, + total=1, + ) + self._session.execute_action(reset) + + server = GameMcpServer(self._session).start() + stalled_calls = 0 + while not self._session.won and not self._session.exhausted: + before = self._session.total_actions + nudge = ( + "Use the MCP tools now and make progress on the live game." + if stalled_calls else "" + ) + result = None + if self.agent: + result = self.agent( + self.run_dir, + self._session.total_actions, + mcp_url=server.container_url, + mcp_token=server.token, + retry_nudge=nudge, + ) + if result: + self._session.record_usage(result.get("meta")) + executed = self._session.total_actions - before + if executed: + stalled_calls = 0 + else: + stalled_calls += 1 + log.warning( + "MCP agent made no progress (%d/%d consecutive calls)", + stalled_calls, + self.agent_retries, + ) + if stalled_calls >= self.agent_retries: + self._session.set_stalled() + break + except Exception as exc: + self._session.metrics.status = Status.ERROR + self._session.metrics.error_message = str(exc) + self._session.attempt_metrics.status = Status.ERROR + self._session.level_metrics.status = Status.ERROR + log.error("interactive MCP runner failed: %s", exc, exc_info=True) + finally: + if server: + server.close() + return self._session.finalize() diff --git a/research/arc-agi-3/prolong_agent/metrics/structures.py b/research/arc-agi-3/prolong_agent/metrics/structures.py index 47d714c..ad1adb2 100644 --- a/research/arc-agi-3/prolong_agent/metrics/structures.py +++ b/research/arc-agi-3/prolong_agent/metrics/structures.py @@ -13,6 +13,7 @@ class Status(str, Enum): ERROR = "ERROR" GAME_OVER = "GAME_OVER" QUEUE_EXHAUSTED = "QUEUE_EXHAUSTED" + AGENT_STALLED = "AGENT_STALLED" @dataclass diff --git a/research/arc-agi-3/pyproject.toml b/research/arc-agi-3/pyproject.toml index a728a6f..0ee682b 100644 --- a/research/arc-agi-3/pyproject.toml +++ b/research/arc-agi-3/pyproject.toml @@ -21,6 +21,7 @@ keywords = ["arc-agi", "agent", "puzzle-solving"] dependencies = [ "arc-agi>=0.9.1", "arcengine>=0.9.3", + "mcp>=2.0,<3", "python-dotenv>=1.0", "requests>=2.31", ] @@ -48,3 +49,8 @@ Issues = "https://github.com/alexisfox7/PRO-LONG/issues" [tool.setuptools.packages.find] where = ["."] include = ["prolong_agent", "prolong_agent.*"] + +[tool.pytest.ini_options] +markers = [ + "container: Docker smoke tests requiring locally built agent images", +] diff --git a/research/arc-agi-3/tests/conftest.py b/research/arc-agi-3/tests/conftest.py new file mode 100644 index 0000000..a27ee4c --- /dev/null +++ b/research/arc-agi-3/tests/conftest.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from copy import deepcopy +from pathlib import Path + +import pytest + +from prolong_agent.environment.game_session import GameSessionController + + +def observation( + *, + game_id: str = "test-game", + state: str = "NOT_FINISHED", + score: int = 0, + actions: list[int] | None = None, + fill: int = 0, +) -> dict: + return { + "game_id": game_id, + "state": state, + "score": score, + "frame": [[[fill, fill], [fill, fill]]], + "available_actions": actions if actions is not None else [1, 2, 6], + "guid": f"guid-{game_id}", + } + + +class FakeEnv: + def __init__(self, initial: dict | None = None, scripted: list[dict] | None = None) -> None: + self.initial = initial or observation() + self.scripted = list(scripted or []) + self.calls: list[dict] = [] + self.last = deepcopy(self.initial) + + def reset(self, task=None): + self.last = deepcopy(self.initial) + return deepcopy(self.last) + + def step(self, action): + self.calls.append(action) + if self.scripted: + self.last = deepcopy(self.scripted.pop(0)) + return deepcopy(self.last), 0.0, False + + +@pytest.fixture +def make_controller(tmp_path: Path): + def factory(*, env: FakeEnv | None = None, max_actions: int = 500, game_id="test-game"): + env = env or FakeEnv(observation(game_id=game_id)) + controller = GameSessionController( + env=env, + game_id=game_id, + agent_name="test-agent", + max_actions=max_actions, + trace_path=tmp_path / game_id / "logs.txt", + log_post_board=True, + ) + controller.start() + return controller, env + + return factory diff --git a/research/arc-agi-3/tests/test_container_mcp.py b/research/arc-agi-3/tests/test_container_mcp.py new file mode 100644 index 0000000..91c86d5 --- /dev/null +++ b/research/arc-agi-3/tests/test_container_mcp.py @@ -0,0 +1,59 @@ +"""Opt-in connectivity smoke tests for the two real agent images.""" + +from __future__ import annotations + +import os +import subprocess +import uuid + +import pytest + +from prolong_agent.environment.mcp_game import GameMcpServer + + +IMAGES = ( + "prolong-agent/codex-sandbox:latest", + "prolong-agent/claude-sandbox:latest", +) + + +@pytest.mark.container +@pytest.mark.parametrize("image", IMAGES) +def test_agent_container_reaches_short_lived_mcp(image, make_controller): + if os.environ.get("RUN_CONTAINER_SMOKE") != "1": + pytest.skip("set RUN_CONTAINER_SMOKE=1 to run Docker connectivity tests") + if subprocess.run( + ["docker", "image", "inspect", image], capture_output=True + ).returncode: + pytest.skip(f"build {image} before running the smoke test") + + network = f"prolong-mcp-test-{uuid.uuid4().hex[:10]}" + subprocess.run( + ["docker", "network", "create", "--internal", network], + check=True, + capture_output=True, + ) + controller, _ = make_controller(game_id=image.split("/")[-1].split(":")[0]) + try: + with GameMcpServer(controller) as server: + result = subprocess.run( + [ + "docker", "run", "--rm", + "--network", network, + "--add-host", "host.docker.internal:host-gateway", + "--entrypoint", "curl", + image, + "-sS", "-o", "/dev/null", "-w", "%{http_code}", + "-H", f"Authorization: Bearer {server.token}", + server.container_url, + ], + capture_output=True, + text=True, + timeout=30, + ) + assert result.returncode == 0, result.stderr + assert result.stdout not in {"000", "401"} + finally: + subprocess.run( + ["docker", "network", "rm", network], capture_output=True, timeout=10 + ) diff --git a/research/arc-agi-3/tests/test_interactive_runner.py b/research/arc-agi-3/tests/test_interactive_runner.py new file mode 100644 index 0000000..41b383e --- /dev/null +++ b/research/arc-agi-3/tests/test_interactive_runner.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from conftest import FakeEnv, observation +from prolong_agent.environment.runner import InteractiveMcpGameRunner +from prolong_agent.metrics.structures import Status + + +def test_zero_progress_calls_end_as_agent_stalled(tmp_path): + calls = [] + + def agent(run_dir, action_num, **kwargs): + calls.append((run_dir, action_num, kwargs)) + return {"meta": {"input_tokens": 1, "output_tokens": 1}} + + runner = InteractiveMcpGameRunner( + env=FakeEnv(initial=observation(state="NOT_FINISHED")), + game_id="stall", + agent_name="fake", + max_actions_per_game=20, + prompts_log_path=tmp_path / "stall" / "logs.txt", + agent=agent, + agent_retries=2, + ) + metrics = runner.run() + assert metrics.status == Status.AGENT_STALLED + assert len(calls) == 2 + assert calls[0][1] == calls[1][1] == 0 + assert "mcp_url" in calls[0][2] + assert "mcp_token" in calls[0][2] + + +def test_early_exit_resumes_same_live_session(tmp_path): + env = FakeEnv(initial=observation(state="NOT_FINISHED")) + seen = [] + + def agent(run_dir, action_num, **kwargs): + seen.append((run_dir, action_num, kwargs["mcp_url"], kwargs["mcp_token"])) + runner._session.execute_action({"name": "ACTION1", "data": {}}) + if len(seen) == 2: + runner._session.arc_state = type(runner._session.arc_state).WIN + return {"meta": {}} + + runner = InteractiveMcpGameRunner( + env=env, + game_id="resume", + agent_name="fake", + max_actions_per_game=10, + prompts_log_path=tmp_path / "resume" / "logs.txt", + agent=agent, + agent_retries=3, + ) + runner.run() + assert len(seen) == 2 + assert seen[0][0] == seen[1][0] + assert seen[0][2:] == seen[1][2:] + assert seen[1][1] == seen[0][1] + 1 diff --git a/research/arc-agi-3/tests/test_mcp_events.py b/research/arc-agi-3/tests/test_mcp_events.py new file mode 100644 index 0000000..6e6148d --- /dev/null +++ b/research/arc-agi-3/tests/test_mcp_events.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import io + +from prolong_agent.agent.claude_events import ClaudeEventParser +from prolong_agent.agent.codex_events import CodexEventParser + + +def test_codex_parser_records_mcp_tool_calls(): + parser = CodexEventParser(io.StringIO()) + parser.handle({ + "type": "item.started", + "item": { + "type": "mcp_tool_call", + "server": "prolong_game", + "tool": "current_board", + "arguments": {}, + }, + }) + assert parser.phase == parser.PHASE_TOOL_RUNNING + parser.handle({ + "type": "item.completed", + "item": { + "type": "mcp_tool_call", + "server": "prolong_game", + "tool": "current_board", + "result": {"state": "PLAYING"}, + }, + }) + assert parser.phase == parser.PHASE_POST_TOOL + assert parser.tool_calls[0][0] == "prolong_game.current_board" + + +def test_claude_parser_records_both_mcp_tools_as_progress(): + parser = ClaudeEventParser(io.StringIO()) + for name in ("mcp__prolong_game__current_board", "mcp__prolong_game__submit_actions"): + parser.handle({ + "type": "assistant", + "message": {"content": [{"type": "tool_use", "name": name, "input": {}}]}, + }) + assert parser.phase == parser.PHASE_TOOL_RUNNING + parser.handle({ + "type": "user", + "message": {"content": [{"type": "tool_result", "content": "ok"}]}, + }) + assert parser.phase == parser.PHASE_LLM + assert [name for name, _ in parser.tool_calls] == [ + "mcp__prolong_game__current_board", + "mcp__prolong_game__submit_actions", + ] diff --git a/research/arc-agi-3/tests/test_mcp_game.py b/research/arc-agi-3/tests/test_mcp_game.py new file mode 100644 index 0000000..2f250b6 --- /dev/null +++ b/research/arc-agi-3/tests/test_mcp_game.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import asyncio + +import httpx2 +import pytest +from mcp import ClientSession +from mcp.client.streamable_http import streamable_http_client +from mcp.server.mcpserver.exceptions import ToolError + +from conftest import FakeEnv, observation +from prolong_agent.environment.mcp_game import GameMcpServer + + +async def _mcp_call(server, tool, arguments): + async with httpx2.AsyncClient( + headers={"Authorization": f"Bearer {server.token}"}, + trust_env=False, + ) as http: + async with streamable_http_client(server.local_url, http_client=http) as streams: + async with ClientSession(*streams[:2]) as session: + await session.initialize() + return await session.call_tool(tool, arguments) + + +def test_mcp_requires_the_per_game_bearer_token(make_controller): + controller, _ = make_controller() + with GameMcpServer(controller) as server: + async def requests(): + async with httpx2.AsyncClient(trust_env=False) as client: + missing = await client.post(server.local_url, json={}) + wrong = await client.post( + server.local_url, + headers={"Authorization": "Bearer wrong"}, + json={}, + ) + return missing, wrong + + missing, wrong = asyncio.run(requests()) + assert missing.status_code == 401 + assert wrong.status_code == 401 + result = asyncio.run(_mcp_call(server, "current_board", {})) + assert result.structured_content["game_id"] == "test-game" + + +def test_parallel_game_servers_are_isolated(make_controller): + first, _ = make_controller(game_id="first") + second, _ = make_controller(game_id="second") + with GameMcpServer(first) as one, GameMcpServer(second) as two: + async def cross_token(): + async with httpx2.AsyncClient( + headers={"Authorization": f"Bearer {one.token}"}, trust_env=False + ) as client: + return await client.post(two.local_url, json={}) + + assert asyncio.run(cross_token()).status_code == 401 + assert asyncio.run(_mcp_call(one, "current_board", {})).structured_content["game_id"] == "first" + assert asyncio.run(_mcp_call(two, "current_board", {})).structured_content["game_id"] == "second" + + +def test_tool_schemas_and_current_board_shape(make_controller): + controller, _ = make_controller(max_actions=12) + with GameMcpServer(controller) as server: + async def schemas(): + async with httpx2.AsyncClient( + headers={"Authorization": f"Bearer {server.token}"}, trust_env=False + ) as http: + async with streamable_http_client(server.local_url, http_client=http) as streams: + async with ClientSession(*streams[:2]) as session: + await session.initialize() + return await session.list_tools() + + tools = {tool.name: tool for tool in asyncio.run(schemas()).tools} + assert set(tools) == {"current_board", "submit_actions"} + assert tools["current_board"].input_schema["properties"] == {} + assert tools["current_board"].input_schema["type"] == "object" + actions_schema = tools["submit_actions"].input_schema["properties"]["actions"] + assert actions_schema["type"] == "array" + assert actions_schema["minItems"] == 1 + assert actions_schema["maxItems"] == 20 + action_definition = next(iter(tools["submit_actions"].input_schema["$defs"].values())) + assert action_definition["additionalProperties"] is False + assert "plan" not in action_definition["properties"] + assert "rationale" not in action_definition["properties"] + board = server.current_board() + assert board["state"] == "PLAYING" + assert board["remaining_actions"] == 12 + assert board["board"] == ["00", "00"] + + +@pytest.mark.parametrize("bad", [ + [], + [{"action": "ACTION1"}] * 21, + [{"action": "NOPE"}], + [{"action": "ACTION4"}], + [{"action": "ACTION1", "plan": "because"}], + [{"action": "ACTION1", "x": 1}], + [{"action": "ACTION6"}], + [{"action": "ACTION6", "x": -1, "y": 2}], + [{"action": "ACTION6", "x": 1, "y": 64}], + [{"action": "ACTION6", "x": True, "y": 2}], +]) +def test_malformed_or_unavailable_batches_are_rejected_atomically(make_controller, bad): + controller, env = make_controller() + server = GameMcpServer(controller) + with pytest.raises(ToolError): + server.submit_actions(bad) + assert env.calls == [] + + +def test_entire_batch_is_validated_before_execution(make_controller): + controller, env = make_controller() + server = GameMcpServer(controller) + with pytest.raises(ToolError): + server.submit_actions([ + {"action": "ACTION1"}, + {"action": "ACTION6", "x": 100, "y": 1}, + ]) + assert env.calls == [] + + +def test_immediate_execution_and_score_change_flush(make_controller): + env = FakeEnv(scripted=[ + observation(score=0, fill=1), + observation(score=1, fill=2), + observation(score=1, fill=3), + ]) + controller, env = make_controller(env=env) + result = GameMcpServer(controller).submit_actions([ + {"action": "ACTION1"}, + {"action": "ACTION2"}, + {"action": "ACTION1"}, + ]) + assert result["submitted_count"] == 3 + assert result["executed_count"] == 2 + assert result["stop_reason"] == "score_changed" + assert result["observation"]["score"] == 1 + assert result["observation"]["board"] == ["22", "22"] + assert len(env.calls) == 2 + + +def test_win_stops_batch(make_controller): + env = FakeEnv(scripted=[observation(state="WIN", score=1)]) + controller, env = make_controller(env=env) + result = GameMcpServer(controller).submit_actions([ + {"action": "ACTION1"}, {"action": "ACTION2"} + ]) + assert result["executed_count"] == 1 + assert result["stop_reason"] == "win" + assert result["observation"]["state"] == "WIN" + + +def test_game_over_stops_and_automatically_resets(make_controller): + env = FakeEnv(scripted=[ + observation(state="GAME_OVER"), + observation(state="NOT_FINISHED", fill=4), + ]) + controller, env = make_controller(env=env) + result = GameMcpServer(controller).submit_actions([ + {"action": "ACTION1"}, {"action": "ACTION2"} + ]) + assert result["executed_count"] == 1 + assert result["automatic_actions"] == ["RESET"] + assert result["stop_reason"] == "game_over_reset" + assert result["observation"]["state"] == "PLAYING" + assert len(env.calls) == 2 + + +def test_action_budget_stops_batch(make_controller): + controller, env = make_controller(max_actions=2) + result = GameMcpServer(controller).submit_actions([ + {"action": "ACTION1"}, {"action": "ACTION2"}, {"action": "ACTION1"} + ]) + assert result["executed_count"] == 2 + assert result["stop_reason"] == "action_budget_exhausted" + assert result["observation"]["remaining_actions"] == 0 + assert len(env.calls) == 2 diff --git a/research/arc-agi-3/tests/test_memory_modes.py b/research/arc-agi-3/tests/test_memory_modes.py new file mode 100644 index 0000000..2a217cf --- /dev/null +++ b/research/arc-agi-3/tests/test_memory_modes.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import itertools + +import pytest + +from prolong_agent.agent.base import BaseAgent +from prolong_agent.agent.codex_agent import CodexAgent +from prolong_agent.agent.claude_code_agent import ClaudeCodeAgent +from prolong_agent.agent.memory import MemoryMode, resolve_memory_mode +from prolong_agent.agent.swarm import _parse_args + + +def test_memory_mode_resolution(): + assert resolve_memory_mode() == (MemoryMode.FULL_LOG, None) + assert resolve_memory_mode(log_window=4) == (MemoryMode.WINDOWED_LOG, 4) + assert resolve_memory_mode(in_prompt=True) == (MemoryMode.IN_PROMPT, None) + assert resolve_memory_mode(no_log=True) == (MemoryMode.MCP_NO_LOG, None) + + +def test_log_window_minus_one_is_deprecated_in_prompt_alias(): + with pytest.warns(FutureWarning, match="--in-prompt"): + assert resolve_memory_mode(log_window=-1) == (MemoryMode.IN_PROMPT, None) + + +@pytest.mark.parametrize( + "values", + [values for size in (2, 3) for values in itertools.combinations( + ("no_log", "in_prompt", "log_window"), size + )], +) +def test_conflicting_memory_flags_are_rejected(values): + kwargs = {"no_log": False, "in_prompt": False, "log_window": None} + for value in values: + kwargs[value] = 2 if value == "log_window" else True + with pytest.raises(ValueError, match="mutually exclusive"): + resolve_memory_mode(**kwargs) + + +def test_cli_exposes_distinct_flags(): + assert _parse_args(["--no-log"]).memory_mode == MemoryMode.MCP_NO_LOG + assert _parse_args(["--in-prompt"]).memory_mode == MemoryMode.IN_PROMPT + + +def test_no_log_prompts_are_state_free(): + agent = BaseAgent(memory_mode=MemoryMode.MCP_NO_LOG) + prompts = [ + agent._build_system_prompt(), + agent._build_prompt("secret.txt", True), + agent._build_prompt("secret.txt", False), + ] + joined = "\n".join(prompts) + assert "logs.txt" not in joined + assert "Score:" not in joined + assert "Action:" not in joined + assert "000000" not in joined + assert "secret.txt" not in joined + + +def test_interactive_workspace_removes_stale_logs_recursively(tmp_path): + nested = tmp_path / "old" / "run" + nested.mkdir(parents=True) + (tmp_path / "logs.txt").write_text("state") + (nested / "logs.txt").write_text("state") + (tmp_path / "current_board.txt").write_text("board") + (nested / "current_board.txt").write_text("board") + (tmp_path / "actions.json").write_text("{}") + + BaseAgent._purge_interactive_game_artifacts(tmp_path) + + assert not list(tmp_path.rglob("logs.txt")) + assert not (tmp_path / "current_board.txt").exists() + assert not list(tmp_path.rglob("current_board.txt")) + assert not (tmp_path / "actions.json").exists() + + +def test_codex_mcp_config_retains_user_config_isolation(tmp_path): + agent = CodexAgent( + codex_home=str(tmp_path / "codex-home"), + memory_mode=MemoryMode.MCP_NO_LOG, + ) + args = agent._build_codex_args( + "state-free prompt", + True, + None, + mcp_url="http://host.docker.internal:1234/mcp", + ) + assert "--ignore-user-config" in args + assert any("mcp_servers.prolong_game.url" in arg for arg in args) + assert any("bearer_token_env_var" in arg for arg in args) + + +def test_claude_mcp_config_exposes_only_the_game_server(): + config = ClaudeCodeAgent._mcp_config("http://host.docker.internal:1234/mcp") + assert set(config["mcpServers"]) == {"prolong_game"} + game = config["mcpServers"]["prolong_game"] + assert game["type"] == "http" + assert game["headers"]["Authorization"] == "Bearer ${PROLONG_MCP_TOKEN}" diff --git a/research/arc-agi-3/tests/test_queue_regression.py b/research/arc-agi-3/tests/test_queue_regression.py new file mode 100644 index 0000000..24bc9d8 --- /dev/null +++ b/research/arc-agi-3/tests/test_queue_regression.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from conftest import FakeEnv, observation +from prolong_agent.agent.action_queue import ActionQueue +from prolong_agent.environment.runner import GameRunner +from prolong_agent.metrics.structures import Status + + +def test_existing_queue_flushes_after_score_change(): + queue = ActionQueue() + assert queue.load([ + {"name": "ACTION1", "data": {}}, + {"name": "ACTION2", "data": {}}, + ]) + queue.check_score(1) + assert not queue + assert queue.score_changed + + +def test_queued_runner_still_generates_metrics_and_private_trace(tmp_path): + env = FakeEnv(scripted=[observation(state="WIN", score=1, fill=5)]) + + def agent(log_path, action_num, **kwargs): + assert log_path.name == "logs.txt" + return { + "hint": "test", + "plan": "take one action", + "actions": [{"name": "ACTION1", "data": {}}], + "meta": { + "output": "test", + "input_tokens": 3, + "output_tokens": 2, + "cached_tokens": 0, + "call_cost_usd": 0.01, + "model": "fake", + }, + } + + trace = tmp_path / "queued" / "logs.txt" + metrics = GameRunner( + env=env, + game_id="queued", + agent_name="fake", + max_actions_per_game=5, + prompts_log_path=trace, + agent=agent, + log_post_board=True, + ).run() + + assert metrics.status == Status.COMPLETED_RUN + assert metrics.run_total_actions == 1 + assert metrics.final_score == 1 + assert trace.exists() + trace_text = trace.read_text() + assert "INITIAL BOARD STATE" in trace_text + assert "Tool Call: ACTION1" in trace_text + assert "POST-ACTION BOARD STATE" in trace_text diff --git a/research/arc-agi-3/uv.lock b/research/arc-agi-3/uv.lock index 0e4adf5..926e361 100644 --- a/research/arc-agi-3/uv.lock +++ b/research/arc-agi-3/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] [[package]] name = "annotated-types" @@ -11,6 +15,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "arc-agi" version = "0.9.2" @@ -42,6 +59,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/ce/efc5dcb66cacfe4c6ada2ba07ed7a6d9971110c9fd6d540793ed6748a12c/arcengine-0.9.3-py3-none-any.whl", hash = "sha256:5f9739d6d0055780a4581fd6fe09066bb08775c4c8212c9adcca2eb008aef59c", size = 38374, upload-time = "2026-01-29T03:06:30.561Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "blinker" version = "1.9.0" @@ -60,6 +86,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -204,6 +315,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, ] +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -271,13 +432,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "idna" -version = "3.11" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -310,6 +519,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -499,6 +735,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, ] +[[package]] +name = "mcp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx2" }, + { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, +] + [[package]] name = "numpy" version = "2.4.2" @@ -560,6 +834,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -654,6 +940,7 @@ source = { editable = "." } dependencies = [ { name = "arc-agi" }, { name = "arcengine" }, + { name = "mcp" }, { name = "python-dotenv" }, { name = "requests" }, ] @@ -668,6 +955,7 @@ dev = [ requires-dist = [ { name = "arc-agi", specifier = ">=0.9.1" }, { name = "arcengine", specifier = ">=0.9.3" }, + { name = "mcp", specifier = ">=2.0,<3" }, { name = "pytest", marker = "extra == 'dev'" }, { name = "python-dotenv", specifier = ">=1.0" }, { name = "requests", specifier = ">=2.31" }, @@ -675,6 +963,15 @@ requires-dist = [ ] provides-extras = ["dev"] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -770,6 +1067,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -816,6 +1127,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://files.pythonhosted.org/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + [[package]] name = "requests" version = "2.32.5" @@ -831,6 +1184,102 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, +] + [[package]] name = "ruff" version = "0.15.4" @@ -865,6 +1314,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sse-starlette" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -895,6 +1379,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "uvicorn" +version = "0.52.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, +] + [[package]] name = "werkzeug" version = "3.1.6" From 05ec91b1bf1bf9d6f711081e65a02fa907a5c7ba Mon Sep 17 00:00:00 2001 From: IsThatYou Date: Fri, 21 Aug 2026 16:48:25 -0700 Subject: [PATCH 2/2] test: tighten MCP validation and smoke coverage --- .../prolong_agent/environment/mcp_game.py | 94 +++++++++---------- .../arc-agi-3/tests/test_container_mcp.py | 50 ++++++++-- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/research/arc-agi-3/prolong_agent/environment/mcp_game.py b/research/arc-agi-3/prolong_agent/environment/mcp_game.py index 1d99c34..95e25e4 100644 --- a/research/arc-agi-3/prolong_agent/environment/mcp_game.py +++ b/research/arc-agi-3/prolong_agent/environment/mcp_game.py @@ -2,7 +2,6 @@ from __future__ import annotations -import logging import secrets import socket import threading @@ -13,25 +12,25 @@ from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.exceptions import ToolError from mcp.server.transport_security import TransportSecuritySettings -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator from prolong_agent.environment.game_session import GameSessionController -log = logging.getLogger(__name__) - MAX_BATCH_ACTIONS = 20 -VALID_ACTIONS = { - "ACTION1", "ACTION2", "ACTION3", "ACTION4", - "ACTION5", "ACTION6", "ACTION7", "RESET", -} class SubmittedAction(BaseModel): model_config = ConfigDict(extra="forbid") action: Literal[ - "ACTION1", "ACTION2", "ACTION3", "ACTION4", - "ACTION5", "ACTION6", "ACTION7", "RESET", + "ACTION1", + "ACTION2", + "ACTION3", + "ACTION4", + "ACTION5", + "ACTION6", + "ACTION7", + "RESET", ] x: Annotated[int, Field(strict=True, ge=0, le=63)] | None = None y: Annotated[int, Field(strict=True, ge=0, le=63)] | None = None @@ -56,15 +55,22 @@ async def __call__(self, scope: dict, receive: Any, send: Any) -> None: headers = dict(scope.get("headers", [])) supplied = headers.get(b"authorization", b"") if not secrets.compare_digest(supplied, self._expected): - await send({ - "type": "http.response.start", - "status": 401, - "headers": [(b"content-type", b"application/json")], - }) - await send({ - "type": "http.response.body", - "body": b'{"error":"unauthorized"}', - }) + await send( + { + "type": "http.response.start", + "status": 401, + "headers": [ + (b"content-type", b"application/json"), + (b"www-authenticate", b"Bearer"), + ], + } + ) + await send( + { + "type": "http.response.body", + "body": b'{"error":"unauthorized"}', + } + ) return await self.app(scope, receive, send) @@ -112,15 +118,15 @@ def submit_actions( ], ) -> dict[str, Any]: """Validate and immediately execute a batch of one to twenty actions.""" - return self.submit_actions([ - action.model_dump(exclude_none=True) for action in actions - ]) + return self.submit_actions(actions) def current_board(self) -> dict[str, Any]: with self._lock: return self.controller.observation - def submit_actions(self, actions: list[dict[str, Any]]) -> dict[str, Any]: + def submit_actions( + self, actions: list[SubmittedAction | dict[str, Any]] + ) -> dict[str, Any]: with self._lock: validated = self._validate_batch(actions) submitted = len(validated) @@ -153,11 +159,13 @@ def submit_actions(self, actions: list[dict[str, Any]]) -> dict[str, Any]: "data": {}, "plan_step": "automatic", } - reset["action_metadata"] = self.controller.build_action_metadata( - reset, - output="MCP automatic RESET after GAME_OVER", - step=1, - total=1, + reset["action_metadata"] = ( + self.controller.build_action_metadata( + reset, + output="MCP automatic RESET after GAME_OVER", + step=1, + total=1, + ) ) self.controller.execute_action(reset) automatic_actions.append("RESET") @@ -187,30 +195,22 @@ def _validate_batch(self, actions: Any) -> list[dict[str, Any]]: available = set(self.controller.available_action_names) validated: list[dict[str, Any]] = [] for index, entry in enumerate(actions): - if not isinstance(entry, dict): - raise ToolError(f"actions[{index}] must be an object") - extra = set(entry) - {"action", "x", "y"} - if extra: - raise ToolError( - f"actions[{index}] contains unsupported fields: {', '.join(sorted(extra))}" + try: + parsed = ( + entry + if isinstance(entry, SubmittedAction) + else SubmittedAction.model_validate(entry) ) - name = entry.get("action") - if not isinstance(name, str) or name not in VALID_ACTIONS: - raise ToolError(f"actions[{index}].action is invalid") + except ValidationError as exc: + message = exc.errors(include_url=False)[0]["msg"] + raise ToolError(f"actions[{index}] is invalid: {message}") from exc + + name = parsed.action if name not in available: raise ToolError(f"actions[{index}].action {name} is unavailable") if name == "ACTION6": - if set(entry) != {"action", "x", "y"}: - raise ToolError(f"actions[{index}] ACTION6 requires x and y") - x, y = entry["x"], entry["y"] - if isinstance(x, bool) or isinstance(y, bool) or not isinstance(x, int) or not isinstance(y, int): - raise ToolError(f"actions[{index}] x and y must be integers") - if not 0 <= x <= 63 or not 0 <= y <= 63: - raise ToolError(f"actions[{index}] coordinates must be between 0 and 63") - validated.append({"name": name, "data": {"x": x, "y": y}}) + validated.append({"name": name, "data": {"x": parsed.x, "y": parsed.y}}) else: - if set(entry) != {"action"}: - raise ToolError(f"actions[{index}] {name} does not accept coordinates") validated.append({"name": name, "data": {}}) return validated diff --git a/research/arc-agi-3/tests/test_container_mcp.py b/research/arc-agi-3/tests/test_container_mcp.py index 91c86d5..8e6e9d3 100644 --- a/research/arc-agi-3/tests/test_container_mcp.py +++ b/research/arc-agi-3/tests/test_container_mcp.py @@ -2,7 +2,9 @@ from __future__ import annotations +import json import os +import shutil import subprocess import uuid @@ -22,6 +24,8 @@ def test_agent_container_reaches_short_lived_mcp(image, make_controller): if os.environ.get("RUN_CONTAINER_SMOKE") != "1": pytest.skip("set RUN_CONTAINER_SMOKE=1 to run Docker connectivity tests") + if shutil.which("docker") is None: + pytest.skip("Docker is not installed") if subprocess.run( ["docker", "image", "inspect", image], capture_output=True ).returncode: @@ -34,17 +38,49 @@ def test_agent_container_reaches_short_lived_mcp(image, make_controller): capture_output=True, ) controller, _ = make_controller(game_id=image.split("/")[-1].split(":")[0]) + initialize_request = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": {"name": "container-smoke", "version": "1.0"}, + }, + } + ) try: with GameMcpServer(controller) as server: result = subprocess.run( [ - "docker", "run", "--rm", - "--network", network, - "--add-host", "host.docker.internal:host-gateway", - "--entrypoint", "curl", + "docker", + "run", + "--rm", + "--network", + network, + "--add-host", + "host.docker.internal:host-gateway", + "--entrypoint", + "curl", image, - "-sS", "-o", "/dev/null", "-w", "%{http_code}", - "-H", f"Authorization: Bearer {server.token}", + "-sS", + "--max-time", + "10", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "-X", + "POST", + "-H", + f"Authorization: Bearer {server.token}", + "-H", + "Content-Type: application/json", + "-H", + "Accept: application/json, text/event-stream", + "--data-binary", + initialize_request, server.container_url, ], capture_output=True, @@ -52,7 +88,7 @@ def test_agent_container_reaches_short_lived_mcp(image, make_controller): timeout=30, ) assert result.returncode == 0, result.stderr - assert result.stdout not in {"000", "401"} + assert result.stdout == "200" finally: subprocess.run( ["docker", "network", "rm", network], capture_output=True, timeout=10