From 5ad745ee6e324848eeeefffd74e9248e015cc773 Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 14:22:16 -0400 Subject: [PATCH 01/22] Runtime worker recipe draft: openhands on GPT-6 through OmniRoute (pinned install, gateway FW rules, token MCP wiring, skills, frozen E2E) Written by GPT-6 (gpt-6-astra, effort max) through the Codex stack-worker profile on the OmniRoute lane from the coordinator's brief; tests red then green; no host install or live run yet. Draft pending the Claude cross-family review, the upstream eval-harness mapping and the skills-lifecycle wiring. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 373 +++ .../openhands/build-requirements.lock | 3 + .../openhands/config/host.example.json | 19 + .../openhands/config/mcp-policy.json | 24 + .../openhands/config/mcp.template.json | 119 + .../openhands/config/serena_config.yml | 9 + .../openhands/config/skills.lock.json | 173 + .../openhands/config/worker.json | 46 + .../runtime-workers/openhands/e2e/check.py | 135 + .../openhands/e2e/fixture-repo/README.md | 14 + .../openhands/e2e/fixture-repo/range_utils.py | 5 + .../e2e/fixture-repo/tests/test_ranges.py | 18 + .../runtime-workers/openhands/e2e/frozen.json | 14 + .../runtime-workers/openhands/e2e/task.txt | 10 + .../openhands/evidence/expanded-tests.txt | 30 + .../openhands/evidence/fail-first.txt | 106 + .../openhands/evidence/publication-first.txt | 2 + .../openhands/evidence/publication-pass.txt | 5 + .../openhands/evidence/receipt-red.txt | 67 + .../openhands/evidence/serena-red.txt | 31 + .../openhands/evidence/unit-pass.txt | 24 + .../openhands/evidence/verification.md | 57 + .../evidence/wheel-source-comparison.json | 16 + blueprints/runtime-workers/openhands/host.py | 320 ++ .../openhands/install-container.sh | 21 + .../runtime-workers/openhands/install.sh | 10 + .../runtime-workers/openhands/mcp_guard.py | 38 + .../runtime-workers/openhands/pins.json | 43 + .../runtime-workers/openhands/qmd-setup.py | 24 + .../runtime-workers/openhands/receipt.py | 180 ++ .../runtime-workers/openhands/recipe.py | 99 + .../openhands/requirements.lock | 2860 +++++++++++++++++ .../runtime-workers/openhands/research.md | 57 + .../runtime-workers/openhands/run-e2e.sh | 8 + .../runtime-workers/openhands/worker.py | 134 + tests/test_runtime_worker_openhands.py | 296 ++ 36 files changed, 5390 insertions(+) create mode 100644 blueprints/runtime-workers/openhands/README.md create mode 100644 blueprints/runtime-workers/openhands/build-requirements.lock create mode 100644 blueprints/runtime-workers/openhands/config/host.example.json create mode 100644 blueprints/runtime-workers/openhands/config/mcp-policy.json create mode 100644 blueprints/runtime-workers/openhands/config/mcp.template.json create mode 100644 blueprints/runtime-workers/openhands/config/serena_config.yml create mode 100644 blueprints/runtime-workers/openhands/config/skills.lock.json create mode 100644 blueprints/runtime-workers/openhands/config/worker.json create mode 100644 blueprints/runtime-workers/openhands/e2e/check.py create mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md create mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py create mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py create mode 100644 blueprints/runtime-workers/openhands/e2e/frozen.json create mode 100644 blueprints/runtime-workers/openhands/e2e/task.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/expanded-tests.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/fail-first.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/publication-first.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/publication-pass.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/receipt-red.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/serena-red.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/unit-pass.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/verification.md create mode 100644 blueprints/runtime-workers/openhands/evidence/wheel-source-comparison.json create mode 100644 blueprints/runtime-workers/openhands/host.py create mode 100755 blueprints/runtime-workers/openhands/install-container.sh create mode 100755 blueprints/runtime-workers/openhands/install.sh create mode 100644 blueprints/runtime-workers/openhands/mcp_guard.py create mode 100644 blueprints/runtime-workers/openhands/pins.json create mode 100644 blueprints/runtime-workers/openhands/qmd-setup.py create mode 100644 blueprints/runtime-workers/openhands/receipt.py create mode 100644 blueprints/runtime-workers/openhands/recipe.py create mode 100644 blueprints/runtime-workers/openhands/requirements.lock create mode 100644 blueprints/runtime-workers/openhands/research.md create mode 100755 blueprints/runtime-workers/openhands/run-e2e.sh create mode 100644 blueprints/runtime-workers/openhands/worker.py create mode 100644 tests/test_runtime_worker_openhands.py diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md new file mode 100644 index 000000000..63889338c --- /dev/null +++ b/blueprints/runtime-workers/openhands/README.md @@ -0,0 +1,373 @@ +# OpenHands coding runtime worker + +This is a **recipe awaiting host execution** for OpenHands Software Agent SDK +1.49.6, powered by the local OmniRoute gateway. The SDK owns the agent loop, +tools, conversations, skills and context condensation. The worker repairs one +frozen Python fixture in an owned copy. It never receives this repository as its +coding workspace. This addition does not change the foundation catalog's adoption +status, install software, start services, pull an image, or perform a model run. + +## Pinned sources and artifacts + +The primary is [OpenHands/software-agent-sdk v1.49.6][release], commit +`fcc102a697874d54a357e36004e02c95040dbdc0`. [pins.json](pins.json) contains full +URLs and hashes. Source/archive and lock hashes were computed from returned +upstream bytes; wheel hashes came from version-specific PyPI release metadata +and were independently checked against downloaded wheel bytes. GHCR index, +platform manifest and config hashes were computed over registry responses. The +config's `OPENHANDS_BUILD_GIT_SHA` and `OPENHANDS_BUILD_GIT_REF` match this tag. + +| Artifact | SHA256 | +| --- | --- | +| Commit-addressed source tarball | `2d3006de86eaec690a82e497fcd4459816637afe6e53f7da87783183792148a8` | +| Upstream `uv.lock` | `d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910` | +| Published agent-server `1.49.6-python` multiarchitecture index | `02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37` | +| Selected Linux amd64 image manifest | `ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12` | +| Linux amd64 image config | `39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f` | +| `openhands_sdk-1.49.6-py3-none-any.whl` | `3b4701f125925e804929ab02b61fe2fe10b4fb1e7d723a2c4afdea35566328bb` | +| `openhands_tools-1.49.6-py3-none-any.whl` | `11e57a6635fd93c9f1fed14f804ab6f24a4873f75a2d35f0d1c3e57df2c51c87` | + +The image reference used by every container invocation is +`ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37`, +with `--platform linux/amd64`. Docker verifies its content-addressed blobs; the +installer also checks the resolved image configuration ID. The pinned image +includes Python 3.13.15, Node 24.21.0 and uv 0.12.15. These facts come from the +[Dockerfile language runtime, lines 39–47][docker-runtime], [uv copy, lines +402–403][docker-uv], and the returned image metadata. No image was pulled here. + +All 295 comparable SDK Python files and 93 tools Python files in the downloaded +wheels matched their corresponding files at the pinned commit. The compact +[byte-comparison record](evidence/wheel-source-comparison.json) is artifact +evidence, not execution. + +## Supported installation paths and the choice + +Upstream's [README, lines 49–81][sdk-readme] shows the standalone SDK API and +directs source development to `make build`. Its [Makefile, lines 34–41][makefile] +runs `uv sync --dev` and installs Git hooks. The [container source build, lines +128–140][docker-sync] copies the four workspace packages and root lockfile, then +uses `uv sync --frozen --no-editable`. The [official getting-started +guide][getting-started], read 2026-09-27, documents the package path +`pip install -U openhands-sdk openhands-tools` and optional workspace/server +packages. This recipe pins those two published packages and their dependencies. + +The selected path runs the standalone SDK **inside the pinned agent-server +image**, overriding its entrypoint. There is no agent-server HTTP listener, port +publication or host workspace execution. The agent loop, LLM calls and every +stdio MCP subprocess run in that container; HTTP MCP calls originate there too. +Host code only provisions owned files, invokes rootless Docker, copies fixture +bytes, supervises deadlines and reads the permitted gateway log columns. + +This choice addresses two details found at the tag: + +- `DockerWorkspace` is a `RemoteWorkspace`; the [remote example, lines + 92–97][remote-example] explicitly asserts `RemoteConversation`. Creating that + object from host Python does not keep the LLM loop on the host. Its [native + launch, lines 218–251][docker-workspace] publishes a port without an explicit + host IP. The chosen listener-free path needs no port from the 3700–3799 range. +- The published `python` image uses the **binary** target, according to the + [release matrix, lines 281–339][image-matrix]. Its entrypoint launches a + PyInstaller server executable. The source venv exists only in the [separate + source targets, lines 564–598][image-targets]. Therefore the installer creates + its own importable SDK venv inside the image, in a mounted owned prefix. + +[install.sh](install.sh) is idempotent and uses `set -euo pipefail`, `umask 077`, +no sudo and the existing `rootless` Docker context. It verifies prerequisites, +private host inputs, installed skill hashes and requirement-file hashes before +download/install. It verifies the source archive and `uv.lock` SHA256, pulls only +the digest pin, and invokes [install-container.sh](install-container.sh). + +The prefix is `$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6`; durable +private state is `$HOME/.local/state/native-agent-stack/runtime-workers/openhands/`. +Owned directories are mode 0700 and files inherit the restrictive umask. The +venv is for the image's interpreter, so its Python symlink may not resolve on the +host. Install logs are retained in separate `install-attempts/attempt-*` folders. +Docker's content store remains owned by the separately provisioned rootless +daemon; temporary container files disappear with that container. + +[requirements.lock](requirements.lock) is an unchanged dependency export from +the upstream lock, with only the two published workspace-package wheel references +and their hashes appended. It was produced using uv 0.12.17: + +```sh +uv export --locked --no-dev --package openhands-tools --no-emit-workspace \ + --no-editable --no-annotate --no-header --format requirements-txt \ + --output-file requirements-dependencies.lock >/dev/null +``` + +An ordinary isolated source build could fetch `setuptools>=83` outside that +lock ([SDK pyproject, lines 41–43][sdk-pyproject]). Conversely, requiring all +binary dependencies would fail: locked `func-timeout==4.3.5` has only a source +distribution. [build-requirements.lock](build-requirements.lock) therefore +bootstraps setuptools 80.9.0, wheel 0.46.3 and packaging 26.3 from hashes in the +same upstream lock. `uv pip install --require-hashes --no-deps +--no-build-isolation` then uses the complete exported requirements. There is no +unlocked dependency or build-dependency resolution. A missing build prerequisite +fails visibly. This follows [uv's documented build-isolation escape hatch][uv-build]. +`uv pip check` and SDK/tool imports are installation checks, never a model trial. + +## Model, headers and native context features + +[config/worker.json](config/worker.json) retains the requested host-side reference +URL `http://127.0.0.1:20128/v1`. [recipe.py](recipe.py) renders +`http://10.0.2.2:20128/v1` for the actual container-side LLM calls, following the +coordinator's measured rootless networking facts. No gateway configuration is +changed. The only API-key value is the keyless placeholder `local-loopback`. + +| Setting | Upstream key and source at the SDK commit | Recipe behavior | +| --- | --- | --- | +| Model and gateway | `LLM.model`, `base_url`, `api_key`; [standalone example][hello] | Default `cx/gpt-6-astra-max`; override with `OPENHANDS_MODEL`. The LiteLLM `openai/` provider prefix is added by the driver and stripped before the gateway request. | +| Responses endpoint | `LLM.api_mode`, `model_canonical_name`, `capability_overrides`; [llm.py:408–439][llm-config] | Explicit `responses` and canonical `openai/gpt-6`; reasoning and Responses capabilities are set explicitly for the alias. | +| Maximum effort | `LLM.reasoning_effort`; [llm.py:537–549][llm-effort], [responses_options.py:58–86][responses-options] | Send `max` on Responses; gateway alias suffix also pins max. Never send `auto`. | +| Sampling/cache safety | `LLM.temperature`; [llm.py:369–380][llm-config] | Omit temperature entirely, including condenser calls. The legacy exact semantic cache is not relied on. | +| Affinity and idempotency | `LLM.extra_headers`; [llm.py:442–445][llm-config]; public [generate/agenerate:1580–1643][llm-generate] | One private `x-omniroute-session` per conversation, shared by agent/condenser; fresh `Idempotency-Key` per logical SDK call. Native retries retain that logical call's key. | +| Streaming | `LLM.stream`, `Conversation.token_callbacks`; [llm.py:464–470][llm-config], [Conversation signature][conversation] | Agent streaming enabled with a token callback. Native condenser disables ordinary streaming because it consumes a whole summary; [condenser:83–95][condenser]. Temperature is absent so the low-temperature streaming condition does not apply to summaries. | +| Context summary | `Agent.condenser=LLMSummarizingCondenser`; [example 14][condenser-example], [condenser:48–78,99–154][condenser] | `max_size=80` events, `keep_first=2`, `max_tokens=60000`; preserve initial task/system context and summarize older events. This is an input-pressure threshold, not a total-spend cap. | +| Output and run bounds | `LLM.max_output_tokens`, `num_retries`, `timeout`; [llm.py:340–407][llm-config]; `Conversation.max_iteration_per_run` [conversation][conversation] | 16,384 output tokens, 2 retries, 180-second request timeout; 40 agent iterations and a 1,200-second container deadline. | +| Terminal output recovery | `TerminalObservation.to_llm_content`, `MAX_CMD_OUTPUT_SIZE`, `full_output_save_dir`; [definition:175–200,320–330][terminal], [constants:18–21][terminal-constants] | Native 30,000-character truncation saves full output in the conversation's private observation persistence directory. | + +The tiny header adapter subclasses `LLM` only at its public generation methods. +It keeps the upstream agent loop, serialization, retry implementation and +condenser. Upstream [model_copy:783–798][llm-copy] preserves the subclass, and the +condenser calls `generate`/`agenerate` at [lines 229 and 423][condenser]. This +behavior was source-reviewed; actual gateway headers still need host observation. + +The upstream [GPT-6 model feature note:190–200][model-features] warns about tools +plus reasoning effort on chat completions. The coordinator measured that this +gateway handles that combination on 2026-09-27; the recipe nevertheless uses the +supported Responses path. Switching to chat is not an automatic fallback. A +future chat-only adaptation must rely on the max alias and omit request effort. +Model currency is configurable, not claimed to be permanently settled. + +No gateway compression, dedup, semantic-cache or contextBudget changes are made. +There is no OmniRoute embedder. The task's small size may never trigger the +condenser; configured condensation and observed condensation are separate claims. +The SDK declares `LLM.max_message_chars`, but source inspection found no active +consumer of that field in this standalone generation path. It is therefore not +used as a claimed universal MCP-output cap. The terminal's real truncation/offload, +Serena's answer limit, bounded retrieval and condenser provide the containment. + +## MCP and skills + +Launch commands are derived from this checkout's +[base Codex template](../../../adoption/templates/codex.config.template.toml) and +[stack-worker limits](../../../adoption/templates/codex.stack-worker.config.toml). +Those files intentionally omit project-scoped jCodeMunch; its command and +`route/menu/order` front door come from the +[project template](../../../adoption/templates/project.codex.config.template.toml). +No command was guessed. [config/mcp.template.json](config/mcp.template.json) uses +the SDK's `mcp_config` and [MCPServer transport/command/args/env/cwd/url fields, +lines 497–530][mcp-config]. All stdio servers start in `/workspace`. + +The SDK does not implement Codex `enabled_tools`, `disabled_tools` or approval +keys in MCPServer; imported unknown fields are dropped at [lines 626–664][mcp-config]. +[recipe.py](recipe.py) therefore translates [mcp-policy.json](config/mcp-policy.json) +into native `Agent.filter_tools_regex`, applied both at initialization and MCP +tool updates ([agent/base.py:153–158,565–568,960–978][agent-base]). Locked FastMCP +3.2.0 uses one underscore, `server_name_tool_name`, for multi-server configs +([transport][fastmcp-config], [namespace implementation][fastmcp-namespace]). +The worker refuses a singleton config rather than silently losing those prefixes. + +| Server | Scope / allowed tools | +| --- | --- | +| context-mode | Native pinned Node command; `CONTEXT_MODE_PROJECT_DIR=/workspace`; `ctx_upgrade` and `ctx_purge` excluded. | +| serena | Native `start-mcp-server --transport stdio --context codex`, adapted to supported `--project /workspace` because the fixture has no Git metadata. Dashboard/GUI disabled. `SERENA_HOME=/state/mcp/serena/home`; metadata lives outside the fixture using `project_serena_folder_location`. | +| jcodemunch | Native `jcodemunch-mcp`; only `route`, `menu`, `order`; private `CODE_INDEX_PATH=/state/mcp/jcodemunch`; savings sharing disabled. This front door is not a read-only sandbox, as the project template notes. | +| qmd | Native `qmd --index native-agent-stack-catalog mcp`; only `query`, `get`, `multi_get`, `status`. Fresh owned index contains exactly `foundation-docs`, `foundation-adoption`, `us-equities-foundation`, `us-equities-catalog`. | +| ai-memory | HTTP `http://10.0.2.2:49474/mcp`; only `memory_query`, `memory_read_page`, `memory_recent`, `memory_status`, `memory_briefing`. | +| socraticode | Native pinned Node command; only `codebase_search`, `codebase_status`, `codebase_list_projects`, `codebase_health`; `SOCRATICODE_WATCHER=manual`. Local embedding endpoint is `10.0.2.2:18232/v1`, never OmniRoute. | +| headroom | Configured but disabled. Its conditional allowlist is exactly `headroom_compress`, `headroom_retrieve`, `headroom_stats`. Native output offload and condensation already contain this task's output; no remaining gap justifies another layer. | + +Serena's pinned source explains why cwd inference fails on a plain fixture +([cli.py:369–383][serena-cli]), its actual `SERENA_HOME` key +([config:65–78][serena-home]) and the external project metadata template +([config:928–936][serena-project]). The native loader fills missing defaults and +requires `projects: []` ([config:1050–1104][serena-loader]). The private copy of +[serena_config.yml](config/serena_config.yml) also bounds default answers to +30,000 characters. No host Serena configuration is changed. + +QMD uses [QMD_CONFIG_DIR][qmd-config] and [INDEX_PATH][qmd-store] for attempt-owned +state. [qmd-setup.py](qmd-setup.py) executes native [collection add and update][qmd-cli] +against only the four mounted document directories. A native SDK +[PreToolUse hook][hooks] requires explicit `collections`, typed lexical +`searches` and `rerank:false` for QMD query calls, following the +[v2.8.3 MCP schema][qmd-schema]. Retrieval therefore needs no embedding/model +download. The model only receives useful excerpts, not the full catalog. + +The 28 skills in [config/skills.lock.json](config/skills.lock.json) retain the +revisions and SKILL.md hashes from +[adoption/skills/manifest.json](../../../adoption/skills/manifest.json). +Installation verifies `$HOME/.agents/skills//SKILL.md` and creates prefix +symlinks to the existing directories. Containers mount each resolved directory +read-only at `/skills/`; no skill content is copied or edited. The driver +uses native `load_skills_from_dir` and `AgentContext.skills`, as in the +[loading-skills example][skills-example]. SDK [skill loading:848–903][skills-loader] +and [InvokeSkillTool][invoke-skill] preserve progressive disclosure and supporting +resources. All pinned names must be discovered or startup fails. Public skill +auto-fetch, unrelated home/project skill discovery and built-in memory are off; +the configured ai-memory read tools remain available. This keeps source pins +and context scope explicit. + +## Coordinator execution and lifecycle + +Fill [config/host.example.json](config/host.example.json) into a private file +outside every checkout, owned by the current user with mode 0600. It contains +paths and local service addresses, **no secrets**. `mcp_readonly_mounts` must +include each existing executable/dependency directory needed by the template +commands, at its original absolute path so wrappers, symlinks and shebangs work. +Supply a container-compatible `HOST_PATH`; do not mount a whole home directory, +authentication directory, Docker socket or unrelated checkout. Scope each QMD +mount to the named document directory. The recipe starts no production service. + +After review, on the coordinator's host: + +```sh +export OPENHANDS_HOST_FILE=/absolute/private/openhands-host.json +bash blueprints/runtime-workers/openhands/install.sh +# Optional model-currency override; otherwise the JSON default is used. +OPENHANDS_MODEL=cx/gpt-6-astra-max bash blueprints/runtime-workers/openhands/run-e2e.sh +``` + +Installation and runs use container UID 0 **inside rootless Docker**, which maps +owned bind-mount writes to the calling host user. They drop all capabilities, +set no-new-privileges, use a read-only image filesystem and temporary private +`/root`/`/tmp` mounts. The worker gets only its writable fixture, private +conversation output and MCP state; recipes, venv, skills and document sources +are read-only. The Docker socket is never mounted. Agent containers have 4 CPU, +8 GiB and 384 PID limits. SDK and model execution cannot fall back to the host. + +Every E2E gets a new `runs/attempt-*` directory; failed attempts remain. Normal +exit, timeout, SIGINT and SIGTERM attempt removal of the exact named container, +then record independent absence/removal status. Cleanup errors preserve the +primary exit code and fail the evidence-complete gate. All owned containers +carry `native-agent-stack.runtime-worker=openhands`; after an uncatchable host +crash, inspect that label and remove only the specific abandoned attempt. +Hard-kill/reboot recovery has not been qualified. Resume means a new attempt +from the frozen fixture; no interrupted trial is silently relabelled successful. +Rollback removes only the owned prefix/state when no labelled container uses +them. Shared rootless Docker, gateway, MCP installations and native sign-ins are +not modified or uninstalled. + +## Frozen task, checker and receipts + +[e2e/fixture-repo](e2e/fixture-repo/) contains one intentionally failing unittest, +a README spec and `range_utils.py`. [frozen.json](e2e/frozen.json) fixes all initial +file hashes, the one-test count, test command and sole allowed changed file. +The initial contract test was written and run before this recipe existed: +**exit 1, six tests, two failures and four missing-file errors**; the sanitized +returned output is [evidence/fail-first.txt](evidence/fail-first.txt). + +`run-e2e.sh` first verifies the failing baseline inside the pinned image. The +worker must invoke `tdd`, run the unchanged failing test before editing, repair +the implementation and rerun the test. The callback retains native ActionEvent +and ObservationEvent fields with fixture byte inventories. [check.py](e2e/check.py) +runs in a separate container with **network disabled** and read-only inputs. It +requires a genuine failing terminal observation while the fixture was unchanged, +an allowed nonempty diff, unchanged test/README bytes, and a fresh passing run of +exactly one test with no skipped/expected-failure cases. Empty, incomplete, +extra-file, symlink, edited-test and wrong-answer results fail. This is an oracle +for a cooperative coding task, not a security proof against a worker deliberately +tampering with its own process or Python's test machinery. +The local checker follows CPython 3.13.15's [unittest runner][unittest-runner] +and [test-result exit handling][unittest-main]; it additionally enforces the +frozen file and test-count contract. + +[receipt.py](receipt.py) reads the live +`$HOME/.local/share/omniroute/storage.sqlite` with SQLite URI `mode=ro` and +`query_only`, following the [official read-only URI example][sqlite-readonly]. +Its only SELECT accesses `call_logs` columns `timestamp`, `path`, +`status`, `model`, `reasoning_effort_requested`, `reasoning_effort_upstream`, +`tokens_in`, `tokens_cache_read`, `tokens_reasoning`, limited to the run window. +There is no other table/column read, no schema introspection, and no prompt or +request identifier export. Missing/unknown counters remain null. + +Rows are **window evidence**, potentially including concurrent callers; the +allowed columns cannot establish exclusive session attribution. The receipt +does not sum them into a worker cost. It records observed package versions, +sanitized gateway rows, actual native MCP observations/errors, successful skill +observations, checker results and container cleanup. Configured tools/skills +are not counted as used. Request/session/tool-call IDs, host paths, email +addresses, raw output and conversations stay out of the receipt. + +The command exits zero only when the task passes and the evidence includes +observed matching model/Responses/max gateway rows, native SDK versions and +events, successful `tdd` use and confirmed container cleanup. A changed gateway +log schema or unavailable log database leaves evidence incomplete rather than +pretending zero usage. Full native persistence, command output and failures stay +in the private attempt directory; only its `receipt.json` is designed for public +review. + +## Evidence and remaining host checks + +| Evidence class | What exists here | +| --- | --- | +| Upstream source/release review | Exact SDK/app pins, executable/install formats, configuration keys, image topology, native MCP/skill/context behavior. [research.md](research.md) records decisions, limitations and corrected assumptions. | +| Artifact verification | Source/lock/wheel/GHCR response hashes; matching wheel/source Python files; exported requirement hashes. No container or package installation. | +| Local structural and synthetic checks | [tests/test_runtime_worker_openhands.py](../../../tests/test_runtime_worker_openhands.py) validates config, model override, headers, tool limits, QMD guard, fixture oracle, restricted SQLite query and failure preservation. [Verification record](evidence/verification.md) retains commands and returned results. Positive controls use synthetic events, not a model transcript. | +| Upstream test execution | **Not run.** Native CI uses `CI=true uv run python -m pytest -vvs` ([tests.yml:117][upstream-tests]); relevant unchanged suites are model features, the summarizing condenser, local conversation MCP and InvokeSkillTool. The production recipe does not install upstream test/dev dependencies. | +| Host/provider acceptance | **Not run.** No installation, image execution, gateway request, live database read, native MCP launch or model-produced fixture repair happened in this build. | + +Host acceptance must establish wheel/sdist installation and import compatibility, +all mounted MCP executables/dependencies and language servers, all 28 skill +loads, tool enumeration/limits, stable affinity and fresh idempotency headers, +Responses/tool streaming, requested/upstream effort, actual test-first repair, +gateway schema compatibility and cleanup. The SDK's aggregate MCP startup limit +is 30 seconds ([local_conversation.py:123,1356–1379][local-mcp]); a server's +`timeout=120` is **not** a promise of a 120-second aggregate startup allowance. +Cold startup may need a separately reviewed upstream-supported provider adapter. +No native savings, cross-host portability, uncatchable-crash recovery or complete +upstream suite acceptance is claimed. + +The OpenHands app, now Agent Canvas, is pinned for comparison at v1.24.0, +`7dc6805406ea3c76cb4a3ce407c3c72d481b0ac6`. Its [README ownership table, lines +139–150][canvas], assigns the frontend/backend selection/local stack orchestration +to Canvas and the agent/runtime APIs to this SDK. From those boundaries, Canvas +adds no required capability to this bounded coding worker. It is **not installed**. + +[release]: https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.49.6 +[sdk-readme]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/README.md#L49-L81 +[hello]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/01_standalone_sdk/01_hello_world.py#L10-L29 +[makefile]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/Makefile#L34-L41 +[getting-started]: https://docs.openhands.dev/sdk/getting-started +[docker-runtime]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L39-L47 +[docker-uv]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L402-L403 +[docker-sync]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L128-L140 +[image-targets]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L564-L598 +[image-matrix]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/.github/workflows/server.yml#L281-L339 +[remote-example]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py#L92-L97 +[docker-workspace]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-workspace/openhands/workspace/docker/workspace.py#L218-L251 +[sdk-pyproject]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/pyproject.toml#L41-L43 +[uv-build]: https://docs.astral.sh/uv/pip/compatibility/#pep-517-build-isolation +[llm-config]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/llm.py#L340-L470 +[llm-effort]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/llm.py#L537-L549 +[llm-copy]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/llm.py#L783-L798 +[llm-generate]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/llm.py#L1580-L1643 +[responses-options]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/options/responses_options.py#L26-L97 +[conversation]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/conversation/conversation.py#L120-L204 +[condenser]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/context/condenser/llm_summarizing_condenser.py#L48-L154 +[condenser-example]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/01_standalone_sdk/14_context_condenser.py#L64-L83 +[terminal]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-tools/openhands/tools/terminal/definition.py#L175-L200 +[terminal-constants]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-tools/openhands/tools/terminal/constants.py#L18-L21 +[model-features]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/utils/model_features.py#L190-L200 +[mcp-config]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/mcp/config.py#L497-L530 +[agent-base]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/agent/base.py#L153-L158 +[fastmcp-config]: https://github.com/PrefectHQ/fastmcp/blob/665514e19a78543709be85b4261153bbe98e882f/src/fastmcp/client/transports/config.py#L25-L125 +[fastmcp-namespace]: https://github.com/PrefectHQ/fastmcp/blob/665514e19a78543709be85b4261153bbe98e882f/src/fastmcp/server/transforms/namespace.py#L44-L62 +[serena-cli]: https://github.com/oraios/serena/blob/c6fbd1c5932df2494ffa0020af5a9fbe80b82143/src/serena/cli.py#L369-L383 +[serena-home]: https://github.com/oraios/serena/blob/c6fbd1c5932df2494ffa0020af5a9fbe80b82143/src/serena/config/serena_config.py#L65-L78 +[serena-project]: https://github.com/oraios/serena/blob/c6fbd1c5932df2494ffa0020af5a9fbe80b82143/src/serena/config/serena_config.py#L928-L936 +[serena-loader]: https://github.com/oraios/serena/blob/c6fbd1c5932df2494ffa0020af5a9fbe80b82143/src/serena/config/serena_config.py#L1050-L1104 +[qmd-config]: https://github.com/tobi/qmd/blob/v2.8.3/src/collections.ts#L112-L125 +[qmd-store]: https://github.com/tobi/qmd/blob/v2.8.3/src/store.ts#L636-L653 +[qmd-cli]: https://github.com/tobi/qmd/blob/v2.8.3/src/cli/qmd.ts#L4451-L4479 +[qmd-schema]: https://github.com/tobi/qmd/blob/v2.8.3/src/mcp/server.ts#L320-L369 +[hooks]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/01_standalone_sdk/33_hooks/main.py#L51-L64 +[skills-example]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/05_skills_and_plugins/01_loading_agentskills/main.py#L72-L146 +[skills-loader]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/skills/skill.py#L848-L903 +[invoke-skill]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/tool/builtins/invoke_skill.py#L25-L139 +[local-mcp]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L1356-L1379 +[upstream-tests]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/.github/workflows/tests.yml#L117 +[canvas]: https://github.com/OpenHands/OpenHands/blob/7dc6805406ea3c76cb4a3ce407c3c72d481b0ac6/README.md#L139-L150 +[unittest-runner]: https://github.com/python/cpython/blob/v3.13.15/Lib/unittest/runner.py +[unittest-main]: https://github.com/python/cpython/blob/v3.13.15/Lib/unittest/main.py +[sqlite-readonly]: https://docs.python.org/3.13/library/sqlite3.html#how-to-work-with-sqlite-uris diff --git a/blueprints/runtime-workers/openhands/build-requirements.lock b/blueprints/runtime-workers/openhands/build-requirements.lock new file mode 100644 index 000000000..66607e35a --- /dev/null +++ b/blueprints/runtime-workers/openhands/build-requirements.lock @@ -0,0 +1,3 @@ +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +setuptools==80.9.0 --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 +wheel==0.46.3 --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d diff --git a/blueprints/runtime-workers/openhands/config/host.example.json b/blueprints/runtime-workers/openhands/config/host.example.json new file mode 100644 index 000000000..dd5781861 --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/host.example.json @@ -0,0 +1,19 @@ +{ + "variables": { + "ECO_ROOT": "", + "HOST_PATH": "", + "AI_MEMORY_URL": "10.0.2.2:49474", + "QDRANT_URL": "", + "EMBED_URL": "10.0.2.2:18232", + "CODE_INDEX_PATH": "/state/mcp/jcodemunch" + }, + "mcp_readonly_mounts": [ + "" + ], + "qmd_collections": { + "foundation-docs": "", + "foundation-adoption": "", + "us-equities-foundation": "", + "us-equities-catalog": "" + } +} diff --git a/blueprints/runtime-workers/openhands/config/mcp-policy.json b/blueprints/runtime-workers/openhands/config/mcp-policy.json new file mode 100644 index 000000000..992d6fc49 --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/mcp-policy.json @@ -0,0 +1,24 @@ +{ + "context-mode": { + "disabled_tools": ["ctx_upgrade", "ctx_purge"], + "env": {"CONTEXT_MODE_PROJECT_DIR": "/workspace"} + }, + "serena": {"enabled": true}, + "jcodemunch": {"enabled_tools": ["route", "menu", "order"]}, + "qmd": { + "enabled_tools": ["query", "get", "multi_get", "status"], + "collections": ["foundation-docs", "foundation-adoption", "us-equities-foundation", "us-equities-catalog"] + }, + "ai-memory": { + "enabled_tools": ["memory_query", "memory_read_page", "memory_recent", "memory_status", "memory_briefing"] + }, + "socraticode": { + "enabled_tools": ["codebase_search", "codebase_status", "codebase_list_projects", "codebase_health"], + "env": {"SOCRATICODE_WATCHER": "manual"} + }, + "headroom": { + "enabled": false, + "enabled_tools": ["headroom_compress", "headroom_retrieve", "headroom_stats"], + "reason": "Native terminal output offload and LLMSummarizingCondenser cover this bounded task; no demonstrated remaining gap." + } +} diff --git a/blueprints/runtime-workers/openhands/config/mcp.template.json b/blueprints/runtime-workers/openhands/config/mcp.template.json new file mode 100644 index 000000000..a19ae7440 --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/mcp.template.json @@ -0,0 +1,119 @@ +{ + "context-mode": { + "command": "${ECO_ROOT}/bin/node", + "args": [ + "${ECO_ROOT}/tools/context-mode-1.0.169/lib/node_modules/context-mode/start.mjs" + ], + "env": { + "RTK_TELEMETRY_DISABLED": "1", + "PATH": "${ECO_ROOT}/bin:${HOST_PATH}", + "CONTEXT_MODE_PLATFORM": "codex", + "CONTEXT_MODE_PROJECT_DIR": "/workspace" + }, + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace" + }, + "serena": { + "command": "${ECO_ROOT}/bin/serena", + "args": [ + "start-mcp-server", + "--transport", + "stdio", + "--project", + "/workspace", + "--context", + "codex", + "--enable-web-dashboard", + "false", + "--open-web-dashboard", + "false", + "--enable-gui-log-window", + "false" + ], + "env": { + "RTK_TELEMETRY_DISABLED": "1", + "PATH": "${ECO_ROOT}/bin:${HOST_PATH}", + "SERENA_HOME": "/state/mcp/serena/home" + }, + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace" + }, + "jcodemunch": { + "command": "${ECO_ROOT}/bin/jcodemunch-mcp", + "env": { + "RTK_TELEMETRY_DISABLED": "1", + "PATH": "${ECO_ROOT}/bin:${HOST_PATH}", + "CODE_INDEX_PATH": "${CODE_INDEX_PATH}", + "JCODEMUNCH_SHARE_SAVINGS": "0" + }, + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace" + }, + "qmd": { + "command": "${ECO_ROOT}/bin/qmd", + "args": [ + "--index", + "native-agent-stack-catalog", + "mcp" + ], + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace", + "env": {} + }, + "ai-memory": { + "url": "http://${AI_MEMORY_URL}/mcp", + "transport": "http", + "timeout": 120, + "env": {} + }, + "socraticode": { + "command": "${ECO_ROOT}/bin/node", + "args": [ + "${ECO_ROOT}/tools/socraticode-1.14.0/lib/node_modules/socraticode/dist/index.js" + ], + "env": { + "RTK_TELEMETRY_DISABLED": "1", + "PATH": "${ECO_ROOT}/bin:${HOST_PATH}", + "QDRANT_MODE": "external", + "QDRANT_URL": "http://${QDRANT_URL}", + "EMBEDDING_PROVIDER": "lmstudio", + "LMSTUDIO_URL": "http://${EMBED_URL}/v1", + "EMBEDDING_MODEL": "nvidia/Nemotron-3-Embed-1B-BF16", + "EMBEDDING_DIMENSIONS": "2048", + "EMBEDDING_CONTEXT_LENGTH": "4096", + "EMBEDDING_QUERY_PREFIX": "query: ", + "EMBEDDING_DOCUMENT_PREFIX": "passage: ", + "EMBEDDING_DOCUMENT_INCLUDE_PATH": "true", + "RESPECT_GITIGNORE": "true", + "INCLUDE_DOT_FILES": "false", + "SOCRATICODE_WATCHER": "manual", + "SEARCH_DEFAULT_LIMIT": "3" + }, + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace" + }, + "headroom": { + "command": "${ECO_ROOT}/bin/headroom", + "args": [ + "mcp", + "serve", + "--proxy-url", + "http://127.0.0.1:1" + ], + "env": { + "HEADROOM_OFFLINE": "1", + "HF_HUB_OFFLINE": "1", + "TRANSFORMERS_OFFLINE": "1", + "DO_NOT_TRACK": "1" + }, + "transport": "stdio", + "timeout": 120, + "cwd": "/workspace", + "enabled": false + } +} diff --git a/blueprints/runtime-workers/openhands/config/serena_config.yml b/blueprints/runtime-workers/openhands/config/serena_config.yml new file mode 100644 index 000000000..4ac4a9fa4 --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/serena_config.yml @@ -0,0 +1,9 @@ +# Serena c6fbd1c5932df2494ffa0020af5a9fbe80b82143: +# src/serena/config/serena_config.py:928-936,1050-1104. +# Missing values are completed by the native loader in the private owned copy. +projects: [] +web_dashboard: false +web_dashboard_open_on_launch: false +gui_log_window: false +project_serena_folder_location: "/state/mcp/serena/projects/$projectFolderName" +default_max_tool_answer_chars: 30000 diff --git a/blueprints/runtime-workers/openhands/config/skills.lock.json b/blueprints/runtime-workers/openhands/config/skills.lock.json new file mode 100644 index 000000000..b25d4efdd --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/skills.lock.json @@ -0,0 +1,173 @@ +{ + "source": "adoption/skills/manifest.json", + "skills": [ + { + "name": "typesafe-ai", + "ref": "65a39f393687675ce170e6094757de20370365b9", + "url": "https://github.com/typesafe-ai/skills/tree/65a39f393687675ce170e6094757de20370365b9/skills/typesafe-ai", + "skill_md_sha256": "71ea90d7906c6554c4f4c460ef7361b2d26f59116ccdae986dc6d997b9389f52" + }, + { + "name": "gh-fix-ci", + "ref": "49f948faa9258a0c61caceaf225e179651397431", + "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/gh-fix-ci", + "skill_md_sha256": "7b326b4a2f0f5f85122144628ec02077e48841e0e0e82efce88b3415bcfb7c26" + }, + { + "name": "security-best-practices", + "ref": "49f948faa9258a0c61caceaf225e179651397431", + "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/security-best-practices", + "skill_md_sha256": "7b3dae1ffc5434d890f3c65c8f552af52d0307fab3b35dec13013c9ca3844c4f" + }, + { + "name": "iterative-retrieval", + "ref": "2b6e839771e53096d8451a213d40dc64ec8acac0", + "url": "https://github.com/affaan-m/ECC/tree/2b6e839771e53096d8451a213d40dc64ec8acac0/skills/iterative-retrieval", + "skill_md_sha256": "b453b16d3e36174b8487c05ab807d0321a66c2d058812b2ae129d82824183725" + }, + { + "name": "search-first", + "ref": "2b6e839771e53096d8451a213d40dc64ec8acac0", + "url": "https://github.com/affaan-m/ECC/tree/2b6e839771e53096d8451a213d40dc64ec8acac0/skills/search-first", + "skill_md_sha256": "d66d744228b9cf860acf35f8a90cbbe8195e61367860da9d3632f9fc65706733" + }, + { + "name": "diagnosing-bugs", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/diagnosing-bugs", + "skill_md_sha256": "77f3cf31bc99b2f49af943222526531fcc9fc41d047626d3640e875e85af3e84" + }, + { + "name": "tdd", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/tdd", + "skill_md_sha256": "cb01f66bebfaa25fa1f88e6b7e769cd9fd9f35b1120b8563749820738814c927" + }, + { + "name": "codebase-design", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/codebase-design", + "skill_md_sha256": "2c20617f87ec8af6a434859f381b2f061a69b530444e74eb39e78bb016a6d1e2" + }, + { + "name": "resolving-merge-conflicts", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/resolving-merge-conflicts", + "skill_md_sha256": "9d8114f8ef0b31f535a265fc05c364bd8cf2e2895a830040e06c22acb11f54b0" + }, + { + "name": "grill-me", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/productivity/grill-me", + "skill_md_sha256": "caaf8b8de1684f96e26b28f3c29189db5c89cce4b73e1c93d86164f66ef88637" + }, + { + "name": "improve-codebase-architecture", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/improve-codebase-architecture", + "skill_md_sha256": "d1ac25511a936ff4250a48dbcefda363837d6bb9321b3cba73df99fa37270a75" + }, + { + "name": "verification-before-completion", + "ref": "8ca22dba9a94f28898bbce59f2537ff4d87c747d", + "url": "https://github.com/obra/superpowers/tree/8ca22dba9a94f28898bbce59f2537ff4d87c747d/skills/verification-before-completion", + "skill_md_sha256": "2befe7fc55bcadaa3d97dd9e8efeb633d2561c0ebe74c5a8b17c4d9e7e4520b3" + }, + { + "name": "security-threat-model", + "ref": "49f948faa9258a0c61caceaf225e179651397431", + "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/security-threat-model", + "skill_md_sha256": "1283c0dd62a8104d9edda4583569b5d8510b4ddaa45120687c999250fd96bad2" + }, + { + "name": "gh-address-comments", + "ref": "49f948faa9258a0c61caceaf225e179651397431", + "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/gh-address-comments", + "skill_md_sha256": "77389eefd3fb6584210668ca8e43f4b8de87e7722ddd953474bc9e24cdfaaedd" + }, + { + "name": "semgrep", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/semgrep", + "skill_md_sha256": "04df36a7685632ee39271aa432a4d32e1203b326e4a8091e6f5c0eb191ef90f1" + }, + { + "name": "codeql", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/codeql", + "skill_md_sha256": "1a6d919ff80f83b262daf650552af6eb6d2c32b19d0c529b70efe0c116b675cf" + }, + { + "name": "supply-chain-risk-auditor", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/supply-chain-risk-auditor/skills/supply-chain-risk-auditor", + "skill_md_sha256": "a1254b7f8d3a1731a9a7e1f9cd6c67b59cf84ed6ef7c7fb455db9a14c528ceb4" + }, + { + "name": "agentic-actions-auditor", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/agentic-actions-auditor/skills/agentic-actions-auditor", + "skill_md_sha256": "b2bdb88d559bc37e6c2c8314e28e7a2eb47e7cdd9da51e77da347d3517326dc0" + }, + { + "name": "property-based-testing", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/property-based-testing/skills/property-based-testing", + "skill_md_sha256": "9448cacb65c6237a7c22dc59602667177ecae405524079ae9ff92240590b995a" + }, + { + "name": "modern-python", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/modern-python/skills/modern-python", + "skill_md_sha256": "ef774b925eb1d1fc12e4f5ddc3f5ad925ecd795315827447228c70cc44c6170c" + }, + { + "name": "sarif-parsing", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/sarif-parsing", + "skill_md_sha256": "cc5f008f40ffb8a1ae03ebd32d7234de43e828293955a0bc251b76ba249554fa" + }, + { + "name": "fp-check", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/fp-check/skills/fp-check", + "skill_md_sha256": "129223b79b8cb1e7c289c90cbe4ba288d9b210e318a0d1464f319e30329481b3" + }, + { + "name": "mcp-builder", + "ref": "33375500bcea98d610eb30ce10ac4e59b89c390d", + "url": "https://github.com/anthropics/skills/tree/33375500bcea98d610eb30ce10ac4e59b89c390d/skills/mcp-builder", + "skill_md_sha256": "0f4592dcb53cf2b5d6b7febee6b4152018b565551a1c29e3c612f57b218ab295" + }, + { + "name": "frontend-design", + "ref": "33375500bcea98d610eb30ce10ac4e59b89c390d", + "url": "https://github.com/anthropics/skills/tree/33375500bcea98d610eb30ce10ac4e59b89c390d/skills/frontend-design", + "skill_md_sha256": "d91970639e9f5c37682ac7ab60094d35f1c7c1f38d731bd56396563aee10c1d3" + }, + { + "name": "agent-browser", + "ref": "d01253d9db28d75080e36da3c1c31ef89454731e", + "url": "https://github.com/vercel-labs/agent-browser/tree/d01253d9db28d75080e36da3c1c31ef89454731e/skills/agent-browser", + "skill_md_sha256": "3a7520e64de2dfaa516b6721c001e84e06e3c06a187b82af029ae721540ebfac" + }, + { + "name": "find-skills", + "ref": "7407f3893ad4dceab546ac002c3ef806e4000c73", + "url": "https://github.com/vercel-labs/skills/tree/7407f3893ad4dceab546ac002c3ef806e4000c73/skills/find-skills", + "skill_md_sha256": "c00eeea0e13e74fe4a9d84ba0a8542205a1b736d65f13134fe1a6647eb14976f" + }, + { + "name": "variant-analysis", + "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", + "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/variant-analysis/skills/variant-analysis", + "skill_md_sha256": "7b6b78e499bcb88d05c68254ffc91ebc28edabf4e9808adcc40e2628ebb9407e" + }, + { + "name": "writing-for-agents", + "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", + "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/productivity/writing-for-agents", + "skill_md_sha256": "551adca942227b44192edba88acd4e8db911f0121ce58ad16944ccf6a896a74a" + } + ] +} diff --git a/blueprints/runtime-workers/openhands/config/worker.json b/blueprints/runtime-workers/openhands/config/worker.json new file mode 100644 index 000000000..8ccd8bbef --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/worker.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "model_env": "OPENHANDS_MODEL", + "llm": { + "model": "cx/gpt-6-astra-max", + "model_canonical_name": "openai/gpt-6", + "base_url": "http://127.0.0.1:20128/v1", + "api_key": "local-loopback", + "api_mode": "responses", + "reasoning_effort": "max", + "stream": true, + "native_tool_calling": true, + "max_output_tokens": 16384, + "num_retries": 2, + "timeout": 180, + "capability_overrides": { + "supports_reasoning_effort": true, + "supports_sampling_params": false, + "supports_responses_api": true + } + }, + "runtime": { + "agent_loop": "container", + "llm_calls": "container", + "stdio_mcp": "container", + "gateway_base_url": "http://10.0.2.2:20128/v1", + "published_ports": [], + "max_iteration_per_run": 40, + "timeout_seconds": 1200, + "cpus": "4", + "memory": "8g", + "pids_limit": 384 + }, + "condenser": { + "kind": "LLMSummarizingCondenser", + "max_size": 80, + "keep_first": 2, + "max_tokens": 60000 + }, + "agent_context": { + "load_user_skills": false, + "load_public_skills": false, + "load_project_skills": false, + "load_memory": false + } +} diff --git a/blueprints/runtime-workers/openhands/e2e/check.py b/blueprints/runtime-workers/openhands/e2e/check.py new file mode 100644 index 000000000..2a8ebdced --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/check.py @@ -0,0 +1,135 @@ +"""Independent frozen fixture oracle. Run in the pinned image with no network. + +The local test runner follows CPython unittest's TestLoader/TextTestRunner public +interfaces. This is a synthetic task check, never an upstream acceptance suite. +It reexecutes the frozen tests and checks bytes against the pristine fixture. +""" + +from __future__ import annotations + +import argparse +import io +import json +from pathlib import Path +import subprocess +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from recipe import inventory, read_json # noqa: E402 + + +HERE = Path(__file__).resolve().parent + + +def run_tests(workspace): + """Isolated child imports candidate code, bounded by time and container scope.""" + command = [sys.executable, "-I", "-B", str(Path(__file__).resolve()), "--test-child", str(workspace)] + completed = subprocess.run(command, capture_output=True, text=True, timeout=30, check=False) + lines = completed.stdout.splitlines() + try: + report = json.loads(lines[-1]) + except (IndexError, json.JSONDecodeError): + report = {"tests_run": 0, "failures": None, "errors": None, "skipped": None} + report["exit_code"] = completed.returncode + report["stdout"] = completed.stdout + report["stderr"] = completed.stderr + return report + + +def test_child(workspace): + # -I ignores environment/site/user paths; insert only the owned fixture. + sys.path.insert(0, str(workspace)) + stream = io.StringIO() + suite = unittest.defaultTestLoader.discover(str(workspace / "tests"), pattern="test_*.py") + result = unittest.TextTestRunner(stream=stream, verbosity=2).run(suite) + print(json.dumps({ + "tests_run": result.testsRun, "failures": len(result.failures), "errors": len(result.errors), + "skipped": len(result.skipped), "expected_failures": len(result.expectedFailures), + "unexpected_successes": len(result.unexpectedSuccesses), "output": stream.getvalue(), + })) + return 0 if result.wasSuccessful() else 1 + + +def test_first(events, pristine, command): + """Require an actual failing terminal observation while bytes were pristine.""" + pending = {} + for event in events: + if event.get("workspace") != pristine: + return False + if event.get("kind") == "ActionEvent": + pending[event.get("tool_call_id")] = event + elif event.get("kind") == "ObservationEvent": + action = pending.get(event.get("tool_call_id"), {}) + obs = event.get("observation", {}) + if ( + action.get("tool_name") == "terminal" + and action.get("action", {}).get("command", "").strip() == command + and obs.get("exit_code") == 1 + and "FAILED (failures=1)" in json.dumps(obs) + and "test_ranges_follow_the_spec" in json.dumps(obs) + ): + return True + return False + + +def check(result): + failures = [] + required = [result / "workspace", result / "baseline.json", result / "worker/events.jsonl"] + if not all(path.exists() for path in required): + return {"passed": False, "failures": ["missing_result"]} + frozen = read_json(HERE / "frozen.json") + pristine = inventory(HERE / "fixture-repo") + expected = {**frozen["files"], "tests/": "directory"} + if pristine != expected: + failures.append("frozen_fixture_changed") + current = inventory(result / "workspace") + changed = sorted(name for name in set(pristine) | set(current) if pristine.get(name) != current.get(name)) + if not changed or not set(changed) <= set(frozen["allowed_files"]): + failures.append("diff_outside_allowed_files_or_empty") + if any(value in ("symlink", "special") for value in current.values()): + failures.append("nonregular_result") + baseline = read_json(result / "baseline.json") + if not (baseline.get("exit_code") == 1 and baseline.get("tests_run") == 1 + and baseline.get("failures") == 1 and baseline.get("errors") == 0 and baseline.get("skipped") == 0): + failures.append("baseline_not_one_failing_test") + events = [json.loads(line) for line in (result / "worker/events.jsonl").read_text().splitlines() if line] + if not test_first(events, pristine, frozen["test_command"]): + failures.append("no_native_test_failure_before_edit") + # Never execute a scope-violating submission, even inside the checker container. + report = None + if not failures: + before = inventory(result / "workspace") + report = run_tests(result / "workspace") + if before != inventory(result / "workspace"): + failures.append("tests_mutated_workspace") + if not (report.get("exit_code") == 0 and report.get("tests_run") == frozen["expected_test_count"] + and all(report.get(key) == 0 for key in ("failures", "errors", "skipped", "expected_failures", "unexpected_successes"))): + failures.append("tests_not_passing_unskipped") + return {"passed": not failures, "failures": failures, "changed_files": changed, "tests": report} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--result", type=Path) + parser.add_argument("--baseline", type=Path) + parser.add_argument("--test-child", type=Path) + args = parser.parse_args() + if args.test_child: + return test_child(args.test_child) + try: + if args.baseline: + report = run_tests(args.baseline) + print(json.dumps(report)) + return report["exit_code"] + if args.result is None: + parser.error("--result is required") + report = check(args.result) + except (OSError, ValueError, TypeError, subprocess.TimeoutExpired): + report = {"passed": False, "failures": ["invalid_or_incomplete_result"]} + print(json.dumps(report)) + return 0 if report["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md b/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md new file mode 100644 index 000000000..db55f63b7 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md @@ -0,0 +1,14 @@ +# Compact integer ranges + +Implement `compress_ranges(values)` in `range_utils.py`. Accept a finite iterable +of integers. Return a list of inclusive `(start, end)` tuples in ascending order, +merging consecutive integers, ignoring duplicates, and preserving gaps. An empty +input produces `[]`. Do not mutate the input. For example, +`[8, 2, 1, 3, 5, 5, 7]` produces `[(1, 3), (5, 5), (7, 8)]`. + +This is a self-contained synthetic coding task. Only `range_utils.py` may change. +The test and this README are frozen. First run the existing failing test, then +repair the implementation, then run the same test again. Do not delete, skip or +edit tests; do not add files, install dependencies or use Git. + +Run: `python -B -m unittest discover -s tests -v` diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py b/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py new file mode 100644 index 000000000..50d57a662 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py @@ -0,0 +1,5 @@ +"""Synthetic fixture: intentionally fails the frozen range-coalescing test.""" + + +def compress_ranges(values): + return [(value, value) for value in sorted(set(values))] diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py b/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py new file mode 100644 index 000000000..e48cee654 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py @@ -0,0 +1,18 @@ +import unittest + +from range_utils import compress_ranges + + +class RangeTests(unittest.TestCase): + def test_ranges_follow_the_spec(self): + inputs = [[], [4], [8, 2, 1, 3, 5, 5, 7], [-2, -1, 0, 4, 4, 5]] + before = [list(values) for values in inputs] + self.assertEqual( + [compress_ranges(values) for values in inputs], + [[], [(4, 4)], [(1, 3), (5, 5), (7, 8)], [(-2, 0), (4, 5)]], + ) + self.assertEqual(inputs, before) + + +if __name__ == "__main__": + unittest.main() diff --git a/blueprints/runtime-workers/openhands/e2e/frozen.json b/blueprints/runtime-workers/openhands/e2e/frozen.json new file mode 100644 index 000000000..365489760 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/frozen.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "evidence_class": "synthetic fixture", + "allowed_files": [ + "range_utils.py" + ], + "expected_test_count": 1, + "test_command": "python -B -m unittest discover -s tests -v", + "files": { + "README.md": "3af3bcbdb2c08a268481be21a5b3a136d35cafaa8107a83f14db0a0358497f06", + "range_utils.py": "219dfcb3d11544b62e9fd496fc2e544f4689c286253118c48248c88d0802c66d", + "tests/test_ranges.py": "bfa9a8f4fb795f6201b45f7efce2a9477fbbd94041e56382be0756e6b1bd1b73" + } +} diff --git a/blueprints/runtime-workers/openhands/e2e/task.txt b/blueprints/runtime-workers/openhands/e2e/task.txt new file mode 100644 index 000000000..9d8c4f22c --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/task.txt @@ -0,0 +1,10 @@ +Fix the tiny repository at /workspace according to its README. +The allowed edit is range_utils.py only. The existing test seam and test-first +procedure are already authorized. Invoke the pinned tdd skill with invoke_skill. +Before any edit, run exactly: python -B -m unittest discover -s tests -v +Observe its failing assertion, repair the implementation, and rerun that command. +Use context-mode for a bounded inspection when useful, and use the installed +coding/retrieval tools only when they improve this task. Do not manufacture MCP +calls to inflate coverage. Do not edit tests, README, runner state or recipe files. +Do not install software, start services, contact broker APIs or use Git. Finish +with the change and the actual test result. diff --git a/blueprints/runtime-workers/openhands/evidence/expanded-tests.txt b/blueprints/runtime-workers/openhands/evidence/expanded-tests.txt new file mode 100644 index 000000000..04fc3bcf2 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/expanded-tests.txt @@ -0,0 +1,30 @@ +test_broken_answer_and_symlink_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_broken_answer_and_symlink_cannot_pass) ... ok +test_deleted_or_skipped_test_and_extra_files_are_rejected (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_deleted_or_skipped_test_and_extra_files_are_rejected) ... ok +test_fixed_copy_passes_and_frozen_source_stays_broken (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_fixed_copy_passes_and_frozen_source_stays_broken) ... ok +test_no_native_red_observation_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_no_native_red_observation_cannot_pass) ... ok +test_red_after_edit_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_red_after_edit_cannot_pass) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ERROR +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok + +====================================================================== +ERROR: test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 128, in test_fresh_call_headers_keep_session_affinity + first = worker.headers_for_call(original) + ^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'openhands_recipe_test_worker' has no attribute 'headers_for_call' + +---------------------------------------------------------------------- +Ran 16 tests in 0.552s + +FAILED (errors=1) diff --git a/blueprints/runtime-workers/openhands/evidence/fail-first.txt b/blueprints/runtime-workers/openhands/evidence/fail-first.txt new file mode 100644 index 000000000..64195548e --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/fail-first.txt @@ -0,0 +1,106 @@ +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... FAIL +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ERROR +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ERROR +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ERROR +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ERROR +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... FAIL + +====================================================================== +ERROR: test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 27, in test_gateway_model_and_container_topology + config = self.read_json("config/worker.json") + File "/tests/test_runtime_worker_openhands.py", line 24, in read_json + return json.loads((RECIPE / relative).read_text()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 546, in read_text + return PathBase.read_text(self, encoding, errors, newline) + ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_abc.py", line 632, in read_text + with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f: + ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 537, in open + return io.open(self, mode, buffering, encoding, errors, newline) + ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/config/worker.json' + +====================================================================== +ERROR: test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 62, in test_immutable_artifacts + pins = self.read_json("pins.json") + File "/tests/test_runtime_worker_openhands.py", line 24, in read_json + return json.loads((RECIPE / relative).read_text()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 546, in read_text + return PathBase.read_text(self, encoding, errors, newline) + ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_abc.py", line 632, in read_text + with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f: + ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 537, in open + return io.open(self, mode, buffering, encoding, errors, newline) + ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/pins.json' + +====================================================================== +ERROR: test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 71, in test_installer_and_runtime_are_scoped + text = (RECIPE / filename).read_text() + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 546, in read_text + return PathBase.read_text(self, encoding, errors, newline) + ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_abc.py", line 632, in read_text + with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f: + ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 537, in open + return io.open(self, mode, buffering, encoding, errors, newline) + ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/install.sh' + +====================================================================== +ERROR: test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 42, in test_mcp_limits + policy = self.read_json("config/mcp-policy.json") + File "/tests/test_runtime_worker_openhands.py", line 24, in read_json + return json.loads((RECIPE / relative).read_text()) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 546, in read_text + return PathBase.read_text(self, encoding, errors, newline) + ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_abc.py", line 632, in read_text + with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f: + ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/pathlib/_local.py", line 537, in open + return io.open(self, mode, buffering, encoding, errors, newline) + ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/config/mcp-policy.json' + +====================================================================== +FAIL: test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 99, in test_checker_rejects_an_empty_result + self.assertTrue(checker.is_file()) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^ +AssertionError: False is not true + +====================================================================== +FAIL: test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 87, in test_no_public_binds_or_secret_material + self.assertTrue(RECIPE.is_dir()) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ +AssertionError: False is not true + +---------------------------------------------------------------------- +Ran 6 tests in 0.003s + +FAILED (failures=2, errors=4) diff --git a/blueprints/runtime-workers/openhands/evidence/publication-first.txt b/blueprints/runtime-workers/openhands/evidence/publication-first.txt new file mode 100644 index 000000000..ee5275eb8 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/publication-first.txt @@ -0,0 +1,2 @@ +Publication validation failed: +blueprints/runtime-workers/openhands/host.py: contains possible personal home path diff --git a/blueprints/runtime-workers/openhands/evidence/publication-pass.txt b/blueprints/runtime-workers/openhands/evidence/publication-pass.txt new file mode 100644 index 000000000..7f0d5ac37 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/publication-pass.txt @@ -0,0 +1,5 @@ +Command: python3 scripts/validate.py +Exit code: 0 + +{"components": 69, "hashed_files": 7345, "profiles": 4, "receipts": 159, "status": "passed"} +Integrity and scope checks only; no live provider or GPU execution. diff --git a/blueprints/runtime-workers/openhands/evidence/receipt-red.txt b/blueprints/runtime-workers/openhands/evidence/receipt-red.txt new file mode 100644 index 000000000..db1eb34bd --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/receipt-red.txt @@ -0,0 +1,67 @@ +test_broken_answer_and_symlink_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_broken_answer_and_symlink_cannot_pass) ... ok +test_deleted_or_skipped_test_and_extra_files_are_rejected (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_deleted_or_skipped_test_and_extra_files_are_rejected) ... ok +test_fixed_copy_passes_and_frozen_source_stays_broken (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_fixed_copy_passes_and_frozen_source_stays_broken) ... ok +test_no_native_red_observation_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_no_native_red_observation_cannot_pass) ... ok +test_red_after_edit_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_red_after_edit_cannot_pass) ... ok +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ERROR +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ERROR +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok + +====================================================================== +ERROR: test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 256, in test_cleanup_timeout_does_not_erase_primary_exit + self.assertEqual(host.execute_container(["synthetic"], "fixture", log, 1), 7) + ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/blueprints/runtime-workers/openhands/host.py", line 136, in execute_container + subprocess.run(DOCKER + ["rm", "-f", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + check=False, timeout=30) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1169, in __call__ + return self._mock_call(*args, **kwargs) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1173, in _mock_call + return self._execute_mock_call(*args, **kwargs) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/unittest/mock.py", line 1238, in _execute_mock_call + raise result +subprocess.TimeoutExpired: Command '['synthetic-cleanup']' timed out after 30 seconds + +====================================================================== +ERROR: test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 244, in test_truncated_native_summary_retains_failed_attempt_receipt + report = module.create_receipt(result, database=result / "absent.sqlite") + File "/blueprints/runtime-workers/openhands/receipt.py", line 126, in create_receipt + native = read_json(summary) + File "/blueprints/runtime-workers/openhands/recipe.py", line 20, in read_json + return json.loads(Path(path).read_text()) + ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/json/__init__.py", line 352, in loads + return _default_decoder.decode(s) + ~~~~~~~~~~~~~~~~~~~~~~~^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/json/decoder.py", line 345, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^ + File "/.local/share/uv/python/cpython-3.13.15-linux-x86_64-gnu/lib/python3.13/json/decoder.py", line 363, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 13 (char 12) + +---------------------------------------------------------------------- +Ran 19 tests in 0.539s + +FAILED (errors=2) diff --git a/blueprints/runtime-workers/openhands/evidence/serena-red.txt b/blueprints/runtime-workers/openhands/evidence/serena-red.txt new file mode 100644 index 000000000..6fe926cd7 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/serena-red.txt @@ -0,0 +1,31 @@ +test_broken_answer_and_symlink_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_broken_answer_and_symlink_cannot_pass) ... ok +test_deleted_or_skipped_test_and_extra_files_are_rejected (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_deleted_or_skipped_test_and_extra_files_are_rejected) ... ok +test_fixed_copy_passes_and_frozen_source_stays_broken (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_fixed_copy_passes_and_frozen_source_stays_broken) ... ok +test_no_native_red_observation_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_no_native_red_observation_cannot_pass) ... ok +test_red_after_edit_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_red_after_edit_cannot_pass) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... FAIL + +====================================================================== +FAIL: test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 155, in test_serena_activates_plain_fixture_and_keeps_metadata_outside_it + self.assertNotIn("--project-from-cwd", config["args"]) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: '--project-from-cwd' unexpectedly found in ['start-mcp-server', '--transport', 'stdio', '--project-from-cwd', '--context', 'codex', '--enable-web-dashboard', 'false', '--open-web-dashboard', 'false', '--enable-gui-log-window', 'false'] + +---------------------------------------------------------------------- +Ran 17 tests in 0.522s + +FAILED (failures=1) diff --git a/blueprints/runtime-workers/openhands/evidence/unit-pass.txt b/blueprints/runtime-workers/openhands/evidence/unit-pass.txt new file mode 100644 index 000000000..83d604c0c --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/unit-pass.txt @@ -0,0 +1,24 @@ +test_broken_answer_and_symlink_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_broken_answer_and_symlink_cannot_pass) ... ok +test_deleted_or_skipped_test_and_extra_files_are_rejected (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_deleted_or_skipped_test_and_extra_files_are_rejected) ... ok +test_fixed_copy_passes_and_frozen_source_stays_broken (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_fixed_copy_passes_and_frozen_source_stays_broken) ... ok +test_no_native_red_observation_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_no_native_red_observation_cannot_pass) ... ok +test_red_after_edit_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_red_after_edit_cannot_pass) ... ok +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok + +---------------------------------------------------------------------- +Ran 19 tests in 0.546s + +OK diff --git a/blueprints/runtime-workers/openhands/evidence/verification.md b/blueprints/runtime-workers/openhands/evidence/verification.md new file mode 100644 index 000000000..01d140e09 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/verification.md @@ -0,0 +1,57 @@ +# Offline verification, 2026-09-27 + +These are local structural and synthetic-fixture checks. They do not install or +import OpenHands, execute its upstream test suite, run an image, call the gateway, +read its live database, or qualify native MCP/skills behavior. + +The implementation references are the immutable upstream links in +[README.md](../README.md); the integration contract is the user's 2026-09-27 +OpenHands builder request. The fixture oracle uses CPython's public unittest +runner interfaces. No benchmark or token-saving claim is made. + +From the checkout root, the initial and final command was: + +```sh +PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +``` + +| Run | Exit | Returned result | Output | +| --- | --- | --- | --- | +| Initial contract, before any recipe files existed | 1 | `Ran 6 tests`; `FAILED (failures=2, errors=4)` | [fail-first.txt](fail-first.txt) | +| Final contract and synthetic controls | 0 | `Ran 19 tests in 0.546s`; `OK` | [unit-pass.txt](unit-pass.txt) | + +The final run redirected stdout and stderr to a temporary file; the command's +own exit code was observed before retaining that file unchanged. Initial failure +outputs replace only private filesystem prefixes with placeholders. Expanded +negative controls retain their actual results in [expanded-tests.txt](expanded-tests.txt), +[serena-red.txt](serena-red.txt) and [receipt-red.txt](receipt-red.txt). + +The final suite covers an empty result, wrong solution, test deletion/skipping, +extra files, symlinks, missing or late failing-test observations, restricted +gateway columns using a synthetic SQLite database, observation-only tool/skill +counts, incomplete native summaries and cleanup failures. Positive controls use +an explicitly local repair and synthetic events; they are not model transcripts. + +Additional checks returned exit 0: + +```sh +bash -n blueprints/runtime-workers/openhands/install.sh blueprints/runtime-workers/openhands/install-container.sh blueprints/runtime-workers/openhands/run-e2e.sh +git diff --check +``` + +Python AST parsing of 10 files, JSON parsing of 8 files, both requirement-file +SHA256 checks and relative Markdown target checks passed. These checks do not +execute the SDK. `git diff --check` cannot cover these untracked additions; +direct syntax, content and repository publication checks provide their evidence. + +Publication validation uses the repository's unchanged command: + +```sh +python3 scripts/validate.py +``` + +Its initial failure is retained in [publication-first.txt](publication-first.txt). +The corrected run's returned output and exit code are in +[publication-pass.txt](publication-pass.txt). This validates integrity and scope, +not host/provider execution. Only this recipe and its named unit test were added; +no shared manifest, index, catalog, Git metadata or other checkout was changed. diff --git a/blueprints/runtime-workers/openhands/evidence/wheel-source-comparison.json b/blueprints/runtime-workers/openhands/evidence/wheel-source-comparison.json new file mode 100644 index 000000000..4fc4add01 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/wheel-source-comparison.json @@ -0,0 +1,16 @@ +{ + "class": "artifact byte comparison, no installation or execution", + "source_commit": "fcc102a697874d54a357e36004e02c95040dbdc0", + "results": { + "sdk": { + "compared_source_python_files": 295, + "equal_files": 295, + "different_files": [] + }, + "tools": { + "compared_source_python_files": 93, + "equal_files": 93, + "different_files": [] + } + } +} diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py new file mode 100644 index 000000000..db98a927a --- /dev/null +++ b/blueprints/runtime-workers/openhands/host.py @@ -0,0 +1,320 @@ +"""Owned lifecycle for the upstream SDK-in-image recipe; no work on import. + +Host actions: path/hash validation, rootless Docker CLI, fixture byte copies, +bounded process supervision and read-only gateway receipts. Candidate Python +and all SDK/MCP processes execute inside the digest-pinned image. +""" + +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import json +import os +from pathlib import Path +import shutil +import signal +import stat +import subprocess +import sys +import tempfile +import urllib.request +import uuid + +from recipe import HERE, digest, read_json, render_mcp + + +DOCKER = ["docker", "--context", "rootless"] + + +def utc_now(): + return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z") + + +def write_json(path, value): + Path(path).write_text(json.dumps(value, indent=2) + "\n") + + +def private_directory(path): + path.mkdir(parents=True, exist_ok=True, mode=0o700) + if path.is_symlink(): + raise ValueError("owned_directory_is_symlink") + path.chmod(0o700) + + +def mount(source, target, readonly=True): + if any(char in str(source) + str(target) for char in (",", "\n", "\r")): + raise ValueError("unsupported_mount_path") + return ["--mount", f"type=bind,src={source},dst={target}" + (",readonly" if readonly else "")] + + +def check_rootless(): + result = subprocess.run(DOCKER + ["info", "--format", "{{json .SecurityOptions}}"], + capture_output=True, text=True, check=True, timeout=30) + if "name=rootless" not in json.loads(result.stdout): + raise ValueError("rootless_docker_required") + + +def preflight(prefix, state): + pins = read_json(HERE / "pins.json") + if digest(HERE / "requirements.lock") != pins["requirements_sha256"]: + raise ValueError("runtime_lock_hash_mismatch") + if digest(HERE / "build-requirements.lock") != pins["build_requirements_sha256"]: + raise ValueError("build_lock_hash_mismatch") + expected_prefix = Path.home() / ".local/share/codex-ecosystem/tools" / ("openhands-" + pins["version"]) + expected_state = Path.home() / ".local/state/native-agent-stack/runtime-workers/openhands" + if prefix != expected_prefix or state != expected_state: + raise ValueError("unexpected_owned_prefix") + for path in (prefix, state): + if path.resolve() != path: + raise ValueError("owned_path_must_not_follow_symlinks") + private_name = os.environ.get("OPENHANDS_HOST_FILE") + if not private_name: + raise ValueError("OPENHANDS_HOST_FILE_required") + path = Path(private_name).expanduser() + if path.is_symlink() or not path.is_file() or path.stat().st_uid != os.getuid(): + raise ValueError("private_host_file_must_be_owned_regular_file") + if stat.S_IMODE(path.stat().st_mode) != 0o600: + raise ValueError("private_host_file_requires_mode_0600") + if path.resolve().is_relative_to(HERE.parents[2].resolve()): + raise ValueError("private_host_file_must_be_outside_checkout") + host = read_json(path) + if "<" in json.dumps(host): + raise ValueError("host_file_has_unfilled_placeholders") + variables = host["variables"] + if variables["AI_MEMORY_URL"] != "10.0.2.2:49474": + raise ValueError("ai_memory_container_endpoint_required") + if variables["EMBED_URL"] != "10.0.2.2:18232": + raise ValueError("local_embedder_required") + if variables["CODE_INDEX_PATH"] != "/state/mcp/jcodemunch": + raise ValueError("owned_jcodemunch_store_required") + mcp = render_mcp(variables) + required = set(read_json(HERE / "config/mcp-policy.json")["qmd"]["collections"]) + if set(host["qmd_collections"]) != required: + raise ValueError("exact_qmd_collections_required") + for value in [*host["mcp_readonly_mounts"], *host["qmd_collections"].values()]: + candidate = Path(value) + if not candidate.is_absolute() or not candidate.is_dir(): + raise ValueError("existing_absolute_directory_required") + forbidden = (Path("/"), Path.home(), Path("/run"), Path("/var/run"), Path.home() / ".local") + if candidate.resolve() in forbidden or any(part in {".ssh", ".codex", ".claude", "credentials"} for part in candidate.parts): + raise ValueError("broad_or_authentication_mount_refused") + skills = [] + for entry in read_json(HERE / "config/skills.lock.json")["skills"]: + source = Path.home() / ".agents/skills" / entry["name"] + if digest(source / "SKILL.md") != entry["skill_md_sha256"]: + raise ValueError("installed_skill_pin_mismatch:" + entry["name"]) + skills.append((entry["name"], source.resolve())) + check_rootless() + return pins, host, mcp, skills + + +def docker_args(pins, name, *, network=True): + limits = read_json(HERE / "config/worker.json")["runtime"] + args = DOCKER + [ + "run", "--rm", "--pull=never", "--name", name, "--platform", pins["image"]["platform"], + "--label", "native-agent-stack.runtime-worker=openhands", + "--user", "0:0", "--cap-drop=ALL", "--security-opt=no-new-privileges", + "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", "--tmpfs", "/root:rw,nosuid,nodev,mode=0700", + "--env", "PYTHONDONTWRITEBYTECODE=1", "--env", "UV_PYTHON_DOWNLOADS=never", + "--cpus", limits["cpus"], "--memory", limits["memory"], "--pids-limit", str(limits["pids_limit"]), + ] + if not network: + args += ["--network=none"] + # No HTTP listener and no port publication; image EXPOSE is not publishing. + return args + + +def execute_container(args, name, logfile, timeout): + """Bound the Docker client and remove exactly its container on every exit.""" + try: + with logfile.open("w") as log: + result = subprocess.run(args, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, + timeout=timeout, check=False) + return result.returncode + except subprocess.TimeoutExpired: + return 124 + except OSError: + return 127 + finally: + cleanup = {"confirmed_removed": False, "error_type": None} + try: + removed = subprocess.run(DOCKER + ["rm", "-f", name], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=False, timeout=30) + if removed.returncode == 0: + cleanup["confirmed_removed"] = True + else: + # --rm usually removed it already. A successful empty listing + # establishes absence; daemon errors never count as absence. + remaining = subprocess.run(DOCKER + ["ps", "-aq", "--filter", "name=^/" + name + "$"], + capture_output=True, text=True, check=False, timeout=30) + cleanup["confirmed_removed"] = remaining.returncode == 0 and not remaining.stdout.strip() + except (OSError, subprocess.TimeoutExpired) as exc: + cleanup["error_type"] = type(exc).__name__ + write_json(logfile.with_suffix(logfile.suffix + ".cleanup.json"), cleanup) + + +def download_verified(artifact, target): + if target.exists(): + if digest(target) != artifact["sha256"]: + raise ValueError("cached_artifact_hash_mismatch") + return + temporary = target.with_suffix(target.suffix + ".partial") + with urllib.request.urlopen(artifact["url"], timeout=120) as response, temporary.open("wb") as out: + shutil.copyfileobj(response, out) + if digest(temporary) != artifact["sha256"]: + raise ValueError("downloaded_artifact_hash_mismatch") + temporary.replace(target) + + +def install(prefix, state): + pins, _, _, skills = preflight(prefix, state) + for directory in (prefix, state, state / "cache", prefix / "skills"): + private_directory(directory) + # Source bytes are retained for reproducibility, not executed or patched. + download_verified(pins["source_archive"], state / "cache/upstream.tar.gz") + download_verified(pins["uv_lock"], state / "cache/upstream.uv.lock") + subprocess.run(DOCKER + ["pull", "--platform", pins["image"]["platform"], pins["image"]["ref"]], check=True) + image_id = subprocess.check_output(DOCKER + ["image", "inspect", "--format", "{{.Id}}", pins["image"]["ref"]], text=True).strip() + if image_id != "sha256:" + pins["image"]["config_sha256"]: + raise ValueError("image_configuration_hash_mismatch") + # A reviewable snapshot, with no repository metadata or host configuration. + if HERE.resolve() != (prefix / "recipe").resolve(): + shutil.copytree(HERE, prefix / "recipe", dirs_exist_ok=True, + ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) + for name, source in skills: + link = prefix / "skills" / name + if link.is_symlink() and link.resolve() == source: + continue + if link.exists() or link.is_symlink(): + raise ValueError("existing_skill_reference_conflict") + link.symlink_to(source, target_is_directory=True) + name = "openhands-install-" + uuid.uuid4().hex[:12] + args = docker_args(pins, name) + args += mount(prefix, prefix, False) + mount(state, "/state", False) + mount(prefix / "recipe", "/recipe") + args += ["--entrypoint", "/bin/bash", pins["image"]["ref"], "/recipe/install-container.sh", str(prefix)] + private_directory(state / "install-attempts") + attempt = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "install-attempts")) + code = execute_container(args, name, attempt / "install.log", 900) + write_json(state / "installation.json", {"version": pins["version"], "image": pins["image"]["ref"], + "requirements_sha256": pins["requirements_sha256"], "exit_code": code}) + if code: + raise RuntimeError("container_install_failed_see_private_install_log") + print("Installed pinned SDK wheels in owned container venv; model/MCP/E2E acceptance remains pending.") + + +def runtime_mounts(host, skills): + args = [] + for source in host["mcp_readonly_mounts"]: + args += mount(Path(source).resolve(), source) + for name, source in host["qmd_collections"].items(): + args += mount(Path(source).resolve(), "/documents/" + name) + for name, source in skills: + args += mount(source, "/skills/" + name) + return args + + +def run(prefix, state): + pins, host, mcp, skills = preflight(prefix, state) + installed = read_json(state / "installation.json") + if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: + raise ValueError("matching_successful_installation_required") + # The venv's interpreter link resolves inside the image, not on the host. + if not os.path.lexists(prefix / "venv/bin/python"): + raise ValueError("owned_sdk_venv_missing") + cfg = read_json(HERE / "config/worker.json") + private_directory(state / "runs") + result = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "runs")) + for directory in (result / "input", result / "worker", result / "mcp"): + private_directory(directory) + for name in mcp: + for child in ("cache", "config", "data", "state"): + private_directory(result / "mcp" / name / child) + serena_home = result / "mcp/serena/home" + private_directory(serena_home) + shutil.copyfile(HERE / "config/serena_config.yml", serena_home / "serena_config.yml") + shutil.copytree(HERE / "e2e/fixture-repo", result / "workspace") + write_json(result / "input/mcp.json", mcp) + runtime = cfg["runtime"] + stem = "openhands-e2e-" + uuid.uuid4().hex[:12] + window = {"started_at": utc_now(), "finished_at": None, "worker_exit_code": None} + stage = "baseline" + try: + name = stem + "-baseline" + args = docker_args(pins, name, network=False) + mount(prefix, prefix) + mount(HERE, "/recipe") + args += mount(result / "workspace", "/workspace") + args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], + "-I", "-B", "/recipe/e2e/check.py", "--baseline", "/workspace"] + code = execute_container(args, name, result / "baseline.log", 60) + baseline = json.loads((result / "baseline.log").read_text().splitlines()[-1]) + write_json(result / "baseline.json", baseline) + if code != 1 or baseline.get("tests_run") != 1 or baseline.get("failures") != 1 or baseline.get("errors") != 0: + raise ValueError("frozen_baseline_not_red") + base = mount(prefix, prefix) + mount(HERE, "/recipe") + mount(result / "input", "/run-input") + base += mount(result / "mcp", "/state/mcp", False) + mount(result / "workspace", "/workspace", False) + base += runtime_mounts(host, skills) + stage = "qmd_setup" + name = stem + "-qmd" + args = docker_args(pins, name) + base + args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/qmd-setup.py"] + if execute_container(args, name, result / "qmd-setup.log", 900) != 0: + raise RuntimeError("qmd_setup_failed") + stage = "agent" + name = stem + "-agent" + args = docker_args(pins, name) + base + mount(result / "worker", "/run-output", False) + args += ["--workdir", "/workspace", "--env", "OPENHANDS_OWNED_CONTAINER=1", + "--env", f"PATH={prefix}/venv/bin:" + host["variables"]["HOST_PATH"], + "--env", "XDG_CACHE_HOME=/run-output/cache", "--env", "XDG_STATE_HOME=/run-output/state", + "--env", "XDG_DATA_HOME=/run-output/data", "--env", "XDG_CONFIG_HOME=/run-output/config"] + selected_model = os.environ.get(cfg["model_env"], cfg["llm"]["model"]) + # Validate before forwarding. No API credential environment is forwarded. + from recipe import llm_config + llm_config(cfg, selected_model) + window["model"] = selected_model + args += ["--env", cfg["model_env"] + "=" + selected_model, + "--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/worker.py"] + window["started_at"] = utc_now() + window["worker_exit_code"] = execute_container(args, name, result / "worker.log", runtime["timeout_seconds"]) + except (Exception, KeyboardInterrupt) as exc: + # Retain failed attempts, but do not put private exception text in receipts. + window["failure_stage"] = stage + window["failure_type"] = type(exc).__name__ + finally: + window["finished_at"] = utc_now() + write_json(result / "window.json", window) + name = stem + "-check" + args = docker_args(pins, name, network=False) + mount(prefix, prefix) + mount(HERE, "/recipe") + args += mount(result, "/result") + args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], + "-I", "-B", "/recipe/e2e/check.py", "--result", "/result"] + check_code = execute_container(args, name, result / "check.log", 90) + try: + checked = json.loads((result / "check.log").read_text().splitlines()[-1]) + except (ValueError, IndexError): + checked = {"passed": False, "failures": ["checker_did_not_return_json"]} + checked["exit_code"] = check_code + write_json(result / "check.json", checked) + from receipt import create_receipt + receipt = create_receipt(result) + write_json(result / "receipt.json", receipt) + print(json.dumps({"task_passed": receipt["task_passed"], "evidence_complete": receipt["evidence_complete"]})) + return 0 if receipt["task_passed"] and receipt["evidence_complete"] else 1 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("action", choices=("install", "run")) + parser.add_argument("--prefix", required=True, type=Path) + parser.add_argument("--state", required=True, type=Path) + args = parser.parse_args() + os.umask(0o077) + # SIGTERM goes through container cleanup and the failed-attempt receipt path. + signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) + if args.action == "install": + install(args.prefix, args.state) + return 0 + return run(args.prefix, args.state) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/blueprints/runtime-workers/openhands/install-container.sh b/blueprints/runtime-workers/openhands/install-container.sh new file mode 100755 index 000000000..d50e615cd --- /dev/null +++ b/blueprints/runtime-workers/openhands/install-container.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Upstream SDK wheel install; uv lock hashes + uv's documented no-build-isolation. +set -euo pipefail +umask 077 +prefix="$1" +export UV_CACHE_DIR=/state/cache/uv +export UV_PYTHON_DOWNLOADS=never +export UV_NO_CONFIG=1 +export XDG_CACHE_HOME=/state/cache +if [[ ! -x "$prefix/venv/bin/python" ]]; then + /bin/uv venv --python /usr/local/bin/python "$prefix/venv" +fi +# func-timeout is an sdist. Preinstall every build prerequisite from the same +# upstream lock, then prohibit isolated build dependency downloads. +/bin/uv pip install --python "$prefix/venv/bin/python" --require-hashes --no-deps \ + --only-binary :all: --index-url https://pypi.org/simple \ + -r /recipe/build-requirements.lock +/bin/uv pip install --python "$prefix/venv/bin/python" --require-hashes --no-deps \ + --no-build-isolation --index-url https://pypi.org/simple -r /recipe/requirements.lock +/bin/uv pip check --python "$prefix/venv/bin/python" +"$prefix/venv/bin/python" -c 'import importlib.metadata as m; assert m.version("openhands-sdk") == m.version("openhands-tools") == "1.49.6"; from openhands.sdk import LLM, Agent, Conversation; from openhands.tools.terminal import TerminalTool; from openhands.tools.file_editor import FileEditorTool; print("SDK/tools 1.49.6 imports passed; no model request")' diff --git a/blueprints/runtime-workers/openhands/install.sh b/blueprints/runtime-workers/openhands/install.sh new file mode 100755 index 000000000..bb0169e13 --- /dev/null +++ b/blueprints/runtime-workers/openhands/install.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Source: SDK v1.49.6 Dockerfile and wheel installation path; pins.json + README.md. +set -euo pipefail +umask 077 +recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" +state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" +# host.py checks rootless Docker, source/requirement sha256, private host values +# and skill hashes before downloading or installing. No sudo, no service changes. +exec python3 "$recipe_dir/host.py" install --prefix "$prefix" --state "$state" diff --git a/blueprints/runtime-workers/openhands/mcp_guard.py b/blueprints/runtime-workers/openhands/mcp_guard.py new file mode 100644 index 000000000..638651949 --- /dev/null +++ b/blueprints/runtime-workers/openhands/mcp_guard.py @@ -0,0 +1,38 @@ +"""Native PreToolUse hook, following SDK example 33_hooks. + +QMD query scoping is an argument constraint, separate from tool-name filtering. +The four-collection index itself is created by native QMD collection commands. +""" + +import json +from pathlib import Path +import sys + + +def permitted(event): + if event.get("tool_name") != "qmd_query": + return True + args = event.get("tool_input", {}) + # MCPToolAction holds the actual parameters in data at this SDK pin. + args = args.get("data", args) + allowed = json.loads((Path(__file__).parent / "config/mcp-policy.json").read_text())["qmd"]["collections"] + selected = args.get("collections") + searches = args.get("searches") + return bool( + isinstance(selected, list) and selected + and all(isinstance(item, str) and item in allowed for item in selected) + and isinstance(searches, list) and searches + and all(isinstance(item, dict) and item.get("type") == "lex" for item in searches) + and args.get("rerank") is False and not args.get("query") + ) + + +def main(): + if not permitted(json.load(sys.stdin)): + print(json.dumps({"decision": "deny", "reason": "Use explicit allowed collections, lex searches and rerank:false."})) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/blueprints/runtime-workers/openhands/pins.json b/blueprints/runtime-workers/openhands/pins.json new file mode 100644 index 000000000..fa698d788 --- /dev/null +++ b/blueprints/runtime-workers/openhands/pins.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "version": "1.49.6", + "tag": "v1.49.6", + "repository": "https://github.com/OpenHands/software-agent-sdk", + "commit": "fcc102a697874d54a357e36004e02c95040dbdc0", + "source_archive": { + "url": "https://codeload.github.com/OpenHands/software-agent-sdk/tar.gz/fcc102a697874d54a357e36004e02c95040dbdc0", + "sha256": "2d3006de86eaec690a82e497fcd4459816637afe6e53f7da87783183792148a8" + }, + "uv_lock": { + "url": "https://raw.githubusercontent.com/OpenHands/software-agent-sdk/fcc102a697874d54a357e36004e02c95040dbdc0/uv.lock", + "sha256": "d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910" + }, + "image": { + "tag_observed": "1.49.6-python", + "ref": "ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37", + "platform": "linux/amd64", + "manifest_sha256": "ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12", + "config_sha256": "39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f", + "provenance": "GHCR returned manifest bytes SHA256; image config OPENHANDS_BUILD_GIT_SHA and OPENHANDS_BUILD_GIT_REF match the SDK tag." + }, + "wheels": { + "openhands-sdk": { + "url": "https://files.pythonhosted.org/packages/e9/ac/d27c5becc3b59b8cc46a1614ac478cc94eb73e626ab15831906fcdaafb29/openhands_sdk-1.49.6-py3-none-any.whl", + "sha256": "3b4701f125925e804929ab02b61fe2fe10b4fb1e7d723a2c4afdea35566328bb", + "filename": "openhands_sdk-1.49.6-py3-none-any.whl" + }, + "openhands-tools": { + "url": "https://files.pythonhosted.org/packages/68/20/27d6d20cc8f3465ec861c450bc0113c8646285aa6c8f4291b62366dcb8a2/openhands_tools-1.49.6-py3-none-any.whl", + "sha256": "11e57a6635fd93c9f1fed14f804ab6f24a4873f75a2d35f0d1c3e57df2c51c87", + "filename": "openhands_tools-1.49.6-py3-none-any.whl" + } + }, + "requirements_sha256": "c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56", + "build_requirements_sha256": "4485df6339240d8551488bb18f27e644e8bd55d7d50bf439ca9d9da03ab7318a", + "canvas": { + "tag": "v1.24.0", + "repository": "https://github.com/OpenHands/OpenHands", + "commit": "7dc6805406ea3c76cb4a3ce407c3c72d481b0ac6", + "install": false + } +} diff --git a/blueprints/runtime-workers/openhands/qmd-setup.py b/blueprints/runtime-workers/openhands/qmd-setup.py new file mode 100644 index 000000000..8726c470c --- /dev/null +++ b/blueprints/runtime-workers/openhands/qmd-setup.py @@ -0,0 +1,24 @@ +"""Native QMD setup, following qmd v2.8.3 src/cli/qmd.ts collection add/update. + +Only this worker's config/index is writable. No embed/model-download operation. +""" + +import json +import os +from pathlib import Path +import subprocess + + +os.umask(0o077) +config = json.loads(Path("/run-input/mcp.json").read_text())["qmd"] +environment = {**os.environ, **config["env"]} +collections = json.loads(Path("/recipe/config/mcp-policy.json").read_text())["qmd"]["collections"] +command = [config["command"], "--index", "native-agent-stack-catalog"] +# The generated configuration has only these fixed paths and collection names. +# Re-registering is unnecessary; native update refreshes an existing index. +config_file = Path(environment["QMD_CONFIG_DIR"]) / "native-agent-stack-catalog.yml" +if not config_file.exists(): + for name in collections: + subprocess.run(command + ["collection", "add", f"/documents/{name}", "--name", name, "--mask", "**/*.md"], + env=environment, cwd="/workspace", check=True, timeout=180) +subprocess.run(command + ["update"], env=environment, cwd="/workspace", check=True, timeout=300) diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py new file mode 100644 index 000000000..32250c661 --- /dev/null +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -0,0 +1,180 @@ +"""Sanitized evidence from native SDK events, independent check and gateway rows. + +Uses Python's sqlite3 URI mode=ro + query_only. The only SQL SELECT reads the +nine user-authorized columns from call_logs. No schema introspection, other +tables, credentials, prompts, responses, request identifiers or client config. +""" + +from __future__ import annotations + +from collections import Counter +from datetime import datetime, timezone +import json +from pathlib import Path +import re +import sqlite3 + +from recipe import HERE, read_json, tool_filter + + +COLUMNS = ( + "timestamp", "path", "status", "model", "reasoning_effort_requested", + "reasoning_effort_upstream", "tokens_in", "tokens_cache_read", "tokens_reasoning", +) +EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh", "max", "auto"} + + +def time_value(value): + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value / 1000 if value > 100_000_000_000 else value, timezone.utc) + if isinstance(value, str): + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + return parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed + raise ValueError("invalid_timestamp") + + +def gateway_rows(database, started_at, finished_at): + """Time-window evidence only: the allowed columns cannot correlate sessions.""" + start = time_value(started_at) + end = time_value(finished_at) + uri = Path(database).resolve().as_uri() + "?mode=ro" + connection = sqlite3.connect(uri, uri=True, timeout=3) + try: + connection.execute("PRAGMA query_only = ON") + # Timestamp storage may be ISO text, epoch seconds, or epoch milliseconds. + # All predicates still reference the single authorized timestamp column. + query = "SELECT " + ", ".join(COLUMNS) + " FROM call_logs WHERE " + ( + "(typeof(timestamp) = 'text' AND julianday(timestamp) BETWEEN julianday(?) AND julianday(?)) " + "OR (typeof(timestamp) IN ('integer','real') AND (timestamp BETWEEN ? AND ? OR timestamp BETWEEN ? AND ?)) " + "ORDER BY timestamp" + ) + rows = connection.execute(query, (started_at, finished_at, start.timestamp(), end.timestamp(), + start.timestamp() * 1000, end.timestamp() * 1000)).fetchall() + finally: + connection.close() + return [sanitize_row(dict(zip(COLUMNS, row))) for row in rows] + + +def sanitize_row(row): + output = {} + try: + output["timestamp"] = time_value(row["timestamp"]).astimezone(timezone.utc).isoformat() + except (ValueError, TypeError, OverflowError): + output["timestamp"] = None + output["path"] = row["path"] if row["path"] in {"/v1/responses", "/v1/chat/completions"} else "" + status = row["status"] + if isinstance(status, str) and re.fullmatch(r"[1-5][0-9]{2}", status): + status = int(status) + output["status"] = status if (isinstance(status, int) and 100 <= status <= 599) or status in {"success", "error", "failed", "ok"} else None + model = row["model"] + output["model"] = model if isinstance(model, str) and re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.:/-]{0,159}", model) else "" + for key in ("reasoning_effort_requested", "reasoning_effort_upstream"): + output[key] = row[key] if row[key] in EFFORTS else None + for key in ("tokens_in", "tokens_cache_read", "tokens_reasoning"): + value = row[key] + output[key] = value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None + return output + + +def observations(events): + """A call counts as observed only with a native ObservationEvent.""" + policy = read_json(HERE / "config/mcp-policy.json") + pattern = re.compile(tool_filter(policy)) + known_skills = {s["name"] for s in read_json(HERE / "config/skills.lock.json")["skills"]} + completed, failed, skills = Counter(), Counter(), Counter() + for event in events: + if event.get("kind") != "ObservationEvent": + continue + name = event.get("tool_name", "") + observation = event.get("observation", {}) + if any(name.startswith(server + "_") for server in policy) and pattern.fullmatch(name): + completed[name] += 1 + if observation.get("is_error"): + failed[name] += 1 + if name == "invoke_skill" and not observation.get("is_error"): + skill = observation.get("skill_name") + if skill in known_skills: + skills[skill] += 1 + return {"mcp_calls_observed": dict(completed), "mcp_errors_observed": dict(failed), + "skills_observed": dict(skills), "basis": "native ObservationEvent fields; registration alone is not usage"} + + +def create_receipt(result, database=None): + result = Path(result) + window = read_json(result / "window.json") + checked = read_json(result / "check.json") + events_path = result / "worker/events.jsonl" + events = [] + trace_status = "missing" + if events_path.is_file(): + try: + events = [json.loads(line) for line in events_path.read_text().splitlines() if line] + trace_status = "observed" if events else "empty" + except (ValueError, OSError): + trace_status = "unreadable" + db = database or Path.home() / ".local/share/omniroute/storage.sqlite" + rows, db_status = [], "unavailable" + try: + rows = gateway_rows(db, window["started_at"], window["finished_at"]) + db_status = "observed" if rows else "empty_window" + except (OSError, sqlite3.Error, ValueError, TypeError): + pass # Unknown usage is not zero usage. No raw exception text is exported. + pins = read_json(HERE / "pins.json") + versions = None + summary = result / "worker/native-summary.json" + if summary.is_file(): + try: + native = read_json(summary) + candidate = native.get("versions", {}) if isinstance(native, dict) else {} + if isinstance(candidate, dict) and all(candidate.get(name) == pins["version"] for name in ("openhands-sdk", "openhands-tools")): + versions = {name: candidate[name] for name in ("openhands-sdk", "openhands-tools")} + except (OSError, ValueError, TypeError): + pass + tests = checked.get("tests") or {} + # Never copy arbitrary checker output, file paths or error strings into public evidence. + allowed_failures = { + "missing_result", "frozen_fixture_changed", "diff_outside_allowed_files_or_empty", "nonregular_result", + "baseline_not_one_failing_test", "no_native_test_failure_before_edit", "tests_mutated_workspace", + "tests_not_passing_unskipped", "invalid_or_incomplete_result", "checker_did_not_return_json", + } + task_passed = checked.get("passed") is True and checked.get("exit_code") == 0 and window.get("worker_exit_code") == 0 + observed = observations(events) + expected_model = window.get("model") + matching_gateway_rows = [row for row in rows if ( + row["model"] == expected_model and row["path"] == "/v1/responses" + and row["status"] in (200, "success", "ok") and row["reasoning_effort_upstream"] == "max" + )] + cleanup = [] + for path in sorted(result.glob("*.log.cleanup.json")): + try: + cleanup.append(read_json(path).get("confirmed_removed") is True) + except (OSError, ValueError, AttributeError): + cleanup.append(False) + return { + "schema_version": 1, "evidence_class": "local integration on a synthetic fixture", + "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], "observed_versions": versions, + "image": pins["image"]["ref"], "source_commit": pins["commit"], + "window": {key: window[key] for key in ("started_at", "finished_at")}, + "worker_exit_code": window.get("worker_exit_code"), + "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"baseline", "qmd_setup", "agent"} else None, + "task_passed": task_passed, + "evidence_complete": bool(matching_gateway_rows) and trace_status == "observed" + and versions is not None and observed["skills_observed"].get("tdd", 0) > 0 and bool(cleanup) and all(cleanup), + "container_cleanup": {"attempts": len(cleanup), "confirmed_removed": sum(cleanup), "complete": bool(cleanup) and all(cleanup)}, + "gateway": { + "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, "rows": rows, + "matching_model_responses_max_rows": len(matching_gateway_rows), + "attribution": "Time window only; may include concurrent callers. Session/request identifiers are forbidden by this receipt contract.", + "totals": None, + "usage_note": "Rows are not summed or claimed as exclusive worker cost. Cache-read is a subset of input; missing counters remain null.", + }, + "trace_status": trace_status, **observed, + "check": { + "passed": checked.get("passed") is True, + "exit_code": checked.get("exit_code"), + "failures": [reason if reason in allowed_failures else "invalid_or_incomplete_result" for reason in checked.get("failures", [])], + "allowed_file_changed": "range_utils.py" in checked.get("changed_files", []), + "tests": {key: tests.get(key) for key in ("tests_run", "failures", "errors", "skipped", "exit_code")}, + }, + "limits": ["Not an upstream SDK test suite", "No matched token-savings comparison", "No SDK cold-start/crash-resume acceptance"], + } diff --git a/blueprints/runtime-workers/openhands/recipe.py b/blueprints/runtime-workers/openhands/recipe.py new file mode 100644 index 000000000..1b40e1564 --- /dev/null +++ b/blueprints/runtime-workers/openhands/recipe.py @@ -0,0 +1,99 @@ +"""Pure configuration helpers; SDK references are listed in README.md. + +No SDK imports, service calls, credential discovery or installation on import. +Codex tool limits are translated to the SDK's native filter_tools_regex. +""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import re +from string import Template + + +HERE = Path(__file__).resolve().parent + + +def read_json(path): + return json.loads(Path(path).read_text()) + + +def digest(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def tool_filter(policy): + """FastMCP 3.2.0 multi-server names use one underscore, not two.""" + parts = [r"terminal", r"file_editor"] + for server, limits in policy.items(): + if not limits.get("enabled", True): + continue + prefix = re.escape(server + "_") + if "enabled_tools" in limits: + parts.extend(prefix + re.escape(name) for name in limits["enabled_tools"]) + else: + denied = limits.get("disabled_tools", []) + exclusion = "(?!(?:" + "|".join(map(re.escape, denied)) + ")$)" if denied else "" + parts.append(prefix + exclusion + r"[A-Za-z][A-Za-z0-9_]*") + return r"^(?:" + "|".join(parts) + r")$" + + +def render_mcp(variables): + config = read_json(HERE / "config/mcp.template.json") + + def expand(value): + if isinstance(value, str): + return Template(value).substitute(variables) + if isinstance(value, list): + return [expand(item) for item in value] + if isinstance(value, dict): + return {key: expand(item) for key, item in value.items()} + return value + + config = expand(config) + for name, server in config.items(): + if server["transport"] == "stdio": + server["cwd"] = "/workspace" + server.setdefault("env", {}).update({ + "XDG_CACHE_HOME": f"/state/mcp/{name}/cache", + "XDG_CONFIG_HOME": f"/state/mcp/{name}/config", + "XDG_DATA_HOME": f"/state/mcp/{name}/data", + "XDG_STATE_HOME": f"/state/mcp/{name}/state", + }) + config["qmd"]["env"].update({ + "QMD_CONFIG_DIR": "/state/mcp/qmd/config", + "INDEX_PATH": "/state/mcp/qmd/catalog.sqlite", + }) + return config + + +def llm_config(config, model=None): + result = dict(config["llm"]) + selected = model or result["model"] + if not isinstance(selected, str) or not re.fullmatch(r"[A-Za-z0-9_./:-]+", selected): + raise ValueError("invalid_model") + # LiteLLM strips its openai provider prefix; the gateway receives selected. + result["model"] = "openai/" + selected + result["base_url"] = config["runtime"]["gateway_base_url"] + return result + + +def inventory(root): + """Byte and type inventory; symlinks, special files and additions are visible.""" + root = Path(root) + if not root.is_dir(): + return {} + result = {} + for path in sorted(root.rglob("*")): + relative = path.relative_to(root).as_posix() + if path.is_symlink(): + result[relative] = "symlink" + elif path.is_file(): + result[relative] = digest(path) + elif path.is_dir(): + result[relative + "/"] = "directory" + else: + result[relative] = "special" + return result diff --git a/blueprints/runtime-workers/openhands/requirements.lock b/blueprints/runtime-workers/openhands/requirements.lock new file mode 100644 index 000000000..fd21cfd03 --- /dev/null +++ b/blueprints/runtime-workers/openhands/requirements.lock @@ -0,0 +1,2860 @@ +agent-client-protocol==0.12.1 \ + --hash=sha256:9963ccb590ed96d7dc542c9c023e90676fc76b1c6562e21a9ee112f4fbcb0095 \ + --hash=sha256:aaf3cc301ed87d9e2c4ccb91adfe1cd58784355f6ce7e513f50e7ca2391862ef +aiofile==3.9.0 \ + --hash=sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa \ + --hash=sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b +aiofiles==25.1.0 \ + --hash=sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2 \ + --hash=sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695 +aiohappyeyeballs==2.6.1 \ + --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ + --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +aiohttp==3.14.3 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 +aiosignal==1.4.0 \ + --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ + --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 +annotated-types==0.7.0 \ + --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ + --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +anthropic==0.75.0 \ + --hash=sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb \ + --hash=sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b +anyio==4.11.0 \ + --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ + --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 +attrs==25.3.0 \ + --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ + --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b +authlib==1.6.12 \ + --hash=sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd \ + --hash=sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab +backoff==2.2.1 \ + --hash=sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba \ + --hash=sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8 +beartype==0.22.9 \ + --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ + --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 +beautifulsoup4==4.14.2 \ + --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ + --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 +binaryornot==0.4.4 \ + --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ + --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 +blake3==1.0.9 \ + --hash=sha256:021309d760b390706fecf13498f9a25aa8f689bbb65a0896029b8fa223aae18b \ + --hash=sha256:09a69fcedf06785bb81d4d3d39f95ee65dbaf2cb246e174cfc9ff64d027f7551 \ + --hash=sha256:0aba416bb2e3ef0c65e74d5eba21062483c714cd78e7e303c9d03c547fc7d015 \ + --hash=sha256:0cdfeff65488089ef86f7587c76055ff72b28d28d10e427b547f5711477c376d \ + --hash=sha256:128a62136c9a39c7cb9fdaa5fb38471f2418853da7f5a89f31495735d0ba6f2c \ + --hash=sha256:15566065ff90ab3da46ec0be1417406f00507af902b6fb0fbc6563e77f02fc42 \ + --hash=sha256:172d44245a19dfec08ab771c1b7a506b97783163cdc65f559fe020007e403c99 \ + --hash=sha256:1bbdff61e049297ef3180867ce1f079cea7e5b372fd76953c3183da5b8124206 \ + --hash=sha256:1bd981dc318c05375c3160a99df493b7cc4c83fffa1a34d14b18a071b47b262b \ + --hash=sha256:249e5964fa9e768924bc7cc3d4efe75a425bb5dd3fb7671c3eda8eeddfa50591 \ + --hash=sha256:28404301de485e9546365d01b30f65eaa835520c4211d6ef61242975b6722b60 \ + --hash=sha256:2a800b87433955f37691b5f361ad29c7dd3ee089c9cd109adc5aea8e24bc4c1f \ + --hash=sha256:32bd4521ec2d477627ad93eb70f9ac4d01e12d1489024159bcaeff79466332f6 \ + --hash=sha256:3cbe7f190164896dc3908e920716ee66bc31d40f1a0fb603ed59ac53290fb9cf \ + --hash=sha256:42609e4adc4b2d7423137f2cb35135bca598b925c5af09d2bc0a2c368b25aeb1 \ + --hash=sha256:42fa57bf462285ef16400601b0fd32214c248ba92505bbb94b1221ab9af5a092 \ + --hash=sha256:4865a8cfb2b3d7c0baf5267f2fa6816a3384e836cd1bd0caf359f406cb1e8fba \ + --hash=sha256:508ccaf8f9377cc47e6026c2897fdc37de61faeb1420dc023b6379cc2474eb65 \ + --hash=sha256:5b35abe24a66a7b3db423eb4f8668ed7be1a362aa9c0024ab6483ec0b2c16058 \ + --hash=sha256:5ea0c60dd9c1e3d05610606579e4bf80f562854c46ed55f9ee8545e18987a480 \ + --hash=sha256:5fa374fa5070ca084368776c19b420157eb0f2d3f091343d6bc59189929d62e2 \ + --hash=sha256:66c84dbc2a31eda88b55bbf5c5b711037bf0698eba0fd1faf06bdaf313c39048 \ + --hash=sha256:6879739e7904b9c42afbedbcc2e8c36cebe140fb3fc3f5c492993579cf5cd516 \ + --hash=sha256:689a7e4069de681d9c5d9445b8b6473ee880ad04d7960a6789c60bd788980250 \ + --hash=sha256:69ff5aebc7650954443aa701feff2028d7c7ea5b5e18ee265f15e2104e892328 \ + --hash=sha256:6edeb3d49a24c307995899b70dd47aa901d0e9ad51d2f8a79aba4f074f32d8c5 \ + --hash=sha256:72cdecf088a9d25e6ec79948a578995649b0dbee407e7a46c543a9ecc0f6f281 \ + --hash=sha256:73a48f7e9f0e047f51a445d9b0361ab1907bdc72b6857815a84dacd2e59556f8 \ + --hash=sha256:766f1555cbe614f14f399c2fbec0983568d20edb36837ba04040807eb9e1a609 \ + --hash=sha256:83b4a2336105af3800f7e17ac4b943f293a3927a2d66a6308d50dba944a6953e \ + --hash=sha256:8a99f896e7718050ed033a888245098aab3d6a5338f91cc9450c563b53f90ad5 \ + --hash=sha256:8adb0b0032e53919ee95b3d4f911448d3268316c28cd7df232ff2a1e7c9a4ba4 \ + --hash=sha256:91db52a809b68b5bebe7c413ddcd230e1f759398e7fa7a873104595a4fa648b6 \ + --hash=sha256:95fc3545f80901b0dcd0508d16bc40f15ae39556709fa6cf86675f742d4f3c9c \ + --hash=sha256:9767f16199b99aa022b61ff825ac4dbd39864bf637ae712605a2ce1f8b6a55e0 \ + --hash=sha256:9cef6d4d07a7de0c44f5ba17f6383d55276d9efc8d601f75113538fcaa35008b \ + --hash=sha256:a288664d08dee154cc496e06e62517fc9e655ecec12b0d7db538d244ac79edf1 \ + --hash=sha256:a70c20542d5e7960983a0ff32999049a2b0e5ef1f22dbbbdfb51cf04828a4156 \ + --hash=sha256:ab0c030cf6644c30e786b0e785bde4e4596013ae9ea6ce9877e39d52383e25d7 \ + --hash=sha256:ae47c3d5729ff89baa6ddf6de47fcfcc915985d39eb1bfcd6db653331f3c6fcc \ + --hash=sha256:b25ccde5a64be070f20e5c7a81da70292db40b164b6c77588cbd6230856badbb \ + --hash=sha256:b27550ada40f839aca64c66127940e4318bb6ef3e291890ef913017f6f637448 \ + --hash=sha256:b5d5bf0f68cd77108a942c95db98e960d9c3d5643b95172f783822ce22667759 \ + --hash=sha256:bcd56a7a972c4185070f7042ccc20166927eec3c0f98b8405f375d007b604a0b \ + --hash=sha256:c7f648fa425138452d1e585ac625c7aefddb946d9765906c4c12d564a1523cd8 \ + --hash=sha256:ca7dfe8fb197ff8a3f5c915424183ccd52a99e8afb12680f51b2e1f4c9c6c97f \ + --hash=sha256:caded2806d2cbeed638c5e2517ed8b2a94165b3452fda35e72896142d22070e0 \ + --hash=sha256:cfaa671b07eb73883162ca940442193868358b0b904cfa266e4b74131ce966da \ + --hash=sha256:d1ea0bf17b184b03444007646d902207d2b4d4f3e91a0cac3836552d83db74b9 \ + --hash=sha256:dab59b324aa65c09e937d6c43de5de85ec9581627f4e79dcc9806d85b54a1c34 \ + --hash=sha256:eca281fedcbe5c56655bd5a4176e6036eddbbe57df96114a03838fce08b1e0ca \ + --hash=sha256:f5c9d57f0dcb92243b6ae575c3065793edc9df9008d0ebd98d8245cdeb7c3f84 \ + --hash=sha256:f65d77eb05331495485048f6804f53885b192b998acb7e6fe1487d941bf08435 +browser-use==0.11.9 \ + --hash=sha256:a66accf4dded24d056eb2733e354e159fd7d82cab986d75dc0f14b395467c82c \ + --hash=sha256:c3d8a420acf26d26c688c42916358ba546e7159e1a699c06064b899cb7927577 +browser-use-sdk==2.0.15 \ + --hash=sha256:0832ae0998736e6386457e6cf506e2820db0d626c4d9dbf0f567ea2b6c6888d3 \ + --hash=sha256:5d8ba3836070c67a9774baac556b233e22f266823d3dee34295d98869bec7752 +bubus==1.5.6 \ + --hash=sha256:1a5456f0a576e86613a7bd66e819891b677778320b6e291094e339b0d9df2e0d \ + --hash=sha256:254ae37cd9299941f5e9d6afb11f8e3ce069f83e5b9476f88c6b2e32912f237d +cachetools==7.1.8 \ + --hash=sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43 \ + --hash=sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b +caio==0.9.25 \ + --hash=sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40 \ + --hash=sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6 \ + --hash=sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7 \ + --hash=sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10 \ + --hash=sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f \ + --hash=sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77 \ + --hash=sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb \ + --hash=sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79 \ + --hash=sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69 \ + --hash=sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64 \ + --hash=sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7 \ + --hash=sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451 \ + --hash=sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db \ + --hash=sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044 +cdp-use==1.4.4 \ + --hash=sha256:330a848b517006eb9ad1dc468aa6434d913cf0c6918610760c36c3fdfdba0fab \ + --hash=sha256:e37e80e067db2653d6fdf953d4ff9e5d80d75daa27b7c6d48c0261cccbef73e1 +certifi==2025.8.3 \ + --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ + --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 +cffi==2.0.0 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 +chardet==5.2.0 \ + --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ + --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 +charset-normalizer==3.4.3 \ + --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ + --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ + --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ + --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ + --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ + --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ + --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ + --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ + --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ + --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ + --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ + --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ + --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ + --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ + --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ + --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ + --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ + --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ + --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ + --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ + --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ + --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ + --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ + --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ + --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ + --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ + --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ + --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ + --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ + --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ + --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ + --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ + --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ + --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ + --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 +click==8.1.8 \ + --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ + --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a +cloudpickle==3.1.2 \ + --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ + --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a +colorama==0.4.6 ; sys_platform == 'win32' \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +cryptography==48.0.1 ; platform_machine == 'x86_64' and sys_platform == 'darwin' \ + --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ + --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ + --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ + --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 +cryptography==50.0.0 ; platform_machine != 'x86_64' or sys_platform != 'darwin' \ + --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ + --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ + --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ + --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ + --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ + --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ + --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ + --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ + --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ + --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ + --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ + --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ + --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ + --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ + --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ + --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ + --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ + --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ + --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ + --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ + --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ + --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ + --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ + --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ + --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ + --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ + --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ + --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ + --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ + --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ + --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ + --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ + --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ + --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ + --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ + --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ + --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ + --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ + --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 +cyclopts==4.6.0 \ + --hash=sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2 \ + --hash=sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5 +cython==3.1.4 ; platform_system != 'darwin' and sys_platform == 'darwin' \ + --hash=sha256:0d26af46505d0e54fe0f05e7ad089fd0eed8fa04f385f3ab88796f554467bcb9 \ + --hash=sha256:52eae5d9bcc515441a436dcae2cbadfd00c5063d4d7809bd0178931690c06a76 \ + --hash=sha256:66ac8bb5068156c92359e3f0eefa138c177d59d1a2e8a89467881fa7d06aba3b \ + --hash=sha256:9aefefe831331e2d66ab31799814eae4d0f8a2d246cbaaaa14d1be29ef777683 \ + --hash=sha256:ab549d0fc187804e0f14fc4759e4b5ad6485ffc01554b2f8b720cc44aeb929cd \ + --hash=sha256:b2d522ee8d3528035e247ee721fb40abe92e9ea852dc9e48802cec080d5de859 \ + --hash=sha256:c7258739d5560918741cb040bd85ba7cc2f09d868de9116a637e06714fec1f69 \ + --hash=sha256:d194d95e4fa029a3f6c7d46bdd16d973808c7ea4797586911fdb67cb98b1a2c6 +deprecation==2.1.0 \ + --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ + --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a +distro==1.9.0 \ + --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ + --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 +dnspython==2.8.0 \ + --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ + --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f +docstring-parser==0.17.0 \ + --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ + --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 +docutils==0.22.2 \ + --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ + --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 +email-validator==2.3.0 \ + --hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \ + --hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426 +exceptiongroup==1.3.0 \ + --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ + --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 +fakeredis==2.34.1 \ + --hash=sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12 \ + --hash=sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b +fastmcp==3.2.0 \ + --hash=sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef \ + --hash=sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681 +fastuuid==0.14.0 \ + --hash=sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995 \ + --hash=sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796 \ + --hash=sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed \ + --hash=sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab \ + --hash=sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26 \ + --hash=sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75 \ + --hash=sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714 \ + --hash=sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b \ + --hash=sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8 \ + --hash=sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87 \ + --hash=sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8 \ + --hash=sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021 \ + --hash=sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a \ + --hash=sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09 \ + --hash=sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176 \ + --hash=sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4 \ + --hash=sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc \ + --hash=sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad \ + --hash=sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f \ + --hash=sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f \ + --hash=sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f \ + --hash=sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741 \ + --hash=sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5 \ + --hash=sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad \ + --hash=sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057 \ + --hash=sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b \ + --hash=sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d \ + --hash=sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022 \ + --hash=sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070 \ + --hash=sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397 \ + --hash=sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a \ + --hash=sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06 \ + --hash=sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8 \ + --hash=sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad +filelock==3.20.3 \ + --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ + --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 +frozenlist==1.8.0 \ + --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ + --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ + --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ + --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ + --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ + --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ + --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ + --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ + --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ + --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ + --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ + --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ + --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ + --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ + --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ + --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ + --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ + --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ + --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ + --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ + --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ + --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ + --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ + --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ + --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ + --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ + --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ + --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ + --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ + --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ + --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ + --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ + --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ + --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ + --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ + --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ + --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ + --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ + --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ + --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ + --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ + --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ + --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ + --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ + --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ + --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ + --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ + --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ + --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ + --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ + --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ + --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ + --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ + --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ + --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ + --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ + --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ + --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ + --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ + --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ + --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ + --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ + --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ + --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ + --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ + --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ + --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ + --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ + --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ + --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ + --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ + --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ + --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ + --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ + --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ + --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ + --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ + --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ + --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ + --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ + --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ + --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd +fsspec==2025.9.0 \ + --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ + --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 +func-timeout==4.3.5 \ + --hash=sha256:74cd3c428ec94f4edfba81f9b2f14904846d5ffccc27c92433b8b5939b5575dd +google-api-core==2.26.0 ; python_full_version < '3.13' \ + --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ + --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 +google-api-core==2.31.0 ; python_full_version >= '3.13' \ + --hash=sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2 \ + --hash=sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab +google-api-python-client==2.184.0 \ + --hash=sha256:15a18d02f42de99416921c77be235d12ead474e474a1abc348b01a2b92633fa4 \ + --hash=sha256:ef2a3330ad058cdfc8a558d199c051c3356f6ed012436c3ad3d08b67891b039f +google-auth==2.53.0 \ + --hash=sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68 \ + --hash=sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c +google-auth-httplib2==0.2.0 \ + --hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \ + --hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d +google-auth-oauthlib==1.2.2 \ + --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ + --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 +google-genai==1.75.0 \ + --hash=sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf \ + --hash=sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c +googleapis-common-protos==1.70.0 \ + --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ + --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 +groq==0.32.0 \ + --hash=sha256:0ed0be290042f8826f851f3a1defaac4f979dcfce86ec4a0681a23af00ec800b \ + --hash=sha256:fb1ade61f47a06d1a1c1dc0fab690d269b799ebd57ad1dd867efaeaa7adeb2af +grpcio==1.76.0 \ + --hash=sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280 \ + --hash=sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd \ + --hash=sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc \ + --hash=sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba \ + --hash=sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03 \ + --hash=sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2 \ + --hash=sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb \ + --hash=sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958 \ + --hash=sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468 \ + --hash=sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc \ + --hash=sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09 \ + --hash=sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980 \ + --hash=sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f \ + --hash=sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882 \ + --hash=sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae \ + --hash=sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77 \ + --hash=sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e \ + --hash=sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73 \ + --hash=sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8 \ + --hash=sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3 \ + --hash=sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e \ + --hash=sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42 \ + --hash=sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6 \ + --hash=sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11 \ + --hash=sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a \ + --hash=sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347 \ + --hash=sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4 \ + --hash=sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8 \ + --hash=sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8 \ + --hash=sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc \ + --hash=sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62 +h11==0.16.0 \ + --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ + --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 +hf-xet==1.1.10 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' \ + --hash=sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06 \ + --hash=sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97 \ + --hash=sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045 \ + --hash=sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d \ + --hash=sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435 \ + --hash=sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b \ + --hash=sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c \ + --hash=sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f +httpcore==1.0.9 \ + --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ + --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 +httplib2==0.32.0 \ + --hash=sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025 \ + --hash=sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816 +httpx==0.28.1 \ + --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ + --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad +httpx-sse==0.4.3 \ + --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ + --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d +huggingface-hub==0.35.3 \ + --hash=sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba \ + --hash=sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a +idna==3.15 \ + --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ + --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc +importlib-metadata==8.5.0 \ + --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ + --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 +inquirerpy==0.3.4 \ + --hash=sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e \ + --hash=sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4 +jaraco-classes==3.4.0 \ + --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ + --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 +jaraco-context==6.1.0 \ + --hash=sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f \ + --hash=sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda +jaraco-functools==4.4.0 \ + --hash=sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 \ + --hash=sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb +jeepney==0.9.0 ; sys_platform == 'linux' \ + --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ + --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 +jiter==0.11.0 \ + --hash=sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1 \ + --hash=sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33 \ + --hash=sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206 \ + --hash=sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72 \ + --hash=sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4 \ + --hash=sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1 \ + --hash=sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03 \ + --hash=sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c \ + --hash=sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada \ + --hash=sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb \ + --hash=sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6 \ + --hash=sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921 \ + --hash=sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73 \ + --hash=sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b \ + --hash=sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5 \ + --hash=sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406 \ + --hash=sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4 \ + --hash=sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0 \ + --hash=sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80 \ + --hash=sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72 \ + --hash=sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99 \ + --hash=sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba \ + --hash=sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7 \ + --hash=sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a \ + --hash=sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774 \ + --hash=sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df \ + --hash=sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64 \ + --hash=sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758 \ + --hash=sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6 \ + --hash=sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2 \ + --hash=sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd \ + --hash=sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773 \ + --hash=sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2 \ + --hash=sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09 \ + --hash=sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591 \ + --hash=sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166 \ + --hash=sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2 \ + --hash=sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982 \ + --hash=sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0 \ + --hash=sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40 \ + --hash=sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471 +joserfc==1.7.5 \ + --hash=sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159 \ + --hash=sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea +json-repair==0.60.1 \ + --hash=sha256:841661cdd2df507c9a4e189097f38ca6bc372e06d4b4e36d72e590f68176c290 \ + --hash=sha256:ba6ff974f2a8bef2f7768144a7f03f870a816443f03da27a49cdd0ec31a78049 +jsonref==1.1.0 \ + --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ + --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 +jsonschema==4.23.0 \ + --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ + --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 +jsonschema-path==0.3.4 \ + --hash=sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001 \ + --hash=sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8 +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d +keyring==25.7.0 \ + --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ + --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b +libtmux==0.53.0 \ + --hash=sha256:024b7ae6a12aae55358e8feb914c8632b3ab9bd61c0987c53559643c6a58ee4f \ + --hash=sha256:1d19af4cea0c19543954d7e7317c7025c0739b029cccbe3b843212fae238f1bd +litellm==1.93.0 \ + --hash=sha256:0784172435de48f66ef7ad89d421604db1ad0db1321ef7f39dbcfe6b20111417 \ + --hash=sha256:140bf215e264c71601bca9c06d2436c5451bb59e1e195ea23fc2d3d87b6929ec \ + --hash=sha256:1a476ebc340c070c982eab15b4673fa63a70f5936935f9f20e4d2acb5f35d23b \ + --hash=sha256:23b8eea4fb8b3b6ade05e7b6085ec9ca12daffcfb38d033d0bbce9c1d5894da5 \ + --hash=sha256:3daf5c5aceb07f5d68871071e0ecdc678caccebadca9143cc00bb76f5a8c54e8 \ + --hash=sha256:98c15e84d32e922a821c105308bf9ddae8700de9ed396530bee2cbb1cacf4cbb \ + --hash=sha256:cd70ccd4ba3ef1a395535c287bce72d30c7d937ecca4290c0db37a00738f7ada +lmnr==0.7.60 \ + --hash=sha256:35076b302a8f4349824deb08f79d1c7e4c2486b5a14704c0e6efee9879942096 \ + --hash=sha256:efec73de28eb8b66bcda6aaab7db0bda8247a26f982e63295a754412bee607f0 +lmnr-claude-code-proxy==0.1.23 \ + --hash=sha256:0147eecb2704a39eceaf01a23768be1de49c3f2f909d5ad5e5ad5a1c8a4b8d94 \ + --hash=sha256:0ca8b8badb3bf137fc58cf3e98542869d983691b91d431508c1f92d8c7b1267c \ + --hash=sha256:0fd8e1bfeef82802c8a74533a6d27e7c7912c7db68358ca67976f1635311503f \ + --hash=sha256:1304486204ddc39a3f5639153df6ae4ee53152ea83063b0ce3331360f05ee026 \ + --hash=sha256:18147e58da5c3cd2901adfbbdf0851c5aef4a0feb0fed66732f4f0ef64549531 \ + --hash=sha256:1b04bca678761d37e60508494b8c04c3368bf23bd6569a4d215e93ca5daac89a \ + --hash=sha256:207f6d423467909a130308d2c33eba05b25232e6934c9c23b928b24037d51f3b \ + --hash=sha256:21fdef811f3f06fc507ffd795bca64a21b525faa7f3f6e2f33856910d0f96192 \ + --hash=sha256:29e19c7fb7d14e6df820a6c2e4a8ee62f13974fc23d966086a75f1d3d90bc68a \ + --hash=sha256:2d6c4bf49bd060146577022d2f45a9921ce5604289d7f52db357fd3d5aeac838 \ + --hash=sha256:44b270a4c40daf3b11a5d96694357c4ff99842e82c514281b2910eae4a5d653a \ + --hash=sha256:48479d6205854919139c748cac2407a39061eca363ce00a2664e05623933cada \ + --hash=sha256:4a42afb4bc589f0d1d6947b88d8a33e3c44029620212d143320875a3d997331a \ + --hash=sha256:50a9659f4e06cdf783740cef04811e09017e091993da2d26aafef6e0cc1526a9 \ + --hash=sha256:53c6d47ad2bea70dda75a642a3d8fa7b4fbb68c718958d37a5f994ff6f98d210 \ + --hash=sha256:5853e6fa123af8611c53b3fcac86231a6944d0861fe7808cdc2281c8168d66bd \ + --hash=sha256:5b59f21e7baa11212e61a300736fca7c2a724950811f1301e605c7d08c886fd5 \ + --hash=sha256:5bb8df798ad59b5ba096a7ce1c9a1a08fab5fcb9a6701b3e304bac6a060f109c \ + --hash=sha256:5e67485c946f716e6f4c7522c67ecb0a7925f3ed15a0d40bafd4632409c1acc6 \ + --hash=sha256:5f90acf340a618cfa445bcff29b0c942f231368fcaaca590755ca47960437373 \ + --hash=sha256:6704d8e779950f643c901d1c298428ed06eb331f6e86dd95758fd89ebb17f9cb \ + --hash=sha256:68fdb6a26d30977b5d4e7ce4a117b2a427a16b1cc0d075f4cecd1839da12975a \ + --hash=sha256:6bfb54627cf1a400e18e4c51a7bcb312da1bac701ef56059c82f094d5edf75c7 \ + --hash=sha256:6d7fe54463ff075d557dc1941d7d271bc79e2663e2a90ab1ff45594804b6958c \ + --hash=sha256:6d8fa044c2f5774a6b70de9025a91c13c74361d4349ae528b4b4fcca48219a13 \ + --hash=sha256:7164c71707717a6e242ef5fe0ddeb33095a69e8566a80bb979f4d81037e4dcf2 \ + --hash=sha256:7192a72122633ce7d92e2ae4d3df53854c2aa3dddccf4f8c5155d0dfe3e574ab \ + --hash=sha256:724ac0371eef99c034762521e580a986fc2b012ffc8c650dd8d66f1765d9a102 \ + --hash=sha256:7b883a547e444a095f93eebbc42ed0d27e84346d986251097b7cb62fc2a0a492 \ + --hash=sha256:7c372c3678aa21d422644bef5955a68b4b53cafb40a0c0804b09eab418443fb8 \ + --hash=sha256:8646625c70cd2730799e6d3266cf42af86c47101e7eae7254e2c6b482c4db05f \ + --hash=sha256:88c39ab3a86fcafcad48297bda0f91bd1b2d0e4d9dbf4ac2a94e7ecace3f6165 \ + --hash=sha256:8b43a95fdf053fc4ebc0723363d9f88aa5060078733f9df6b3e991235c473a96 \ + --hash=sha256:92233f1c19971678225ac884d26b77cbbe9e90ec0a154752f8feee9ffa3f21c2 \ + --hash=sha256:962aafb103e435461a66c3ca0a77bfddb7d2bde5abe56922f418076472eb27cc \ + --hash=sha256:97bd9997af125df8f2a0fb72ac135c0c0fe89ae67f497ac5da51b45ac142f5a4 \ + --hash=sha256:9cccd9219743a8539ab9711fe01e84eb91d95388b19a3bfaa0d215cc946d7bf2 \ + --hash=sha256:9e7a58160b8c327e9fddbba828c86bb1084967519a160ae4f64d0ae01d65730b \ + --hash=sha256:a3769f25f3341ae04885f3b8668fd48214b8eb9b72faa97b9d7dbc9095c8a40d \ + --hash=sha256:a3a3ec1b6b015d8fb6fb373cedb55bfc90c1187333456eb9e6a4a8124b23ee1d \ + --hash=sha256:a44d51db3002164ea5795721e3d8d8927f7981a89ffe1ae9359b6b69ebe02836 \ + --hash=sha256:a5ace97624a755d5c79fcbf9d0ab6564ac17a9d1dc361747fcfa299a3a004249 \ + --hash=sha256:a77258d487989bf23ec6807e21cd2c334e9e457e9f854875a0c253cb04260387 \ + --hash=sha256:a99809eea013706b24b260a2f095551d2b82ddd6e0655e0b0ff2a5d468fdfbea \ + --hash=sha256:b4288560b0076ba68d6103065043d5258594eed4d1fe59631a74e20c6cddf9c2 \ + --hash=sha256:b5c64fcdd623ceec9f41cf0cd6761409a1c0add4e85409de294faf0647f5eb86 \ + --hash=sha256:bcd83941a84b89fc2f5dac40b10b7736a5602766f33afcd554545b0bf770fdc2 \ + --hash=sha256:cff0ef6dbe34324bada6d538a8dd71bae07184715f6e9ba8408ab85bf251b1ab \ + --hash=sha256:cffbd2436215675119b94de508d20a87c49e0bcf9de7b2dba1974f1af35f0bfc \ + --hash=sha256:d2b41beaf87b99e12943dfd4c28343b9fbf7771582ac68bb8dbb847eeb7b7446 \ + --hash=sha256:d575823c9002c55e67a4e8277ff0477739f84b419e3356a974a3dbedbda90b63 \ + --hash=sha256:d7ac385abe119b21aaf8ab1c1aee7f9dcdd8a03a3a00aad331f3326b6aac075c \ + --hash=sha256:e140b8e9c0b56e63ef86e8ccda0faf6fa9e171c3d0efa11467afd35853770daf \ + --hash=sha256:e5bf63f3f99f011200a6efcc3312a8a2c72cf4a7457580056654df71ef4cd87b \ + --hash=sha256:e747afab3475d07f0c8941e7d1bbb27d2e88284afef03b7824fc40175adb047c \ + --hash=sha256:e8ef14e4b592cfd27d057b47a25e9f62e6d900a95ed244c4acb3316b76f6034f \ + --hash=sha256:e9d9d94f552a1ae8e7f871b8af2440653b614ba818054c60080d457a6fefd07a \ + --hash=sha256:f05a1978c707ba5df0be531dd2cac0da8117c86c6172d84cb4f5180c8123fc49 \ + --hash=sha256:f06ff39da6c5bdedd7daadd4b33337aacc33b51ed57aa6a6a4f035de243ab59e \ + --hash=sha256:f82487d932680337e3335573a8d582cfd02cee5bc95a4e28cb4befa38b658a27 \ + --hash=sha256:f8d636068b2b141f210dcadf433f86dc69f810245771f95f647c1a57d2a37dd9 +lupa==2.8 \ + --hash=sha256:1628371c6592a6d5650497a9e31fb2bb3a7e9883c1f301d1111265e484045af9 \ + --hash=sha256:24b4d8af5558e549b70daf1547f5c1c1d664ecea9fc790f83efe5d75e9a93797 \ + --hash=sha256:250e035fdaffe8c87093e3ebc206ac29a26131b1568ea711d780c26001ce96e7 \ + --hash=sha256:27044f3363047f946b3d3aab9157cbd172b3538ada9ec1baef43432bf7d03a78 \ + --hash=sha256:281bedc5deb92d31e649a3552edd662449365a635904fa4d5cb4509c7245e34e \ + --hash=sha256:2e64acbbd47e9b82a64405a39e0d2b36a5a7dad8ab41c0f3437f572f7d282ba3 \ + --hash=sha256:348c3f8ecabb6324dcbc05c2740d762ef8fcec7b06c79e45262ab97a217684e3 \ + --hash=sha256:360056453a7a4eaa4ac5a204c31a5a014b1eb2ee5490603234d2ba831684f1f2 \ + --hash=sha256:3903c9cf628dae2f56405503247b77a61a3a61bd2dda470e336950c74776d55d \ + --hash=sha256:4203fa1659315e939a5304e75001b8cc14234fb3cbb3ed86c049b0cc5d90fcee \ + --hash=sha256:450650f91c48c2415b0d59ab3abfcfda3b6efb5b858205f4d4bda8ad141fa529 \ + --hash=sha256:45fc9da0145ecb0083ef5ff9975116cc784bd0258bdc2bd131ba15483ce18398 \ + --hash=sha256:4f7c553c1d8cfffbe85d81daef730d12cae4b6002d457542914da0ac8a1145b3 \ + --hash=sha256:4f81a02806e7c7ad26d8c6fa222c8bef1b0c1b124347c879be880b41339d41e4 \ + --hash=sha256:4fe5d7a810b64ea8511eb885fc8cdde042ee5ff7b7d08ae78f32449756acb177 \ + --hash=sha256:54cff414f21f8cd8c6be4aae52541f3b9cd39602b59e3a3db9b5c9f9f674ff18 \ + --hash=sha256:58e18afed57955b41130e269c78f53d4123ab86e236b53816f4cbffa25cb5d30 \ + --hash=sha256:7f210d5a8353e510ea1199c42cf3cbdd630553bf2bc8fb4c00fea06fdec7c798 \ + --hash=sha256:81f2d843ce668b653146c007467570210ae44be51dac6926666c51d49536f307 \ + --hash=sha256:891f72e0bffbed1e4175f975aeb2a083956586a100066525e1be485f617f7b25 \ + --hash=sha256:8cf4f064a0e5531afce2d7d750120c10c10f9529139af6ca6150d13151034398 \ + --hash=sha256:91d622777febda3ab1bed1d45295f2f32a4680c7b3d7caf8c669998ed5c44118 \ + --hash=sha256:951496471056061598a7d1729a6cdf48d662fec777a9f2d8aa5a1e62fd30e5a5 \ + --hash=sha256:9e0d11b8f3a8dac6413f704fef7161d048bb10c58bdac6cbffa5e60efa56e9a3 \ + --hash=sha256:9e304fb1c50cf23fd8882afbe1aa87525ef8a72667bcab3b37b2bbb2bc542269 \ + --hash=sha256:a295f87b5b7ebbfd5191932e8cb0e51df3c7769101ac6b6c7d7c9fb27bfd1307 \ + --hash=sha256:a591b9947ca347b41a63370e121d6e2b1458fe6dde9ae065029ec10a37f25ff4 \ + --hash=sha256:ac6b6e8d0e617e26a98cbb44880bcd75de5d32b3ad7b3b3793583909292b47ed \ + --hash=sha256:b036738282a5acd2e71fdddb317c9df8b87c1673aa57f403d05fcc2be8abc4ba \ + --hash=sha256:b9bddb09acfffb4f828f790f444b11dc0cca591afea1a244d9329eea2d20c003 \ + --hash=sha256:ba3a7dd839f90c3d2e53bebe3c192b1f3f9fd720a6781256405123211fd0dce6 \ + --hash=sha256:bfc470012ef66ad064c7bd77416af03a3452ef630b04b9012595ea13f2e54518 \ + --hash=sha256:c2a5fd15dc62374e1661a55f01744c9ec1c56f291ba4a0749d3af2174556e78f \ + --hash=sha256:ce86dff1ee7f7cf45f5622065ae991949dd7bb1703581cbc58a630137bb7ccf9 \ + --hash=sha256:ce9404c661dbac65cc9bed351ad45e797af93d30d70be309a3fa8209ac86d93b \ + --hash=sha256:d3d0cde2c77588d1c60875a4f34f059513476c6e1775351897195b51e0f3df08 \ + --hash=sha256:d7edb13a7a5250b5c6c22d1495d9e842b5c9fc5081c8fe6b5efe2112fe3e41f9 \ + --hash=sha256:d8022641b9ec8ecf2c5ecbe9f47e5a70e0b87c4b5ae921b92cb02a638e0acd08 \ + --hash=sha256:d8766aff03a78c80ad2d188a8bdb216de5ec838359cd87e05bbdfa56394a6105 \ + --hash=sha256:dc51250e76367a3e27fcd01dc769b9bfcbbc34f48df48dde53d6af6e75b7eaa5 \ + --hash=sha256:f4342f4de76ae7ce2ab0672d36003bdb7e1a33252f293b569298ddd792e70e33 \ + --hash=sha256:f4d01b2a08c70bbb883a9e082b6b36b89121ed5910b710f1ba11c73295ff4fba \ + --hash=sha256:f711a8ab0486b9ac6fdda94a22ddcfbc9f0d4a27e3a8cf1bf79c6e48b33017c1 \ + --hash=sha256:f8a22088a552828958603323f0a5c4b3e11e03b75d0bf4c965ef879de9b60a8d \ + --hash=sha256:fc47f536ac13a79cef47d29a2b205576a22841f042a2bcec1676b95806e7706a +lxml==6.1.0 \ + --hash=sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2 \ + --hash=sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773 \ + --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ + --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ + --hash=sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c \ + --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ + --hash=sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd \ + --hash=sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405 \ + --hash=sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b \ + --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ + --hash=sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac \ + --hash=sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62 \ + --hash=sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f \ + --hash=sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad \ + --hash=sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292 \ + --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ + --hash=sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120 \ + --hash=sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb \ + --hash=sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32 \ + --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ + --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ + --hash=sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43 \ + --hash=sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03 \ + --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ + --hash=sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 \ + --hash=sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb \ + --hash=sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9 \ + --hash=sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f \ + --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ + --hash=sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7 \ + --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ + --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ + --hash=sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9 \ + --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ + --hash=sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb \ + --hash=sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f \ + --hash=sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb \ + --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ + --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ + --hash=sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5 \ + --hash=sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585 \ + --hash=sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946 \ + --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ + --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ + --hash=sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28 \ + --hash=sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c \ + --hash=sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9 \ + --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ + --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ + --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ + --hash=sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8 \ + --hash=sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f \ + --hash=sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366 \ + --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ + --hash=sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690 \ + --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ + --hash=sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819 \ + --hash=sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c \ + --hash=sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86 \ + --hash=sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180 \ + --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ + --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ + --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ + --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ + --hash=sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d \ + --hash=sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d \ + --hash=sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9 \ + --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ + --hash=sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f \ + --hash=sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93 \ + --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ + --hash=sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d \ + --hash=sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086 +markdown-it-py==4.0.0 \ + --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ + --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +markdownify==1.2.0 \ + --hash=sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351 \ + --hash=sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c +markupsafe==3.0.2 \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ + --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ + --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ + --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ + --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ + --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ + --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ + --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ + --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ + --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ + --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ + --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ + --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ + --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ + --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 +mcp==1.28.1 \ + --hash=sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df \ + --hash=sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683 +mdurl==0.1.2 \ + --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ + --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba +more-itertools==10.8.0 \ + --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ + --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd +multidict==6.7.0 \ + --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ + --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ + --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ + --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ + --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ + --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ + --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ + --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ + --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ + --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ + --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ + --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ + --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ + --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ + --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ + --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ + --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ + --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ + --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ + --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ + --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ + --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ + --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ + --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ + --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ + --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ + --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ + --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ + --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ + --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ + --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ + --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ + --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ + --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ + --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ + --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ + --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ + --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ + --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ + --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ + --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ + --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ + --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ + --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ + --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ + --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ + --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ + --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ + --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ + --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ + --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ + --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ + --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ + --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ + --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ + --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ + --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ + --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ + --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ + --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ + --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ + --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ + --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ + --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ + --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ + --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ + --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ + --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ + --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ + --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ + --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ + --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ + --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ + --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ + --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ + --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ + --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ + --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ + --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ + --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ + --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ + --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ + --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ + --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ + --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ + --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ + --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ + --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ + --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ + --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ + --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ + --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 +oauthlib==3.3.1 \ + --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ + --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 +ollama==0.6.0 \ + --hash=sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0 \ + --hash=sha256:da2b2d846b5944cfbcee1ca1e6ee0585f6c9d45a2fe9467cbcd096a37383da2f +openai==2.33.0 \ + --hash=sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5 \ + --hash=sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a +openapi-pydantic==0.5.1 \ + --hash=sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146 \ + --hash=sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d +opentelemetry-api==1.39.1 \ + --hash=sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950 \ + --hash=sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c +opentelemetry-exporter-otlp-proto-common==1.39.1 \ + --hash=sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde \ + --hash=sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464 +opentelemetry-exporter-otlp-proto-grpc==1.39.1 \ + --hash=sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad \ + --hash=sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18 +opentelemetry-exporter-otlp-proto-http==1.39.1 \ + --hash=sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb \ + --hash=sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985 +opentelemetry-instrumentation==0.60b1 \ + --hash=sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d \ + --hash=sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a +opentelemetry-instrumentation-threading==0.60b1 \ + --hash=sha256:20b18a68abe5801fa9474336b7c27487d4af3e00b66f6a8734e4fdd75c8b0b43 \ + --hash=sha256:92a52a60fee5e32bc6aa8f5acd749b15691ad0bc4457a310f5736b76a6d9d1de +opentelemetry-proto==1.39.1 \ + --hash=sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007 \ + --hash=sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8 +opentelemetry-sdk==1.39.1 \ + --hash=sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c \ + --hash=sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6 +opentelemetry-semantic-conventions==0.60b1 \ + --hash=sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953 \ + --hash=sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb +opentelemetry-semantic-conventions-ai==0.4.13 \ + --hash=sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5 \ + --hash=sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036 +orjson==3.11.7 \ + --hash=sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f \ + --hash=sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733 \ + --hash=sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223 \ + --hash=sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650 \ + --hash=sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5 \ + --hash=sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1 \ + --hash=sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8 \ + --hash=sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3 \ + --hash=sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6 \ + --hash=sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2 \ + --hash=sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d \ + --hash=sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a \ + --hash=sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5 \ + --hash=sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e \ + --hash=sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892 \ + --hash=sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3 \ + --hash=sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539 \ + --hash=sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785 \ + --hash=sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1 \ + --hash=sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab \ + --hash=sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576 \ + --hash=sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b \ + --hash=sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141 \ + --hash=sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2 \ + --hash=sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b \ + --hash=sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49 \ + --hash=sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705 \ + --hash=sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace \ + --hash=sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1 \ + --hash=sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157 \ + --hash=sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10 \ + --hash=sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757 \ + --hash=sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d \ + --hash=sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f \ + --hash=sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf \ + --hash=sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183 \ + --hash=sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74 \ + --hash=sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0 \ + --hash=sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539 \ + --hash=sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4 \ + --hash=sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0 \ + --hash=sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad \ + --hash=sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa \ + --hash=sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f \ + --hash=sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1 \ + --hash=sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +pathable==0.4.4 \ + --hash=sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2 \ + --hash=sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2 +pfzy==0.3.4 \ + --hash=sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96 \ + --hash=sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1 +pillow==12.3.0 \ + --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ + --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ + --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ + --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ + --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ + --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ + --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ + --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ + --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ + --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ + --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ + --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ + --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ + --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ + --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ + --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ + --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ + --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ + --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ + --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ + --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ + --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ + --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ + --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ + --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ + --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ + --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ + --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ + --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ + --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ + --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ + --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ + --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ + --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ + --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ + --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ + --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ + --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ + --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ + --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ + --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ + --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ + --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ + --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ + --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ + --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ + --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ + --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ + --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ + --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ + --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ + --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ + --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ + --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ + --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ + --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ + --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ + --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ + --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ + --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ + --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ + --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ + --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ + --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 +platformdirs==4.4.0 \ + --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ + --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf +portalocker==2.10.1 \ + --hash=sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf \ + --hash=sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f +posthog==6.7.7 \ + --hash=sha256:bf8187ce8e6da33d5c122ee03a759efa8f181630380260526294454dab0c8bab \ + --hash=sha256:ffb7bac388f66e5c9e2e3d6028572386ad3c98bba7a9a4669db05b10ac23357a +prompt-toolkit==3.0.52 \ + --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ + --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 +propcache==0.4.1 \ + --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ + --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ + --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ + --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ + --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ + --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ + --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ + --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ + --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ + --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ + --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ + --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ + --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ + --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ + --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ + --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ + --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ + --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ + --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ + --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ + --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ + --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ + --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ + --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ + --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ + --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ + --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ + --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ + --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ + --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ + --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ + --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ + --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ + --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ + --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ + --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ + --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ + --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ + --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ + --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ + --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ + --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ + --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ + --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ + --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ + --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ + --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ + --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ + --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ + --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ + --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ + --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ + --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ + --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ + --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ + --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ + --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ + --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ + --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ + --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ + --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ + --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ + --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ + --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ + --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ + --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ + --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ + --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ + --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ + --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ + --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ + --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ + --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ + --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ + --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ + --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ + --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af +proto-plus==1.26.1 \ + --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ + --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 +protobuf==6.33.5 \ + --hash=sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c \ + --hash=sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02 \ + --hash=sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c \ + --hash=sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd \ + --hash=sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190 \ + --hash=sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5 \ + --hash=sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0 \ + --hash=sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b +psutil==7.2.2 \ + --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ + --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ + --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ + --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ + --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ + --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ + --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ + --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ + --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ + --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ + --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ + --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ + --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ + --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ + --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ + --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ + --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ + --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ + --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ + --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 +py-key-value-aio==0.4.4 \ + --hash=sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d \ + --hash=sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55 +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b +pyasn1-modules==0.4.2 \ + --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ + --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 +pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ + --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ + --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 +pydantic==2.12.5 \ + --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ + --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d +pydantic-core==2.41.5 \ + --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ + --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ + --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ + --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ + --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ + --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ + --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ + --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ + --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ + --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ + --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ + --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ + --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ + --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ + --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ + --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ + --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ + --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ + --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ + --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ + --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ + --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ + --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ + --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ + --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ + --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ + --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ + --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ + --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ + --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ + --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ + --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ + --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ + --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ + --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ + --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ + --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ + --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ + --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ + --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ + --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ + --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ + --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ + --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ + --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ + --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ + --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ + --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ + --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ + --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ + --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ + --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ + --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ + --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ + --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ + --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ + --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ + --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ + --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ + --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ + --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 +pydantic-settings==2.11.0 \ + --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ + --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c +pygments==2.20.0 \ + --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ + --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 +pyjwt==2.13.0 \ + --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ + --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 +pyobjc==11.1 ; platform_system == 'darwin' \ + --hash=sha256:903f822cba40be53d408b8eaf834514937ec0b4e6af1c5ecc24fcb652812dd85 \ + --hash=sha256:a71b14389657811d658526ba4d5faba4ef7eadbddcf9fe8bf4fb3a6261effba3 +pyobjc-core==11.1 ; platform_system == 'darwin' or sys_platform == 'darwin' \ + --hash=sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c \ + --hash=sha256:765b97dea6b87ec4612b3212258024d8496ea23517c95a1c5f0735f96b7fd529 \ + --hash=sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2 \ + --hash=sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731 \ + --hash=sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe \ + --hash=sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96 +pyobjc-framework-accessibility==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3496c55569a421ef3c98ea66fc0ebaf68c686ede5b26db0fdcb0b0ad4191a20b \ + --hash=sha256:7c4124981a5d84b71464babb4babfbeb5bfab145bc75b6f3577bd046a9579226 \ + --hash=sha256:a049b63b32514da68aaaeef0d6c00a125e0618e4042aa6dbe3867b74fb2a8b2b \ + --hash=sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656 \ + --hash=sha256:ea98239e339136e3d20d753afe7908006cf29567ba39b8e83ceda7c221e6aad1 \ + --hash=sha256:fd5a03b731d1a2bbb2bf706b58889a5e82df82ac69210ec3245c7dc69e42a63a +pyobjc-framework-accounts==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:384fec156e13ff75253bb094339013f4013464f6dfd47e2f7de3e2ae7441c030 \ + --hash=sha256:9c3fe342be7b8e73cba735e5a38affbe349cf8bc19091aa4fd788eabf2074b72 +pyobjc-framework-addressbook==11.1 ; platform_system == 'darwin' \ + --hash=sha256:411adf4874cc4343f2928a26fe4cb3673d2f5f73365b45cd3650aa7304a45e24 \ + --hash=sha256:6735f297f0e5fd109fa77ca90cace57eb2e10eb65e3c15ccd249df2228030d3b \ + --hash=sha256:6bc42832e85f418a9f978b7e001e219faf52cbb279a0df185115cd4292c381cb \ + --hash=sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc \ + --hash=sha256:e4004bdf134a069c58d91b231cbeb9e0adad26a73d2689015baaf6a98c411c54 \ + --hash=sha256:fb3d0a710f8342a0c63a8e4caf64a044b4d7e42d6d242c8e1b54470238b938cb +pyobjc-framework-adservices==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1744f59a75b2375e139c39f3e85658e62cd10cc0f12b158a80421f18734e9ffc \ + --hash=sha256:44c72f8163705c9aa41baca938fdb17dde257639e5797e6a5c3a2b2d8afdade9 +pyobjc-framework-adsupport==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:78b9667c275785df96219d205bd4309731869c3298d0931e32aed83bede29096 \ + --hash=sha256:c3e009612778948910d3a7135b9d77b9b7c06aab29d40957770834c083acf825 +pyobjc-framework-applescriptkit==11.1 ; platform_system == 'darwin' \ + --hash=sha256:477707352eaa6cc4a5f8c593759dc3227a19d5958481b1482f0d59394a4601c3 \ + --hash=sha256:e22cbc9d1a25a4a713f21aa94dd017c311186b02062fc7ffbde3009495fb0067 +pyobjc-framework-applescriptobjc==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:ac22526fd1f0a3b07ac1d77f90046b77f10ec9549182114f2428ee1e96d3de2b \ + --hash=sha256:c8a0ec975b64411a4f16a1280c5ea8dbe949fd361e723edd343102f0f95aba6e +pyobjc-framework-applicationservices==11.1 ; platform_system == 'darwin' \ + --hash=sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf \ + --hash=sha256:385a89f4d0838c97a331e247519d9e9745aa3f7427169d18570e3c664076a63c \ + --hash=sha256:a0ce40a57a9b993793b6f72c4fd93f80618ef54a69d76a1da97b8360a2f3ffc5 \ + --hash=sha256:e8dee91c6a14fd042f98819dc0ac4a182e0e816282565534032f0e544bfab143 \ + --hash=sha256:f480fab20f3005e559c9d06c9a3874a1f1c60dde52c6d28a53ab59b45e79d55f \ + --hash=sha256:f4a85ccd78bab84f7f05ac65ff9be117839dfc09d48c39edd65c617ed73eb01c +pyobjc-framework-apptrackingtransparency==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:796cc5f83346c10973806cfb535d4200b894a5d2626ff2eeb1972d594d14fed4 \ + --hash=sha256:e25c3eae25d24ee8b523b7ecc4d2b07af37c7733444b80c4964071dea7b0cb19 +pyobjc-framework-audiovideobridging==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991 \ + --hash=sha256:30a12be3784f41e1c6b5ef532c08e73bae7071d9a036b26b1e36b919ee5b6f57 \ + --hash=sha256:3bef4383dc9233dbd9efc3817ce9c8fe8670c61d21a94de3c149e7f460245792 \ + --hash=sha256:591e80ff6973ea51a12f7c1a2e3fd59496633a51d5a1bf73f4fb989a43e23681 \ + --hash=sha256:6159b94448af08c9b119eb6ecf3fdbc2b3348ad66fb99586f991939779e412ec \ + --hash=sha256:e466561bd9eb77be050aabead6ad7313a480d05389d9892e1db2cbc06ce1f475 +pyobjc-framework-authenticationservices==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:364035d265129192e6906f7a94cbdf714d737b6b9f20e56bfe74d0007c8761b1 \ + --hash=sha256:60bf585e561d885cc88a21713ef2db259baf6434ce7116f82265a0c727f29dba \ + --hash=sha256:6655dd53d9135ef85265a4297da5e7459ed7836973f2796027fdfbfd7f08e433 \ + --hash=sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a \ + --hash=sha256:e92bf7e829229fbecba4f7f649d3ae38760cf25aa9e909c0e737b1945f36b62d \ + --hash=sha256:f19ea757ecfda6ac929559c779c3afb001855dd5e41e4acc4c42343c7d912da6 +pyobjc-framework-automaticassessmentconfiguration==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:378d233879bb011ed9d0bcf1b0e3c048fb756023d0f6819e997f62acc2c32bc3 \ + --hash=sha256:4b11c33fb6f6092b9e1fb63747f2402f516b7ff0f815be4ece4625f2a2ec954f \ + --hash=sha256:55d1684dd676730fb1afbc7c67e0669e3a7159f18c126fea7453fe6182c098f9 \ + --hash=sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032 \ + --hash=sha256:e5fa297c7d4db225f75e5d11121fa68e0956c104e14b24250a52157a180e5f6c \ + --hash=sha256:fbcbe406c2a02d632885f6b23285c259b715f019b938d666cc554a66ecf5f9c3 +pyobjc-framework-automator==11.1 ; platform_system == 'darwin' \ + --hash=sha256:112815d2e1b6002b4f9bc644bdae6b02257d249145c79346d7b8bb11e6f76b03 \ + --hash=sha256:203b888152a78b39a8c67be663ff78a749ebff208ce993b4419fc4409faa1fda \ + --hash=sha256:3458f836671ea922ad0771f617c927e9c52841c0a6e71b4a5a9dbb438736c207 \ + --hash=sha256:651760236cb2d2481faa5afb66da97054850d34fdbebc5e4ee2f83a683a8be10 \ + --hash=sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4 \ + --hash=sha256:af5941f8d90167244209b352512b7779e5590d17dc1e703e087a6cfe79ee3d64 +pyobjc-framework-avfoundation==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1ab2108b652496b13b9758c295f0f6de53b6d12125cf574ddae84ce28044bce1 \ + --hash=sha256:5dd6ac6a57f86b7ed5ac0a965ce54328f6ce77816b4a1fbf0d85c06fb251867a \ + --hash=sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377 \ + --hash=sha256:94f065db4e87b1baebb5cf9f464cf9d82c5f903fff192001ebc974d9e3132c7e \ + --hash=sha256:dd3965aad0b236b8ac12f216d688c1a22b963f63e7e4fdb7107dd6790e80ee12 \ + --hash=sha256:e204d155a09c186601490e4402dcffb2845a5831079e389b47bd6a341fe5ee63 +pyobjc-framework-avkit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:023b1cdb78c3aa5873d8abe69697396872b47278208991ec5e5aea4464309b01 \ + --hash=sha256:3a5f22bc4f4b0b82c8039d37996882bf4a38f509963d1afa3275a45ddd4a0b00 \ + --hash=sha256:a6b418603fc270a8e63c2a5efffa753704fd14bf8bca0657901c49a7cc9b22b5 \ + --hash=sha256:c5810b349745078ef8b4a562e85afe40de3245127f633d8cabe98aeca765c7fc \ + --hash=sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263 \ + --hash=sha256:ef9cd9fe37c6199bfde7ee5cd6e76ede23a6797932882785c53ef3070e209afb +pyobjc-framework-avrouting==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:120c9d65d4f9047b9921f8dced0b4f26d799156bc08ff7e3974217cd036b1bfc \ + --hash=sha256:6a7b335161d327792f42054acb3ff415f7778e1492582df8e91b8609b4b02244 \ + --hash=sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0 \ + --hash=sha256:9aa9b0a7ae7ee5874e7d92bebefca4525d5cf1f0aa1f50e78e558984a39cad2e \ + --hash=sha256:dc309e175abf3961f933f8b341c0504b17f4717931242ebb121a83256b8b5c13 \ + --hash=sha256:f52f9d62a3c8485b5687187ea58d905d7edccac9941c444b4add8129841cd031 +pyobjc-framework-backgroundassets==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:13bf451c59b409b6ce1ac0e717a970a1b03bca7a944a7f19219da0d46ab7c561 \ + --hash=sha256:2484a2f9c87e8cae2fc375a39d68ea7ff02e4fb786e4afe88237c51fd5e78ec9 \ + --hash=sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2 \ + --hash=sha256:708466d847a479e1798f31c59fbc5307473d03fa1083f40cfcaa18fd31819c40 \ + --hash=sha256:a4db45048d1021900be5b03136b927773820bcbb40d623aeac54712e1c86d6f6 \ + --hash=sha256:a72536ed18cf2462085bbb2184d0a3eecf9b97669c0ef4db45418555a609b534 +pyobjc-framework-browserenginekit==11.1 ; platform_release >= '23.4' and platform_system == 'darwin' \ + --hash=sha256:1f4cce594a94d0bc0a020122153f8149c16578fa4761b0e27d868c013f76214c \ + --hash=sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479 \ + --hash=sha256:9b815b167533015d62832b956e9cfb962bd2026f5a4ccd66718cf3bb2e15ab27 \ + --hash=sha256:c3195c4fb3b84150fac6dd18ce318eaae17f246f98678825397ed80d6da3c371 \ + --hash=sha256:dfe469f8eb1313ea0cbe0616cd3bbc56f62bdd8a683c959819ef01d7e9ac0de7 \ + --hash=sha256:f3332ffa9ae74cc6633fd17f6d998ac77b8939abbe9ecf95ae56df200ee93853 +pyobjc-framework-businesschat==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:69589d2f0cb4e7892e5ecc6aed79b1abd1ec55c099a7faacae6a326bc921259d \ + --hash=sha256:7fdc1219b988ce3ae896bffd01f547c06cec3b4e4b2d0aa04d251444d7f1c2db +pyobjc-framework-calendarstore==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:858ee00e6a380d9c086c2d7db82c116a6c406234038e0ec8fc2ad02e385dc437 \ + --hash=sha256:bf066e17392c978becf17a61863eb81727bf593a2bfdab261177126072557e24 +pyobjc-framework-callkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:554e09ca3dab44d93a89927d9e300f004d2ef0db020b10425a4622b432e7b684 \ + --hash=sha256:8266ee797fdabb657f7cb4fa808404fc33fcf3f31d4bcab1ab3c53d272e1ff83 \ + --hash=sha256:b69b4262897f2701348ea0da36afe32d60f84e2a036baf13e258a97875b25a6c \ + --hash=sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94 \ + --hash=sha256:bc1d2349dab93f7a0d298b01893828d7f46aded9122a341469b835d977a0646d \ + --hash=sha256:fc5e638ddbc9dd3e9993205d2b077f5db41b6cd4e97b9c5592b7249575f23f04 +pyobjc-framework-carbon==11.1 ; platform_system == 'darwin' \ + --hash=sha256:047f098535479efa3ab89da1ebdf3cf9ec0b439a33a4f32806193886e9fcea71 \ + --hash=sha256:1bf66853e939315ad7ee968170b16dd12cb838c42b80dfcd5354687760998825 +pyobjc-framework-cfnetwork==11.1 ; platform_system == 'darwin' \ + --hash=sha256:2e9a4ce6b416bff881df499d9060c1096220ef8c20e519108a7b91692d1fd1d7 \ + --hash=sha256:4b998daa3e6ce253c48455365f004647b3b1da2f313fbc8a5a607e460b4d5567 \ + --hash=sha256:5dd866fcbe6870931373636d19144544344f0f89685f6720e4a45453957702dd \ + --hash=sha256:62ccc6dcaaa5877534d21f93a15861a3d8af95888123d659f9ff5383d1a2a1f4 \ + --hash=sha256:70beb8095df76e0e8eb7ab218be1e69ae180e01a4d77f7cad73c97b4eb7a296a \ + --hash=sha256:ad600163eeadb7bf71abc51a9b6f2b5462a018d3f9bb1510c5ce3fdf2f22959d +pyobjc-framework-cinematic==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:b62c024c1a9c7890481bc2fdfaf0cd3c251a4a08357d57dc1795d98920fcdbd1 \ + --hash=sha256:efde39a6a2379e1738dbc5434b2470cd187cf3114ffb81390b3b1abda470b382 +pyobjc-framework-classkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:08000deb43004d16fb39ccd83b3de30e1e3b72639a79d05206d7d5c15f005b3a \ + --hash=sha256:161dcb9b718649e6331a5eab5a76c2b43a9b322b15b37b3f8f9c5faad12ee6d1 \ + --hash=sha256:9a1b9d31f9b23e05b92769bbdb4ef2167a59b3b24aefa6af86448f5087a2e105 \ + --hash=sha256:be279d91f10d68ad9a256e96d26d8975e35b9b1bb304c82491766d29ad252b0d \ + --hash=sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4 \ + --hash=sha256:ef28d042964b0f757569e72df737bb049b531c33b7d06a705ce2dcfa4e6e45d8 +pyobjc-framework-cloudkit==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:40d2dc4bf28c5be9b836b01e4d267a15d847d756c2a65530e1fcd79b2825e86d \ + --hash=sha256:c583e40c710cf85ebe34173d1d2995e832a20127edc8899b2f35b13f98498af1 +pyobjc-framework-cocoa==11.1 ; platform_system == 'darwin' or sys_platform == 'darwin' \ + --hash=sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71 \ + --hash=sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da \ + --hash=sha256:806de56f06dfba8f301a244cce289d54877c36b4b19818e3b53150eb7c2424d0 \ + --hash=sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038 \ + --hash=sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0 \ + --hash=sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350 +pyobjc-framework-collaboration==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3629ea5b56c513fb330d43952afabb2df2a2ac2f9048b8ec6e8ab4486191390a \ + --hash=sha256:4564e3931bfc51773623d4f57f2431b58a39b75cb964ae5c48d27ee4dde2f4ea +pyobjc-framework-colorsync==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:7a346f71f34b2ccd1b020a34c219b85bf8b6f6e05283d503185aeb7767a269dd \ + --hash=sha256:d19d6da2c7175a3896a63c9b40a8ab98ade0779a5b40062789681501c33efd5c +pyobjc-framework-contacts==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:09b873d2bd739fea63d744430defb04ce4b44af064aaf0b6bf558eea23f82bd7 \ + --hash=sha256:23312bb4bfc5aafecdac84ca402189e312e754e9dc0586d8f282d225c3952c00 \ + --hash=sha256:3409aba6e23cb179b3fe932c1a0a53d7b273ac8292d5adf1bf6849e925cc0955 \ + --hash=sha256:576ee4aec05d755444bff10b45833f73083b5b3d1b2740e133b92111f7765e54 \ + --hash=sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc \ + --hash=sha256:910f40a2e4d80a97f282bfdecba0f5ff95201b11844acd3f9cb9522db364ab57 +pyobjc-framework-contactsui==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f \ + --hash=sha256:666586174b306b33b791d2edee021cd979a8c970d444f906ed294e27583a6b54 \ + --hash=sha256:7901eed3c669ad52cca86089c443fd30820b21586bf758e03fb83696f435ba87 \ + --hash=sha256:8b03bd175095b4774c55bd5f38a01942e945b668bea15b9dc3b4f1a28b1a8696 \ + --hash=sha256:c34a6f27ef5aa4742cc44fd5b4d16fe1e1745ff839578b4c059faf2c58eee3ca \ + --hash=sha256:f3b4f0225645a26ed9e6c008c2e8c217035b4a50fa9cd6623c628a11c37924d0 +pyobjc-framework-coreaudio==11.1 ; platform_system == 'darwin' \ + --hash=sha256:2538d1242dab4e27efb346eafbad50594e7e95597fa7220f0bab2099c825da55 \ + --hash=sha256:67dae111b78d91c26c753dbfbccc3ea5498cfda3dfe83c6f3778628b435e1e7b \ + --hash=sha256:6ba8b67f185c0e3f26b17ae525cee3f411bc8d6e9c9a8bfd899a28f594623d2f \ + --hash=sha256:9527a16a2b88b37bace578d499f21229f9a33b9afdcdd35d4f44374cb8eb9ab6 \ + --hash=sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b \ + --hash=sha256:f73d996df1e721931d9f78050e1708735a173dbe3a76d9c71fb36e04f7208478 +pyobjc-framework-coreaudiokit==11.1 ; platform_system == 'darwin' \ + --hash=sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40 \ + --hash=sha256:11d42770dfbc6a8af8d5fa39a4f700f0067d7e6c7ba9335e6624d89de3c599a9 \ + --hash=sha256:20440a2926b1d91da8efc8bc060e77c7a195cb0443dbf3770eaca9e597276748 \ + --hash=sha256:6fea7c7ea5305e8cbd75808ec4edcde8e2320137f227b3d771266dd9a71e1fa5 \ + --hash=sha256:8d012561eb95877f0214aa0cd13043b1a2693add4a9534d1e6fb82f6d7183c7c \ + --hash=sha256:a71447196a48869b551a2e3b6ba92f39241cb64d0257120505c62ddb611aef0f +pyobjc-framework-corebluetooth==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190 \ + --hash=sha256:36bef95a822c68b72f505cf909913affd61a15b56eeaeafea7302d35a82f4f05 \ + --hash=sha256:4da1106265d7efd3f726bacdf13ba9528cc380fb534b5af38b22a397e6908291 \ + --hash=sha256:992404b03033ecf637e9174caed70cb22fd1be2a98c16faa699217678e62a5c7 \ + --hash=sha256:e84cbf52006a93d937b90421ada0bc4a146d6d348eb40ae10d5bd2256cc92206 \ + --hash=sha256:ebb8648f5e33d98446eb1d6c4654ba4fcc15d62bfcb47fa3bbd5596f6ecdb37c +pyobjc-framework-coredata==11.1 ; platform_system == 'darwin' \ + --hash=sha256:007160eb10bb8c789076f231e3d625d8875ca42eb5a806fdab5d0277c48866f8 \ + --hash=sha256:699ad568f98f58e88e642159c91ffff0c68ce3d1ec798e4af8333b27431fd058 \ + --hash=sha256:89dde863eff01ed6b5f8d88c764a08b154ef37078397c98c5f403e8798723b9d \ + --hash=sha256:c9b2374784e67694a18fc8c120a12f11b355a20b643c01f23ae2ce87330a75e0 \ + --hash=sha256:d84afaccbb4f18dbda4c557cd059b7adc2116436a065353e25e7cbc840d9f8b4 \ + --hash=sha256:fe9fd985f8e06c70c0fb1e6bbea5b731461f9e76f8f8d8e89c7c72667cdc6adf +pyobjc-framework-corehaptics==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:8f8c47ccca5052d07f95d2f35e6e399c5ac1f2072ba9d9eaae902edf4e3a7af4 \ + --hash=sha256:e5da3a97ed6aca9b7268c8c5196c0a339773a50baa72d1502d3435dc1a2a80f1 +pyobjc-framework-corelocation==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0f8182835429118a55ed65963c80f5b2892d190747b986e8395b1cd99f41a1d0 \ + --hash=sha256:17ce2530bd5a0dca9059eb11bc647d920490bcdd35b5cac1e160f51f0297bdc8 \ + --hash=sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206 \ + --hash=sha256:562e31124f80207becfd8df01868f73fa5aa70169cc4460e1209fb16916e4fb4 \ + --hash=sha256:a384d9fcba2c041d8f8115b51a07ef11c391bc30f72560aaea8b94db6b3b225c \ + --hash=sha256:bc3f27802415aa62330a2d2507adc3a9b98a89d6de7d1033ebe6b8c461610831 +pyobjc-framework-coremedia==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:7ecdb64c743ffe9fd3949c7cc9109891b9f399a0852717fcb969d33c4e7ba527 \ + --hash=sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3 \ + --hash=sha256:969ce357c616f6835f47e27d1e73964374cdb671476571dfd358894a8ced06f2 \ + --hash=sha256:aa942d9ad0cf5bc4d3ede8779c3fac2f04cf3857687f2fb8505bae3378d04b95 \ + --hash=sha256:bf1da05c297776c297ab3489ebf18d954efdff530acbdd6e70c32be811e20ec6 \ + --hash=sha256:ea5055298af91e463ffa7977d573530f9bada57b8f2968dcc76a75e339b9a598 +pyobjc-framework-coremediaio==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0a7ffded00a7dc6f0bf4a44a6832f0150d45a83886486148b71ccc67c70ef215 \ + --hash=sha256:39ad2518de9943c474e5ca0037e78f92423c3352deeee6c513a489016dac1266 \ + --hash=sha256:5a94f9e507b470ce7dcb887e79ccf19e98693a606ad34462d711004e3edd88c3 \ + --hash=sha256:5ff161025ef28d5e2eed90db0e8b828cb361281b799b16b1885711ca0addc1aa \ + --hash=sha256:9e0a079fe790ce8a69d11bea46b315c9a0d3f3999a2f09e2ef4fcc4430a47c42 \ + --hash=sha256:bccd69712578b177144ded398f4695d71a765ef61204da51a21f0c90b4ad4c64 +pyobjc-framework-coremidi==11.1 ; platform_system == 'darwin' \ + --hash=sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376 \ + --hash=sha256:2ef1a10f6230fce82b931670470158404657d9fb9ac558a77b46b547e9978524 \ + --hash=sha256:3462a158214adb7ebc785fb6924e674c58dcd471888dbca5e2e77381f3f1bbdc \ + --hash=sha256:c2e1ab122501206ceae07123fdc433e91a5f1a97224f80ece0717b6f36ad2029 \ + --hash=sha256:deb9120478a831a898f22f68737fc683bb9b937e77556e78b75986aebd349c55 \ + --hash=sha256:f4b70864cae295f27b5d51817c0768fade7c1335a59410910146e5f2a54c475c +pyobjc-framework-coreml==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:10d51f8a5fe8d30c7ec70304a2324df76b48b9fbef30ee0f0c33b99a49ae8853 \ + --hash=sha256:287a2a059016d02d8c40e0d29e70226142a4969db97ad79cefc70ec9bf0ab29e \ + --hash=sha256:4df25ee233430f016ffcb4e88506b54c8e7b668c93197e6a1341761530a5922c \ + --hash=sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7 \ + --hash=sha256:a479c3d759aff3695f72c7915a78df6e92e0eca7027abaa8b4a07e876ba1dbfb \ + --hash=sha256:c768b03d72488b964d753392e9c587684961d8237b69cca848b3a5a00aea79c9 +pyobjc-framework-coremotion==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:42fb307b86999d078503ff79bdf8df4d1c27d38763db6b1c5c0f4054241f67a3 \ + --hash=sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e \ + --hash=sha256:708431c53f483bc6da199375227ffea1b4e8e7d8c81d162492db3fc36893fb53 \ + --hash=sha256:8c3b33228a170bf8495508a8923451ec600435c7bff93d7614f19c913baeafd1 \ + --hash=sha256:ac5302deaab99a7443cad63f125061a90040852d4f8efb58492542a612b2afe3 \ + --hash=sha256:d67413a56989154dab7bf1b69c14b0b2387d87d3a4c8e3c8a9fc0230f061e8ab +pyobjc-framework-coreservices==11.1 ; platform_system == 'darwin' \ + --hash=sha256:21e9e86192d719cd5c899cc0e931110733da0b5bbbf606681e5fccd4dd39c174 \ + --hash=sha256:4ffa188322ab9d05c6964926959dedba5cc04534232f1eff03aee5f09faa499e \ + --hash=sha256:6bd313ec326efd715b4b10c3ebcc9f054e3ee3178be407b97ea225cd871351d2 \ + --hash=sha256:74dcc295245f07754328bada9577b189e3abef71607d013e939751c1b5b55729 \ + --hash=sha256:8aee505dca56afc5363d8d0dff0b2d26583a8d0f3ac37674cef86f66c51a2934 \ + --hash=sha256:cf8eb5e272c60a96d025313eca26ff2487dcd02c47034cc9db39f6852d077873 +pyobjc-framework-corespotlight==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2 \ + --hash=sha256:546d0d9b101de4ca20449f3807d1f88e5c26de0345a8bfefc70f12f87efb8433 \ + --hash=sha256:7cedd3792fe1fe2a8dc65a8ff1f70baf12415a5dc9dc4d88f987059567d7e694 \ + --hash=sha256:bce3d84f97014228b244c734aea3ec03b257573b22c097dff4eb176a80cd29a9 \ + --hash=sha256:f562cc65865066f8e2e5d96c868fd7f463d8280f1ef01df85250fc1150feed0e \ + --hash=sha256:f59d0d2f0411db102d16490e47b457b994c613f1b980869fa3a151863da7aa4c +pyobjc-framework-coretext==11.1 ; platform_system == 'darwin' \ + --hash=sha256:15650ba99692d00953e91e53118c11636056a22c90d472020f7ba31500577bf5 \ + --hash=sha256:1597bf7234270ee1b9963bf112e9061050d5fb8e1384b3f50c11bde2fe2b1570 \ + --hash=sha256:37e051e8f12a0f47a81b8efc8c902156eb5bc3d8123c43e5bd4cebd24c222228 \ + --hash=sha256:56a3a02202e0d50be3c43e781c00f9f1859ab9b73a8342ff56260b908e911e37 \ + --hash=sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6 \ + --hash=sha256:fb27f66a56660c31bb956191d64b85b95bac99cfb833f6e99622ca0ac4b3ba12 +pyobjc-framework-corewlan==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:14c7af9135ba0a920192af4dc50219bbf6185fcbb5de7041f097e1a1c8509587 \ + --hash=sha256:3c66643a97fcf3aa797fda997a3afc28d8d9bba9727dd5c0e68a313899d780f7 \ + --hash=sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce \ + --hash=sha256:6dc28264b56b18096c8869cce3f85e519fd27936f19524bb77458572ccfd7518 \ + --hash=sha256:7bd0775d2466ad500aad4747d8a889993db3a14240239f30ef53c087745e9c8e \ + --hash=sha256:872de75409a710bb9a461e64e97185f8489d01898ec1b02c3e058c04606b61cf +pyobjc-framework-cryptotokenkit==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328 \ + --hash=sha256:6384cb1d86fc586e2da934a5a37900825bd789e3a5df97517691de9af354af0c \ + --hash=sha256:a55c0e57ab164aa5ce562e4d9e69026339067ecb4888638995690f1c43b79cfa \ + --hash=sha256:cb3e1bd344e794cb98343171b5501a1a3b75548ef5385bda3d5ec613c0b98045 \ + --hash=sha256:efd89e5b024475701f6e9bec4cf1c2563e1bab37e79288397e09d9ad4e53d174 \ + --hash=sha256:faab9493e36095c0257598e25ef81c50bcdb3afb5843a82e6dfad8c7d1f47bcf +pyobjc-framework-datadetection==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:5afd3dde7bba3324befb7a3133c9aeaa5088efd72dccc0804267a74799f4a12f \ + --hash=sha256:cbe0080b51e09b2f91eaf2a9babec3dcf2883d7966bc0abd8393ef7abfcfc5db +pyobjc-framework-devicecheck==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:8b05973eb2673571144d81346336e749a21cec90bd7fcaade76ffd3b147a0741 \ + --hash=sha256:8edb36329cdd5d55e2c2c57c379cb5ba1f500f74a08fe8d2612b1a69b7a26435 +pyobjc-framework-devicediscoveryextension==11.1 ; (platform_release >= '24.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '24' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:96e5b13c718bd0e6c80fbd4e14b8073cffc88b3ab9bb1bbb4dab7893a62e4f11 \ + --hash=sha256:ae160ea40f25d3ee5e7ce80ac9c1b315f94d0a4c7ccb86920396f71c6bf799a0 +pyobjc-framework-dictionaryservices==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:39c24452d0ddd037afeb73a1742614c94535f15b1c024a8a6cc7ff081e1d22e7 \ + --hash=sha256:92f4871066653f18e2394ac93b0a2ab50588d60020f6b3bd93e97b67cd511326 +pyobjc-framework-discrecording==11.1 ; platform_system == 'darwin' \ + --hash=sha256:2d18158366d124852ad58291954611ebdcc43263a3bb75d7fd273408e67720e2 \ + --hash=sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87 \ + --hash=sha256:7c33421d6bed0993d9f1861dbf38b717b9a9e49dfb98fdf8b3cd8d558fdd50eb \ + --hash=sha256:9cb36715bebdbbe1ad95e3c17359c2f5d3f6479a26b527ea1032154ca7cf3e09 \ + --hash=sha256:b027eca3a0391196d4335fcbd50c03ef1e8f5ce095411ed51a081328b4945bf5 \ + --hash=sha256:e29bc8c3741ae52fae092f892de856dbab2363e71537a8ae6fd026ecb88e2252 +pyobjc-framework-discrecordingui==11.1 ; platform_system == 'darwin' \ + --hash=sha256:33233b87d7b85ce277a51d27acca0f5b38485cf1d1dc8e28a065910047766ee2 \ + --hash=sha256:a9f10e2e7ee19582c77f0755ae11a64e3d61c652cbd8a5bf52756f599be24797 +pyobjc-framework-diskarbitration==11.1 ; platform_system == 'darwin' \ + --hash=sha256:6a8e551e54df481a9081abba6fd680f6633babe5c7735f649731b22896bb6f08 \ + --hash=sha256:a933efc6624779a393fafe0313e43378bcae2b85d6d15cff95ac30048c1ef490 +pyobjc-framework-dvdplayback==11.1 ; platform_system == 'darwin' \ + --hash=sha256:6094e4651ea29540ac817294b27e1596b9d1883d30e78fb5f9619daf94ed30cb \ + --hash=sha256:b44c36a62c8479e649133216e22941859407cca5796b5f778815ef9340a838f4 +pyobjc-framework-eventkit==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:5643150f584243681099c5e9435efa833a913e93fe9ca81f62007e287349b561 \ + --hash=sha256:c303207610d9c742f4090799f60103cede466002f3c89cf66011c8bf1987750b +pyobjc-framework-exceptionhandling==11.1 ; platform_system == 'darwin' \ + --hash=sha256:31e6538160dfd7526ac0549bc0fce5d039932aea84c36abbe7b49c79ffc62437 \ + --hash=sha256:e010f56bf60ab4e9e3225954ebb53e9d7135d37097043ac6dd2a3f35770d4efa +pyobjc-framework-executionpolicy==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3280ad2f4c5eaf45901f310cee0c52db940c0c63e959ad082efb8df41055d986 \ + --hash=sha256:7d4141e572cb916e73bb34bb74f6f976a8aa0a396a0bffd1cf66e5505f7c76c8 +pyobjc-framework-extensionkit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2767635e57b277e051719fa53c7683396ebdbcf3d40d44c1296758978ca8c92a \ + --hash=sha256:3507f67dd06285c09bbdf5216a1148f5dd3a2f10eee7a9318dd14430bf6e67ee \ + --hash=sha256:5c2e203cb8134be1dd7df73d74c630adbaaf43d78eba04be451ea4f8bf582e22 \ + --hash=sha256:abbadbea5b18e4a6944c3c428753ee298a133cbf601c70e9586b14e3aebf649b \ + --hash=sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409 \ + --hash=sha256:d505a64617c9db4373eb386664d62a82ba9ffc909bffad42cb4da8ca8e244c66 +pyobjc-framework-externalaccessory==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:00caf75b959db5d14118d78c04085e2148255498839cdee735a0b9f6ef86b6a2 \ + --hash=sha256:318772e698c6363e8c3c81229d93b639f5066a02a742ba1ab10cfdef3101d88b \ + --hash=sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371 \ + --hash=sha256:50b796a4721db87863a28cd55668cb1547fcc28834afda2032e500cdab5b3d95 \ + --hash=sha256:569124b686569c48e3855fff128f438a2b46af06280eac2a516aaa214ad325de \ + --hash=sha256:d259724665617fc4f3e666d353b756a67cabb74e6f9d7b8f6f250a2d4bf05cb7 +pyobjc-framework-fileprovider==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0e48015bf50b3e56312c640ec6efde73cf3855e29b6d70d173a88957d9d74d27 \ + --hash=sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8 \ + --hash=sha256:95ed3a03741076a4479aabb616b1e3ea022025a0ad842147a1200c27709019e2 \ + --hash=sha256:9af41255df395a40a6e0b08c4410be5463f3ea91d8c9be61f6bd114252490ab2 \ + --hash=sha256:ce6092dfe74c78c0b2abc03bfc18a0f5d8ddc624fc6a1d8dfef26d7796653072 \ + --hash=sha256:d2720acdd582756ebda34418981e7646b7b85588b0b8fdafba7016eb657be6b8 +pyobjc-framework-fileproviderui==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:162a23e67f59e1bb247e84dda88d513d7944d815144901a46be6fe051b6c7970 \ + --hash=sha256:f2765f114c2f4356aa41fb45c621fa8f0a4fae0b6d3c6b1a274366f5fe7fe829 +pyobjc-framework-findersync==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:692364937f418f0e4e4abd395a09a7d4a0cdd55fd4e0184de85ee59642defb6e \ + --hash=sha256:c72b0fd8b746b99cfa498da36c5bb333121b2080ad73fa8cbea05cd47db1fa82 +pyobjc-framework-fsevents==11.1 ; platform_system == 'darwin' \ + --hash=sha256:3289192f4d60e5b26f8ac88ae4049a11eff47caa6fb76ce34e3f7df405119905 \ + --hash=sha256:8b51d120b8f12a1ca94e28cf74113bf2bfd4c5aee7035b452e895518f4df7630 \ + --hash=sha256:9191ee2819f1d5dcae1559e4a66f19be03da3a103bccdc417e6888bcb5659f8f \ + --hash=sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d \ + --hash=sha256:fad5ada269f137afabd622b5fc04884c668ae1c7914a8791bab73b1d972f7713 \ + --hash=sha256:ff064cfa9d9cffb5d4ab476fb5091604568744d961c670aced037b2b6f0d0185 +pyobjc-framework-fskit==11.1 ; platform_release >= '24.4' and platform_system == 'darwin' \ + --hash=sha256:1d3793938e6d9b871483d4a6fad8f93d554bcbebd1fe7bed20e3f5d2feaa814b \ + --hash=sha256:1e50b8f949f1386fada73b408463c87eb81ef7fd0b3482bacf0c206a73723013 \ + --hash=sha256:44fe7b6781c8fd0552b13ab3d0ec21176cd7cd685a8a61d712f9e4e42eb2f736 \ + --hash=sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f \ + --hash=sha256:cc2390934a23b6407aa7802b11978374301444c3135835ad3373f7b4930c24eb \ + --hash=sha256:e38f9c449647109e5b14dc4a17f425efca10c7e539a3836ebdd1f9c0ef725a3b +pyobjc-framework-gamecenter==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2a2cb6471d4d4b19f124c7e91a32882a0fab6e326bb0415915fd8f3b91cfc311 \ + --hash=sha256:6ff8905a5a7bfd86cb2b95671b452be0836f79db065b8d8b3bb2a1a5750ffd0d \ + --hash=sha256:779cdf8f52348be7f64d16e3ea37fd621d5ee933c032db3a22a8ccad46d69c59 \ + --hash=sha256:90132bb32f5ed6607e13c6f39346ad621611cb92cea308ced661a6ba1305b94e \ + --hash=sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c \ + --hash=sha256:a73ca7027b2b827e26075b46551fe42425d4a68985022baa4413329a3a2c16ff +pyobjc-framework-gamecontroller==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:4866b25df05f583af06095e7103ddd2fbb2484b0ac2c78fd2cd825f995e524fa \ + --hash=sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b \ + --hash=sha256:98f3f7afcbbe473a53537da42b2cdc0363df2647289eb66e8c762e4b46c23e73 \ + --hash=sha256:afe9f3aed8c900ebe63ee4f6e53c73c2fef7e503f6388afd39f46b31487f84a3 \ + --hash=sha256:d2cbc0c6c7d9c63e6b5b0b124d0c2bad01bb4b136f3cbc305f27d31f8aab6083 \ + --hash=sha256:de3892b8d09a65a3413d85a2f0762eba092afda8d97cbf9cda0417689cfb7027 +pyobjc-framework-gamekit==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:10331a69282b9554ce7ae618dc9ff68e96451759f6cfc687e188c82ba6b0e2ff \ + --hash=sha256:555cb8d868fd2699ad70d4f9e7efccaa5df1995893050d05d478cb8f24dbf876 \ + --hash=sha256:8c7f2bf7ecf44ca678cfdf76f23b32d9c2d03006a0af9ad8e60d9114d6be640a \ + --hash=sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269 \ + --hash=sha256:a7c8fce8a2c4614e3dd88b002540e67423e3efd41aa26d576db2de0fc61651b9 \ + --hash=sha256:f945c7cfe53c4a349a03a1272f2736cc5cf88fe9e7a7a407abb03899635d860c +pyobjc-framework-gameplaykit==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0e4f34db8177b8b4d89fd22a2a882a6c9f6e50cb438ea2fbbf96845481bcd80d \ + --hash=sha256:30e15e4e8df9b1c0ca92bfabf79f6b12a286e544e67762b14dd3023c53e41978 \ + --hash=sha256:4dbea3471b5d4a82b37ddca41bfddd63380c31050de7392e2467fabebcd110b8 \ + --hash=sha256:51abecafc1b55fcc9a5d73c078ea2d5a75964e0facf2c867a25d7f4f40238331 \ + --hash=sha256:78c513bc53bafd996d896f6f4535f2700b4916013417f8b41f47045790c6208d \ + --hash=sha256:9ae2bee69b0cc1afa0e210b4663c7cdbb3cc94be1374808df06f98f992e83639 +pyobjc-framework-healthkit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76 \ + --hash=sha256:2d1b76b04e9e33ac9441cafa695766938eac04f8c8c69f7efd93a6aceb6eca40 \ + --hash=sha256:547ac283f84b5024be75290f351863f86eb48a950ec61e3150760230e6eba773 \ + --hash=sha256:8b6c739e17362897f0b1ba4aa4dc395b3d0c3855b87423eaeb6a89f910adc43f \ + --hash=sha256:ab4350f9fe65909107dd7992b367a6c8aac7dc31ed3d5b52eeb2310367d0eb0b \ + --hash=sha256:c693725d8476b745232df90ef01487e75e1e1c448e599dd34adf3dce859de760 +pyobjc-framework-imagecapturecore==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:254ae4502d651526c500533b8e2aee77ae7939f9acfd7d706dba2d464417deba \ + --hash=sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624 \ + --hash=sha256:bab8ed798598ddaa53f5b39707b58e16a1b1152858c87fd3fa0d64081f0c0364 \ + --hash=sha256:ded8dc6a8c826a6ae1b6a6d0a31542bd1eb85345f86201689c54e51193b572dc \ + --hash=sha256:e01c29456d0560667f8fcd3ff2749e79ad51bf72512e699646ce32227f91b447 \ + --hash=sha256:ed296c23d3d8d1d9af96a6486d09fb8d294cc318e4a2152e6f134151c76065f8 +pyobjc-framework-inputmethodkit==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:013919a4d766a7e66045fa5dd5d819bfa0450ccb59baba2b89d7449bce637d6b \ + --hash=sha256:2228bf58369351767294fe1aa400e98ec61e397a74a178788c24c98a1cff97ee \ + --hash=sha256:5095005809a4108f362998b46994f99b5a57f9ba367c01141c1b9eaea311bc5b \ + --hash=sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496 \ + --hash=sha256:92b9ce788ce4b094e352a64508050ff8e24307b8670d33488304b941d118894e \ + --hash=sha256:dd0c591a9d26967018a781fa4638470147ef2a9af3ab4a28612f147573eeefba +pyobjc-framework-installerplugins==11.1 ; platform_system == 'darwin' \ + --hash=sha256:363e59c7e05553d881f0facd41884f17b489ff443d7856e33dd0312064c746d9 \ + --hash=sha256:f92b06c9595f3c800b7aabf1c1a235bfb4b2de3f5406d5f604d8e2ddd0aecb4e +pyobjc-framework-instantmessage==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:a70b716e279135eec5666af031f536c0f32dec57cfeae55cc9ff8457f10d4f3d \ + --hash=sha256:c222aa61eb009704b333f6e63df01a0e690136e7e495907e5396882779bf9525 +pyobjc-framework-intents==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695 \ + --hash=sha256:4e3ec70c02d3166088223938a7433e479659cbd8ce04be5bf515ea8d6e3c353d \ + --hash=sha256:55498040123904b685cd38555eb84d95833fcb467b497d31757d6ac648a11817 \ + --hash=sha256:9e21b3bc33de2d5f69b5c1d581e5c724a08686fe84ec324a4be365bef769e482 \ + --hash=sha256:a663e2de1b7ae7b547de013f89773963f8180023e36f2cebfe8060395dc34c33 \ + --hash=sha256:e008d542abe38fd374c9ada7c833ad6e34a2db92b4dcbfba0a59ff830b9093bc +pyobjc-framework-intentsui==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:154fd92112184e8ef29ce81e685c377422dffcff4f7900ea6e5956a0e2be2268 \ + --hash=sha256:1bd950f808efb7ba7fbbc977300d7932a1dad41fbd3c78c8002870ca602e22d5 \ + --hash=sha256:35ef9f190f480147ce797809a63cc2b5f2ea64b51255d691e5e94bd8337e01ef \ + --hash=sha256:6d7d5402c05840a45047cf905fa550c2898cf5580cdee00a36bd35dd624c7542 \ + --hash=sha256:99a3ae40eb2a6ef1125955dd513c8acc88ce7d8d90130a8cdeaec8336e6fbec5 \ + --hash=sha256:c8182155af4dce369c18d6e6ed9c25bbd8110c161ed5f1b4fb77cf5cdb99d135 +pyobjc-framework-iobluetooth==11.1 ; platform_system == 'darwin' \ + --hash=sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1 \ + --hash=sha256:2c99ade82a79263ea71c51d430696a2ad155beb01a67df59d52be63e181e0482 \ + --hash=sha256:2ef72cef1e03468e91a2f01af2390143bd6e4fcad1c6d0494dd857c99fa0d1a7 \ + --hash=sha256:4a8b1caba9ac51435f64a6cf9c1a2be867603161af8bebdd1676072ebed2fed9 \ + --hash=sha256:883781e7223cb0c63fab029d640721ded747f2e2b067645bc8b695ef02a4a4dd \ + --hash=sha256:a9a7e11a4bbb4a364b0412ca8632a1e853270c98c24d28421133f69c0c0ecaff +pyobjc-framework-iobluetoothui==11.1 ; platform_system == 'darwin' \ + --hash=sha256:060c721f1cd8af4452493e8153b72b572edcd2a7e3b635d79d844f885afee860 \ + --hash=sha256:3c5a382d81f319a1ab9ab11b7ead04e53b758fdfeb604755d39c3039485eaac6 +pyobjc-framework-iosurface==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0c36ad56f8ec675dd07616418a2bc29126412b54627655abd21de31bcafe2a79 \ + --hash=sha256:a468b3a31e8cd70a2675a3ddc7176ab13aa521c035f11188b7a3af8fff8b148b +pyobjc-framework-ituneslibrary==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:4e87d41f82acb6d98cf70ac3c932a568ceb3c2035383cbf177f54e63de6b815f \ + --hash=sha256:e2212a9340e4328056ade3c2f9d4305c71f3f6af050204a135f9fa9aa3ba9c5e +pyobjc-framework-kernelmanagement==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:e934d1638cd89e38d6c6c5d4d9901b4295acee2d39cbfe0bd91aae9832961b44 \ + --hash=sha256:ec74690bd3383a7945c4a038cc4e1553ec5c1d2408b60e2b0003a3564bff7c47 +pyobjc-framework-latentsemanticmapping==11.1 ; platform_system == 'darwin' \ + --hash=sha256:57f3b183021759a100d2847a4d8aa314f4033be3d2845038b62e5e823d96e871 \ + --hash=sha256:c6c3142301e4d375c24a47dfaeebc2f3d0fc33128a1c0a755794865b9a371145 +pyobjc-framework-launchservices==11.1 ; platform_system == 'darwin' \ + --hash=sha256:80b55368b1e208d6c2c58395cc7bc12a630a2a402e00e4930493e9bace22b7bb \ + --hash=sha256:8b58f1156651058b2905c87ce48468f4799db86a7edf760e1897fedd057a3908 +pyobjc-framework-libdispatch==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87 \ + --hash=sha256:5d9985b0e050cae72bf2c6a1cc8180ff4fa3a812cd63b2dc59e09c6f7f6263a1 \ + --hash=sha256:a9357736cb47b4a789f59f8fab9b0d10b0a9c84f9876367c398718d3de085888 \ + --hash=sha256:c4e219849f5426745eb429f3aee58342a59f81e3144b37aa20e81dacc6177de1 \ + --hash=sha256:cd08f32ea7724906ef504a0fd40a32e2a0be4d64b9239530a31767ca9ccfc921 \ + --hash=sha256:dc9a7b8c2e8a63789b7cf69563bb7247bde15353208ef1353fff0af61b281684 +pyobjc-framework-libxpc==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2df539d11b65e229f8436a3660d0d1dce2cc7ba571054c5b91350b836db22576 \ + --hash=sha256:4f3083fde3c366cc58bcdb2c183fae9c531fb556d35a495818019f1a5d85c24d \ + --hash=sha256:654db8e822e60a1246d4d55c7127a140e10d6faa0da5a7366a16cc10def44deb \ + --hash=sha256:6862e63f565823d4eeb56f18f90a3ee8682c52a8d4bcd486d3535c9959464eda \ + --hash=sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a \ + --hash=sha256:c29b2df8d74ff6f489afa7c39f7c848c5f3d0531a6bbe704571782ee6c820084 +pyobjc-framework-linkpresentation==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:018093469d780a45d98f4e159f1ea90771caec456b1599abcc6f3bf3c6873094 \ + --hash=sha256:a785f393b01fdaada6d7d6d8de46b7173babba205b13b44f1dc884b3695c2fc9 +pyobjc-framework-localauthentication==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1 \ + --hash=sha256:9c9446c017b13c8dcadf485b76ab1d7bc12099b504bf5c2df1aae33b5dc4ab2c \ + --hash=sha256:aa3815f936612d78e51b53beed9115c57ae2fd49500bb52c4030a35856e6569e \ + --hash=sha256:d5a2e1ea2fe8233dc244f6029d5d0c878102b2e0615cb4b81b2f30d9ee101fca \ + --hash=sha256:e41be8e2132d1517e597401c7858b22531db2e7760d898993acc03ea13edb834 \ + --hash=sha256:f49c9dbbecfa0b0a7a633c60bda8179575e3685b6a696658a835c63afee90f9a +pyobjc-framework-localauthenticationembeddedui==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:22baf3aae606e5204e194f02bb205f244e27841ea7b4a4431303955475b4fa56 \ + --hash=sha256:3539a947b102b41ea6e40e7c145f27280d2f36a2a9a1211de32fa675d91585eb +pyobjc-framework-mailkit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:8e6026462567baba194468e710e83787f29d9e8c98ea0583f7b401ea9515966e \ + --hash=sha256:bf97dc44cb09b9eb9d591660dc0a41f077699976144b954caa4b9f0479211fd7 +pyobjc-framework-mapkit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0b6fa1c4fffc3ae91adb965731a0cc943b3b6e82c8f21919a53a68b43a67b534 \ + --hash=sha256:1dc27d315849ac96647d13c82eeefce5d1d2db8c64767ce10bd3e77cbaad2291 \ + --hash=sha256:91976c6dbc8cbb020e059a0ccdeab8933184712f77164dbad5a5526c1a49599d \ + --hash=sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1 \ + --hash=sha256:fb9b1d8cd5c0e8a097438369771d296de808621bc6013aa0065bc83716f5bdb0 \ + --hash=sha256:fe4581f5370dc7a209c1135e9c664a5a78950d3f5c39613bfb15c1e02a6258f3 +pyobjc-framework-mediaaccessibility==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:52479a998fec3d079d2d4590a945fc78c41fe7ac8c76f1964c9d8156880565a4 \ + --hash=sha256:cd07e7fc375ff1e8d225e0aa2bd9c2c1497a4d3aa5a80bfb13b08800fcd7f034 +pyobjc-framework-mediaextension==11.1 ; (platform_release >= '24.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '24' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:06cb19004413a4b08dd75cf1e5dadea7f2df8d15feeeb7adb529d0cf947fa789 \ + --hash=sha256:29edab42d9ecd394ac26f2ae2dfd7e2118452fc60a5623843919c1e9659c9dbc \ + --hash=sha256:40f1440ccc8da6deb80810866f8c807c17567db67b53e1576ea3a3b1330c85f9 \ + --hash=sha256:5efd284932ed0e7cfbca90a142b84a3966c73e51308688f8c230af41f9fb8c39 \ + --hash=sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709 \ + --hash=sha256:ca3a3ef1f3a759b53f297ccd701d29091eec66cc629a2b48c9acbe6c297bf256 +pyobjc-framework-medialibrary==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:102f4326f789734b7b2dfe689abd3840ca75a76fb8058bd3e4f85398ae2ce29d \ + --hash=sha256:779be84bd280f63837ce02028ca46b41b090902aa4205887ffd5777f49377669 +pyobjc-framework-mediaplayer==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:b655cf537ea52d73209eb12935a047301c30239b318a366600f0f44335d51c9a \ + --hash=sha256:d07a634b98e1b9eedd82d76f35e616525da096bd341051ea74f0971e0f2f2ddd +pyobjc-framework-mediatoolbox==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2867c91645a335ee29b47e9c0e9fd3ea8c9daad0c0719c50b8bf244d76998056 \ + --hash=sha256:975de470af8e52104bd1548eb9b4b0ef98524f35a6263c0bb4182797b9c5975b \ + --hash=sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a \ + --hash=sha256:bf26348d20caef38efb9cfc02d28af83c930b2f2c9581407f8ec04b3d8321a7a \ + --hash=sha256:d781e45fb1a7e532bcbae38c0f491629eaa641cdc226019544123b51794baf34 \ + --hash=sha256:e30fd2ffdea1b2c7c314d07266bce7614197c2b3ffd5b09f7012e7df7aa5c7a6 +pyobjc-framework-metal==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1f3aae0f9a4192a7f4f158dbee126ab5ef63a81bf9165ec63bc50c353c8d0e6f \ + --hash=sha256:432fefd3b27ab58c703b2f07afbc4690af815a9a8b4f8a997c4aefa8652e71d7 \ + --hash=sha256:b4c4dcab1db5750575a49a0a903528ea64b5bb93a9f3aaac5c810117a9c07e9c \ + --hash=sha256:d9b24d0ddb98b34a9a19755e5ca507c62fcef40ee5eae017e39be29650137f8c \ + --hash=sha256:de71b46062cb533be2c025cd6018fd4db9d7fd6a65bd67131d8e484c3616321a \ + --hash=sha256:f9fd3b7574a824632ee9b7602973da30f172d2b575dd0c0f5ef76b44cfe9f6f9 +pyobjc-framework-metalfx==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8 \ + --hash=sha256:60e1dcdf133d2504d810c3a9ba5a02781c9d54c2112a9238de8e3ca4e8debf31 \ + --hash=sha256:934cbc969182c57f5094389fe4afe6695595757d0d61f1ab663257475fdcc593 \ + --hash=sha256:aedfee1218b5784b010d618332a2cc088ba2ff9414eaa06e5db465eb5ef0aa43 \ + --hash=sha256:e1b2819bd6a66ba55fb7019b45d38a803ea21b8258fa41c8e9ad7c28cfe74092 \ + --hash=sha256:fdced91f6b2012c556db954de0e17f6d7985d52b4af83262f4d083bcd87aa01c +pyobjc-framework-metalkit==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3e0776886fcd79fe7f0c55c718ebcdf073ac3e05d03040ab284ee09902fe1c70 \ + --hash=sha256:4854cf74fccf6ce516b49bf7cf8fc7c22da9a3743914a2f4b00f336206ad47ec \ + --hash=sha256:62e261b7798b276fee1fee065030a5d19d173863e9c697a80d1fc9a22258ec2c \ + --hash=sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e \ + --hash=sha256:b8a378135566e3c48838c19044e17ed2598a4050516ee1c23eee7d42439ef3c8 \ + --hash=sha256:ce886f3966144774d9222148eaf29fb08097d7dab5658186ded597b7c088f927 +pyobjc-framework-metalperformanceshaders==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:06b2a4e446fe859e30f7efc7ccfbaefd443225a6ec53d949a113a6a4acc16c4c \ + --hash=sha256:21ca31e4246e491df788f00978744d37db975266065f7ccbf393f027b4c6e248 \ + --hash=sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071 \ + --hash=sha256:97be4bd0ded06c663205bd1cf821e148352346f147da48dba44cf7680f0ea23b \ + --hash=sha256:c651e62ce58e75a88cfd287357fdd8d9a7f729c87248c8f43ce16025986afe6a \ + --hash=sha256:c905a3f5a34a95c1fd26bf07da505ed84b9b0a0c88a8f004914d9173f5037142 +pyobjc-framework-metalperformanceshadersgraph==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:9b8b014e8301c2ae608a25f73bbf23c8f3f73a6f5fdbafddad509a21b84df681 \ + --hash=sha256:d25225aab4edc6f786b29fe3d9badc4f3e2d0caeab1054cd4f224258c1b6dbe2 +pyobjc-framework-metrickit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:14de8dcaa107fe15546df91b1f7d51dc398169c3d1b06e02291fdb8722c6bf41 \ + --hash=sha256:75c5a62abc535387eea6a1e1612cfa5b1d59512ebfa8a3352596d481b18cc714 \ + --hash=sha256:92483af233a2c31ef73dd0f7a32988a323f9560699f2f1c6c10a8a282a7b9cfd \ + --hash=sha256:95e98e96b8f122b0141e84f13ae9e0f91d09d0803b1c093fdc7d19123f000f9e \ + --hash=sha256:a034e6b982e915da881edef87d71b063e596511d52aef7a32c683571f364156e \ + --hash=sha256:a79d37575489916c35840e6a07edd958be578d3be7a3d621684d028d721f0b85 +pyobjc-framework-mlcompute==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:975150725e919f8d3d33f830898f3cd2fd19a440999faab320609487f4eae19d \ + --hash=sha256:f6c4c3ea6a62e4e3927abf9783c40495aa8bb9a8c89def744b0822da58c2354b +pyobjc-framework-modelio==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1b1393ddb315c0e8bed3f6ce4e4b355869a30c81ff79bda3ca3a201c0fd06dad \ + --hash=sha256:214a4078950bc7b86a1ea70504ecf292cccebe6515c70023efdddaaa6423f455 \ + --hash=sha256:327e1f3020001fd15bfbf4d4228581a8f64bd85872fd697b7c306343c11e25a6 \ + --hash=sha256:34fabde55d28aa8a12dd4476ad40182513cf87ee2fa928043aa6702961de302b \ + --hash=sha256:5d5e11389bde0852490b2a37896aaf9eb674b2a3586f2c572f9101cecb7bc576 \ + --hash=sha256:fad0fa2c09d468ac7e49848e144f7bbce6826f2178b3120add8960a83e5bfcb7 +pyobjc-framework-multipeerconnectivity==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:296e10d289887cc4141c660f884cced1ec4ce64a19b3e406f13f6ce453a9425f \ + --hash=sha256:35c1a4a4b16df68b658b8531f97799995816a5bf49efd66805e3057b9bb9e474 \ + --hash=sha256:970031deb3dbf8da1fcb04e785d4bd2eeedae8f6677db92881df6d92b05c31d6 \ + --hash=sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b \ + --hash=sha256:c28ad5c0c6d28cbc897aaebcc5f14798762aa9fec7f9110171570fef4d8d8a36 \ + --hash=sha256:c92c95ea611d5272ab37fd73bc8e68c3d8fde515a75b97d8b22dafa8acbc7daf +pyobjc-framework-naturallanguage==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:65a780273d2cdd12a3fa304e9c9ad822cb71facd9281f1b35a71640c53826f7c \ + --hash=sha256:ab1fc711713aa29c32719774fc623bf2d32168aed21883970d4896e901ff4b41 +pyobjc-framework-netfs==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:9c49f050c8171dc37e54d05dd12a63979c8b6b565c10f05092923a2250446f50 \ + --hash=sha256:f202e8e0c2e73516d3eac7a43b1c66f9911cdbb37ea32750ed197d82162c994a +pyobjc-framework-network==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:04582fef567392c2a10dcee9519356b79b17ab73ded050d14592da938d95b01a \ + --hash=sha256:2e45d8fdc0ad553cc35839cae5eab221fe5f7ce28758d693b8159e619ea06eac \ + --hash=sha256:8cdc9be8ec3b0ae95e5c649e4bbcdf502cffd357dacc566223be707bdd5ac271 \ + --hash=sha256:acf16738ab447a31a9f6167171b2a00d65a9370a8e84482d435b2b31c58eed94 \ + --hash=sha256:cafdf953aa80934d30726baa681c1af61daf2cc9fe9e3ca582f4e3796bd0d053 \ + --hash=sha256:f6df7a58a1279bbc976fd7e2efe813afbbb18427df40463e6e2ee28fba07d2df +pyobjc-framework-networkextension==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b \ + --hash=sha256:4c9d6c08b8f1cf374351bcecf8bbc91e6a8999b84d52f30964f4f1e6a323943c \ + --hash=sha256:6d730540d97662867f3cfd90c9a1e69a6adae0f5eb554c1b94a1b067e7ebc728 \ + --hash=sha256:7dea914e7b26e28c6e4f8ffd03dd8fce612d38876043944fb0cf191774634566 \ + --hash=sha256:853458aae8b43634461f6c44759750e2dc784c9aba561f9468ab14529b5a7fbe \ + --hash=sha256:d3d6e9810cb01c3a8f99aed5ee2d75f6f785204338b99b32e5f64370a18cc9dd +pyobjc-framework-notificationcenter==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717 \ + --hash=sha256:15e49491d7f091eaa643f2fd89787becbf767dd6c609aa3d01e53132cb1d9fa1 \ + --hash=sha256:65fc67374a471890245c7a1d60cf67dcf160075a9c048a5d89608a8290f33b03 \ + --hash=sha256:c3e79e9c57f130099b47bde48f26fcd90ab3b52e01d989ea15b7cdb7fa5a34d8 \ + --hash=sha256:e46285290d04e84c167606ccfcb9a20c2567f5a2a6a9c6e96760fc9d561c2740 \ + --hash=sha256:f5ce98882e301adef07651ba495ddd57b661d4c0398afd39f4591c1b44673cca +pyobjc-framework-opendirectory==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:319ac3424ed0350be458b78148914468a8fc13a069d62e7869e3079108e4f118 \ + --hash=sha256:bb4219b0d98dff4a952c50a79b1855ce74e1defd0d241f3013def5b09256fd7b +pyobjc-framework-osakit==11.1 ; platform_system == 'darwin' \ + --hash=sha256:1b0c0cc537ffb8a8365ef9a8b46f717a7cc2906414b6a3983777a6c0e4d53d5a \ + --hash=sha256:920987da78b67578367c315d208f87e8fab01dd35825d72242909f29fb43c820 +pyobjc-framework-oslog==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:7174ca2cdc073e555d5f5aea3baa7410c61a83a3741eaec23e8581340037680e \ + --hash=sha256:7ae29c31ce51c476d3a37ca303465dd8bdfa98df2f6f951cf14c497e984a1ba9 \ + --hash=sha256:a302272aa40d1655be635e0f0dd0ca71b5fce562dfcb88a87165a170a648b2fd \ + --hash=sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979 \ + --hash=sha256:cade8869e185a29fb88fc48e2e5c984548433f669c1a40ec7f5640994fa36603 \ + --hash=sha256:f03789f8d5638e1075652b331b8ebf98c03dfa809c57545f0313583a7688bb86 +pyobjc-framework-passkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:779eaea4e1931cfda4c8701e1111307b14bf9067b359a319fc992b6848a86932 \ + --hash=sha256:a6306dda724ca812dca70154d40f32ec9bbdaff765a12f3cc45391723efe147e \ + --hash=sha256:bfff2a63850afe702ba25f661360393389ffb58e127d47488c414caa9e676aa7 \ + --hash=sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f \ + --hash=sha256:d7948d5b3369b60808a85dcadffdebb0a44e8d2c4716edc10b78cb76fa762070 \ + --hash=sha256:f6b7f3cd7c6855af1b6fc4036ae2f10779a312182107c94d36ef63c2dd4a6f87 +pyobjc-framework-pencilkit==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:9c173e0fe70179feadc3558de113a8baad61b584fe70789b263af202bfa4c6be \ + --hash=sha256:b7824907bbcf28812f588dda730e78f662313baf40befd485c6f2fcb49018019 +pyobjc-framework-phase==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:a940d81ac5c393ae3da94144cf40af33932e0a9731244e2cfd5c9c8eb851e3fc \ + --hash=sha256:cfa61f9c6c004161913946501538258aed48c448b886adbf9ed035957d93fa15 +pyobjc-framework-photos==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3a759ebcf46493cd09e5c89c0a09096ad83ae837d9236e437571bb22ca6eab3f \ + --hash=sha256:541d8fafdb2f111f2f298e1aa0542f2d5871ce1dd481c3e9be4ed33916b38c3a \ + --hash=sha256:72e0ed9bc5f1890f882df55333797da95c0ed1c1d7a0fe7d869a8d4ee4e1bdfd \ + --hash=sha256:7cded282eaebd77645a4262f6fb63379c7a226d20f8f1763910b19927709aea2 \ + --hash=sha256:8dbfffd29cfa63a8396ede0030785c15a5bc36065d3dd98fc6176a59e7abb3d3 \ + --hash=sha256:c8c3b25b14a2305047f72c7c081ff3655b3d051f7ed531476c03246798f8156d +pyobjc-framework-photosui==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df \ + --hash=sha256:6f0fa9c9e363c0db54957dfe4e26214379f2698caaba1e4ff4c9e3eba5e690d9 \ + --hash=sha256:91aff7caae16a7a7f25e35692aa92b796155510b8a0575668e75f351fbf63a68 \ + --hash=sha256:b2f278f569dfd596a32468351411518a651d12cb91e60620094e852c525a5f10 \ + --hash=sha256:e607242e09fb7d4bcad2f3eb2e88529d8f2ff7cf7341cd2c6c5b3f4d6744218e \ + --hash=sha256:f11f6043c83b2c65ecad69c48844fff6368127af3956ec8df9726bbd1e5da17e +pyobjc-framework-preferencepanes==11.1 ; platform_system == 'darwin' \ + --hash=sha256:6e4a55195ec9fc921e0eaad6b3038d0ab91f0bb2f39206aa6fccd24b14a0f1d8 \ + --hash=sha256:6ee5f5a7eb294e03ea3bac522ac4b69e6dc83ceceff627a0a2d289afe1e01ad9 +pyobjc-framework-pubsub==11.1 ; platform_release >= '9' and platform_release < '18' and platform_system == 'darwin' and sys_platform == 'darwin' \ + --hash=sha256:47221f63466c523516ab5d2297dd3c644d915a77ca1f1867b0055d735486f1f8 \ + --hash=sha256:cea6bd9e0af46f9ea1c8d002a92e462576dd5a772a7e0688d40c7903755af11f +pyobjc-framework-pushkit==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:21993b7e9127b05575a954faa68e85301c6a4c04e34e38aff9050f67a05c562a \ + --hash=sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4 \ + --hash=sha256:68c4f44354eab84cb54d43310fa65ca3a5ba68299c868378764cc50803cf2adc \ + --hash=sha256:80d5d8240b71631d81cfa96f398fae1d137be98f224739e50edaf9e5afc21a9d \ + --hash=sha256:bac3ee77dfbe936998f207c1579e346993485bab8849db537ed250261cf12ab3 \ + --hash=sha256:cfec36cdca24654be0465282eb31b7ff3674ea4b7f3ce696b07edbe33b000aa5 +pyobjc-framework-quartz==11.1 ; platform_system == 'darwin' \ + --hash=sha256:07cbda78b4a8fcf3a2d96e047a2ff01f44e3e1820f46f0f4b3b6d77ff6ece07c \ + --hash=sha256:39d02a3df4b5e3eee1e0da0fb150259476910d2a9aa638ab94153c24317a9561 \ + --hash=sha256:43a1138280571bbf44df27a7eef519184b5c4183a588598ebaaeb887b9e73e76 \ + --hash=sha256:9ac806067541917d6119b98d90390a6944e7d9bd737f5c0a79884202327c9204 \ + --hash=sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75 \ + --hash=sha256:b23d81c30c564adf6336e00b357f355b35aad10075dd7e837cfd52a9912863e5 +pyobjc-framework-quicklookthumbnailing==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1614dc108c1d45bbf899ea84b8691288a5b1d25f2d6f0c57dfffa962b7a478c3 \ + --hash=sha256:4d1863c6c83c2a199c1dbe704b4f8b71287168f4090ed218d37dc59277f0d9c9 +pyobjc-framework-replaykit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:22c6d09be9a6e758426d723a6c3658ad6bbb66f97ba9a1909bfcf29a91d99921 \ + --hash=sha256:2bf2180feae500fdd6f14360200fda0b6650a4ec39fe5d84a5dde9e8cdd307b6 \ + --hash=sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b \ + --hash=sha256:7742ee18c8c9b61f5668698a05b88d25d34461fcdd95a8f669ecdfd8db8c4d42 \ + --hash=sha256:b503fabc33ee02117fd82c78db18cba3f0be90dea652f5553101a45185100402 \ + --hash=sha256:da84e48ba5d529ae72b975f0d81c5bd5427983c2b05d3d2c7fd54a6cbdf0d0f9 +pyobjc-framework-safariservices==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0 \ + --hash=sha256:8a4371d64052a3ffe9993a89c45f9731f86e7b6c21fd1d968815fd7930ff501a \ + --hash=sha256:8a6ec417d35a0600629eba97c0ab2f2d09fae171e8bca3d3d6aa1c7ff272c4d7 \ + --hash=sha256:8b4d4169dd21e69246d90a42f872b7148064b63de6bbbf6bc6ddabe33f143843 \ + --hash=sha256:abdbe0d8a79caa994a1d2be8ea4e5a1e4c80f7d8e1f0750f9c365129d1f1a968 \ + --hash=sha256:c92eb9e35f98368ea1bfaa8cdd41138ca8b004ea5a85833390a44e5626ca5061 +pyobjc-framework-safetykit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:24d5ce9dfb80abb634a95ceda3da0f0cdb52c765db0f47de953a4f66b918c957 \ + --hash=sha256:761304365978d650015fe05fb624ba13ea4af6c6a76ef8e344673f5b0fed2e92 \ + --hash=sha256:8130de57f701dbccb1d84c76ec007fe04992da58cbf0eb906324393eeac3d08d \ + --hash=sha256:b76fccdb970d3d751a540c47712e9110afac9abea952cb9b7bc0d5867db896e3 \ + --hash=sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4 \ + --hash=sha256:cd8091c902037eac4a403d8462424afd711f43206af3548a34bebe1f59d2c340 +pyobjc-framework-scenekit==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2be143172b43c2cf4a2b3fad9e15ffb5d29df677d3678160cd125b94a30caaca \ + --hash=sha256:2f347d5ae42af8acddb86a45f965046bb91f8d83d33851390954439961e2a7b7 \ + --hash=sha256:3f62f2b8f26375ecfec71f7fdb23f2739cf93d213968c6ffac6a8525516ffc6e \ + --hash=sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306 \ + --hash=sha256:c803d95b30c4ce49f46ff7174806f5eb84e4c3a152f8f580c5da0313c5c67041 \ + --hash=sha256:ea2f02eea982872994d7c366f6a51060a90cc17b994c017f85c094e2bc346847 +pyobjc-framework-screencapturekit==11.1 ; platform_release >= '21.4' and platform_system == 'darwin' \ + --hash=sha256:1119d6258d6c668564ab39154cfc745fd2bb8b3beeaa4f9b2a8a4c93926678c0 \ + --hash=sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0 \ + --hash=sha256:641fa7834f54558859209e174c83551d5fa239ca6943ace52665f7d45e562ff2 \ + --hash=sha256:9972db69064b69e78fbc6a00f1de2d8eaa225b990b23687970328b061e60e26d \ + --hash=sha256:9e135b414d3829fcf7fd8a66c94e8b51135fb9f630c10488fb9d78f27f622906 \ + --hash=sha256:f93f8198741bd904d423a7b1ef941445246bdf6cb119597d981e61a13cc479a4 +pyobjc-framework-screensaver==11.1 ; platform_system == 'darwin' \ + --hash=sha256:2c01a9646bc118445cbb117e7016bd1df9fe93a65db991ab5496d59b1a7bc66d \ + --hash=sha256:46d65c1e14d35f287e7be351e2f98daf9489e31e7ca0d306e6102904ce6c40fb \ + --hash=sha256:7852c2281148cb99c87c4c25b83dca7fdd11e6eed04deadcf2201ed5a2079e5f \ + --hash=sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2 \ + --hash=sha256:e32c83e1d9e5044d482916ac42257a87d1f1068f3f6bccaa04edda40fb9f9ad1 \ + --hash=sha256:f2d22293cf9d715e4692267a1678096afd6793c0519d9417cf77c8a6c706a543 +pyobjc-framework-screentime==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:50a4e4ab33d6643a52616e990aa1c697d5e3e8f9f9bdab8d631e6d42d8287b4f \ + --hash=sha256:9bb8269456bbb674e1421182efe49f9168ceefd4e7c497047c7bf63e2f510a34 +pyobjc-framework-scriptingbridge==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:226ba12d9cbd504411b702323b0507dd1690e81b4ce657c5f0d8b998c46cf374 \ + --hash=sha256:5381e9be1299e1134489e4d46662c649613214265b3b691264cfba0b083929f5 \ + --hash=sha256:57f5401826e3a008d9cfb7c164187859cadc1b1f96194dc0a7c596f502548c26 \ + --hash=sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699 \ + --hash=sha256:a84d0a8ff4fa1f0016f5d797ad93e22e437212a2fc8e6417a3b8d68f89229680 \ + --hash=sha256:c2ba0ad3d3e4e3c6a43fe3e84ab02c5c4e74000bb6f130ae47bf82a3dcd4af98 +pyobjc-framework-searchkit==11.1 ; platform_system == 'darwin' \ + --hash=sha256:13a194eefcf1359ce9972cd92f2aadddf103f3efb1b18fd578ba5367dff3c10c \ + --hash=sha256:9c9d6ca71cef637ccc3627225fb924a460b3d0618ed79bb0b3c12fcbe9270323 +pyobjc-framework-security==11.1 ; platform_system == 'darwin' \ + --hash=sha256:141cc3ee08627ae0698264efc3dbbaf28d2255e0fe690e336eb8f0f387c4af01 \ + --hash=sha256:158da3b2474e2567fd269531c4ee9f35b8ba4f1eccbd1fb4a37c85a18bf1243c \ + --hash=sha256:2eb4ba6d8b221b9ad5d010e026247e8aa26ee43dcaf327e848340ed227d22d7e \ + --hash=sha256:4db1ebf6395cd370139cb35ff172505fc449c7fdf5d3a28f2ada8a30ef132cd0 \ + --hash=sha256:858a18303711eb69d18d1a64cf8bb2202f64a3bd1c82203c511990dbd8326514 \ + --hash=sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da +pyobjc-framework-securityfoundation==11.1 ; platform_system == 'darwin' \ + --hash=sha256:25f2cf10f80c122f462e9d4d43efe9fd697299c194e0c357e76650e234e6d286 \ + --hash=sha256:b3c4cf70735a93e9df40f3a14478143959c415778f27be8c0dc9ae0c5b696b92 +pyobjc-framework-securityinterface==11.1 ; platform_system == 'darwin' \ + --hash=sha256:0232f947b4f906097a5d758305097a8688835a52e0721b75ae3f1180eac30f50 \ + --hash=sha256:26056441b325029da06a7c7b8dd1a0c9a4ad7d980596c1b04d132a502b4cacc0 \ + --hash=sha256:2c20bedead75de7bf1f2ceda562755f64c70ee86180ed45480dc9dbc55609a0b \ + --hash=sha256:708dd1d65309f3d4043ecaf152591c240601a5d3da7ae7a500f511c54317537b \ + --hash=sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9 \ + --hash=sha256:e9ebfb32177eb06f5c894be97c6af3802f09b9890fce8e0956cc0e680af4eafd +pyobjc-framework-securityui==11.1 ; platform_release >= '24.4' and platform_system == 'darwin' \ + --hash=sha256:3cdb101b03459fcf8e4064b90021d06761003f669181e02f43ff585e6ba2403d \ + --hash=sha256:e80c93e8a56bf89e4c0333047b9f8219752dd6de290681e9e2e2b2e26d69e92d +pyobjc-framework-sensitivecontentanalysis==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:5b310515c7386f7afaf13e4632d7d9590688182bb7b563f8026c304bdf317308 \ + --hash=sha256:dbb78f5917f986a63878bb91263bceba28bd86fc381bad9461cf391646db369f +pyobjc-framework-servicemanagement==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:104f56557342a05ad68cd0c9daf63b7f4678957fe1f919f03a872f1607a50710 \ + --hash=sha256:90a07164da49338480e0e135b445acc6ae7c08549a2037d1e512d2605fedd80a +pyobjc-framework-sharedwithyou==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:481362f0bde6def86634fc687abe6f4dee650c09c22b48bfe5af5322f9947cef \ + --hash=sha256:63b1cb673b844ebfeddc032d0539f913bbd6b67ab2a310a1fcff7842dba9c714 \ + --hash=sha256:70421a8fd326afd99eeae273b693a7b4d2d200c38e883d8219a84123a4ba0861 \ + --hash=sha256:99e1749187ae370be7b9c55dd076d1b8143f0d8db3e83f52540586f32e7abb33 \ + --hash=sha256:c1a1770aa2c417f17010623414fb12943570baa726d8780dd7446ba5bcee8c3d \ + --hash=sha256:ece3a28a3083d0bcad0ac95b01f0eb699b9d2d0c02c61305bfd402678753ff6e +pyobjc-framework-sharedwithyoucore==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:4e19bfc74f392546ca4b7ea5271d4802617445ad493428370eafd3cddd4d977e \ + --hash=sha256:5a45c562c99017f8e057d4080012b63a9bb660c696334707c54d7b4018ca1017 \ + --hash=sha256:6aee3df8bed97a74e1f79609f9884edcaab2d305db20bdcae39e47b3e513c559 \ + --hash=sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73 \ + --hash=sha256:a3fb0e745fd022fed48cc9a5e0dcbf8d1abcb5bfc192150e3a2584f4351791fc \ + --hash=sha256:dd18c588b29de322c25821934d6aa6d2bbbdbb89b6a4efacdb248b4115fc488d +pyobjc-framework-shazamkit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:8bac17f285742e0f13a54c7085ef3035d8034ffc43d18d3d68fb41283c5064ff \ + --hash=sha256:b323f5409b01711aa2b6e2113306084fab2cc83fa57a0c3d55bd5876358b68d8 \ + --hash=sha256:b3304c3a67e3722b895d874f215dd4277b49cedddb72fa780a791ef79e5c3d45 \ + --hash=sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6 \ + --hash=sha256:ef51f461672234076b3791ad4be05adad20a2e24b9d7d93acd7bf18d7f9b1714 \ + --hash=sha256:f7d191fb187dbb05e3f88f546d5207618d65e270d7a4316b51b1171cc491e268 +pyobjc-framework-social==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:ab5878c47d7a0639704c191cee43eeb259e09688808f0905c42551b9f79e1d57 \ + --hash=sha256:fbc09d7b00dad45b547f9b2329f4dcee3f5a50e2348de1870de0bd7be853a5b7 +pyobjc-framework-soundanalysis==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:42cd25b7e0f343d8b59367f72b5dae96cf65696bdb8eeead8d7424ed37aa1434 \ + --hash=sha256:6cf983c24fb2ad2aa5e7499ab2d30ff134d887fe91fd2641acf7472e546ab4e5 +pyobjc-framework-speech==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:3c80670dbad921bf1d4954a9de29525acb53ee84e064a95fbbdfddff1db2f14f \ + --hash=sha256:7726eff52cfa9cc7178ddcd1285cbc23b5f89ee55b4b850b0d2e90bb4f8e044b \ + --hash=sha256:80e577e3dfc1c10a1280deae172cdb64e105f99f47343099e3968b720a3f68da \ + --hash=sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c \ + --hash=sha256:f19778a4ace37c538a34a10ac1f595c80b83489210e6fa60c703399aee264c7e \ + --hash=sha256:f36ca8a3cfc12b7a5cdf00712eec3ad0fac34e3da36b5737c5302e224525aa70 +pyobjc-framework-spritekit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2277e74d7be426181ae5ca7dd9d6c776426e8e825ad83b6046a7cb999015f27d \ + --hash=sha256:4e3673196b7cbc007e4aa7f14d711f3cda00e32e120bc4f6e896d54edd517c61 \ + --hash=sha256:8b470a890db69e70ef428dfff88da499500fca9b2d44da7120dc588d13a2dbdb \ + --hash=sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108 \ + --hash=sha256:d6ea27fc202b40945729db50fdc6f75a0a11a07149febf4b99e14caf96ef33b0 \ + --hash=sha256:e04d0825109a0158e551e9e2a61c56e83eadfdc5a44a47b64cb410b0498d33be +pyobjc-framework-storekit==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2e2607116b0d53d7fda2fc48e37b1deb1d26a60e7b723a6b7c391a3f48b2ac3b \ + --hash=sha256:4944bd1fd01f486623453b68accf4445d3c5686714820c8329a0c4e4672d6fff \ + --hash=sha256:5ca3373272b6989917c88571ca170ce6d771180fe1a2b44c7643fe084569b93e \ + --hash=sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164 \ + --hash=sha256:be6c894a9f9c2b40e300005c3a3cf46f352e1711f65c0b7a8dd5035d1f6333aa \ + --hash=sha256:d312c392962e15fc842d11b0f7d937e3bd9f3ed3a80f7a6be77518475564f04d +pyobjc-framework-symbols==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0e09b7813ef2ebdca7567d3179807444dd60f3f393202b35b755d4e1baf99982 \ + --hash=sha256:1de6fc3af15fc8d5fd4869663a3250311844ec33e99ec8a1991a352ab61d641d +pyobjc-framework-syncservices==11.1 ; platform_system == 'darwin' \ + --hash=sha256:03124c8c7c7ce837f51e1c9bdcf84c6f1d5201f92c8a1c172ec34908d5e57415 \ + --hash=sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe \ + --hash=sha256:24c2b62e94d9e0e5e64abbf6d1f9994212b2a5cb8cad5a8d0394d694b20731b5 \ + --hash=sha256:711d493c7967682bee605c5909a49d268d9b3dd3cb7a71d8ab5dbe01a069eb44 \ + --hash=sha256:a0ff222472b2cb5c345c92ae4bde245f4181843379f4fd9462cd5c096ed7b2f1 \ + --hash=sha256:e5b29d6e8fe5b0015dcac5485e4fe6ede35bae7beeb647fb81d86120365029ea +pyobjc-framework-systemconfiguration==11.1 ; platform_system == 'darwin' \ + --hash=sha256:6881929b828a566bf1349f09db4943e96a2b33f42556e1f7f6f28b192420f6fc \ + --hash=sha256:ab2ff52e4228f42182b7ef398d0da504f9f8f4a889963422af9aa1f495668db2 \ + --hash=sha256:b994c613b5bea9f1c9a64f57f373563c7f424ffae5e4cb20e76c8448a35543f7 \ + --hash=sha256:c236f19cadc9fff56c0afb3e4ad6f8c8e11c5679e31ed413fe6876bf2ea73353 \ + --hash=sha256:ef266e9f83c2fc9a999709626138b427ff052a0acf4851d797c3a7654878c046 \ + --hash=sha256:f30ed0e9a8233fecb06522e67795918ab230ddcc4a18e15494eff7532f4c3ae1 +pyobjc-framework-systemextensions==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1801413066d1cbf2a0319e228060820c51ea0fb27aec339716d8c82f2e1b3125 \ + --hash=sha256:2ed65857244f18b88107e5d3ea8ea21c9da662490895b430e376423ee7c0b963 \ + --hash=sha256:3a2b1e84e4a118bfe13efb9f2888b065dc937e2a7e60afd4d0a82b51b8301a10 \ + --hash=sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6 \ + --hash=sha256:97c1b5f415f3981d0426516e014e94392f054f3898252bf6c88c3f50700c1d70 \ + --hash=sha256:9aa7595de4f8f6a252c50419c0343f7326c6a4de47da5b933a17880d1cadfa36 +pyobjc-framework-threadnetwork==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:55021455215a0d3ad4e40152f94154e29062e73655558c5f6e71ab097d90083e \ + --hash=sha256:73a32782f44b61ca0f8a4a9811c36b1ca1cdcf96c8a3ba4de35d8e8e58a86ad5 +pyobjc-framework-uniformtypeidentifiers==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:6e2e8ea89eb8ca03bc2bc8e506fff901e71d916276475c8d81fbf0280059cb4c \ + --hash=sha256:86c499bec8953aeb0c95af39b63f2592832384f09f12523405650b5d5f1ed5e9 +pyobjc-framework-usernotifications==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996 \ + --hash=sha256:62a4bd242b761a6f00a4374a369391346d225d68be07691e042ec7db452084c8 \ + --hash=sha256:9efa3004059a8fe3f3c52f638f0401dbcdbc7b2f539587c8868da2486a64d674 \ + --hash=sha256:bad5e650c014757159523466e5b2c127e066045e2a5579a5cac9aeca46bda017 \ + --hash=sha256:ce6006989fd4a59ec355f6797ccdc9946014ea5241ff7875854799934dbba901 \ + --hash=sha256:dcdcb657d2fa47108e4ef93ec3320025576857e8f69a15f082f5eda930b35e86 +pyobjc-framework-usernotificationsui==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:18e0182bddd10381884530d6a28634ebb3280912592f8f2ad5bac2a9308c6a65 \ + --hash=sha256:b84d73d90ab319acf8fad5c59b7a5e2b6023fbb2efd68c58b532e3b3b52f647a +pyobjc-framework-videosubscriberaccount==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:2dd78586260fcee51044e129197e8bf2e157176e02babeec2f873afa4235d8c6 \ + --hash=sha256:d5a95ae9f2a6f0180a5bbb10e76c064f0fd327aae00a2fe90aa7b65ed4dad7ef +pyobjc-framework-videotoolbox==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:0e54bd6cfcbdda4add24e8e873baab11dfb436633100cc6664f3c068e615a6ff \ + --hash=sha256:31acfb12cea4f0624ecb92e74404f15e2755fbf0a3f4133dc93add44cf4a6a9f \ + --hash=sha256:65a96385e80cb9ad3eab7d1f3156452ff805a925c9ca287ff1491a97cca191ba \ + --hash=sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9 \ + --hash=sha256:c55285c3c78183fd2a092d582e30b562777a82985cccca9e7e99a0aff2601591 \ + --hash=sha256:e282cb07f6a51647ac19a3b5d31e26f1619285bac24171e403921d671e4756d9 +pyobjc-framework-virtualization==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:12a5ef32d2b7a56b675ea34fcb68bb9dddb7cf2c0a5ac5131f35551767bdacf1 \ + --hash=sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022 \ + --hash=sha256:59df6702b3e63200752be7d9c0dc590cb4c3b699c886f9a8634dd224c74b3c3c \ + --hash=sha256:5f35d823003a613bde27c2c699a8a7de45dc2bdd2e1121e0c4a337b877dfc64e \ + --hash=sha256:790bd2e42e8c5890319f8c576d5e171f87f95655e6fc55cf19a5f85f9e23558a \ + --hash=sha256:b2e7ab5204fe80249dd8d031b761cf9c0106d0d5e61d88930e0f334f5060d820 +pyobjc-framework-vision==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ + --hash=sha256:1e5617e37dd2a7cff5e69e9aab039ea74b39ccdc528f6c828f2b60c1254e61e5 \ + --hash=sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b \ + --hash=sha256:d00830c71a30fc893b3c5ee65119c7e5e5a95a16af53b8e56a0e58cff57e3b56 \ + --hash=sha256:d1f8fdccc6135fdbfd66d8f21240d6c84465cb8e116a8e5b43601aed020051e5 \ + --hash=sha256:df076c3e3e672887182953efc934c1f9683304737e792ec09a29bfee90d2e26a \ + --hash=sha256:dfd148a6df30ac70a9c41dd90a6c8f8c7f339bd9ca6829629a902f272e02b6b4 +pyobjc-framework-webkit==11.1 ; platform_system == 'darwin' \ + --hash=sha256:13b774d4244734cb77bf3c3648149c163f62acaa105243d7c48bb3fd856b5628 \ + --hash=sha256:1a6e6f64ca53c4953f17e808ecac11da288d9a6ade738156ba161732a5e0c96a \ + --hash=sha256:1d01008756c3912b02b7c02f62432467fbee90a93e3b8e31fa351b4ca97c9c98 \ + --hash=sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae \ + --hash=sha256:864f9867a2caaeaeb83e5c0fa3dcf78169622233cf93a9a5eeb7012ced3b8076 \ + --hash=sha256:c1c00d549ab1d50e3d7e8f5f71352b999d2c32dc2365c299f317525eb9bff916 +pyotp==2.9.0 \ + --hash=sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63 \ + --hash=sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612 +pyparsing==3.2.5 \ + --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ + --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e +pypdf==6.14.2 \ + --hash=sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946 \ + --hash=sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25 +pyperclip==1.11.0 \ + --hash=sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6 \ + --hash=sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273 +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 +python-docx==1.2.0 \ + --hash=sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7 \ + --hash=sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce +python-dotenv==1.2.2 \ + --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ + --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 +python-frontmatter==1.1.0 \ + --hash=sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1 \ + --hash=sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d +python-json-logger==4.0.0 \ + --hash=sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 \ + --hash=sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f +python-multipart==0.0.31 \ + --hash=sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28 \ + --hash=sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680 +pywin32==311 ; sys_platform == 'win32' \ + --hash=sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87 \ + --hash=sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d \ + --hash=sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31 \ + --hash=sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a \ + --hash=sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42 \ + --hash=sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee \ + --hash=sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067 \ + --hash=sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852 \ + --hash=sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d +pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ + --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ + --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 +pyyaml==6.0.2 \ + --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ + --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ + --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ + --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ + --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ + --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ + --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ + --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ + --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ + --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ + --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ + --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ + --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ + --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ + --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ + --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ + --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ + --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba +redis==7.2.1 \ + --hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \ + --hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5 +referencing==0.36.2 \ + --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \ + --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 +regex==2025.9.18 \ + --hash=sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5 \ + --hash=sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282 \ + --hash=sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef \ + --hash=sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41 \ + --hash=sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3 \ + --hash=sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788 \ + --hash=sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac \ + --hash=sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56 \ + --hash=sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946 \ + --hash=sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203 \ + --hash=sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12 \ + --hash=sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e \ + --hash=sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d \ + --hash=sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af \ + --hash=sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3 \ + --hash=sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23 \ + --hash=sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494 \ + --hash=sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1 \ + --hash=sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2 \ + --hash=sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096 \ + --hash=sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5 \ + --hash=sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d \ + --hash=sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77 \ + --hash=sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e \ + --hash=sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e \ + --hash=sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad \ + --hash=sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f \ + --hash=sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb \ + --hash=sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352 \ + --hash=sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a \ + --hash=sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3 \ + --hash=sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425 \ + --hash=sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379 \ + --hash=sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d \ + --hash=sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d \ + --hash=sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d \ + --hash=sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743 \ + --hash=sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742 \ + --hash=sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47 \ + --hash=sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164 \ + --hash=sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a \ + --hash=sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0 \ + --hash=sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea \ + --hash=sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783 \ + --hash=sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7 \ + --hash=sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68 \ + --hash=sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e \ + --hash=sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b \ + --hash=sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368 \ + --hash=sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282 \ + --hash=sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306 \ + --hash=sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01 \ + --hash=sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95 \ + --hash=sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb \ + --hash=sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29 \ + --hash=sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a \ + --hash=sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4 \ + --hash=sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129 \ + --hash=sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb \ + --hash=sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6 \ + --hash=sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f \ + --hash=sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773 \ + --hash=sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730 \ + --hash=sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a \ + --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 \ + --hash=sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459 \ + --hash=sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90 \ + --hash=sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab \ + --hash=sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f \ + --hash=sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b \ + --hash=sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2 +reportlab==4.4.4 \ + --hash=sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb \ + --hash=sha256:cb2f658b7f4a15be2cc68f7203aa67faef67213edd4f2d4bdd3eb20dab75a80d +requests==2.33.0 \ + --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ + --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 +requests-oauthlib==2.0.0 \ + --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \ + --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9 +rich==14.3.3 \ + --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ + --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b +rich-rst==1.3.2 \ + --hash=sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4 \ + --hash=sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a +rpds-py==0.27.1 \ + --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ + --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ + --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ + --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ + --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ + --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ + --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ + --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ + --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ + --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ + --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ + --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ + --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ + --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ + --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ + --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ + --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ + --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ + --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ + --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ + --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ + --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ + --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ + --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ + --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ + --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ + --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ + --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ + --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ + --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ + --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ + --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ + --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ + --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ + --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ + --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ + --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ + --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ + --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ + --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ + --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ + --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ + --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ + --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ + --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ + --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ + --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ + --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ + --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ + --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ + --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ + --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ + --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ + --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ + --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ + --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ + --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ + --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ + --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ + --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ + --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ + --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ + --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ + --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ + --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ + --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ + --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ + --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ + --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ + --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ + --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ + --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ + --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ + --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec +screeninfo==0.8.1 ; platform_system != 'darwin' \ + --hash=sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1 \ + --hash=sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c +secretstorage==3.5.0 ; sys_platform == 'linux' \ + --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ + --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 +sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc +socksio==1.0.0 \ + --hash=sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3 \ + --hash=sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac +sortedcontainers==2.4.0 \ + --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ + --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 +soupsieve==2.8.4 \ + --hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \ + --hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 +sse-starlette==3.0.2 \ + --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ + --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a +starlette==1.3.1 \ + --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ + --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 +tenacity==9.1.2 \ + --hash=sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb \ + --hash=sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138 +tiktoken==0.12.0 \ + --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ + --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ + --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ + --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ + --hash=sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff \ + --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ + --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ + --hash=sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3 \ + --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ + --hash=sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a \ + --hash=sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0 \ + --hash=sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0 \ + --hash=sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b \ + --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ + --hash=sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3 \ + --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ + --hash=sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b \ + --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ + --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ + --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ + --hash=sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646 \ + --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ + --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ + --hash=sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697 \ + --hash=sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8 \ + --hash=sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365 \ + --hash=sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e \ + --hash=sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830 \ + --hash=sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16 \ + --hash=sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88 \ + --hash=sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f \ + --hash=sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63 \ + --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ + --hash=sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71 \ + --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 \ + --hash=sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd +tokenizers==0.23.1 \ + --hash=sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4 \ + --hash=sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288 \ + --hash=sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3 \ + --hash=sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa \ + --hash=sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4 \ + --hash=sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51 \ + --hash=sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a \ + --hash=sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948 \ + --hash=sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e \ + --hash=sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9 \ + --hash=sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959 \ + --hash=sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224 \ + --hash=sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e \ + --hash=sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96 \ + --hash=sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7 \ + --hash=sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a \ + --hash=sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6 +tom-swe==1.0.3 \ + --hash=sha256:57c97d0104e563f15bd39edaf2aa6ac4c3e9444afd437fb92458700d22c6c0f5 \ + --hash=sha256:7b1172b29eb5c8fb7f1975016e7b6a238511b9ac2a7a980bd400dcb4e29773f2 +tqdm==4.67.1 \ + --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ + --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 +tree-sitter==0.25.2 \ + --hash=sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd \ + --hash=sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5 \ + --hash=sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358 \ + --hash=sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b \ + --hash=sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721 \ + --hash=sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0 \ + --hash=sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897 \ + --hash=sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0 \ + --hash=sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8 \ + --hash=sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9 \ + --hash=sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614 \ + --hash=sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f \ + --hash=sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053 \ + --hash=sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87 \ + --hash=sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c \ + --hash=sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae \ + --hash=sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99 \ + --hash=sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960 \ + --hash=sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab \ + --hash=sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601 \ + --hash=sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac \ + --hash=sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20 +tree-sitter-bash==0.25.1 \ + --hash=sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf \ + --hash=sha256:3f484c4bb8796cde7a87ca351e6116f09653edac0eb3c6d238566359dd28b117 \ + --hash=sha256:52a6802d9218f86278aa3e8b459c3abdad67eed0fde1f9f13aca5b6c634217a6 \ + --hash=sha256:59115057ec2bae319e8082ff29559861045002964c3431ccb0fc92aa4bc9bccb \ + --hash=sha256:5e76af6df46d958c7f5b6d5884c9743218e3902a00ccb493ec92728b1084430b \ + --hash=sha256:a3332d71c7b7d5f78259b19d02d0ea111fcb82b72712ee4a93aaa5b226d3f0a8 \ + --hash=sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14 \ + --hash=sha256:e76c4cfb20b076552406782b7f8c2a3946835993df0a44df006de54b7030c7dc \ + --hash=sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512 +typing-extensions==4.15.0 \ + --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ + --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 +typing-inspection==0.4.2 \ + --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ + --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +uncalled-for==0.2.0 \ + --hash=sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f \ + --hash=sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f +uritemplate==4.2.0 \ + --hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \ + --hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +uuid7==0.1.0 \ + --hash=sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61 \ + --hash=sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c +uvicorn==0.52.4 \ + --hash=sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86 \ + --hash=sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1 +watchfiles==1.1.1 \ + --hash=sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c \ + --hash=sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510 \ + --hash=sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0 \ + --hash=sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18 \ + --hash=sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219 \ + --hash=sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803 \ + --hash=sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94 \ + --hash=sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6 \ + --hash=sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce \ + --hash=sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099 \ + --hash=sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae \ + --hash=sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43 \ + --hash=sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd \ + --hash=sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374 \ + --hash=sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051 \ + --hash=sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7 \ + --hash=sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77 \ + --hash=sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b \ + --hash=sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33 \ + --hash=sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42 \ + --hash=sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5 \ + --hash=sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da \ + --hash=sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05 \ + --hash=sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a \ + --hash=sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101 \ + --hash=sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb \ + --hash=sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620 \ + --hash=sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6 \ + --hash=sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef \ + --hash=sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261 \ + --hash=sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02 \ + --hash=sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af \ + --hash=sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9 \ + --hash=sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21 \ + --hash=sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d \ + --hash=sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c \ + --hash=sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81 \ + --hash=sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9 \ + --hash=sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2 \ + --hash=sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc \ + --hash=sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404 \ + --hash=sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01 \ + --hash=sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18 \ + --hash=sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04 \ + --hash=sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3 \ + --hash=sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610 \ + --hash=sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0 \ + --hash=sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150 \ + --hash=sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5 \ + --hash=sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a \ + --hash=sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b \ + --hash=sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d \ + --hash=sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70 \ + --hash=sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e \ + --hash=sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5 \ + --hash=sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb \ + --hash=sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44 \ + --hash=sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428 \ + --hash=sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa \ + --hash=sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf +wcwidth==0.2.14 \ + --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ + --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 +websockets==15.0.1 \ + --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ + --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ + --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ + --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ + --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ + --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ + --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ + --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ + --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ + --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ + --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ + --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ + --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ + --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ + --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ + --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ + --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ + --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ + --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ + --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ + --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ + --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ + --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 +wrapt==1.17.3 \ + --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ + --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ + --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ + --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ + --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ + --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ + --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ + --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ + --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ + --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ + --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ + --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ + --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ + --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ + --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ + --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ + --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ + --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ + --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ + --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ + --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ + --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ + --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ + --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ + --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ + --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ + --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ + --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ + --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ + --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ + --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ + --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ + --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ + --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ + --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ + --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ + --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ + --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ + --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ + --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ + --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ + --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 +yarl==1.22.0 \ + --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ + --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ + --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ + --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ + --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ + --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ + --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ + --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ + --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ + --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ + --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ + --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ + --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ + --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ + --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ + --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ + --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ + --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ + --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ + --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ + --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ + --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ + --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ + --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ + --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ + --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ + --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ + --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ + --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ + --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ + --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ + --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ + --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ + --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ + --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ + --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ + --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ + --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ + --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ + --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ + --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ + --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ + --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ + --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ + --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ + --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ + --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ + --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ + --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ + --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ + --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ + --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ + --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ + --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ + --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ + --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ + --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ + --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ + --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ + --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ + --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ + --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ + --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ + --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ + --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ + --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ + --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ + --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ + --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ + --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ + --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ + --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ + --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ + --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ + --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ + --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ + --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ + --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ + --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ + --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ + --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ + --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 +zipp==3.23.0 \ + --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ + --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 +openhands-sdk @ https://files.pythonhosted.org/packages/e9/ac/d27c5becc3b59b8cc46a1614ac478cc94eb73e626ab15831906fcdaafb29/openhands_sdk-1.49.6-py3-none-any.whl \ + --hash=sha256:3b4701f125925e804929ab02b61fe2fe10b4fb1e7d723a2c4afdea35566328bb +openhands-tools @ https://files.pythonhosted.org/packages/68/20/27d6d20cc8f3465ec861c450bc0113c8646285aa6c8f4291b62366dcb8a2/openhands_tools-1.49.6-py3-none-any.whl \ + --hash=sha256:11e57a6635fd93c9f1fed14f804ab6f24a4873f75a2d35f0d1c3e57df2c51c87 diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md new file mode 100644 index 000000000..c93594fce --- /dev/null +++ b/blueprints/runtime-workers/openhands/research.md @@ -0,0 +1,57 @@ +# Source review, 2026-09-27 + +This recipe is a source-backed integration proposal, not an adopted runtime or a +new model run. The user selected OpenHands for a bounded coding-worker trial. +The existing foundation catalog still describes SDK alternatives as unselected; +this task does not change that catalog or transfer historical 1.49.4 acceptance. + +Research used the installed search-first and find-skills procedures, primary +GitHub release/tree/raw sources, PyPI release metadata and GHCR manifest bytes. +Web pages were indexed with context-mode and queried there; large source trees +were processed with ctx_execute. The skills.sh leaderboard was inspected for +discovery only; selection comes from adoption/skills/manifest.json, including +the pinned tdd and verification-before-completion skills. No skill was installed. +The current Python interpreter has no OpenHands package installed, so native +help/import checks are unavailable under this recipe-only authorization. + +SDK v1.49.6 resolves to fcc102a697874d54a357e36004e02c95040dbdc0. The exact source +examples used are `01_standalone_sdk/01_hello_world.py`, `07_mcp_integration.py`, +`14_context_condenser.py`, `05_skills_and_plugins/01_loading_agentskills/main.py` +and the SDK event/LLM/MCP implementations. README.md contains immutable links. +SDK wheels were downloaded as source artifacts and their bytes verified against +PyPI's SHA256; they were not installed or imported. The dependency requirements +were exported with the installed uv 0.12.17 from the unchanged upstream uv.lock. + +FastMCP 3.2.0 at 665514e19a78543709be85b4261153bbe98e882f confirms the +`server_name_tool_name` namespace. QMD v2.8.3 source confirms QMD_CONFIG_DIR, +INDEX_PATH and native collection add/update. The four-collection index is built +in attempt-owned state; a native PreToolUse hook requires explicit collection +arguments, lexical searches and rerank:false, avoiding embedding dependencies. + +The initial offline contract was written before this directory existed: +`PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v`. +Exit 1: `Ran 6 tests`; `FAILED (failures=2, errors=4)`. Missing recipe files caused +the failures. Retained returned output is in evidence/fail-first.txt, with only +private filesystem prefixes replaced. This is local structural evidence. + +## Decisions and corrections + +| Observation | Decision / prevention | +| --- | --- | +| DockerWorkspace is a RemoteWorkspace; Conversation selects RemoteConversation. | Treat its LLM and stdio MCP execution as container-side. A host Python caller does not imply host LLM calls. | +| DockerWorkspace's native launch does not specify the required host publication address. | Run the standalone SDK entirely inside the digest-pinned agent-server image, without an HTTP listener or published ports. | +| The published python image uses the PyInstaller binary target, not the Dockerfile's source target. | Install the two published wheels into an owned mounted venv; never assume /agent-server/.venv exists. | +| The source package's isolated build requires setuptools>=83 outside the main runtime lock. | Use hashed SDK/tools wheels and exported runtime hashes. Bootstrap hashed build tools and disable build isolation. | +| PyPI func-timeout 4.3.5 contains only an sdist. | A proposed all-binary dependency install would fail. Permit hash-checked sdists with preinstalled pinned build tools and no dependency/build downloads outside the requirement files. | +| A guessed QMD src/mcp.ts URL and guessed flat SDK event source paths returned not-found. | Inspect the pinned repository tree before citing paths. Those misses are not absence evidence. | +| SDK MCPServer has no enabled_tools/disabled_tools fields; unknown imported fields are dropped. | Translate limits into Agent.filter_tools_regex. Never pass Codex-only approval/tool keys as SDK configuration. | +| The SDK condenser disables ordinary streaming for summaries. | Omit temperature everywhere. Dynamic per-call headers remain in the LLM generate/agenerate adapter; retain this upstream behavior. | +| Serena's --project-from-cwd needs .git or its project config; the plain frozen fixture has neither. Its default project metadata would also violate the allowed diff. | At c6fbd1c5932df2494ffa0020af5a9fbe80b82143 use supported --project /workspace, SERENA_HOME and project_serena_folder_location outside the fixture. The offline regression test failed on the original template before this fix; native startup remains unmeasured. | +| A container venv interpreter link can dangle when dereferenced on the host. | Check the link entry with lexists on the host, and perform executable/import checks inside the image. | +| A truncated native summary or a timed-out cleanup could erase a failed attempt's receipt. | Two regression tests reproduced the failures. Guard summary parsing; preserve the primary return code and independently record whether the exact owned container was removed. | +| LLM.max_message_chars is declared, but the pinned standalone SDK path does not consume it. | Removed it as a claimed context limit; rely on the terminal's implemented truncation/offload, Serena answer cap, bounded retrieval and condenser. | +| Publication validation treated a container-owned Serena home/config path as a possible personal home path. | Preserve the scoped location and express the directory and filename separately with Path joining. The retained publication-first.txt records the failed check; the shared validator was not changed. | + +These corrections are kept here because this builder owns only this recipe and +its unit test. The coordinator can promote general lessons to the shared +docs/harness-defaults.md anti-pattern log. No shared file was edited. diff --git a/blueprints/runtime-workers/openhands/run-e2e.sh b/blueprints/runtime-workers/openhands/run-e2e.sh new file mode 100755 index 000000000..1b3a0bc02 --- /dev/null +++ b/blueprints/runtime-workers/openhands/run-e2e.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Real execution is explicitly deferred to the coordinator; no auto-install. +set -euo pipefail +umask 077 +recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" +state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" +exec python3 "$recipe_dir/host.py" run --prefix "$prefix" --state "$state" diff --git a/blueprints/runtime-workers/openhands/worker.py b/blueprints/runtime-workers/openhands/worker.py new file mode 100644 index 000000000..0b826f25c --- /dev/null +++ b/blueprints/runtime-workers/openhands/worker.py @@ -0,0 +1,134 @@ +"""Run the pinned SDK examples' agent/conversation inside the owned container. + +The only LLM adaptation adds gateway headers through public generate/agenerate +kwargs. It preserves upstream retries, Responses serialization and condensation. +See README.md for source lines and unmeasured host acceptance boundaries. +""" + +from __future__ import annotations + +import importlib.metadata +import json +import os +from pathlib import Path +import shlex +import sys +import uuid + +from recipe import HERE, inventory, llm_config, read_json, tool_filter + + +def headers_for_call(static_headers): + return {**(static_headers or {}), "Idempotency-Key": str(uuid.uuid4())} + + +def main(): + os.umask(0o077) + if not Path("/.dockerenv").exists() or os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": + raise RuntimeError("worker_requires_owned_container") + # Imports belong inside this entry point; offline recipe tests need no SDK. + from openhands.sdk import Agent, AgentContext, Conversation, LLM, Tool + from openhands.sdk.context.condenser import LLMSummarizingCondenser + from openhands.sdk.hooks import HookConfig, HookDefinition, HookMatcher + from openhands.sdk.mcp.config import coerce_mcp_config + from openhands.sdk.skills import load_skills_from_dir + from openhands.tools.file_editor import FileEditorTool + from openhands.tools.terminal import TerminalTool + + class GatewayLLM(LLM): + """Per logical SDK call, including condenser calls; retries reuse the key.""" + + def generate(self, *args, **kwargs): + kwargs["extra_headers"] = headers_for_call(self.extra_headers) + return super().generate(*args, **kwargs) + + async def agenerate(self, *args, **kwargs): + kwargs["extra_headers"] = headers_for_call(self.extra_headers) + return await super().agenerate(*args, **kwargs) + + cfg = read_json(HERE / "config/worker.json") + policy = read_json(HERE / "config/mcp-policy.json") + pins = read_json(HERE / "pins.json") + versions = {name: importlib.metadata.version(name) for name in ("openhands-sdk", "openhands-tools")} + if any(version != pins["version"] for version in versions.values()): + raise RuntimeError("installed_version_mismatch") + llm = GatewayLLM( + **llm_config(cfg, os.environ.get(cfg["model_env"])), + usage_id="agent", + extra_headers={"x-omniroute-session": str(uuid.uuid4())}, + ) + condenser_fields = {k: v for k, v in cfg["condenser"].items() if k != "kind"} + condenser = LLMSummarizingCondenser( + llm=llm.model_copy(update={"usage_id": "condenser"}), **condenser_fields, + ) + _, _, skills = load_skills_from_dir("/skills") + expected = {entry["name"] for entry in read_json(HERE / "config/skills.lock.json")["skills"]} + if set(skills) != expected: + raise RuntimeError("skill_discovery_mismatch") + context = AgentContext( + **cfg["agent_context"], skills=list(skills.values()), + system_message_suffix=( + "Use focused original reads for known source. Retrieve only relevant excerpts; " + "do not preload catalogs. Use context-mode to contain large output, recover " + "originals before correctness decisions. Skills are available via invoke_skill. " + "QMD queries must use explicit collections, typed lex searches, rerank:false. " + "No embeddings are available from OmniRoute. Count native usage once; unknown " + "usage remains unknown. MCP tools are capabilities, not mandatory steps." + ), + ) + # Native SDK MCP config; Codex-only keys have been translated, not passed through. + mcp_config = coerce_mcp_config(read_json("/run-input/mcp.json")) + if len([s for s in mcp_config.values() if s.enabled]) < 2: + raise RuntimeError("multi_server_namespace_required") + agent = Agent( + llm=llm, condenser=condenser, agent_context=context, + tools=[Tool(name=TerminalTool.name, params={"terminal_type": "subprocess"}), Tool(name=FileEditorTool.name)], + include_default_tools=["FinishTool"], + mcp_config=mcp_config, filter_tools_regex=tool_filter(policy), + ) + output = Path("/run-output") + output.mkdir(exist_ok=True) + hooks = HookConfig(pre_tool_use=[HookMatcher( + matcher="qmd_query", hooks=[HookDefinition( + command=f"{shlex.quote(sys.executable)} {shlex.quote(str(HERE / 'mcp_guard.py'))}", timeout=10, + )], + )]) + + def record_event(event): + # Native event fields, not claimed tool use inferred from a prompt/config. + # Full upstream events remain in private SDK persistence. Export only the + # fields needed for file-scope/test ordering and tool/skill observations. + kind = type(event).__name__ + selected = {} + for key in ("tool_name", "tool_call_id", "action", "observation"): + value = getattr(event, key, None) + if value is not None: + selected[key] = value.model_dump(mode="json") if hasattr(value, "model_dump") else value + if selected: + selected.update({"kind": kind, "workspace": inventory("/workspace")}) + with (output / "events.jsonl").open("a") as stream: + stream.write(json.dumps(selected) + "\n") + + conversation = Conversation( + agent=agent, workspace="/workspace", persistence_dir="/run-output/conversations", + callbacks=[record_event], token_callbacks=[lambda *_: None], hook_config=hooks, + max_iteration_per_run=cfg["runtime"]["max_iteration_per_run"], visualizer=None, + ) + try: + conversation.send_message((HERE / "e2e/task.txt").read_text()) + conversation.run() + state = conversation.state + summary = { + "versions": versions, + "execution_status": str(state.execution_status), + "invoked_skills": list(state.invoked_skills), + "native_metrics": conversation.conversation_stats.get_combined_metrics().model_dump(mode="json"), + } + (output / "native-summary.json").write_text(json.dumps(summary, indent=2) + "\n") + finally: + conversation.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py new file mode 100644 index 000000000..bda55a82f --- /dev/null +++ b/tests/test_runtime_worker_openhands.py @@ -0,0 +1,296 @@ +"""Offline recipe contract, frozen before the recipe exists. + +References: OpenHands/software-agent-sdk v1.49.6 examples +01_standalone_sdk/{01_hello_world,07_mcp_integration,14_context_condenser}.py; +the user's 2026-09-27 runtime-worker acceptance contract. These are local +structural/fixture checks, not upstream SDK or live gateway acceptance. +""" + +import json +import importlib.util +from pathlib import Path +import re +import shutil +import sqlite3 +import subprocess +import sys +import tempfile +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +RECIPE = ROOT / "blueprints/runtime-workers/openhands" + + +def load_recipe_module(filename): + name = "openhands_recipe_test_" + filename.replace("/", "_").replace(".py", "") + spec = importlib.util.spec_from_file_location(name, RECIPE / filename) + module = importlib.util.module_from_spec(spec) + sys.path.insert(0, str(RECIPE)) + try: + spec.loader.exec_module(module) + finally: + sys.path.pop(0) + return module + + +class OpenHandsRecipeTests(unittest.TestCase): + def read_json(self, relative): + return json.loads((RECIPE / relative).read_text()) + + def test_gateway_model_and_container_topology(self): + config = self.read_json("config/worker.json") + self.assertEqual(config["llm"]["base_url"], "http://127.0.0.1:20128/v1") + self.assertEqual(config["llm"]["model"], "cx/gpt-6-astra-max") + self.assertEqual(config["model_env"], "OPENHANDS_MODEL") + self.assertEqual(config["llm"]["api_key"], "local-loopback") + self.assertEqual(config["llm"]["api_mode"], "responses") + self.assertIsNone(config["llm"].get("temperature")) + self.assertNotEqual(config["llm"].get("reasoning_effort"), "auto") + self.assertEqual(config["runtime"]["gateway_base_url"], "http://10.0.2.2:20128/v1") + self.assertEqual(config["runtime"]["agent_loop"], "container") + self.assertEqual(config["runtime"]["published_ports"], []) + self.assertEqual(config["condenser"]["kind"], "LLMSummarizingCondenser") + self.assertGreater(config["condenser"]["max_tokens"], 0) + + def test_mcp_limits(self): + policy = self.read_json("config/mcp-policy.json") + self.assertEqual(set(policy["context-mode"]["disabled_tools"]), {"ctx_upgrade", "ctx_purge"}) + self.assertEqual(set(policy["ai-memory"]["enabled_tools"]), { + "memory_query", "memory_read_page", "memory_recent", "memory_status", "memory_briefing", + }) + self.assertEqual(set(policy["socraticode"]["enabled_tools"]), { + "codebase_search", "codebase_status", "codebase_list_projects", "codebase_health", + }) + self.assertEqual(policy["socraticode"]["env"]["SOCRATICODE_WATCHER"], "manual") + self.assertEqual(set(policy["jcodemunch"]["enabled_tools"]), {"route", "menu", "order"}) + self.assertEqual(set(policy["qmd"]["collections"]), { + "foundation-docs", "foundation-adoption", "us-equities-foundation", "us-equities-catalog", + }) + self.assertFalse(policy["headroom"]["enabled"]) + self.assertEqual(set(policy["headroom"]["enabled_tools"]), { + "headroom_compress", "headroom_retrieve", "headroom_stats", + }) + self.assertIn("serena", policy) + + def test_immutable_artifacts(self): + pins = self.read_json("pins.json") + self.assertEqual(pins["version"], "1.49.6") + self.assertRegex(pins["commit"], r"^[0-9a-f]{40}$") + for name in ("source_archive", "uv_lock"): + self.assertRegex(pins[name]["sha256"], r"^[0-9a-f]{64}$") + self.assertRegex(pins["image"]["ref"], r"^ghcr\.io/openhands/agent-server@sha256:[0-9a-f]{64}$") + + def test_installer_and_runtime_are_scoped(self): + for filename in ("install.sh", "run-e2e.sh"): + text = (RECIPE / filename).read_text() + self.assertIn("set -euo pipefail", text) + self.assertNotRegex(text, r"(?m)^\s*sudo\s") + installer = (RECIPE / "install.sh").read_text() + self.assertIn(".local/share/codex-ecosystem/tools/openhands-", installer) + self.assertIn(".local/state/native-agent-stack/runtime-workers/openhands", installer) + self.assertIn("umask 077", installer) + self.assertIn("sha256", installer) + self.assertIn("rootless", installer) + driver = (RECIPE / "worker.py").read_text() + self.assertIn("Idempotency-Key", driver) + self.assertIn("x-omniroute-session", driver) + self.assertIn("filter_tools_regex", driver) + self.assertIn("mcp_config", driver) + + def test_no_public_binds_or_secret_material(self): + self.assertTrue(RECIPE.is_dir()) + paths = [p for p in RECIPE.rglob("*") if p.is_file() and p.suffix in {".json", ".py", ".sh", ".toml"}] + self.assertGreater(len(paths), 5) + for path in paths: + text = path.read_text() + with self.subTest(file=path.name): + self.assertNotIn("0.0.0.0", text) + self.assertNotIn("BEGIN PRIVATE KEY", text) + self.assertIsNone(re.search(r"sk-[A-Za-z0-9]{20,}", text)) + + def test_checker_rejects_an_empty_result(self): + checker = RECIPE / "e2e/check.py" + self.assertTrue(checker.is_file()) + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + [sys.executable, str(checker), "--result", tmp], + capture_output=True, text=True, check=False, + ) + self.assertEqual(result.returncode, 1, result.stderr) + report = json.loads(result.stdout) + self.assertFalse(report["passed"]) + self.assertIn("missing_result", report["failures"]) + + def test_fresh_call_headers_keep_session_affinity(self): + worker = load_recipe_module("worker.py") + original = {"x-omniroute-session": "fixture-session"} + first = worker.headers_for_call(original) + second = worker.headers_for_call(original) + self.assertNotEqual(first["Idempotency-Key"], second["Idempotency-Key"]) + self.assertEqual(first["x-omniroute-session"], second["x-omniroute-session"]) + self.assertEqual(original, {"x-omniroute-session": "fixture-session"}) + + def test_runtime_model_override_and_native_tool_filter(self): + helpers = load_recipe_module("recipe.py") + result = helpers.llm_config(self.read_json("config/worker.json"), "cx/future-model-max") + self.assertEqual(result["model"], "openai/cx/future-model-max") + self.assertEqual(result["base_url"], "http://10.0.2.2:20128/v1") + pattern = re.compile(helpers.tool_filter(self.read_json("config/mcp-policy.json"))) + for name in ("terminal", "file_editor", "context-mode_ctx_execute", "serena_find_symbol", "ai-memory_memory_query", "qmd_query", "socraticode_codebase_health", "jcodemunch_order"): + self.assertIsNotNone(pattern.fullmatch(name), name) + for name in ("context-mode_ctx_upgrade", "context-mode_ctx_purge", "ai-memory_memory_write", "socraticode_codebase_index", "jcodemunch_delete_index", "headroom_headroom_compress", "memory_query", "qmd_query_evil"): + self.assertIsNone(pattern.fullmatch(name), name) + + def test_qmd_guard_requires_explicit_scope_and_lexical_query(self): + guard = load_recipe_module("mcp_guard.py") + params = {"collections": ["foundation-docs"], "searches": [{"type": "lex", "query": "fixture"}], "rerank": False} + event = {"tool_name": "qmd_query", "tool_input": {"data": params}} + self.assertTrue(guard.permitted(event)) + for patch in ({"collections": []}, {"collections": ["private"]}, {"rerank": True}, {"query": "expand"}, {"searches": [{"type": "vec", "query": "fixture"}]}): + self.assertFalse(guard.permitted({"tool_name": "qmd_query", "tool_input": {"data": {**params, **patch}}})) + + def test_serena_activates_plain_fixture_and_keeps_metadata_outside_it(self): + config = self.read_json("config/mcp.template.json")["serena"] + self.assertNotIn("--project-from-cwd", config["args"]) + self.assertEqual(config["args"][config["args"].index("--project") + 1], "/workspace") + self.assertEqual(config["env"]["SERENA_HOME"], "/state/mcp/serena/home") + native = (RECIPE / "config/serena_config.yml").read_text() + self.assertIn("/state/mcp/serena/projects/$projectFolderName", native) + self.assertNotIn("$projectDir/.serena", native) + + +class OpenHandsFrozenOracleTests(unittest.TestCase): + # Synthetic positive control for the oracle only. This implementation is + # outside the mounted recipe; a real E2E always starts with the broken copy. + FIX = '''def compress_ranges(values): + ranges = [] + for value in sorted(set(values)): + if ranges and value == ranges[-1][1] + 1: + ranges[-1] = (ranges[-1][0], value) + else: + ranges.append((value, value)) + return ranges +''' + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.result = Path(self.tmp.name) + self.checker = load_recipe_module("e2e/check.py") + self.helpers = load_recipe_module("recipe.py") + shutil.copytree(RECIPE / "e2e/fixture-repo", self.result / "workspace") + (self.result / "worker").mkdir() + baseline = self.checker.run_tests(self.result / "workspace") + self.assertEqual((baseline["exit_code"], baseline["tests_run"], baseline["failures"], baseline["errors"]), (1, 1, 1, 0)) + (self.result / "baseline.json").write_text(json.dumps(baseline)) + pristine = self.helpers.inventory(RECIPE / "e2e/fixture-repo") + self.events = [ + {"kind": "ActionEvent", "tool_name": "terminal", "tool_call_id": "synthetic", "action": {"command": "python -B -m unittest discover -s tests -v"}, "workspace": pristine}, + {"kind": "ObservationEvent", "tool_name": "terminal", "tool_call_id": "synthetic", "observation": {"exit_code": 1, "text": "test_ranges_follow_the_spec FAILED (failures=1)"}, "workspace": pristine}, + ] + self.write_events() + (self.result / "workspace/range_utils.py").write_text(self.FIX) + + def write_events(self): + (self.result / "worker/events.jsonl").write_text("\n".join(map(json.dumps, self.events)) + "\n") + + def test_fixed_copy_passes_and_frozen_source_stays_broken(self): + verdict = self.checker.check(self.result) + self.assertTrue(verdict["passed"], verdict) + self.assertEqual(verdict["changed_files"], ["range_utils.py"]) + self.assertEqual(verdict["tests"]["tests_run"], 1) + + def test_no_native_red_observation_cannot_pass(self): + self.events = self.events[:1] + self.write_events() + verdict = self.checker.check(self.result) + self.assertIn("no_native_test_failure_before_edit", verdict["failures"]) + + def test_red_after_edit_cannot_pass(self): + self.events[0]["workspace"] = self.helpers.inventory(self.result / "workspace") + self.write_events() + self.assertFalse(self.checker.check(self.result)["passed"]) + + def test_deleted_or_skipped_test_and_extra_files_are_rejected(self): + test = self.result / "workspace/tests/test_ranges.py" + original = test.read_text() + for content in ("", "import unittest\n" + original.replace(" def test_ranges", " @unittest.skip('removed')\n def test_ranges")): + test.write_text(content) + self.assertIn("diff_outside_allowed_files_or_empty", self.checker.check(self.result)["failures"]) + test.write_text(original) + (self.result / "workspace/extra.txt").write_text("outside contract") + self.assertFalse(self.checker.check(self.result)["passed"]) + + def test_broken_answer_and_symlink_cannot_pass(self): + source = self.result / "workspace/range_utils.py" + source.write_text("def compress_ranges(values):\n return []\n") + self.assertIn("tests_not_passing_unskipped", self.checker.check(self.result)["failures"]) + source.unlink() + source.symlink_to(RECIPE / "e2e/fixture-repo/range_utils.py") + self.assertIn("nonregular_result", self.checker.check(self.result)["failures"]) + + +class OpenHandsReceiptTests(unittest.TestCase): + def test_truncated_native_summary_retains_failed_attempt_receipt(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp) + (result / "worker").mkdir() + (result / "window.json").write_text(json.dumps({"started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:01Z", "worker_exit_code": 124})) + (result / "check.json").write_text(json.dumps({"passed": False, "exit_code": 1, "failures": ["missing_result"]})) + (result / "worker/native-summary.json").write_text('{"versions":') + report = module.create_receipt(result, database=result / "absent.sqlite") + self.assertFalse(report["task_passed"]) + self.assertFalse(report["evidence_complete"]) + self.assertIsNone(report["observed_versions"]) + self.assertEqual(report["gateway"]["status"], "unavailable") + + def test_cleanup_timeout_does_not_erase_primary_exit(self): + host = load_recipe_module("host.py") + with tempfile.TemporaryDirectory() as tmp: + log = Path(tmp) / "worker.log" + effects = [subprocess.CompletedProcess(["synthetic"], 7), subprocess.TimeoutExpired(["synthetic-cleanup"], 30)] + with patch.object(host.subprocess, "run", side_effect=effects): + self.assertEqual(host.execute_container(["synthetic"], "fixture", log, 1), 7) + cleanup = json.loads(log.with_suffix(".log.cleanup.json").read_text()) + self.assertFalse(cleanup["confirmed_removed"]) + + def test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + db = Path(tmp) / "fixture.sqlite" + connection = sqlite3.connect(db) + connection.execute("CREATE TABLE call_logs (timestamp, path, status, model, reasoning_effort_requested, reasoning_effort_upstream, tokens_in, tokens_cache_read, tokens_reasoning, forbidden_prompt)") + connection.execute("CREATE TABLE forbidden_credentials (secret)") + row = ("2026-09-27T18:00:01Z", "/v1/responses", 200, "cx/gpt-6-astra-max", "max", "max", 30, 20, None, "private") + connection.execute("INSERT INTO call_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", row) + connection.commit() + connection.close() + before = db.read_bytes() + rows = module.gateway_rows(db, "2026-09-27T18:00:00Z", "2026-09-27T18:00:02Z") + self.assertEqual(db.read_bytes(), before) + self.assertEqual(len(rows), 1) + self.assertEqual(set(rows[0]), set(module.COLUMNS)) + self.assertIsNone(rows[0]["tokens_reasoning"]) + self.assertNotIn("private", json.dumps(rows)) + self.assertEqual(module.gateway_rows(db, "2026-09-27T19:00:00Z", "2026-09-27T19:00:01Z"), []) + source = (RECIPE / "receipt.py").read_text() + self.assertNotIn("SELECT *", source) + self.assertNotIn("sqlite_master", source) + self.assertIn("?mode=ro", source) + + def test_skills_and_mcp_require_observations(self): + module = load_recipe_module("receipt.py") + action = {"kind": "ActionEvent", "tool_name": "ai-memory_memory_query"} + observed = {"kind": "ObservationEvent", "tool_name": "ai-memory_memory_query", "observation": {"is_error": False}} + skill = {"kind": "ObservationEvent", "tool_name": "invoke_skill", "observation": {"skill_name": "tdd", "is_error": False}} + self.assertEqual(module.observations([action])["mcp_calls_observed"], {}) + report = module.observations([action, observed, skill]) + self.assertEqual(report["mcp_calls_observed"], {"ai-memory_memory_query": 1}) + self.assertEqual(report["skills_observed"], {"tdd": 1}) + + +if __name__ == "__main__": + unittest.main() From c31a70f4db64ca3a3afa34218f5793ef04a5b20a Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 15:35:55 -0400 Subject: [PATCH 02/22] Runtime worker openhands: round 2 (SWE-bench grading via OpenHands/benchmarks, native LLM transport) GPT-6 (astra, max) round-2 build: the verdict comes from official SWE-bench 4.1.0 through OpenHands/benchmarks at 405bae71 (SDK submodule 43376f18); the standalone worker pins SDK 1.49.6. check.py validates transport only and relays the official report. Native tool calling on a configurable cx/gpt-6-astra-max route with stable conversation headers and fresh idempotency keys; exact native LLM objects keep SDK accounting and condenser registration. No host ports published; owned rw-openhands- resources. Not installed, not run. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 643 +++++++++--------- .../runtime-workers/openhands/e2e/check.py | 182 ++--- .../openhands/e2e/docker_grader.py | 63 ++ .../runtime-workers/openhands/e2e/task.py | 59 ++ .../openhands/evidence/round2-fail-first.txt | 460 +++++++++++++ .../evidence/round2-publication-first.txt | 27 + .../openhands/evidence/round2-publication.txt | 25 + .../evidence/round2-static-checks.txt | 46 ++ .../openhands/evidence/round2-unit-pass.txt | 35 + .../openhands/evidence/round2-verification.md | 81 +++ blueprints/runtime-workers/openhands/host.py | 331 ++++++--- .../openhands/install-grader.sh | 27 + .../runtime-workers/openhands/install.sh | 4 +- .../runtime-workers/openhands/pins.json | 14 + .../runtime-workers/openhands/receipt.py | 62 +- .../runtime-workers/openhands/recipe.py | 8 +- .../runtime-workers/openhands/research.md | 87 +++ .../runtime-workers/openhands/run-e2e.sh | 1 + .../runtime-workers/openhands/worker.py | 78 ++- tests/test_runtime_worker_openhands.py | 320 ++++++--- 20 files changed, 1902 insertions(+), 651 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/e2e/docker_grader.py create mode 100644 blueprints/runtime-workers/openhands/e2e/task.py create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-fail-first.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-publication-first.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-publication.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-static-checks.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-unit-pass.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/round2-verification.md create mode 100644 blueprints/runtime-workers/openhands/install-grader.sh diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 63889338c..6487b67ab 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -1,328 +1,322 @@ # OpenHands coding runtime worker -This is a **recipe awaiting host execution** for OpenHands Software Agent SDK -1.49.6, powered by the local OmniRoute gateway. The SDK owns the agent loop, -tools, conversations, skills and context condensation. The worker repairs one -frozen Python fixture in an owned copy. It never receives this repository as its -coding workspace. This addition does not change the foundation catalog's adoption -status, install software, start services, pull an image, or perform a model run. - -## Pinned sources and artifacts - -The primary is [OpenHands/software-agent-sdk v1.49.6][release], commit -`fcc102a697874d54a357e36004e02c95040dbdc0`. [pins.json](pins.json) contains full -URLs and hashes. Source/archive and lock hashes were computed from returned -upstream bytes; wheel hashes came from version-specific PyPI release metadata -and were independently checked against downloaded wheel bytes. GHCR index, -platform manifest and config hashes were computed over registry responses. The -config's `OPENHANDS_BUILD_GIT_SHA` and `OPENHANDS_BUILD_GIT_REF` match this tag. - -| Artifact | SHA256 | -| --- | --- | -| Commit-addressed source tarball | `2d3006de86eaec690a82e497fcd4459816637afe6e53f7da87783183792148a8` | -| Upstream `uv.lock` | `d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910` | -| Published agent-server `1.49.6-python` multiarchitecture index | `02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37` | -| Selected Linux amd64 image manifest | `ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12` | -| Linux amd64 image config | `39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f` | -| `openhands_sdk-1.49.6-py3-none-any.whl` | `3b4701f125925e804929ab02b61fe2fe10b4fb1e7d723a2c4afdea35566328bb` | -| `openhands_tools-1.49.6-py3-none-any.whl` | `11e57a6635fd93c9f1fed14f804ab6f24a4873f75a2d35f0d1c3e57df2c51c87` | - -The image reference used by every container invocation is -`ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37`, -with `--platform linux/amd64`. Docker verifies its content-addressed blobs; the -installer also checks the resolved image configuration ID. The pinned image -includes Python 3.13.15, Node 24.21.0 and uv 0.12.15. These facts come from the -[Dockerfile language runtime, lines 39–47][docker-runtime], [uv copy, lines -402–403][docker-uv], and the returned image metadata. No image was pulled here. - -All 295 comparable SDK Python files and 93 tools Python files in the downloaded -wheels matched their corresponding files at the pinned commit. The compact -[byte-comparison record](evidence/wheel-source-comparison.json) is artifact -evidence, not execution. - -## Supported installation paths and the choice - -Upstream's [README, lines 49–81][sdk-readme] shows the standalone SDK API and -directs source development to `make build`. Its [Makefile, lines 34–41][makefile] -runs `uv sync --dev` and installs Git hooks. The [container source build, lines -128–140][docker-sync] copies the four workspace packages and root lockfile, then -uses `uv sync --frozen --no-editable`. The [official getting-started -guide][getting-started], read 2026-09-27, documents the package path -`pip install -U openhands-sdk openhands-tools` and optional workspace/server -packages. This recipe pins those two published packages and their dependencies. - -The selected path runs the standalone SDK **inside the pinned agent-server -image**, overriding its entrypoint. There is no agent-server HTTP listener, port -publication or host workspace execution. The agent loop, LLM calls and every -stdio MCP subprocess run in that container; HTTP MCP calls originate there too. -Host code only provisions owned files, invokes rootless Docker, copies fixture -bytes, supervises deadlines and reads the permitted gateway log columns. - -This choice addresses two details found at the tag: - -- `DockerWorkspace` is a `RemoteWorkspace`; the [remote example, lines - 92–97][remote-example] explicitly asserts `RemoteConversation`. Creating that - object from host Python does not keep the LLM loop on the host. Its [native - launch, lines 218–251][docker-workspace] publishes a port without an explicit - host IP. The chosen listener-free path needs no port from the 3700–3799 range. -- The published `python` image uses the **binary** target, according to the - [release matrix, lines 281–339][image-matrix]. Its entrypoint launches a - PyInstaller server executable. The source venv exists only in the [separate - source targets, lines 564–598][image-targets]. Therefore the installer creates - its own importable SDK venv inside the image, in a mounted owned prefix. - -[install.sh](install.sh) is idempotent and uses `set -euo pipefail`, `umask 077`, -no sudo and the existing `rootless` Docker context. It verifies prerequisites, -private host inputs, installed skill hashes and requirement-file hashes before -download/install. It verifies the source archive and `uv.lock` SHA256, pulls only -the digest pin, and invokes [install-container.sh](install-container.sh). - -The prefix is `$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6`; durable -private state is `$HOME/.local/state/native-agent-stack/runtime-workers/openhands/`. -Owned directories are mode 0700 and files inherit the restrictive umask. The -venv is for the image's interpreter, so its Python symlink may not resolve on the -host. Install logs are retained in separate `install-attempts/attempt-*` folders. -Docker's content store remains owned by the separately provisioned rootless -daemon; temporary container files disappear with that container. - -[requirements.lock](requirements.lock) is an unchanged dependency export from -the upstream lock, with only the two published workspace-package wheel references -and their hashes appended. It was produced using uv 0.12.17: - -```sh -uv export --locked --no-dev --package openhands-tools --no-emit-workspace \ - --no-editable --no-annotate --no-header --format requirements-txt \ - --output-file requirements-dependencies.lock >/dev/null -``` - -An ordinary isolated source build could fetch `setuptools>=83` outside that -lock ([SDK pyproject, lines 41–43][sdk-pyproject]). Conversely, requiring all -binary dependencies would fail: locked `func-timeout==4.3.5` has only a source -distribution. [build-requirements.lock](build-requirements.lock) therefore -bootstraps setuptools 80.9.0, wheel 0.46.3 and packaging 26.3 from hashes in the -same upstream lock. `uv pip install --require-hashes --no-deps ---no-build-isolation` then uses the complete exported requirements. There is no -unlocked dependency or build-dependency resolution. A missing build prerequisite -fails visibly. This follows [uv's documented build-isolation escape hatch][uv-build]. -`uv pip check` and SDK/tool imports are installation checks, never a model trial. - -## Model, headers and native context features - -[config/worker.json](config/worker.json) retains the requested host-side reference -URL `http://127.0.0.1:20128/v1`. [recipe.py](recipe.py) renders -`http://10.0.2.2:20128/v1` for the actual container-side LLM calls, following the -coordinator's measured rootless networking facts. No gateway configuration is -changed. The only API-key value is the keyless placeholder `local-loopback`. - -| Setting | Upstream key and source at the SDK commit | Recipe behavior | +This recipe uses OpenHands SDK **1.49.6** for one frozen SWE-bench coding task +through OmniRoute. **Official SWE-bench 4.1.0 supplies the task verdict**, using +the conversion and grading pipeline selected by OpenHands/benchmarks at +405bae7140d7e961a75f4910a0b2e7069731db96. [check.py](e2e/check.py) only validates +transport and relays the official report. Local contract tests cannot establish +an upstream E2E pass. + +Round 2 is **awaiting host execution and the coordinator's skills PR**. No +package was installed, container/service started, model called, or upstream +grader executed in this build. The SDK owns the agent loop, tools, skills, +native trace and condensation. All agent/LLM/MCP execution stays in an owned +container; the grader uses separate official test containers. The worker never +receives this checkout, the frozen oracle row, or a Docker socket. + +## Sources, pins and installation + +[pins.json](pins.json) keeps the worker's existing artifact pins and the new +grader pins. [research.md](research.md) records research and corrections. + +| Component | Exact selected pin | Role | +| --- | --- | --- | +| OpenHands Software Agent SDK | v1.49.6 / fcc102a697874d54a357e36004e02c95040dbdc0 | Standalone coding worker | +| OpenHands/benchmarks | 405bae7140d7e961a75f4910a0b2e7069731db96 | Native output conversion and official grading integration | +| Benchmark SDK submodule | 43376f1868ffd702746080714a59c16d3f69ec12 | Preserved in the benchmark's separate environment | +| SWE-bench | 4.1.0 | Official Docker tests and resolved_ids verdict | +| Harbor, runner-up | v0.23.0 | Not selected: Terminal-Bench has a different task contract | + +The two SDK revisions are intentionally separate. The grader environment keeps +the report's exact submodule; it does not silently upgrade to the worker SDK. +The worker's inference is an explicitly local adaptation of the [standalone +example][hello] and the benchmark's [issue prompt and patch export][benchmark-infer]. +It is not an unchanged swebench-infer run. The official grader's patch +application, tests, scoring and report logic remain unchanged. + +The primary fits this coding task because its converter accepts actual +OpenHands patches and invokes the official SWE-bench harness. +[Harbor's OpenHands adapter][harbor] is a maintained alternative; selecting its +Terminal-Bench tasks would change the oracle. Stars and installation counts +were discovery signals only. This choice follows the supplied eval-framework +reports, particularly report2's W8a mapping. + +[install.sh](install.sh) preserves the round-1 worker installation: the +[official package path][getting-started], pinned published SDK/tools wheels, +[requirements.lock](requirements.lock), [build-requirements.lock](build-requirements.lock), +and a content-addressed agent-server image, used as a Python runtime with its +server entrypoint replaced. [install-container.sh](install-container.sh) runs +inside that image. The source archive, upstream lock, requirements and image +configuration hashes are verified before use. The image is: + +~~~text +ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 +~~~ + +It selects Linux amd64 manifest +ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12 +and image config 39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f. +The image metadata records Python 3.13.15 and uv 0.12.15. No image was pulled +here. The retained [wheel/source comparison](evidence/wheel-source-comparison.json) +is artifact evidence only. + +The worker install follows the upstream lock's dependency export plus hashed +published wheels. It bootstraps locked build tools before the hash-required +install without build isolation; func-timeout is an sdist. Installation checks +package versions/imports, not model behavior. SDK source builds, published binary +images and importable wheels are different installation paths; this recipe +retains the qualified artifact choices from round 1. + +[install-grader.sh](install-grader.sh), called by the host installer, clones +OpenHands/benchmarks into the owned prefix, checks out the exact commit, +initializes its submodules and runs **make build**, the [documented install][benchmark-install]. +The [pinned Makefile][benchmark-make] runs uv sync --dev and installs its own +development hooks in that isolated checkout. The script verifies the submodule +before and after installation, refuses tracked modifications, checks that +uv.lock/pyproject.toml did not drift, and verifies swebench==4.1.0. Grader source +and SDK state remain separate from this repository. No alternative grader is +installed as fallback. + +The prefix is $HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6; +private state is $HOME/.local/state/native-agent-stack/runtime-workers/openhands. +Every trial gets a fresh attempt directory. Failed attempts remain available. +The grader source install has its own isolated venv and cache. The worker venv's +interpreter link resolves inside the pinned image, not necessarily on the host. + +## Model and gateway contract + +[config/worker.json](config/worker.json) selects **cx/gpt-6-astra-max** for the +coding/judgment loop and condenser. OPENHANDS_MODEL can select another cx/gpt-6 +family alias; [recipe.py](recipe.py) rejects Claude and other model families. +OmniRoute does not serve claude-opus-5-5. The official SWE-bench grader is +deterministic and makes no LLM judgment request. + +| Setting | Native SDK interface | Recipe behavior | | --- | --- | --- | -| Model and gateway | `LLM.model`, `base_url`, `api_key`; [standalone example][hello] | Default `cx/gpt-6-astra-max`; override with `OPENHANDS_MODEL`. The LiteLLM `openai/` provider prefix is added by the driver and stripped before the gateway request. | -| Responses endpoint | `LLM.api_mode`, `model_canonical_name`, `capability_overrides`; [llm.py:408–439][llm-config] | Explicit `responses` and canonical `openai/gpt-6`; reasoning and Responses capabilities are set explicitly for the alias. | -| Maximum effort | `LLM.reasoning_effort`; [llm.py:537–549][llm-effort], [responses_options.py:58–86][responses-options] | Send `max` on Responses; gateway alias suffix also pins max. Never send `auto`. | -| Sampling/cache safety | `LLM.temperature`; [llm.py:369–380][llm-config] | Omit temperature entirely, including condenser calls. The legacy exact semantic cache is not relied on. | -| Affinity and idempotency | `LLM.extra_headers`; [llm.py:442–445][llm-config]; public [generate/agenerate:1580–1643][llm-generate] | One private `x-omniroute-session` per conversation, shared by agent/condenser; fresh `Idempotency-Key` per logical SDK call. Native retries retain that logical call's key. | -| Streaming | `LLM.stream`, `Conversation.token_callbacks`; [llm.py:464–470][llm-config], [Conversation signature][conversation] | Agent streaming enabled with a token callback. Native condenser disables ordinary streaming because it consumes a whole summary; [condenser:83–95][condenser]. Temperature is absent so the low-temperature streaming condition does not apply to summaries. | -| Context summary | `Agent.condenser=LLMSummarizingCondenser`; [example 14][condenser-example], [condenser:48–78,99–154][condenser] | `max_size=80` events, `keep_first=2`, `max_tokens=60000`; preserve initial task/system context and summarize older events. This is an input-pressure threshold, not a total-spend cap. | -| Output and run bounds | `LLM.max_output_tokens`, `num_retries`, `timeout`; [llm.py:340–407][llm-config]; `Conversation.max_iteration_per_run` [conversation][conversation] | 16,384 output tokens, 2 retries, 180-second request timeout; 40 agent iterations and a 1,200-second container deadline. | -| Terminal output recovery | `TerminalObservation.to_llm_content`, `MAX_CMD_OUTPUT_SIZE`, `full_output_save_dir`; [definition:175–200,320–330][terminal], [constants:18–21][terminal-constants] | Native 30,000-character truncation saves full output in the conversation's private observation persistence directory. | - -The tiny header adapter subclasses `LLM` only at its public generation methods. -It keeps the upstream agent loop, serialization, retry implementation and -condenser. Upstream [model_copy:783–798][llm-copy] preserves the subclass, and the -condenser calls `generate`/`agenerate` at [lines 229 and 423][condenser]. This -behavior was source-reviewed; actual gateway headers still need host observation. - -The upstream [GPT-6 model feature note:190–200][model-features] warns about tools -plus reasoning effort on chat completions. The coordinator measured that this -gateway handles that combination on 2026-09-27; the recipe nevertheless uses the -supported Responses path. Switching to chat is not an automatic fallback. A -future chat-only adaptation must rely on the max alias and omit request effort. -Model currency is configurable, not claimed to be permanently settled. - -No gateway compression, dedup, semantic-cache or contextBudget changes are made. -There is no OmniRoute embedder. The task's small size may never trigger the -condenser; configured condensation and observed condensation are separate claims. -The SDK declares `LLM.max_message_chars`, but source inspection found no active -consumer of that field in this standalone generation path. It is therefore not -used as a claimed universal MCP-output cap. The terminal's real truncation/offload, -Serena's answer limit, bounded retrieval and condenser provide the containment. - -## MCP and skills - -Launch commands are derived from this checkout's -[base Codex template](../../../adoption/templates/codex.config.template.toml) and -[stack-worker limits](../../../adoption/templates/codex.stack-worker.config.toml). -Those files intentionally omit project-scoped jCodeMunch; its command and -`route/menu/order` front door come from the -[project template](../../../adoption/templates/project.codex.config.template.toml). -No command was guessed. [config/mcp.template.json](config/mcp.template.json) uses -the SDK's `mcp_config` and [MCPServer transport/command/args/env/cwd/url fields, -lines 497–530][mcp-config]. All stdio servers start in `/workspace`. - -The SDK does not implement Codex `enabled_tools`, `disabled_tools` or approval -keys in MCPServer; imported unknown fields are dropped at [lines 626–664][mcp-config]. -[recipe.py](recipe.py) therefore translates [mcp-policy.json](config/mcp-policy.json) -into native `Agent.filter_tools_regex`, applied both at initialization and MCP -tool updates ([agent/base.py:153–158,565–568,960–978][agent-base]). Locked FastMCP -3.2.0 uses one underscore, `server_name_tool_name`, for multi-server configs -([transport][fastmcp-config], [namespace implementation][fastmcp-namespace]). -The worker refuses a singleton config rather than silently losing those prefixes. - -| Server | Scope / allowed tools | +| Gateway | LLM.base_url and api_key | Host reference http://127.0.0.1:20128/v1; actual rootless-container URL http://10.0.2.2:20128/v1; keyless placeholder local-loopback | +| Model | LLM.model, model_canonical_name | LiteLLM openai/ provider prefix is added before cx/gpt-6-astra-max | +| Effort/API | LLM.api_mode, reasoning_effort | Responses, max; no automatic chat fallback | +| Structured actions | ToolDefinition.to_responses_tool | Native function tools, strict:false; no JSON response format | +| Sampling | LLM.temperature | Omitted, including summaries; values at or below 0.1 are rejected | +| Affinity | LLM.extra_headers | Stable x-omniroute-session for one conversation and its condenser | +| Idempotency | Public generate/agenerate kwargs | Fresh Idempotency-Key per logical call; upstream internal retries retain it | +| Context | LLMSummarizingCondenser | 80 events / 60,000 input-token trigger, keep_first=2 | +| Bounds | Native SDK run/request fields | 40 iterations, 1,200-second worker deadline, 16,384 output tokens, two retries, 180-second request timeout | + +The gateway owner's cognee 1.6.1 measurements establish that JSON-object mode +needs the word “json” in input messages; system text moves to instructions. +Strict JSON schemas need additionalProperties:false on every object. +This recipe chooses **tool calling** for structured actions, following +[the pinned serializer][tool-serialization]. It does not issue JSON-object or +JSON-schema calls. Its [native condenser][condenser] consumes ordinary summary +text. No gateway/schema acceptance is inferred from these offline checks. + +[worker.py](worker.py) changes only the public LLM generation boundary to supply +headers; the upstream serialization, retries and condenser remain native. +The adapter scopes its method wrapping to one worker process and restores the +methods afterward. Agent and condenser keep exact native LLM objects: +[SDK registration][llm-discovery] explicitly checks type(obj) is LLM, and +[conversation setup][llm-registration] uses that registry for native metrics +and condenser context. A subclass would silently skip this registration. +[SDK header merging][header-merging] preserves per-call headers. Actual +wire-level header stability, fresh logical call keys, max effort, streaming and +tool results still require host observation. Gateway log columns available to +this recipe cannot independently prove header/session attribution. + +## MCP and skills lifecycle + +[config/mcp.template.json](config/mcp.template.json) retains the native command +selection from the repository's adopted client templates. +[recipe.py](recipe.py) translates tool limits into Agent.filter_tools_regex; +Codex-only approval/allowlist keys are not passed as SDK configuration. +The locked FastMCP uses the server_name_tool_name namespace and the worker +requires multiple servers. See [MCP config][mcp-config] and [agent filtering][agent-base]. + +| Server | Scope | | --- | --- | -| context-mode | Native pinned Node command; `CONTEXT_MODE_PROJECT_DIR=/workspace`; `ctx_upgrade` and `ctx_purge` excluded. | -| serena | Native `start-mcp-server --transport stdio --context codex`, adapted to supported `--project /workspace` because the fixture has no Git metadata. Dashboard/GUI disabled. `SERENA_HOME=/state/mcp/serena/home`; metadata lives outside the fixture using `project_serena_folder_location`. | -| jcodemunch | Native `jcodemunch-mcp`; only `route`, `menu`, `order`; private `CODE_INDEX_PATH=/state/mcp/jcodemunch`; savings sharing disabled. This front door is not a read-only sandbox, as the project template notes. | -| qmd | Native `qmd --index native-agent-stack-catalog mcp`; only `query`, `get`, `multi_get`, `status`. Fresh owned index contains exactly `foundation-docs`, `foundation-adoption`, `us-equities-foundation`, `us-equities-catalog`. | -| ai-memory | HTTP `http://10.0.2.2:49474/mcp`; only `memory_query`, `memory_read_page`, `memory_recent`, `memory_status`, `memory_briefing`. | -| socraticode | Native pinned Node command; only `codebase_search`, `codebase_status`, `codebase_list_projects`, `codebase_health`; `SOCRATICODE_WATCHER=manual`. Local embedding endpoint is `10.0.2.2:18232/v1`, never OmniRoute. | -| headroom | Configured but disabled. Its conditional allowlist is exactly `headroom_compress`, `headroom_retrieve`, `headroom_stats`. Native output offload and condensation already contain this task's output; no remaining gap justifies another layer. | - -Serena's pinned source explains why cwd inference fails on a plain fixture -([cli.py:369–383][serena-cli]), its actual `SERENA_HOME` key -([config:65–78][serena-home]) and the external project metadata template -([config:928–936][serena-project]). The native loader fills missing defaults and -requires `projects: []` ([config:1050–1104][serena-loader]). The private copy of -[serena_config.yml](config/serena_config.yml) also bounds default answers to -30,000 characters. No host Serena configuration is changed. - -QMD uses [QMD_CONFIG_DIR][qmd-config] and [INDEX_PATH][qmd-store] for attempt-owned -state. [qmd-setup.py](qmd-setup.py) executes native [collection add and update][qmd-cli] -against only the four mounted document directories. A native SDK -[PreToolUse hook][hooks] requires explicit `collections`, typed lexical -`searches` and `rerank:false` for QMD query calls, following the -[v2.8.3 MCP schema][qmd-schema]. Retrieval therefore needs no embedding/model -download. The model only receives useful excerpts, not the full catalog. - -The 28 skills in [config/skills.lock.json](config/skills.lock.json) retain the -revisions and SKILL.md hashes from -[adoption/skills/manifest.json](../../../adoption/skills/manifest.json). -Installation verifies `$HOME/.agents/skills//SKILL.md` and creates prefix -symlinks to the existing directories. Containers mount each resolved directory -read-only at `/skills/`; no skill content is copied or edited. The driver -uses native `load_skills_from_dir` and `AgentContext.skills`, as in the -[loading-skills example][skills-example]. SDK [skill loading:848–903][skills-loader] -and [InvokeSkillTool][invoke-skill] preserve progressive disclosure and supporting -resources. All pinned names must be discovered or startup fails. Public skill -auto-fetch, unrelated home/project skill discovery and built-in memory are off; -the configured ai-memory read tools remain available. This keeps source pins -and context scope explicit. - -## Coordinator execution and lifecycle - -Fill [config/host.example.json](config/host.example.json) into a private file -outside every checkout, owned by the current user with mode 0600. It contains -paths and local service addresses, **no secrets**. `mcp_readonly_mounts` must -include each existing executable/dependency directory needed by the template -commands, at its original absolute path so wrappers, symlinks and shebangs work. -Supply a container-compatible `HOST_PATH`; do not mount a whole home directory, -authentication directory, Docker socket or unrelated checkout. Scope each QMD -mount to the named document directory. The recipe starts no production service. - -After review, on the coordinator's host: - -```sh +| context-mode | Native stdio; ctx_upgrade and ctx_purge excluded | +| serena | Native stdio, explicit /workspace, dashboard off, external attempt-owned metadata | +| jcodemunch | Native stdio; route, menu and order only | +| qmd | Native stdio; query/get/multi_get/status over exactly four named catalog/document collections | +| ai-memory | Existing HTTP endpoint; query/read_page/recent/status/briefing only | +| socraticode | Native stdio; search/status/list_projects/health; watcher manual; existing local embedder | +| headroom | Configured but disabled; no measured remaining gap justifies it for this trial | + +Serena state and QMD indices live under the attempt's MCP state. +[qmd-setup.py](qmd-setup.py) uses native collection add/update only on the four +mounted document directories. [mcp_guard.py](mcp_guard.py) requires explicit +collections, lexical searches and rerank:false. There is no OmniRoute embedder. +[config/host.example.json](config/host.example.json) describes the private, +mode-0600 paths file; no credentials or authentication-store mounts are needed. + +**Skills installation has exactly one path**, executed from OPENHANDS_STACK_ROOT: + +~~~sh +PYTHONDONTWRITEBYTECODE=1 python3 tools/adoption/install_skills.py \ + --manifest blueprints/runtime-workers/skills/manifest.json \ + --project-dir "$WORKER_WORKSPACE" --agent universal +~~~ + +The new project-dir/agent options and runtime-worker manifest are **pending the +coordinator's skills PR** in this worktree. The call and regression test are +present now. Missing manifest/options fail the trial; there is no global, +copy, symlink or alternate installer fallback. The old +[config/skills.lock.json](config/skills.lock.json) records round-1 provenance only +and is no longer read for installation, loading or receipt decisions. + +The expected manifest contract is a skills array with name and +skill_md_sha256, including tdd and verification-before-completion. The installer +owns the project-local .agents/skills tree. The host verifies those bytes and +mounts .agents read-only; [native load_skills_from_dir][skills-loader] discovers +them and AgentContext.skills exposes them progressively. Public, home and project +auto-discovery stays disabled. Discovery mismatches fail startup. +The [upstream project installer][skills-local-lock] also writes skills-lock.json +at the workspace root. Both installer paths are excluded from the worker patch +and mounted read-only when present. A frozen task already containing .agents or +skills-lock.json is rejected before installation so its own files stay intact. +Additional artifacts from the pending shared installer must be reviewed at +integration; the recipe does not assume that extension has been accepted. + +Each attempt records the **actual names listed at start** in +worker/skills-startup.json and input/skills.json, rather than claiming a fixed +28-skill list in advance of the pending manifest. The E2E requires successful +native **ObservationEvent / invoke_skill / skill_name** results for tdd and +verification-before-completion, corroborated by the SDK's persisted trace. +[InvokeSkillTool][invoke-skill] returns the rendered skill content and records +the invocation. A name in config, startup listing or the task prompt alone +does not prove activation. No activation was observed in this build. + +## Frozen task and official verdict + +Before host acceptance, select one original SWE-bench Verified row from a +recorded dataset revision; retain the complete JSON-list or JSONL row, its +source revision and SHA256 outside every worktree. The [official loader][swe-dataset] +accepts both formats. OPENHANDS_TASK_FILE and OPENHANDS_TASK_SHA256 identify +these frozen bytes. A concrete instance/revision is a remaining host input; +the recipe does not invent a replacement benchmark task. + +[e2e/task.py](e2e/task.py) validates the hash, single-instance scope, repository, +base commit and native test fields. Only the issue, repository and base commit +enter the worker's prompt. Gold patch, test patch and test-name oracle fields +remain outside every worker mount. The worker gets a checkout at the frozen +base commit with protected Git/skill metadata. This bare-checkout adaptation +does not claim the benchmark's preinstalled Conda testbed; dependency and native +test availability must be established for the selected task. + +After inference, [host.py](host.py) exports the worker's staged diff against +that base commit using the benchmark's [native patch pattern][benchmark-infer] +into output.jsonl. The adapter rejects missing, malformed, duplicate or +wrong-instance predictions before upstream conversion. Empty patches remain +valid submissions that the official grader can fail. + +The primary pipeline is split at the official converter/grader boundary: + +~~~sh +# In the pinned benchmark environment; paths below are private attempt paths. +swebench-eval "$OUTPUT_JSONL" --dataset "$FROZEN_DATASET" \ + --run-id "$RUN_ID" --workers 1 --no-modal --skip-evaluation +python /path/to/recipe/e2e/docker_grader.py \ + --dataset_name "$FROZEN_DATASET" --predictions_path "$PREDICTIONS_JSONL" \ + --instance_ids "$INSTANCE_ID" --run_id "$RUN_ID" --max_workers 1 \ + --split test --timeout 1800 --namespace swebench \ + --cache_level instance --clean false --modal false +~~~ + +The first command is the unchanged [OpenHands converter][benchmark-eval]. +The second executes the **unchanged swebench.harness.run_evaluation CLI**, +the same module invoked by swebench-eval. [docker_grader.py](e2e/docker_grader.py) +adapts only the [Docker creation boundary][swe-container] for names, ownership +labels and resource limits. The split allows the native cache/cleanup flags, +which swebench-eval does not expose. There is no local test grader. + +The benchmark converter intentionally removes edits to pyproject.toml, +tox.ini and setup.py. Both original output.jsonl and converted +output.swebench.jsonl are retained with hashes; host acceptance must examine +any filtered changes. SWE-bench writes OpenHands.RUN_ID.json plus per-instance +report.json and test output in its native logs. [receipt.py](receipt.py) +re-reads that official report; resolved_ids supplies the verdict. A grader +exit code of zero alone is insufficient. Missing/conflicting/malformed report +fields leave the verdict unknown. [check.py](e2e/check.py) returns 0 for an +upstream-resolved ID, 1 for an upstream negative outcome, and 2 for invalid +transport; input-only mode reports ready_to_grade without a correctness claim. + +The old e2e/fixture-repo, frozen.json and task.txt are retained solely as +historical round-1 artifacts. They are not mounted or executed in this E2E. + +## Ownership, ports and coordinator execution + +Every created container has prefix **rw-openhands-** and label +**com.native-agent-stack.owner=gpt6-omniroute-framework-integration**. +No named volume or custom network is created; worker persistence uses owned +bind mounts. The grader rejects new volume/network/build operations and uses +official prebuilt SWE-bench images. Their actual resolved digests must be +recorded during host acceptance; the native per-instance image tags are not +claimed here to be immutable artifacts. + +| Port/resource | Mapping | +| --- | --- | +| Worker, install, QMD and grader containers | No host ports published; no worker HTTP service configured | +| Permitted future worker listener allocation | 127.0.0.1:3730–3799 only | +| Existing gateway dependency | Host 127.0.0.1:20128; container access 10.0.2.2:20128 | +| Existing ai-memory / local embedder dependencies | 10.0.2.2:49474 / 10.0.2.2:18232; not created by this recipe | +| S3 memory arms and cognee | 3710–3729, 5433–5439 and 3800–3819 remain unallocated by this worker | + +On the coordinator's host, after the shared skills PR and a frozen native task: + +~~~sh export OPENHANDS_HOST_FILE=/absolute/private/openhands-host.json +export OPENHANDS_STACK_ROOT=/absolute/native-agent-stack +export OPENHANDS_TASK_FILE=/absolute/private/frozen-instance.jsonl +export OPENHANDS_TASK_SHA256='' bash blueprints/runtime-workers/openhands/install.sh -# Optional model-currency override; otherwise the JSON default is used. OPENHANDS_MODEL=cx/gpt-6-astra-max bash blueprints/runtime-workers/openhands/run-e2e.sh -``` - -Installation and runs use container UID 0 **inside rootless Docker**, which maps -owned bind-mount writes to the calling host user. They drop all capabilities, -set no-new-privileges, use a read-only image filesystem and temporary private -`/root`/`/tmp` mounts. The worker gets only its writable fixture, private -conversation output and MCP state; recipes, venv, skills and document sources -are read-only. The Docker socket is never mounted. Agent containers have 4 CPU, -8 GiB and 384 PID limits. SDK and model execution cannot fall back to the host. - -Every E2E gets a new `runs/attempt-*` directory; failed attempts remain. Normal -exit, timeout, SIGINT and SIGTERM attempt removal of the exact named container, -then record independent absence/removal status. Cleanup errors preserve the -primary exit code and fail the evidence-complete gate. All owned containers -carry `native-agent-stack.runtime-worker=openhands`; after an uncatchable host -crash, inspect that label and remove only the specific abandoned attempt. -Hard-kill/reboot recovery has not been qualified. Resume means a new attempt -from the frozen fixture; no interrupted trial is silently relabelled successful. -Rollback removes only the owned prefix/state when no labelled container uses -them. Shared rootless Docker, gateway, MCP installations and native sign-ins are -not modified or uninstalled. - -## Frozen task, checker and receipts - -[e2e/fixture-repo](e2e/fixture-repo/) contains one intentionally failing unittest, -a README spec and `range_utils.py`. [frozen.json](e2e/frozen.json) fixes all initial -file hashes, the one-test count, test command and sole allowed changed file. -The initial contract test was written and run before this recipe existed: -**exit 1, six tests, two failures and four missing-file errors**; the sanitized -returned output is [evidence/fail-first.txt](evidence/fail-first.txt). - -`run-e2e.sh` first verifies the failing baseline inside the pinned image. The -worker must invoke `tdd`, run the unchanged failing test before editing, repair -the implementation and rerun the test. The callback retains native ActionEvent -and ObservationEvent fields with fixture byte inventories. [check.py](e2e/check.py) -runs in a separate container with **network disabled** and read-only inputs. It -requires a genuine failing terminal observation while the fixture was unchanged, -an allowed nonempty diff, unchanged test/README bytes, and a fresh passing run of -exactly one test with no skipped/expected-failure cases. Empty, incomplete, -extra-file, symlink, edited-test and wrong-answer results fail. This is an oracle -for a cooperative coding task, not a security proof against a worker deliberately -tampering with its own process or Python's test machinery. -The local checker follows CPython 3.13.15's [unittest runner][unittest-runner] -and [test-result exit handling][unittest-main]; it additionally enforces the -frozen file and test-count contract. - -[receipt.py](receipt.py) reads the live -`$HOME/.local/share/omniroute/storage.sqlite` with SQLite URI `mode=ro` and -`query_only`, following the [official read-only URI example][sqlite-readonly]. -Its only SELECT accesses `call_logs` columns `timestamp`, `path`, -`status`, `model`, `reasoning_effort_requested`, `reasoning_effort_upstream`, -`tokens_in`, `tokens_cache_read`, `tokens_reasoning`, limited to the run window. -There is no other table/column read, no schema introspection, and no prompt or -request identifier export. Missing/unknown counters remain null. - -Rows are **window evidence**, potentially including concurrent callers; the -allowed columns cannot establish exclusive session attribution. The receipt -does not sum them into a worker cost. It records observed package versions, -sanitized gateway rows, actual native MCP observations/errors, successful skill -observations, checker results and container cleanup. Configured tools/skills -are not counted as used. Request/session/tool-call IDs, host paths, email -addresses, raw output and conversations stay out of the receipt. - -The command exits zero only when the task passes and the evidence includes -observed matching model/Responses/max gateway rows, native SDK versions and -events, successful `tdd` use and confirmed container cleanup. A changed gateway -log schema or unavailable log database leaves evidence incomplete rather than -pretending zero usage. Full native persistence, command output and failures stay -in the private attempt directory; only its `receipt.json` is designed for public -review. - -## Evidence and remaining host checks - -| Evidence class | What exists here | -| --- | --- | -| Upstream source/release review | Exact SDK/app pins, executable/install formats, configuration keys, image topology, native MCP/skill/context behavior. [research.md](research.md) records decisions, limitations and corrected assumptions. | -| Artifact verification | Source/lock/wheel/GHCR response hashes; matching wheel/source Python files; exported requirement hashes. No container or package installation. | -| Local structural and synthetic checks | [tests/test_runtime_worker_openhands.py](../../../tests/test_runtime_worker_openhands.py) validates config, model override, headers, tool limits, QMD guard, fixture oracle, restricted SQLite query and failure preservation. [Verification record](evidence/verification.md) retains commands and returned results. Positive controls use synthetic events, not a model transcript. | -| Upstream test execution | **Not run.** Native CI uses `CI=true uv run python -m pytest -vvs` ([tests.yml:117][upstream-tests]); relevant unchanged suites are model features, the summarizing condenser, local conversation MCP and InvokeSkillTool. The production recipe does not install upstream test/dev dependencies. | -| Host/provider acceptance | **Not run.** No installation, image execution, gateway request, live database read, native MCP launch or model-produced fixture repair happened in this build. | - -Host acceptance must establish wheel/sdist installation and import compatibility, -all mounted MCP executables/dependencies and language servers, all 28 skill -loads, tool enumeration/limits, stable affinity and fresh idempotency headers, -Responses/tool streaming, requested/upstream effort, actual test-first repair, -gateway schema compatibility and cleanup. The SDK's aggregate MCP startup limit -is 30 seconds ([local_conversation.py:123,1356–1379][local-mcp]); a server's -`timeout=120` is **not** a promise of a 120-second aggregate startup allowance. -Cold startup may need a separately reviewed upstream-supported provider adapter. -No native savings, cross-host portability, uncatchable-crash recovery or complete -upstream suite acceptance is claimed. - -The OpenHands app, now Agent Canvas, is pinned for comparison at v1.24.0, -`7dc6805406ea3c76cb4a3ce407c3c72d481b0ac6`. Its [README ownership table, lines -139–150][canvas], assigns the frontend/backend selection/local stack orchestration -to Canvas and the agent/runtime APIs to this SDK. From those boundaries, Canvas -adds no required capability to this bounded coding worker. It is **not installed**. +~~~ + +The existing rootless Docker context is required. The worker uses container +UID 0 mapped to the host user, dropped capabilities, no-new-privileges, +a read-only image and temporary /root and /tmp. The worker and grader have +4 CPU, 8 GiB and 384 PID limits; the grader uses the existing rootless socket +from the host, never a worker socket mount. Its process deadline is 2,400 +seconds in addition to the upstream per-instance test timeout. + +Cleanup removes only each exact literal container name, then independently +checks absence. There is no global prune or upstream broad cleanup helper. +The grader's instance cache/clean=false setting retains images. Native logs +and failures stay in private attempt state. A hard-killed host may still need +literal-name cleanup; reboot/crash recovery has not been accepted. +Inspect only the owner label and the relevant rw-openhands attempt before +recovering. Do not remove shared Docker, gateway, native sign-ins or MCP state. + +## Evidence and remaining gates + +[Round-2 verification](evidence/round2-verification.md) records fail-first and +final commands, exits and returned output. The unit suite provides **local +synthetic transport controls**, including known-pass, known-fail, malformed, +missing, duplicate and conflicting results. It tests the shared installer +invocation, GPT-6 routing and resource adapter. None is reported as an unchanged +upstream test or a live model evaluation. + +The read-only gateway receipt still reads only the nine authorized call_logs +columns and never sums window rows into an exclusive worker cost. Native +metrics and full traces remain private. Unknown usage stays unknown. +Attribution, byte/token counts, cache subsets and provider spending are separate. + +Host checks remaining: install/import compatibility in both pinned environments; +frozen dataset source/instance and testbed compatibility; official pass/fail +controls using retained gold/empty patches before a real worker patch; actual +grader image digests; container ownership/cleanup; native MCP startup and tool +limits; exact skill listing and both activation observations; gateway headers, +Responses/tool behavior and requested/upstream max effort. Native MCP aggregate +startup is 30 seconds even when individual server timeouts are longer. +No matched A/B, skill ablation, token savings, full upstream SDK suite or +crash-recovery acceptance has been performed. A future comparison must freeze +the same task/oracle, environment, models, tools and cache policy across arms. + +The coordinator owns shared manifests. Changed round-1 evidence hashes require +re-registration in manifests/evidence.json after integration; this builder edits +only this recipe and its assigned test. Publication validation is still run and +its actual result is retained even when that shared-file boundary prevents a +clean hash check. [release]: https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.49.6 [sdk-readme]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/README.md#L49-L81 @@ -371,3 +365,16 @@ adds no required capability to this bounded coding worker. It is **not installed [unittest-runner]: https://github.com/python/cpython/blob/v3.13.15/Lib/unittest/runner.py [unittest-main]: https://github.com/python/cpython/blob/v3.13.15/Lib/unittest/main.py [sqlite-readonly]: https://docs.python.org/3.13/library/sqlite3.html#how-to-work-with-sqlite-uris + +[benchmark-install]: https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/README.md +[benchmark-make]: https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/Makefile +[benchmark-infer]: https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/benchmarks/swebench/run_infer.py +[benchmark-eval]: https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/benchmarks/swebench/eval_infer.py +[harbor]: https://github.com/harbor-framework/harbor/blob/v0.23.0/src/harbor/agents/installed/openhands_sdk.py +[swe-dataset]: https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/utils.py#L133-L177 +[swe-container]: https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/docker_build.py#L470-L536 +[tool-serialization]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/tool/tool.py#L776-L804 +[header-merging]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/options/common.py#L26-L44 +[llm-discovery]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/agent/base.py#L739-L775 +[llm-registration]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L1566-L1579 +[skills-local-lock]: https://github.com/vercel-labs/skills/blob/7407f3893ad4dceab546ac002c3ef806e4000c73/src/add.ts#L2130-L2160 diff --git a/blueprints/runtime-workers/openhands/e2e/check.py b/blueprints/runtime-workers/openhands/e2e/check.py index 2a8ebdced..ea5b56a29 100644 --- a/blueprints/runtime-workers/openhands/e2e/check.py +++ b/blueprints/runtime-workers/openhands/e2e/check.py @@ -1,134 +1,82 @@ -"""Independent frozen fixture oracle. Run in the pinned image with no network. +"""Thin transport validation/relay; never run tests or decide patch correctness. -The local test runner follows CPython unittest's TestLoader/TextTestRunner public -interfaces. This is a synthetic task check, never an upstream acceptance suite. -It reexecutes the frozen tests and checks bytes against the pristine fixture. +Input: OpenHands/benchmarks 405bae7 benchmarks/swebench/eval_infer.py:35-105. +Verdict: swebench==4.1.0 swebench/harness/reporting.py:127-142 (schema 2). +The official Docker grader alone produces resolved_ids. A successful process, +empty report or absent task is not a resolved task. See README for source pins. """ - from __future__ import annotations import argparse -import io +import hashlib import json from pathlib import Path -import subprocess -import sys -import unittest - -sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from recipe import inventory, read_json # noqa: E402 - - -HERE = Path(__file__).resolve().parent - - -def run_tests(workspace): - """Isolated child imports candidate code, bounded by time and container scope.""" - command = [sys.executable, "-I", "-B", str(Path(__file__).resolve()), "--test-child", str(workspace)] - completed = subprocess.run(command, capture_output=True, text=True, timeout=30, check=False) - lines = completed.stdout.splitlines() - try: - report = json.loads(lines[-1]) - except (IndexError, json.JSONDecodeError): - report = {"tests_run": 0, "failures": None, "errors": None, "skipped": None} - report["exit_code"] = completed.returncode - report["stdout"] = completed.stdout - report["stderr"] = completed.stderr - return report - - -def test_child(workspace): - # -I ignores environment/site/user paths; insert only the owned fixture. - sys.path.insert(0, str(workspace)) - stream = io.StringIO() - suite = unittest.defaultTestLoader.discover(str(workspace / "tests"), pattern="test_*.py") - result = unittest.TextTestRunner(stream=stream, verbosity=2).run(suite) - print(json.dumps({ - "tests_run": result.testsRun, "failures": len(result.failures), "errors": len(result.errors), - "skipped": len(result.skipped), "expected_failures": len(result.expectedFailures), - "unexpected_successes": len(result.unexpectedSuccesses), "output": stream.getvalue(), - })) - return 0 if result.wasSuccessful() else 1 - - -def test_first(events, pristine, command): - """Require an actual failing terminal observation while bytes were pristine.""" - pending = {} - for event in events: - if event.get("workspace") != pristine: - return False - if event.get("kind") == "ActionEvent": - pending[event.get("tool_call_id")] = event - elif event.get("kind") == "ObservationEvent": - action = pending.get(event.get("tool_call_id"), {}) - obs = event.get("observation", {}) - if ( - action.get("tool_name") == "terminal" - and action.get("action", {}).get("command", "").strip() == command - and obs.get("exit_code") == 1 - and "FAILED (failures=1)" in json.dumps(obs) - and "test_ranges_follow_the_spec" in json.dumps(obs) - ): - return True - return False -def check(result): - failures = [] - required = [result / "workspace", result / "baseline.json", result / "worker/events.jsonl"] - if not all(path.exists() for path in required): - return {"passed": False, "failures": ["missing_result"]} - frozen = read_json(HERE / "frozen.json") - pristine = inventory(HERE / "fixture-repo") - expected = {**frozen["files"], "tests/": "directory"} - if pristine != expected: - failures.append("frozen_fixture_changed") - current = inventory(result / "workspace") - changed = sorted(name for name in set(pristine) | set(current) if pristine.get(name) != current.get(name)) - if not changed or not set(changed) <= set(frozen["allowed_files"]): - failures.append("diff_outside_allowed_files_or_empty") - if any(value in ("symlink", "special") for value in current.values()): - failures.append("nonregular_result") - baseline = read_json(result / "baseline.json") - if not (baseline.get("exit_code") == 1 and baseline.get("tests_run") == 1 - and baseline.get("failures") == 1 and baseline.get("errors") == 0 and baseline.get("skipped") == 0): - failures.append("baseline_not_one_failing_test") - events = [json.loads(line) for line in (result / "worker/events.jsonl").read_text().splitlines() if line] - if not test_first(events, pristine, frozen["test_command"]): - failures.append("no_native_test_failure_before_edit") - # Never execute a scope-violating submission, even inside the checker container. - report = None - if not failures: - before = inventory(result / "workspace") - report = run_tests(result / "workspace") - if before != inventory(result / "workspace"): - failures.append("tests_mutated_workspace") - if not (report.get("exit_code") == 0 and report.get("tests_run") == frozen["expected_test_count"] - and all(report.get(key) == 0 for key in ("failures", "errors", "skipped", "expected_failures", "unexpected_successes"))): - failures.append("tests_not_passing_unskipped") - return {"passed": not failures, "failures": failures, "changed_files": changed, "tests": report} +def unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError("duplicate_json_key") + result[key] = value + return result + + +def read_json(path): + return json.loads(Path(path).read_text(), object_pairs_hook=unique_object) + + +def check_input(path, instance_id): + rows = [json.loads(line, object_pairs_hook=unique_object) + for line in Path(path).read_text().splitlines() if line.strip()] + if len(rows) != 1 or not isinstance(rows[0], dict): + raise ValueError("exactly_one_prediction_required") + row = rows[0] + result = row.get("test_result") + if (row.get("instance_id") != instance_id or not isinstance(result, dict) + or not isinstance(result.get("git_patch"), str) or row.get("error") is not None): + raise ValueError("malformed_prediction") + # Empty/wrong patches remain submissions for the upstream grader to fail. + return {"ready_to_grade": True, "instance_id": instance_id, + "evidence_class": "transport sanity only"} + + +def read_report(path, instance_id): + data = read_json(path) + fields = ("submitted_ids", "resolved_ids", "unresolved_ids", "error_ids", + "empty_patch_ids", "incomplete_ids") + if not isinstance(data, dict) or data.get("schema_version") != 2: + raise ValueError("malformed_upstream_report") + for key in fields: + values = data.get(key) + if (not isinstance(values, list) or any(not isinstance(v, str) for v in values) + or len(set(values)) != len(values)): + raise ValueError("malformed_upstream_report") + if data["submitted_ids"] != [instance_id]: + raise ValueError("submission_scope_mismatch") + buckets = [key for key in fields[1:] if instance_id in data[key]] + if len(buckets) != 1: + raise ValueError("missing_or_conflicting_upstream_outcome") + return {"verdict_source": "swebench==4.1.0", "instance_id": instance_id, + "upstream_resolved": buckets == ["resolved_ids"], "upstream_bucket": buckets[0], + "report_sha256": hashlib.sha256(Path(path).read_bytes()).hexdigest()} def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--result", type=Path) - parser.add_argument("--baseline", type=Path) - parser.add_argument("--test-child", type=Path) + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--input", type=Path) + mode.add_argument("--report", type=Path) + parser.add_argument("--instance-id", required=True) args = parser.parse_args() - if args.test_child: - return test_child(args.test_child) try: - if args.baseline: - report = run_tests(args.baseline) - print(json.dumps(report)) - return report["exit_code"] - if args.result is None: - parser.error("--result is required") - report = check(args.result) - except (OSError, ValueError, TypeError, subprocess.TimeoutExpired): - report = {"passed": False, "failures": ["invalid_or_incomplete_result"]} - print(json.dumps(report)) - return 0 if report["passed"] else 1 + result = (check_input(args.input, args.instance_id) if args.input + else read_report(args.report, args.instance_id)) + except (OSError, ValueError, TypeError): + print(json.dumps({"transport_error": "missing_or_malformed_output", "upstream_resolved": None})) + return 2 + print(json.dumps(result)) + return 0 if args.input or result["upstream_resolved"] else 1 if __name__ == "__main__": diff --git a/blueprints/runtime-workers/openhands/e2e/docker_grader.py b/blueprints/runtime-workers/openhands/e2e/docker_grader.py new file mode 100644 index 000000000..439b6f6eb --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/docker_grader.py @@ -0,0 +1,63 @@ +"""Run the unchanged official SWE-bench CLI with owned Docker transport. + +Reference: swebench==4.1.0 harness/docker_build.py:516 and +harness/run_evaluation.py:580. Only Docker creation metadata/limits are adapted. +No tests, scoring, patch application or report-writing logic is replaced. +""" +from __future__ import annotations + +import importlib.metadata +import json +import re +import runpy +from pathlib import Path + + +OWNER_KEY = "com.native-agent-stack.owner" +OWNER = "gpt6-omniroute-framework-integration" + + +def owned_container_options(options): + result = dict(options) + name = result.get("name", "") + if not re.fullmatch(r"sweb\.eval\.[a-z0-9_.-]+", name): + raise ValueError("unexpected_upstream_container_name") + if any(result.get(key) for key in ("ports", "publish_all_ports", "volumes", "mounts", "network", "network_mode")): + raise ValueError("grader_requires_no_ports_volumes_or_custom_networks") + result["name"] = "rw-openhands-" + name + result["labels"] = {**result.get("labels", {}), OWNER_KEY: OWNER} + result.update(mem_limit="8g", nano_cpus=4_000_000_000, pids_limit=384) + return result + + +def main(): + if importlib.metadata.version("swebench") != "4.1.0": + raise RuntimeError("exact_upstream_grader_version_required") + from docker.models.containers import ContainerCollection + from docker.models.networks import NetworkCollection + from docker.models.volumes import VolumeCollection + from docker.api.client import APIClient + + create = ContainerCollection.create + + def owned_create(collection, *args, **kwargs): + kwargs = owned_container_options(kwargs) + # A separate host supervisor retains these exact names for cleanup. + with Path("docker-created.jsonl").open("a") as stream: + stream.write(json.dumps({"name": kwargs["name"]}) + "\n") + return create(collection, *args, **kwargs) + + def forbidden(*args, **kwargs): + raise RuntimeError("use_prebuilt_images_without_new_volumes_or_networks") + + ContainerCollection.create = owned_create + # Namespace swebench uses prebuilt images. Never create unlabelled build + # intermediates or additional resources if the selected upstream changes. + APIClient.build = forbidden + VolumeCollection.create = forbidden + NetworkCollection.create = forbidden + runpy.run_module("swebench.harness.run_evaluation", run_name="__main__") + + +if __name__ == "__main__": + main() diff --git a/blueprints/runtime-workers/openhands/e2e/task.py b/blueprints/runtime-workers/openhands/e2e/task.py new file mode 100644 index 000000000..67e42c8f7 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/task.py @@ -0,0 +1,59 @@ +"""Transport a frozen SWE-bench row to the standalone worker. + +Sources: SWE-bench 4.1.0 harness/utils.py (local JSON/JSONL datasets); +OpenHands/benchmarks 405bae7 swebench/run_infer.py:61-88 and prompts/default.j2. +This is a bare-checkout SDK adaptation, not unchanged benchmark inference. +""" +import hashlib +import json +from pathlib import Path +import re + + +def load_task(path, expected_sha256): + path = Path(path) + raw = path.read_bytes() + if (not re.fullmatch(r"[0-9a-f]{64}", expected_sha256) + or hashlib.sha256(raw).hexdigest() != expected_sha256): + raise ValueError("frozen_task_hash_mismatch") + if path.suffix == ".jsonl": + rows = [json.loads(line) for line in raw.decode().splitlines()] + elif path.suffix == ".json": + rows = json.loads(raw) + else: + raise ValueError("native_json_or_jsonl_dataset_required") + if not isinstance(rows, list) or len(rows) != 1 or not isinstance(rows[0], dict): + raise ValueError("one_frozen_swebench_row_required") + row = rows[0] + for key in ("repo", "instance_id", "base_commit", "version", "problem_statement", "test_patch"): + if not isinstance(row.get(key), str) or not row[key].strip(): + raise ValueError("missing_swebench_field") + if (not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", row["repo"]) + or not re.fullmatch(r"[a-zA-Z0-9_-]+", row["instance_id"]) + or not re.fullmatch(r"[0-9a-f]{40}", row["base_commit"])): + raise ValueError("invalid_swebench_identity") + for key in ("FAIL_TO_PASS", "PASS_TO_PASS"): + value = row.get(key) + cases = json.loads(value) if isinstance(value, str) else value + if not isinstance(cases, list) or any(not isinstance(case, str) for case in cases): + raise ValueError("invalid_swebench_test_contract") + if key == "FAIL_TO_PASS" and not cases: + raise ValueError("empty_swebench_test_contract") + return row + + +def worker_instruction(row): + # Deliberately exclude patch, test_patch and test-name oracle fields. + return ( + "Repair the repository at /workspace for the following issue.\n" + f"Repository: {row['repo']}\nBase commit: {row['base_commit']}\n\n" + + row["problem_statement"] + + "\n\nInvoke the tdd skill using invoke_skill before editing and the " + "verification-before-completion skill before finishing. The existing code/test " + "interfaces and test-first work are authorized. Inspect the repository's native " + "test instructions; reproduce the issue and retain test results in the trace. " + "This is a bare checkout, not the upstream Conda testbed. Do not claim an " + "unavailable dependency or test passed. Keep changes scoped to the issue. " + "Git metadata and installed skills are read-only; do not commit or alter them. " + "The independent official SWE-bench grader will apply and test your patch." + ) diff --git a/blueprints/runtime-workers/openhands/evidence/round2-fail-first.txt b/blueprints/runtime-workers/openhands/evidence/round2-fail-first.txt new file mode 100644 index 000000000..58814fa86 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-fail-first.txt @@ -0,0 +1,460 @@ +Round-2 local synthetic integration controls; not upstream E2E acceptance. +Returned output from incremental test-first snapshots, ordered by execution. +Only private filesystem prefixes are replaced. The first output includes the tool's own 17-token truncation marker; it is not a full log. +Earlier snapshots used the same unittest discovery command without RTK; the final skills-state snapshot used RTK after the updated AGENTS.md arrived. + +=== red-routing === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +Warning: truncated output (original token count: 3017) +Total output lines: 105 + +test_broken_answer_and_symlink_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_broken_answer_and_symlink_cannot_pass) ... ok +test_deleted_or_skipped_test_and_extra_files_are_rejected (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_deleted_or_skipped_test_and_extra_files_are_rejected) ... ok +test_fixed_copy_passes_and_frozen_source_stays_broken (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_fixed_copy_passes_and_frozen_source_stays_broken) ... ok +test_no_native_red_observation_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_no_native_red_observation_cannot_pass) ... ok +test_red_after_edit_cannot_pass (test_runtime_worker_openhands.OpenHandsFrozenOracleTests.test_red_after_edit_cannot_pass) ... ok +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_checker_rejects_an_empty_result (test_runtime_worker_openhands.OpenHandsRecipeTests.test_checker_rejects_an_empty_result) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... FAIL +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='claude-opus-5-5') ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='cx/claude-opus-5-5') ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='cx/gpt-5') ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='openai/cx/gpt-6-astra-max') ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'temperature': 0.1}) ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'response_format': {'type': 'json_object'}}) ... FAIL + test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'native_tool_calling': False}) ... FAIL +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok + +====================================================================== +FAIL: test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 160, in test_every_container_is_owned_and_has_no_published_port + self.assertIn("com.native-agent-stack.owner=gpt6-omniroute-framework-integration", args) + ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 'com.native-agent-stack.owner=gpt6-omniroute-framework-integration' not found in ['docker', '--context', 'rootless', 'run', '--rm', '--pull=never', '--name', 'rw-openhands-control', '--platform', 'linux/amd64', '--label', 'native-agent-stack.runtime-worker=openhands', '--user', '0:0', '--cap-drop=ALL', '--security-opt=no-new-privileges', '--rea…17 tokens truncated…oot:rw,nosuid,nodev,mode=0700', '--env', 'PYTHONDONTWRITEBYTECODE=1', '--env', 'UV_PYTHON_DOWNLOADS=never', '--cpus', '4', '--memory', '8g', '--pids-limit', '384'] + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='claude-opus-5-5') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 149, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(model=model), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='cx/claude-opus-5-5') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 149, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(model=model), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='cx/gpt-5') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 149, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(model=model), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (model='openai/cx/gpt-6-astra-max') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 149, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(model=model), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'temperature': 0.1}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 154, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(change=change), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'response_format': {'type': 'json_object'}}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 154, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(change=change), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) (change={'native_tool_calling': False}) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 154, in test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes + with self.subTest(change=change), self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +---------------------------------------------------------------------- +Ran 21 tests in 0.707s + +FAILED (failures=8) + +=== red-grader === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ERROR +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... + test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='unresolved_ids') ... FAIL + test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='empty_patch_ids') ... FAIL + test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='error_ids') ... FAIL + test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='incomplete_ids') ... FAIL +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... FAIL +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ERROR + +====================================================================== +ERROR: test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 232, in test_input_sanity_does_not_grade_the_patch + checked = self.checker.check_input(path, self.INSTANCE) + ^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'openhands_recipe_test_e2e_check' has no attribute 'check_input' + +====================================================================== +ERROR: test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 226, in test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed + self.checker.read_report(path, self.INSTANCE) + ^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'openhands_recipe_test_e2e_check' has no attribute 'read_report' + +====================================================================== +FAIL: test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='unresolved_ids') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 212, in test_known_fail_is_not_a_successful_process_verdict + self.assertEqual(completed.returncode, 1, completed.stderr) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 2 != 1 : usage: check.py [-h] [--result RESULT] [--baseline BASELINE] + [--test-child TEST_CHILD] +check.py: error: unrecognized arguments: --report /tmppysfzva8/report.json --instance-id django__django-11333 + + +====================================================================== +FAIL: test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='empty_patch_ids') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 212, in test_known_fail_is_not_a_successful_process_verdict + self.assertEqual(completed.returncode, 1, completed.stderr) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 2 != 1 : usage: check.py [-h] [--result RESULT] [--baseline BASELINE] + [--test-child TEST_CHILD] +check.py: error: unrecognized arguments: --report /tmppysfzva8/report.json --instance-id django__django-11333 + + +====================================================================== +FAIL: test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='error_ids') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 212, in test_known_fail_is_not_a_successful_process_verdict + self.assertEqual(completed.returncode, 1, completed.stderr) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 2 != 1 : usage: check.py [-h] [--result RESULT] [--baseline BASELINE] + [--test-child TEST_CHILD] +check.py: error: unrecognized arguments: --report /tmppysfzva8/report.json --instance-id django__django-11333 + + +====================================================================== +FAIL: test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='incomplete_ids') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 212, in test_known_fail_is_not_a_successful_process_verdict + self.assertEqual(completed.returncode, 1, completed.stderr) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 2 != 1 : usage: check.py [-h] [--result RESULT] [--baseline BASELINE] + [--test-child TEST_CHILD] +check.py: error: unrecognized arguments: --report /tmppysfzva8/report.json --instance-id django__django-11333 + + +====================================================================== +FAIL: test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 203, in test_known_pass_relays_upstream_resolution + self.assertEqual(completed.returncode, 0, completed.stderr) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: 2 != 0 : usage: check.py [-h] [--result RESULT] [--baseline BASELINE] + [--test-child TEST_CHILD] +check.py: error: unrecognized arguments: --report /tmpchnquw_9/report.json --instance-id django__django-11333 + + +---------------------------------------------------------------------- +Ran 19 tests in 0.589s + +FAILED (failures=5, errors=2) + +=== red-lifecycle === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok +test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ERROR +test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ERROR +test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ERROR + +====================================================================== +ERROR: test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 262, in test_official_grader_container_transport_preserves_upstream_settings + module = load_recipe_module("e2e/docker_grader.py") + File "/tests/test_runtime_worker_openhands.py", line 32, in load_recipe_module + spec.loader.exec_module(module) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^ + File "", line 1019, in exec_module + File "", line 1156, in get_code + File "", line 1214, in get_data +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/docker_grader.py' + +====================================================================== +ERROR: test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 250, in test_shared_skills_installer_is_the_only_install_path + host.install_workspace_skills(ROOT, workspace) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'openhands_recipe_test_host' has no attribute 'install_workspace_skills' + +====================================================================== +ERROR: test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 276, in test_task_adapter_hides_oracle_and_requires_frozen_original_bytes + module = load_recipe_module("e2e/task.py") + File "/tests/test_runtime_worker_openhands.py", line 32, in load_recipe_module + spec.loader.exec_module(module) + ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^ + File "", line 1019, in exec_module + File "", line 1156, in get_code + File "", line 1214, in get_data +FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/task.py' + +---------------------------------------------------------------------- +Ran 22 tests in 0.369s + +FAILED (errors=3) + +=== red-receipt === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_local_pass_field_is_never_a_worker_verdict (test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... FAIL +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... FAIL +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok +test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok +test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok +test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok + +====================================================================== +FAIL: test_local_pass_field_is_never_a_worker_verdict (test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 308, in test_local_pass_field_is_never_a_worker_verdict + self.assertFalse(receipt["task_passed"]) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: True is not false + +====================================================================== +FAIL: test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 330, in test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass + self.assertTrue(module.create_receipt(result, database=result / "absent.sqlite")["task_passed"]) + ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: False is not true + +---------------------------------------------------------------------- +Ran 24 tests in 0.381s + +FAILED (failures=2) + +=== red-native-llm === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_local_pass_field_is_never_a_worker_verdict (test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_header_transport_preserves_exact_native_llm_type_and_restores_methods (test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ERROR +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok +test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok +test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok +test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok + +====================================================================== +ERROR: test_header_transport_preserves_exact_native_llm_type_and_restores_methods (test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 138, in test_header_transport_preserves_exact_native_llm_type_and_restores_methods + with worker.gateway_transport(NativeLLM): + ^^^^^^^^^^^^^^^^^^^^^^^^ +AttributeError: module 'openhands_recipe_test_worker' has no attribute 'gateway_transport' + +---------------------------------------------------------------------- +Ran 25 tests in 0.409s + +FAILED (errors=1) + +=== red-skills-state === +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= rtk python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 1 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_local_pass_field_is_never_a_worker_verdict (test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_header_transport_preserves_exact_native_llm_type_and_restores_methods (test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok +test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok +test_project_skills_bookkeeping_is_excluded_from_worker_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... FAIL +test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... + test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) (reserved='.agents') ... FAIL + test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) (reserved='skills-lock.json') ... FAIL +test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok +test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok + +====================================================================== +FAIL: test_project_skills_bookkeeping_is_excluded_from_worker_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 311, in test_project_skills_bookkeeping_is_excluded_from_worker_patch + self.assertEqual(checked.stdout.splitlines(), [".agents/skills/tdd/SKILL.md", "skills-lock.json"]) + ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError: Lists differ: [] != ['.agents/skills/tdd/SKILL.md', 'skills-lock.json'] + +Second list contains 2 additional elements. +First extra element 0: +'.agents/skills/tdd/SKILL.md' + +- [] ++ ['.agents/skills/tdd/SKILL.md', 'skills-lock.json'] + +====================================================================== +FAIL: test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) (reserved='.agents') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 320, in test_project_skills_install_refuses_task_path_conflicts + with patch.object(host.subprocess, "run") as run, self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +====================================================================== +FAIL: test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) (reserved='skills-lock.json') +---------------------------------------------------------------------- +Traceback (most recent call last): + File "/tests/test_runtime_worker_openhands.py", line 320, in test_project_skills_install_refuses_task_path_conflicts + with patch.object(host.subprocess, "run") as run, self.assertRaises(ValueError): + ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^ +AssertionError: ValueError not raised + +---------------------------------------------------------------------- +Ran 27 tests in 0.414s + +FAILED (failures=3) diff --git a/blueprints/runtime-workers/openhands/evidence/round2-publication-first.txt b/blueprints/runtime-workers/openhands/evidence/round2-publication-first.txt new file mode 100644 index 000000000..793ee9e56 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-publication-first.txt @@ -0,0 +1,27 @@ +Intermediate publication check; not a passing result. +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 scripts/validate.py +Exit code: 1 +Publication validation failed: +blueprints/runtime-workers/openhands/README.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/README.md: byte count mismatch +blueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch +blueprints/runtime-workers/openhands/host.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/host.py: byte count mismatch +blueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/install.sh: byte count mismatch +blueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/pins.json: byte count mismatch +blueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/receipt.py: byte count mismatch +blueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/recipe.py: byte count mismatch +blueprints/runtime-workers/openhands/research.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/research.md: byte count mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch +blueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/worker.py: byte count mismatch +tests/test_runtime_worker_openhands.py: SHA-256 mismatch +tests/test_runtime_worker_openhands.py: byte count mismatch +blueprints/runtime-workers/openhands/research.md: contains possible personal home path diff --git a/blueprints/runtime-workers/openhands/evidence/round2-publication.txt b/blueprints/runtime-workers/openhands/evidence/round2-publication.txt new file mode 100644 index 000000000..334be9ebf --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-publication.txt @@ -0,0 +1,25 @@ +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 scripts/validate.py +Exit code: 1 +Publication validation failed: +blueprints/runtime-workers/openhands/README.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/README.md: byte count mismatch +blueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch +blueprints/runtime-workers/openhands/host.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/host.py: byte count mismatch +blueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/install.sh: byte count mismatch +blueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/pins.json: byte count mismatch +blueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/receipt.py: byte count mismatch +blueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/recipe.py: byte count mismatch +blueprints/runtime-workers/openhands/research.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/research.md: byte count mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch +blueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/worker.py: byte count mismatch +tests/test_runtime_worker_openhands.py: SHA-256 mismatch +tests/test_runtime_worker_openhands.py: byte count mismatch diff --git a/blueprints/runtime-workers/openhands/evidence/round2-static-checks.txt b/blueprints/runtime-workers/openhands/evidence/round2-static-checks.txt new file mode 100644 index 000000000..064e179ed --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-static-checks.txt @@ -0,0 +1,46 @@ +Local structural checks only; no native runtime acceptance. + +Command: rtk bash -n blueprints/runtime-workers/openhands/install.sh +Exit code: 0 +Returned output: (empty) + +Command: rtk bash -n blueprints/runtime-workers/openhands/install-container.sh +Exit code: 0 +Returned output: (empty) + +Command: rtk bash -n blueprints/runtime-workers/openhands/install-grader.sh +Exit code: 0 +Returned output: (empty) + +Command: rtk bash -n blueprints/runtime-workers/openhands/run-e2e.sh +Exit code: 0 +Returned output: (empty) + +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 - (AST/JSON parsing, pinned lock hashes, recipe scratch/cache/bytecode scan) +Exit code: 0 +Returned output: PASS: Python AST (12 files), JSON (8 files), 2 lock hashes; no recipe scratch/cache/bytecode. + +Command: rtk git diff --check +Exit code: 0 +Returned output: (empty) + +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 - (relative Markdown links, assigned-file boundary and whole-checkout cache sweep) +Exit code: 1 +Returned output: +Traceback (most recent call last): + File "", line 23, in +AssertionError: ['scripts/__pycache__', 'scripts/__pycache__/catalog_decisions.cpython-313.pyc', 'scripts/__pycache__/validate.cpython-313.pyc', 'scripts/__pycache__/host_receipts.cpython-313.pyc', 'scripts/__pycache__/credential_status.cpython-313.pyc'] + +The suite wrote no recipe bytecode. The checkout-wide sweep additionally found +the four ignored scripts bytecode files above; their generating process was not +observed. Removed only those four files and their empty directory, then reran +the same sweep. PYTHONDONTWRITEBYTECODE=1 was independently confirmed through +RTK (sys.dont_write_bytecode returned True). + +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 - (relative Markdown links, assigned-file boundary and whole-checkout cache sweep) +Exit code: 0 +Returned output: PASS: 39 relative Markdown links; 20 changed/new files within assigned paths; no cache or bytecode directories/files in checkout. + +Command: PYTHONDONTWRITEBYTECODE=1 rtk python3 scripts/validate.py --scan-file (each of the 9 round-2 additions listed by git status) +Exit code: 0 +Returned output: {"scanned_files": 9, "status": "passed"} diff --git a/blueprints/runtime-workers/openhands/evidence/round2-unit-pass.txt b/blueprints/runtime-workers/openhands/evidence/round2-unit-pass.txt new file mode 100644 index 000000000..c6b67df69 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-unit-pass.txt @@ -0,0 +1,35 @@ +Local synthetic integration controls; not upstream E2E acceptance. +Command: PYTHONDONTWRITEBYTECODE=1 TMPDIR= rtk python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +Exit code: 0 +test_cleanup_timeout_does_not_erase_primary_exit (test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok +test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok +test_local_pass_field_is_never_a_worker_verdict (test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok +test_skills_and_mcp_require_observations (test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok +test_truncated_native_summary_retains_failed_attempt_receipt (test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok +test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok +test_every_container_is_owned_and_has_no_published_port (test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok +test_fresh_call_headers_keep_session_affinity (test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok +test_gateway_model_and_container_topology (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok +test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok +test_header_transport_preserves_exact_native_llm_type_and_restores_methods (test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok +test_immutable_artifacts (test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok +test_installer_and_runtime_are_scoped (test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok +test_mcp_limits (test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok +test_no_public_binds_or_secret_material (test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok +test_qmd_guard_requires_explicit_scope_and_lexical_query (test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok +test_runtime_model_override_and_native_tool_filter (test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok +test_serena_activates_plain_fixture_and_keeps_metadata_outside_it (test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok +test_input_sanity_does_not_grade_the_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok +test_known_fail_is_not_a_successful_process_verdict (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok +test_known_pass_relays_upstream_resolution (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok +test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok +test_official_grader_container_transport_preserves_upstream_settings (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok +test_project_skills_bookkeeping_is_excluded_from_worker_patch (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok +test_project_skills_install_refuses_task_path_conflicts (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok +test_shared_skills_installer_is_the_only_install_path (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok +test_task_adapter_hides_oracle_and_requires_frozen_original_bytes (test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok + +---------------------------------------------------------------------- +Ran 27 tests in 0.451s + +OK diff --git a/blueprints/runtime-workers/openhands/evidence/round2-verification.md b/blueprints/runtime-workers/openhands/evidence/round2-verification.md new file mode 100644 index 000000000..477420d90 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round2-verification.md @@ -0,0 +1,81 @@ +# Round 2 verification, 2026-09-27 + +This is local transport and structural evidence. No package was installed, +service or container started, model called, gateway database read, upstream +grader executed, or upstream test suite run. Tests use synthetic predictions, +official-report-shaped controls, mocked installation and native Git ignore +queries in temporary scratch checkouts. Nothing was staged, committed or pushed. + +The source and integration choices are in [research.md](../research.md) and +[README.md](../README.md). Verdict ownership is OpenHands/benchmarks +405bae7140d7e961a75f4910a0b2e7069731db96, its SDK submodule +43376f1868ffd702746080714a59c16d3f69ec12, and official SWE-bench 4.1.0. +Our check.py only validates predictions and relays the official report. +The standalone worker remains SDK 1.49.6 at +fcc102a697874d54a357e36004e02c95040dbdc0. These environments stay separate. + +All test runs used this command from the checkout root, with TMPDIR set to +the task's approved external research scratch directory: + +~~~sh +PYTHONDONTWRITEBYTECODE=1 TMPDIR="$RESEARCH_SCRATCH" python3 -m unittest discover -s tests -p test_runtime_worker_openhands.py -v +~~~ + +After the updated AGENTS.md arrived, the final two runs prefixed python3 with +rtk. The retained logs state the exact command form for each run, replacing +only private filesystem prefixes with placeholders. These are incremental +test-first snapshots; removing the old local correctness oracle changed the +suite's test count. They are not repeated evaluations of one frozen model task. + +| Stage | Exit | Actual returned result | +| --- | --- | --- | +| Ownership, route and structured-output constraints, fail first | 1 | Ran 21 tests; FAILED (failures=8) | +| Official-report adapter, fail first | 1 | Ran 19 tests; FAILED (failures=5, errors=2) | +| Shared installer, task and Docker transport, fail first | 1 | Ran 22 tests; FAILED (errors=3) | +| Independent report receipt, fail first | 1 | Ran 24 tests; FAILED (failures=2) | +| Exact native LLM type and scoped headers, fail first | 1 | Ran 25 tests; FAILED (errors=1) | +| Project skill bookkeeping and conflicts, fail first | 1 | Ran 27 tests; FAILED (failures=3) | +| Final local suite | 0 | Ran 27 tests in 0.451s; OK | + +[Fail-first output](round2-fail-first.txt) retains each returned failure. +The first routing output contains the tool's own 17-token truncation marker; +it is explicitly incomplete and was not reconstructed. Later retained failures +are complete returned output. [Final suite output](round2-unit-pass.txt) retains +the passing result. + +Known-pass and known-fail report controls exercise schema-2 resolved, unresolved, +empty-patch, error and incomplete outcomes. Malformed, missing, conflicting, +duplicate and wrong-scope transport cannot supply a passing verdict. An empty +patch remains valid input for the real upstream grader to reject. The receipt +re-reads the official report even if a wrapper claims success. Skill names at +startup are distinct from successful native invoke_skill observations. + +Shell syntax was checked separately for install.sh, install-container.sh, +install-grader.sh and run-e2e.sh with rtk bash -n; each returned exit 0 with +empty output. Each script was a separate bash invocation, so all four were +parsed. Remaining structural checks and returned results are retained in +[round2-static-checks.txt](round2-static-checks.txt). + +Publication command: + +~~~sh +PYTHONDONTWRITEBYTECODE=1 rtk python3 scripts/validate.py +~~~ + +The intermediate publication result is retained in +[round2-publication-first.txt](round2-publication-first.txt); its prose-path +false positive was corrected in research.md. The final result is retained in +[round2-publication.txt](round2-publication.txt): exit 1 with 22 hash/byte-count +mismatches only. The coordinator must refresh the 11 changed round-1 file +registrations in manifests/evidence.json and register the round-2 additions. +That shared manifest is outside this builder's +file ownership; this recipe does not change or bypass the validator. + +Remaining host acceptance: merge the shared skills options/manifest and verify +its output contract; freeze an original SWE-bench instance/revision; install the +two pinned environments through their documented paths; qualify the selected +task's dependencies; run official gold/empty-patch controls before the worker +patch; observe MCP startup, skill listing and both native skill activations; +observe actual gateway headers, tool calling and max effort; capture grader +image digests; verify owned-container cleanup. Grading has not run and therefore +has no pass, quality, A/B, token-savings or crash-recovery claim. diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index db98a927a..f7d1bbd1d 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -1,8 +1,8 @@ """Owned lifecycle for the upstream SDK-in-image recipe; no work on import. -Host actions: path/hash validation, rootless Docker CLI, fixture byte copies, -bounded process supervision and read-only gateway receipts. Candidate Python -and all SDK/MCP processes execute inside the digest-pinned image. +Host actions: frozen task checkout/patch transport, supported upstream grader +installation, owned Docker supervision and read-only gateway receipts. Candidate +code executes in worker/grader containers. No installation or execution on import. """ from __future__ import annotations @@ -12,6 +12,7 @@ import json import os from pathlib import Path +import re import shutil import signal import stat @@ -99,21 +100,50 @@ def preflight(prefix, state): forbidden = (Path("/"), Path.home(), Path("/run"), Path("/var/run"), Path.home() / ".local") if candidate.resolve() in forbidden or any(part in {".ssh", ".codex", ".claude", "credentials"} for part in candidate.parts): raise ValueError("broad_or_authentication_mount_refused") - skills = [] - for entry in read_json(HERE / "config/skills.lock.json")["skills"]: - source = Path.home() / ".agents/skills" / entry["name"] - if digest(source / "SKILL.md") != entry["skill_md_sha256"]: - raise ValueError("installed_skill_pin_mismatch:" + entry["name"]) - skills.append((entry["name"], source.resolve())) check_rootless() - return pins, host, mcp, skills + return pins, host, mcp + + +def install_workspace_skills(stack_root, workspace): + """Pending shared skills PR: no copy/symlink/global-install fallback.""" + # Vercel skills 1.7.0 add.ts:2130-2160 also creates a root local lock. + # A task's existing paths must never become installer-owned or ignored. + if any(os.path.lexists(workspace / name) for name in (".agents", "skills-lock.json")): + raise ValueError("task_conflicts_with_skill_installer_paths") + subprocess.run([ + sys.executable, str(stack_root / "tools/adoption/install_skills.py"), + "--manifest", "blueprints/runtime-workers/skills/manifest.json", + "--project-dir", str(workspace), "--agent", "universal", + ], cwd=stack_root, check=True, timeout=600) + with (workspace / ".git/info/exclude").open("a") as stream: + stream.write("\n/.agents/\n/skills-lock.json\n") + + +def workspace_skills(stack_root, workspace): + manifest = stack_root / "blueprints/runtime-workers/skills/manifest.json" + entries = read_json(manifest)["skills"] + installed = workspace / ".agents/skills" + names = [entry["name"] for entry in entries] + if len(names) != len(set(names)) or not {"tdd", "verification-before-completion"} <= set(names): + raise ValueError("runtime_skills_manifest_contract") + for entry in entries: + path = installed / entry["name"] / "SKILL.md" + if not re.fullmatch(r"[a-z0-9-]+", entry["name"]): + raise ValueError("invalid_skill_name") + if not path.resolve().is_relative_to(installed.resolve()): + raise ValueError("skills_must_be_project_local") + if digest(path) != entry["skill_md_sha256"]: + raise ValueError("installed_project_skill_pin_mismatch") + return {"names": sorted(names), "manifest_sha256": digest(manifest)} def docker_args(pins, name, *, network=True): + if not re.fullmatch(r"rw-openhands-[a-z0-9-]+", name): + raise ValueError("owned_container_name_required") limits = read_json(HERE / "config/worker.json")["runtime"] args = DOCKER + [ "run", "--rm", "--pull=never", "--name", name, "--platform", pins["image"]["platform"], - "--label", "native-agent-stack.runtime-worker=openhands", + "--label", "com.native-agent-stack.owner=gpt6-omniroute-framework-integration", "--user", "0:0", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", "--tmpfs", "/root:rw,nosuid,nodev,mode=0700", "--env", "PYTHONDONTWRITEBYTECODE=1", "--env", "UV_PYTHON_DOWNLOADS=never", @@ -137,21 +167,98 @@ def execute_container(args, name, logfile, timeout): except OSError: return 127 finally: - cleanup = {"confirmed_removed": False, "error_type": None} - try: - removed = subprocess.run(DOCKER + ["rm", "-f", name], stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, check=False, timeout=30) - if removed.returncode == 0: - cleanup["confirmed_removed"] = True - else: - # --rm usually removed it already. A successful empty listing - # establishes absence; daemon errors never count as absence. - remaining = subprocess.run(DOCKER + ["ps", "-aq", "--filter", "name=^/" + name + "$"], - capture_output=True, text=True, check=False, timeout=30) - cleanup["confirmed_removed"] = remaining.returncode == 0 and not remaining.stdout.strip() - except (OSError, subprocess.TimeoutExpired) as exc: - cleanup["error_type"] = type(exc).__name__ - write_json(logfile.with_suffix(logfile.suffix + ".cleanup.json"), cleanup) + cleanup_container(name, logfile) + + +def cleanup_container(name, logfile): + cleanup = {"confirmed_removed": False, "error_type": None} + try: + removed = subprocess.run(DOCKER + ["rm", "-f", name], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=False, timeout=30) + if removed.returncode == 0: + cleanup["confirmed_removed"] = True + else: + remaining = subprocess.run(DOCKER + ["ps", "-aq", "--filter", "name=^/" + re.escape(name) + "$"], + capture_output=True, text=True, check=False, timeout=30) + cleanup["confirmed_removed"] = remaining.returncode == 0 and not remaining.stdout.strip() + except (OSError, subprocess.TimeoutExpired) as exc: + cleanup["error_type"] = type(exc).__name__ + write_json(logfile.with_suffix(logfile.suffix + ".cleanup.json"), cleanup) + + +def logged_command(argv, logfile, *, cwd, timeout, env=None): + """Private command/exit capture; no claim that exit zero is task success.""" + record = {"argv": list(map(str, argv)), "started_at": utc_now(), "exit_code": None} + try: + with logfile.open("w") as log: + completed = subprocess.run(argv, cwd=cwd, env=env, stdin=subprocess.DEVNULL, + stdout=log, stderr=subprocess.STDOUT, + timeout=timeout, check=False) + record["exit_code"] = completed.returncode + except subprocess.TimeoutExpired: + record["exit_code"] = 124 + except OSError: + record["exit_code"] = 127 + finally: + record["finished_at"] = utc_now() + write_json(logfile.with_suffix(".command.json"), record) + return record["exit_code"] + + +def grade(prefix, result, dataset, instance_id, run_id): + """Upstream conversion, then its unchanged official Docker grading CLI.""" + from e2e.check import check_input, read_report + grader = prefix / "benchmarks" + pin = read_json(HERE / "pins.json")["grader"] + for checkout, expected in ((grader, pin["commit"]), + (grader / "vendor/software-agent-sdk", pin["sdk_submodule_commit"])): + actual = subprocess.check_output(["git", "-C", str(checkout), "rev-parse", "HEAD"], text=True, timeout=30).strip() + if actual != expected: + raise ValueError("grader_checkout_pin_mismatch") + if subprocess.check_output(["git", "-C", str(checkout), "status", "--porcelain", + "--untracked-files=no"], text=True, timeout=30).strip(): + raise ValueError("grader_checkout_modified") + source = result / "output.jsonl" + check_input(source, instance_id) + # The SDK Docker API uses DOCKER_HOST, not the CLI's --context option. + endpoint = subprocess.check_output(DOCKER + ["context", "inspect", "rootless", "--format", + "{{.Endpoints.docker.Host}}"], text=True, timeout=30).strip() + if not endpoint.startswith("unix://"): + raise ValueError("rootless_local_docker_endpoint_required") + environment = {**os.environ, "DOCKER_HOST": endpoint, "PYTHONDONTWRITEBYTECODE": "1", + "HF_HOME": str(result / "grader-cache")} + convert = [str(grader / ".venv/bin/swebench-eval"), str(source), + "--dataset", str(dataset), "--run-id", run_id, "--workers", "1", + "--no-modal", "--skip-evaluation"] + code = logged_command(convert, result / "convert.log", cwd=result, env=environment, timeout=180) + if code: + return {"upstream_resolved": None, "grader_exit_code": None, "conversion_exit_code": code} + predictions = result / "output.swebench.jsonl" + converted = [json.loads(line) for line in predictions.read_text().splitlines() if line] + if (len(converted) != 1 or converted[0].get("instance_id") != instance_id + or converted[0].get("model_name_or_path") != "OpenHands" + or not isinstance(converted[0].get("model_patch"), str)): + raise ValueError("upstream_conversion_scope_mismatch") + command = [ + str(grader / ".venv/bin/python"), str(HERE / "e2e/docker_grader.py"), + "--dataset_name", str(dataset), "--predictions_path", str(predictions), + "--instance_ids", instance_id, "--run_id", run_id, "--max_workers", "1", + "--split", "test", "--timeout", "1800", "--namespace", "swebench", + "--cache_level", "instance", "--clean", "false", "--modal", "false", + ] + # swebench TestSpec.get_instance_container_name, with our prefix only. + name = "rw-openhands-sweb.eval." + instance_id.lower() + "." + run_id + try: + code = logged_command(command, result / "grader.log", cwd=result, env=environment, timeout=2400) + finally: + cleanup_container(name, result / "grader.log") + report = result / ("OpenHands." + run_id + ".json") + try: + relayed = read_report(report, instance_id) + except (OSError, ValueError, TypeError): + relayed = {"upstream_resolved": None, "transport_error": "missing_or_malformed_output"} + return {**relayed, "grader_exit_code": code, "conversion_exit_code": 0, + "raw_prediction_sha256": digest(source), "converted_prediction_sha256": digest(predictions)} def download_verified(artifact, target): @@ -168,8 +275,8 @@ def download_verified(artifact, target): def install(prefix, state): - pins, _, _, skills = preflight(prefix, state) - for directory in (prefix, state, state / "cache", prefix / "skills"): + pins, _, _ = preflight(prefix, state) + for directory in (prefix, state, state / "cache"): private_directory(directory) # Source bytes are retained for reproducibility, not executed or patched. download_verified(pins["source_archive"], state / "cache/upstream.tar.gz") @@ -182,14 +289,7 @@ def install(prefix, state): if HERE.resolve() != (prefix / "recipe").resolve(): shutil.copytree(HERE, prefix / "recipe", dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) - for name, source in skills: - link = prefix / "skills" / name - if link.is_symlink() and link.resolve() == source: - continue - if link.exists() or link.is_symlink(): - raise ValueError("existing_skill_reference_conflict") - link.symlink_to(source, target_is_directory=True) - name = "openhands-install-" + uuid.uuid4().hex[:12] + name = "rw-openhands-install-" + uuid.uuid4().hex[:12] args = docker_args(pins, name) args += mount(prefix, prefix, False) + mount(state, "/state", False) + mount(prefix / "recipe", "/recipe") args += ["--entrypoint", "/bin/bash", pins["image"]["ref"], "/recipe/install-container.sh", str(prefix)] @@ -200,99 +300,144 @@ def install(prefix, state): "requirements_sha256": pins["requirements_sha256"], "exit_code": code}) if code: raise RuntimeError("container_install_failed_see_private_install_log") - print("Installed pinned SDK wheels in owned container venv; model/MCP/E2E acceptance remains pending.") + subprocess.run(["bash", str(HERE / "install-grader.sh"), str(prefix / "benchmarks"), + str(state / "cache")], check=True, timeout=1200) + print("Installed SDK and pinned upstream grader; model/MCP/E2E acceptance remains pending.") -def runtime_mounts(host, skills): +def runtime_mounts(host): args = [] for source in host["mcp_readonly_mounts"]: args += mount(Path(source).resolve(), source) for name, source in host["qmd_collections"].items(): args += mount(Path(source).resolve(), "/documents/" + name) - for name, source in skills: - args += mount(source, "/skills/" + name) return args def run(prefix, state): - pins, host, mcp, skills = preflight(prefix, state) + from e2e.task import load_task, worker_instruction + from recipe import llm_config + pins, host, mcp = preflight(prefix, state) installed = read_json(state / "installation.json") if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: raise ValueError("matching_successful_installation_required") - # The venv's interpreter link resolves inside the image, not on the host. if not os.path.lexists(prefix / "venv/bin/python"): raise ValueError("owned_sdk_venv_missing") cfg = read_json(HERE / "config/worker.json") + selected_model = os.environ.get(cfg["model_env"], cfg["llm"]["model"]) + llm_config(cfg, selected_model) private_directory(state / "runs") result = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "runs")) for directory in (result / "input", result / "worker", result / "mcp"): private_directory(directory) - for name in mcp: - for child in ("cache", "config", "data", "state"): - private_directory(result / "mcp" / name / child) - serena_home = result / "mcp/serena/home" - private_directory(serena_home) - shutil.copyfile(HERE / "config/serena_config.yml", serena_home / "serena_config.yml") - shutil.copytree(HERE / "e2e/fixture-repo", result / "workspace") - write_json(result / "input/mcp.json", mcp) - runtime = cfg["runtime"] - stem = "openhands-e2e-" + uuid.uuid4().hex[:12] - window = {"started_at": utc_now(), "finished_at": None, "worker_exit_code": None} - stage = "baseline" + window = {"started_at": utc_now(), "finished_at": None, "worker_exit_code": None, "model": selected_model} + checked = {"upstream_resolved": None, "grader_exit_code": None} + stem = "rw-openhands-e2e-" + uuid.uuid4().hex[:12] + stage = "prepare" try: - name = stem + "-baseline" - args = docker_args(pins, name, network=False) + mount(prefix, prefix) + mount(HERE, "/recipe") - args += mount(result / "workspace", "/workspace") - args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], - "-I", "-B", "/recipe/e2e/check.py", "--baseline", "/workspace"] - code = execute_container(args, name, result / "baseline.log", 60) - baseline = json.loads((result / "baseline.log").read_text().splitlines()[-1]) - write_json(result / "baseline.json", baseline) - if code != 1 or baseline.get("tests_run") != 1 or baseline.get("failures") != 1 or baseline.get("errors") != 0: - raise ValueError("frozen_baseline_not_red") - base = mount(prefix, prefix) + mount(HERE, "/recipe") + mount(result / "input", "/run-input") - base += mount(result / "mcp", "/state/mcp", False) + mount(result / "workspace", "/workspace", False) - base += runtime_mounts(host, skills) + original = Path(os.environ["OPENHANDS_TASK_FILE"]).resolve() + task_sha = os.environ["OPENHANDS_TASK_SHA256"] + task = load_task(original, task_sha) + window.update(instance_id=task["instance_id"], run_id=stem) + # Frozen oracle bytes are outside every worker mount. + dataset = result / ("dataset" + original.suffix) + shutil.copyfile(original, dataset) + load_task(dataset, task_sha) + write_json(result / "task-identity.json", { + "instance_id": task["instance_id"], "repo": task["repo"], + "base_commit": task["base_commit"], "dataset_sha256": task_sha, + }) + workspace = result / "workspace" + code = logged_command(["git", "clone", "--no-checkout", "--single-branch", "--", "https://github.com/" + task["repo"] + ".git", + str(workspace)], result / "clone.log", cwd=result, timeout=300) + if code: + raise RuntimeError("task_clone_failed") + code = logged_command(["git", "-C", str(workspace), "reset", "--hard", task["base_commit"]], + result / "checkout.log", cwd=result, timeout=120) + if code: + raise RuntimeError("task_checkout_failed") + # SWE-bench 4.1.0 test_spec/python.py:274-292 removes future refs and + # reflogs before exposing the checkout. This bare-checkout adaptation + # drops every tag (not only newer tags) and checks surviving refs below. + code = logged_command(["git", "-C", str(workspace), "remote", "remove", "origin"], + result / "remove-remote.log", cwd=result, timeout=30) + if code: + raise RuntimeError("task_history_isolation_failed") + tags = subprocess.check_output(["git", "-C", str(workspace), "tag", "--list"], text=True, timeout=30).splitlines() + for tag in tags: + subprocess.run(["git", "-C", str(workspace), "update-ref", "-d", "refs/tags/" + tag], + check=True, timeout=30, stdin=subprocess.DEVNULL) + for phase, argv in (("reflog", ["reflog", "expire", "--expire=now", "--all"]), + ("gc", ["gc", "--prune=now"])): + if logged_command(["git", "-C", str(workspace), *argv], result / (phase + ".log"), + cwd=result, timeout=300): + raise RuntimeError("task_history_isolation_failed") + refs = subprocess.check_output(["git", "-C", str(workspace), "for-each-ref", "--format=%(objectname)"], + text=True, timeout=30).splitlines() + if not refs or any(ref != task["base_commit"] for ref in refs): + raise ValueError("unexpected_task_reference") + stage = "skills" + stack_root = Path(os.environ.get("OPENHANDS_STACK_ROOT", str(HERE.parents[2]))).resolve() + if not (stack_root / "blueprints/runtime-workers/skills/manifest.json").is_file(): + raise ValueError("skills_program_pending_pr") + install_workspace_skills(stack_root, workspace) + skills = workspace_skills(stack_root, workspace) + write_json(result / "input/skills.json", skills) + write_json(result / "input/mcp.json", mcp) + (result / "input/task.txt").write_text(worker_instruction(task)) + for name in mcp: + for child in ("cache", "config", "data", "state"): + private_directory(result / "mcp" / name / child) + serena_home = result / "mcp/serena/home" + private_directory(serena_home) + shutil.copyfile(HERE / "config/serena_config.yml", serena_home / "serena_config.yml") + # Only the worker venv, not the grader checkout/environment, is mounted. + base = mount(prefix / "venv", prefix / "venv") + mount(HERE, "/recipe") + base += mount(result / "input", "/run-input") + mount(result / "mcp", "/state/mcp", False) + base += mount(workspace, "/workspace", False) + mount(workspace / ".git", "/workspace/.git") + base += mount(workspace / ".agents", "/workspace/.agents") + runtime_mounts(host) + if (workspace / "skills-lock.json").is_file(): + base += mount(workspace / "skills-lock.json", "/workspace/skills-lock.json") stage = "qmd_setup" - name = stem + "-qmd" - args = docker_args(pins, name) + base + args = docker_args(pins, stem + "-qmd") + base args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/qmd-setup.py"] - if execute_container(args, name, result / "qmd-setup.log", 900) != 0: + if execute_container(args, stem + "-qmd", result / "qmd-setup.log", 900): raise RuntimeError("qmd_setup_failed") stage = "agent" - name = stem + "-agent" - args = docker_args(pins, name) + base + mount(result / "worker", "/run-output", False) + args = docker_args(pins, stem + "-agent") + base + mount(result / "worker", "/run-output", False) args += ["--workdir", "/workspace", "--env", "OPENHANDS_OWNED_CONTAINER=1", "--env", f"PATH={prefix}/venv/bin:" + host["variables"]["HOST_PATH"], "--env", "XDG_CACHE_HOME=/run-output/cache", "--env", "XDG_STATE_HOME=/run-output/state", - "--env", "XDG_DATA_HOME=/run-output/data", "--env", "XDG_CONFIG_HOME=/run-output/config"] - selected_model = os.environ.get(cfg["model_env"], cfg["llm"]["model"]) - # Validate before forwarding. No API credential environment is forwarded. - from recipe import llm_config - llm_config(cfg, selected_model) - window["model"] = selected_model - args += ["--env", cfg["model_env"] + "=" + selected_model, + "--env", "XDG_DATA_HOME=/run-output/data", "--env", "XDG_CONFIG_HOME=/run-output/config", + "--env", cfg["model_env"] + "=" + selected_model, "--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/worker.py"] window["started_at"] = utc_now() - window["worker_exit_code"] = execute_container(args, name, result / "worker.log", runtime["timeout_seconds"]) + window["worker_exit_code"] = execute_container(args, stem + "-agent", result / "worker.log", + cfg["runtime"]["timeout_seconds"]) + window["finished_at"] = utc_now() + if window["worker_exit_code"]: + raise RuntimeError("worker_failed") + stage = "export" + # Native benchmarks/run_infer.py get_patch uses git add -A then cached + # diff for unfinished/uncommitted work. No candidate Python runs here. + code = logged_command(["git", "-C", str(workspace), "add", "-A"], + result / "stage-patch.log", cwd=result, timeout=60) + if code: + raise RuntimeError("patch_export_failed") + patch_text = subprocess.check_output( + ["git", "-C", str(workspace), "--no-pager", "diff", "--no-color", "--cached", task["base_commit"]], + text=True, timeout=60) + (result / "output.jsonl").write_text(json.dumps({ + "instance_id": task["instance_id"], "test_result": {"git_patch": patch_text}, "error": None, + }) + "\n") + stage = "grader" + checked = grade(prefix, result, dataset, task["instance_id"], stem) except (Exception, KeyboardInterrupt) as exc: - # Retain failed attempts, but do not put private exception text in receipts. window["failure_stage"] = stage window["failure_type"] = type(exc).__name__ finally: - window["finished_at"] = utc_now() + window["finished_at"] = window["finished_at"] or utc_now() write_json(result / "window.json", window) - name = stem + "-check" - args = docker_args(pins, name, network=False) + mount(prefix, prefix) + mount(HERE, "/recipe") - args += mount(result, "/result") - args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], - "-I", "-B", "/recipe/e2e/check.py", "--result", "/result"] - check_code = execute_container(args, name, result / "check.log", 90) - try: - checked = json.loads((result / "check.log").read_text().splitlines()[-1]) - except (ValueError, IndexError): - checked = {"passed": False, "failures": ["checker_did_not_return_json"]} - checked["exit_code"] = check_code write_json(result / "check.json", checked) from receipt import create_receipt receipt = create_receipt(result) diff --git a/blueprints/runtime-workers/openhands/install-grader.sh b/blueprints/runtime-workers/openhands/install-grader.sh new file mode 100644 index 000000000..7f36bdcc1 --- /dev/null +++ b/blueprints/runtime-workers/openhands/install-grader.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# OpenHands/benchmarks 405bae7 README + Makefile: supported make build path. +# This is a coordinator host recipe; never invoked by offline contract tests. +set -euo pipefail +umask 077 +grader_dir="$1" +cache_dir="$2" +benchmark_pin=405bae7140d7e961a75f4910a0b2e7069731db96 +sdk_pin=43376f1868ffd702746080714a59c16d3f69ec12 +export PYTHONDONTWRITEBYTECODE=1 +export UV_CACHE_DIR="$cache_dir/grader-uv" +export PRE_COMMIT_HOME="$cache_dir/grader-pre-commit" +export XDG_CACHE_HOME="$cache_dir/grader" +if [[ ! -d "$grader_dir" ]]; then + git clone --no-checkout https://github.com/OpenHands/benchmarks.git "$grader_dir" + git -C "$grader_dir" checkout --detach "$benchmark_pin" +fi +[[ "$(git -C "$grader_dir" rev-parse HEAD)" == "$benchmark_pin" ]] +[[ -z "$(git -C "$grader_dir" status --porcelain --untracked-files=no)" ]] +git -C "$grader_dir" submodule update --init --recursive +[[ "$(git -C "$grader_dir/vendor/software-agent-sdk" rev-parse HEAD)" == "$sdk_pin" ]] +# Makefile runs uv sync --dev against its workspace/submodule and installs its +# own development hooks in this isolated checkout, never in native-agent-stack. +make -C "$grader_dir" build +git -C "$grader_dir" diff --exit-code -- uv.lock pyproject.toml +[[ "$(git -C "$grader_dir/vendor/software-agent-sdk" rev-parse HEAD)" == "$sdk_pin" ]] +"$grader_dir/.venv/bin/python" -c 'from importlib.metadata import version; assert version("swebench") == "4.1.0"; print("swebench==4.1.0 installed; grader not executed")' diff --git a/blueprints/runtime-workers/openhands/install.sh b/blueprints/runtime-workers/openhands/install.sh index bb0169e13..c407ba975 100755 --- a/blueprints/runtime-workers/openhands/install.sh +++ b/blueprints/runtime-workers/openhands/install.sh @@ -6,5 +6,7 @@ recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" # host.py checks rootless Docker, source/requirement sha256, private host values -# and skill hashes before downloading or installing. No sudo, no service changes. +# before downloading or installing. Project skills use the shared installer +# when a task workspace is created. No sudo, no service changes. +export PYTHONDONTWRITEBYTECODE=1 exec python3 "$recipe_dir/host.py" install --prefix "$prefix" --state "$state" diff --git a/blueprints/runtime-workers/openhands/pins.json b/blueprints/runtime-workers/openhands/pins.json index fa698d788..d53f742ef 100644 --- a/blueprints/runtime-workers/openhands/pins.json +++ b/blueprints/runtime-workers/openhands/pins.json @@ -34,6 +34,20 @@ }, "requirements_sha256": "c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56", "build_requirements_sha256": "4485df6339240d8551488bb18f27e644e8bd55d7d50bf439ca9d9da03ab7318a", + "grader": { + "repository": "https://github.com/OpenHands/benchmarks", + "commit": "405bae7140d7e961a75f4910a0b2e7069731db96", + "sdk_submodule_commit": "43376f1868ffd702746080714a59c16d3f69ec12", + "swebench_version": "4.1.0", + "swebench_wheel_sha256": "1243776f720047cc9e20a427f7a52b75c13a07abda6154fb60fe77f82ec8af57", + "installation": "make build", + "verdict": "swebench.harness.run_evaluation official Docker tests and resolved_ids", + "runner_up": { + "repository": "https://github.com/harbor-framework/harbor", + "tag": "v0.23.0", + "reason": "Terminal-Bench uses a different task contract." + } + }, "canvas": { "tag": "v1.24.0", "repository": "https://github.com/OpenHands/OpenHands", diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 32250c661..b45f220f0 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -15,6 +15,7 @@ import sqlite3 from recipe import HERE, read_json, tool_filter +from e2e.check import read_report COLUMNS = ( @@ -76,11 +77,11 @@ def sanitize_row(row): return output -def observations(events): +def observations(events, known_skills=()): """A call counts as observed only with a native ObservationEvent.""" policy = read_json(HERE / "config/mcp-policy.json") pattern = re.compile(tool_filter(policy)) - known_skills = {s["name"] for s in read_json(HERE / "config/skills.lock.json")["skills"]} + known_skills = set(known_skills) completed, failed, skills = Counter(), Counter(), Counter() for event in events: if event.get("kind") != "ObservationEvent": @@ -130,15 +131,28 @@ def create_receipt(result, database=None): versions = {name: candidate[name] for name in ("openhands-sdk", "openhands-tools")} except (OSError, ValueError, TypeError): pass - tests = checked.get("tests") or {} - # Never copy arbitrary checker output, file paths or error strings into public evidence. - allowed_failures = { - "missing_result", "frozen_fixture_changed", "diff_outside_allowed_files_or_empty", "nonregular_result", - "baseline_not_one_failing_test", "no_native_test_failure_before_edit", "tests_mutated_workspace", - "tests_not_passing_unskipped", "invalid_or_incomplete_result", "checker_did_not_return_json", - } - task_passed = checked.get("passed") is True and checked.get("exit_code") == 0 and window.get("worker_exit_code") == 0 - observed = observations(events) + # Re-read the actual upstream report; a wrapper's own passed field is never + # a task oracle. The official grader determines all resolved/unresolved IDs. + verdict = {"upstream_resolved": None, "upstream_bucket": None, "report_sha256": None} + run_id, instance_id = window.get("run_id"), window.get("instance_id") + if isinstance(run_id, str) and re.fullmatch(r"rw-openhands-[a-z0-9-]+", run_id) and isinstance(instance_id, str): + try: + verdict = read_report(result / ("OpenHands." + run_id + ".json"), instance_id) + except (OSError, ValueError, TypeError): + pass + task_passed = (verdict["upstream_resolved"] is True and checked.get("grader_exit_code") == 0 + and checked.get("conversion_exit_code") == 0 and window.get("worker_exit_code") == 0) + listed_skills, skills_match = [], False + try: + expected = read_json(result / "input/skills.json")["names"] + listed = read_json(result / "worker/skills-startup.json")["listed_skills"] + if (isinstance(expected, list) and isinstance(listed, list) and expected + and all(isinstance(name, str) and re.fullmatch(r"[a-z0-9-]+", name) for name in expected) + and sorted(expected) == sorted(listed)): + listed_skills, skills_match = sorted(listed), True + except (OSError, ValueError, TypeError, KeyError): + pass + observed = observations(events, listed_skills) expected_model = window.get("model") matching_gateway_rows = [row for row in rows if ( row["model"] == expected_model and row["path"] == "/v1/responses" @@ -151,15 +165,19 @@ def create_receipt(result, database=None): except (OSError, ValueError, AttributeError): cleanup.append(False) return { - "schema_version": 1, "evidence_class": "local integration on a synthetic fixture", + "schema_version": 2, "evidence_class": "standalone SDK inference adapter with official SWE-bench grading", "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], "observed_versions": versions, "image": pins["image"]["ref"], "source_commit": pins["commit"], "window": {key: window[key] for key in ("started_at", "finished_at")}, "worker_exit_code": window.get("worker_exit_code"), - "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"baseline", "qmd_setup", "agent"} else None, + "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"prepare", "skills", "qmd_setup", "agent", "export", "grader"} else None, "task_passed": task_passed, "evidence_complete": bool(matching_gateway_rows) and trace_status == "observed" - and versions is not None and observed["skills_observed"].get("tdd", 0) > 0 and bool(cleanup) and all(cleanup), + and versions is not None and skills_match + and all(observed["skills_observed"].get(name, 0) > 0 for name in ("tdd", "verification-before-completion")) + and checked.get("grader_exit_code") == 0 and verdict["upstream_resolved"] is not None + and bool(cleanup) and all(cleanup), + "skills_listed_at_start": listed_skills, "skill_listing_matches_manifest": skills_match, "container_cleanup": {"attempts": len(cleanup), "confirmed_removed": sum(cleanup), "complete": bool(cleanup) and all(cleanup)}, "gateway": { "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, "rows": rows, @@ -169,12 +187,14 @@ def create_receipt(result, database=None): "usage_note": "Rows are not summed or claimed as exclusive worker cost. Cache-read is a subset of input; missing counters remain null.", }, "trace_status": trace_status, **observed, - "check": { - "passed": checked.get("passed") is True, - "exit_code": checked.get("exit_code"), - "failures": [reason if reason in allowed_failures else "invalid_or_incomplete_result" for reason in checked.get("failures", [])], - "allowed_file_changed": "range_utils.py" in checked.get("changed_files", []), - "tests": {key: tests.get(key) for key in ("tests_run", "failures", "errors", "skipped", "exit_code")}, + "upstream_grader": { + **{key: verdict.get(key) for key in ("upstream_resolved", "upstream_bucket", "report_sha256")}, + "benchmark_commit": pins["grader"]["commit"], + "sdk_submodule_commit": pins["grader"]["sdk_submodule_commit"], + "swebench_version": pins["grader"]["swebench_version"], + "grader_exit_code": checked.get("grader_exit_code"), + "conversion_exit_code": checked.get("conversion_exit_code"), }, - "limits": ["Not an upstream SDK test suite", "No matched token-savings comparison", "No SDK cold-start/crash-resume acceptance"], + "limits": ["Not unchanged benchmark inference or an upstream SDK test suite", + "No matched A/B or token-savings comparison", "No SDK cold-start/crash-resume acceptance"], } diff --git a/blueprints/runtime-workers/openhands/recipe.py b/blueprints/runtime-workers/openhands/recipe.py index 1b40e1564..5f5ee93a6 100644 --- a/blueprints/runtime-workers/openhands/recipe.py +++ b/blueprints/runtime-workers/openhands/recipe.py @@ -72,8 +72,12 @@ def expand(value): def llm_config(config, model=None): result = dict(config["llm"]) selected = model or result["model"] - if not isinstance(selected, str) or not re.fullmatch(r"[A-Za-z0-9_./:-]+", selected): - raise ValueError("invalid_model") + if not isinstance(selected, str) or not re.fullmatch(r"cx/gpt-6(?:-[a-z0-9]+)*", selected): + raise ValueError("gateway_requires_gpt6_route") + if result.get("temperature") is not None and result["temperature"] <= 0.1: + raise ValueError("gateway_temperature_must_exceed_0_1_or_be_omitted") + if result.get("response_format") is not None or result.get("native_tool_calling") is not True: + raise ValueError("use_native_tool_calling_for_structured_output") # LiteLLM strips its openai provider prefix; the gateway receives selected. result["model"] = "openai/" + selected result["base_url"] = config["runtime"]["gateway_base_url"] diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index c93594fce..cf50afa53 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -1,5 +1,40 @@ # Source review, 2026-09-27 +## Round 2 research before changes + +The supplied eval-framework reports select **OpenHands/benchmarks** at +`405bae7140d7e961a75f4910a0b2e7069731db96`, its SDK submodule at +`43376f1868ffd702746080714a59c16d3f69ec12`, and `swebench==4.1.0`. +The documented source install is recursive submodule initialization followed by +`make build` (its Makefile runs `uv sync --dev`). The source of truth for the verdict is SWE-bench's +Docker evaluation, reached through `swebench-eval`; its process exit alone is +not a task verdict. Sources: +[installation](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/README.md), +[benchmark commands](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/benchmarks/swebench/README.md), +[conversion and report handling](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/benchmarks/swebench/eval_infer.py). +Harbor `v0.23.0` is the runner-up; its Terminal-Bench task contract differs from +this coding-worker SWE-bench contract. Popularity was discovery information only. + +Preflight found neither an installed OpenHands SDK nor SWE-bench in the current +interpreter; no executable or native runtime claim is made. The SDK `v1.49.6` +release notes and pinned source were re-read. Public web-open was unavailable; +public source fetching through context-mode worked. The skills.sh leaderboard +was reachable and listed TDD and verification; no install was performed. +The installed search-first/find-skills procedures informed discovery. Source +review confirms the SDK's condenser returns plain text, while agent actions use +native tool calling; no JSON-mode response format is needed. + +The round-2 contract supplies resource ownership, GPT-6-only routing, gateway +JSON restrictions and the pending shared skills installer interface. Supporting +SDK sources are `llm/llm.py:1580-1642`, `llm/options/responses_options.py`, +`context/condenser/llm_summarizing_condenser.py:224-229,420-423`, +`skills/skill.py:848-932`, and `tool/builtins/invoke_skill.py:38-123`, all at +`fcc102a697874d54a357e36004e02c95040dbdc0`. +Only recipe-local adapters/contract tests change; upstream graders and tests +remain upstream. Research and returned test logs stay outside the checkout +until sanitized evidence is retained. No host install, service or model run is +authorized in this build. + This recipe is a source-backed integration proposal, not an adopted runtime or a new model run. The user selected OpenHands for a bounded coding-worker trial. The existing foundation catalog still describes SDK alternatives as unselected; @@ -55,3 +90,55 @@ private filesystem prefixes replaced. This is local structural evidence. These corrections are kept here because this builder owns only this recipe and its unit test. The coordinator can promote general lessons to the shared docs/harness-defaults.md anti-pattern log. No shared file was edited. + +## Round 2 integration decisions and anti-pattern log + +| Proven issue / source finding | Correction and evidence boundary | +| --- | --- | +| Round-1 check.py computed task correctness using a local synthetic fixture. The round-2 adapter tests first failed with five failures and two errors. | Replace it with shape validation and a relay of the official SWE-bench report; known-pass/fail/malformed controls remain synthetic adapter checks, never an upstream run. | +| A local passed:true field alone was accepted as a task verdict. The new receipt control failed on that behavior. | receipt.py re-reads the official OpenHands.RUN_ID.json report and requires successful conversion/grader processes plus an upstream-resolved ID. | +| Existing resource labels and arbitrary model overrides did not meet the new contract; the new controls produced eight failures. | Require the exact owner label and rw-openhands prefix, retain zero published ports, reject non-GPT-6 routes and unsafe configured output/sampling modes. | +| Native SWE-bench creation supplies sweb.eval names and no labels; native Docker grading has no label/name override argument. | e2e/docker_grader.py adapts only ContainerCollection.create. Official tests/grading remain unchanged. No new volumes/networks/build intermediates are allowed. Actual Docker acceptance is pending. | +| The official converter strips setup-file changes and can skip malformed rows. | Prevalidate one exact instance; retain original and converted patches. No local replacement converter or test verdict is added. | +| The benchmark SDK's DockerWorkspace runs a remote conversation and cannot carry a host LLM subclass into the agent-server process. | Retain the standalone 1.49.6 worker inside its image; keep the report-pinned SDK submodule isolated in the grading environment. Describe inference as an adaptation, not unchanged swebench-infer. | +| The proposed GatewayLLM subclass bypassed SDK 1.49.6 native registration: AgentBase.get_all_llms uses type(obj) is LLM. LocalConversation uses that registry for aggregate metrics and condenser context binding. | Keep exact native LLM objects and scope header wrapping to their public generate/agenerate methods. The regression control failed first on the missing transport context; it covers sync/async calls, stable affinity, fresh keys and restoration. This is synthetic compatibility evidence, not native accounting acceptance. | +| A plain clone exposes newer refs. SWE-bench 4.1.0 test_spec/python.py:274-292 removes the remote/newer tags, expires reflogs and prunes unreachable objects. | Follow that upstream history boundary before the worker sees its checkout. This adaptation removes all tags and confirms surviving refs equal base_commit; task dependency/Conda setup still needs host acceptance. | +| Project skills options/manifest are absent on this branch. Earlier manual links bypassed the newly required lifecycle. | Call only tools/adoption/install_skills.py with the runtime-worker manifest, --project-dir and --agent universal. Mark pending the coordinator's skills PR; no fallback. The invocation control failed first with the missing helper. | +| Vercel skills 1.7.0 project installation also writes a root skills-lock.json. Only excluding .agents would leak installer bookkeeping into the graded patch. | Reserve both paths before installation, use root-anchored Git exclusions and read-only mounts. The added controls failed first with three failures, then verify Git's actual ignore behavior and that conflicts refuse installation. | +| The SDK skill implementation was initially looked up at a guessed tools/skill path and returned 404. | The existing recipe citation resolves it to sdk/tool/builtins/invoke_skill.py at fcc102a; a failed URL is not capability absence. | +| Publication scanning interpreted slash-separated discovery-scope prose as a personal home path, including its first correction-log quotation. | Spell the three scopes as words in both documentation and the correction log. Do not weaken the shared scanner. Shared evidence hashes still require coordinator re-registration. | +| The recipe-only cache check missed four ignored scripts bytecode files elsewhere in the checkout; their generating process was not observed. | The final sweep covers the whole checkout. Remove only the identified generated files and empty directory; independently confirm sys.dont_write_bytecode and repeat the sweep. No source file outside the assigned paths was changed. | + +Additional exact sources: SWE-bench 4.1.0 +[repository setup](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/test_spec/python.py#L274-L292), +[Docker creation](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/docker_build.py#L516), +[report schema](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/reporting.py#L127-L142), +[local dataset loader](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/utils.py#L133-L177). +The 4.1.0 wheel hash in pins.json was verified against returned wheel bytes; +six inspected grading/resource source files matched the v4.1.0 tag. +No downloaded package was installed or executed. + +The SDK's tool serializer sets strict:false for function tools at +tool/tool.py:776-804. The condenser emits plain text. Thus this selected +structured-output path needs neither json_object nor a strict JSON schema. +Actual gateway acceptance remains unmeasured. Header merging at +llm/options/common.py:26-44 preserves the caller's extra_headers. +The registration correction follows +[AgentBase.get_all_llms](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/agent/base.py#L739-L775) +and [LocalConversation registration](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py#L1566-L1579). +Both exact source sections were independently re-read after the researcher's +finding; the native SDK was not installed or executed. + +The final skills transport review also inspected Vercel skills v1.7.0 at +7407f3893ad4dceab546ac002c3ef806e4000c73: +[universal placement](https://github.com/vercel-labs/skills/blob/7407f3893ad4dceab546ac002c3ef806e4000c73/src/agents.ts#L815-L822), +[project install bookkeeping](https://github.com/vercel-labs/skills/blob/7407f3893ad4dceab546ac002c3ef806e4000c73/src/add.ts#L2130-L2160), +and [local lock writer](https://github.com/vercel-labs/skills/blob/7407f3893ad4dceab546ac002c3ef806e4000c73/src/local-lock.ts#L65-L122). +A project install writes skills-lock.json at the workspace root in addition +to .agents/skills. Excluding only the skill directory would submit installer +bookkeeping to the grader. The adapter must reserve and exclude both root paths; +preexisting task files at those paths are a conflict, not installer ownership. +The pending shared installer PR must still confirm its exact output contract. + +These new corrections remain recipe-local under the assigned file ownership; +the coordinator can promote general entries into docs/harness-defaults.md. diff --git a/blueprints/runtime-workers/openhands/run-e2e.sh b/blueprints/runtime-workers/openhands/run-e2e.sh index 1b3a0bc02..f0e70c927 100755 --- a/blueprints/runtime-workers/openhands/run-e2e.sh +++ b/blueprints/runtime-workers/openhands/run-e2e.sh @@ -2,6 +2,7 @@ # Real execution is explicitly deferred to the coordinator; no auto-install. set -euo pipefail umask 077 +export PYTHONDONTWRITEBYTECODE=1 recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" diff --git a/blueprints/runtime-workers/openhands/worker.py b/blueprints/runtime-workers/openhands/worker.py index 0b826f25c..cb8d32bc7 100644 --- a/blueprints/runtime-workers/openhands/worker.py +++ b/blueprints/runtime-workers/openhands/worker.py @@ -8,6 +8,8 @@ from __future__ import annotations import importlib.metadata +from contextlib import contextmanager +from functools import wraps import json import os from pathlib import Path @@ -15,14 +17,49 @@ import sys import uuid -from recipe import HERE, inventory, llm_config, read_json, tool_filter +from recipe import HERE, llm_config, read_json, tool_filter def headers_for_call(static_headers): return {**(static_headers or {}), "Idempotency-Key": str(uuid.uuid4())} -def main(): +@contextmanager +def gateway_transport(llm_type): + """Scope header transport to this single worker process, preserving LLM type. + + SDK agent/base.py:739-775 registers only exact LLM objects. A subclass skips + native usage/context binding. Keep the objects native and restore methods + after the conversation. Upstream retries remain within the original method. + """ + original_generate, original_agenerate = llm_type.generate, llm_type.agenerate + + def call_kwargs(llm, kwargs): + if kwargs.get("response_format") is not None or kwargs.get("text") is not None: + raise ValueError("use_native_tool_calling_for_structured_output") + if kwargs.get("temperature") is not None and kwargs["temperature"] <= 0.1: + raise ValueError("gateway_temperature_must_exceed_0_1_or_be_omitted") + static = llm.extra_headers or {} + if not static.get("x-omniroute-session"): + raise ValueError("conversation_affinity_required") + return {**kwargs, "extra_headers": headers_for_call({**(kwargs.get("extra_headers") or {}), **static})} + + @wraps(original_generate) + def generate(llm, *args, **kwargs): + return original_generate(llm, *args, **call_kwargs(llm, kwargs)) + + @wraps(original_agenerate) + async def agenerate(llm, *args, **kwargs): + return await original_agenerate(llm, *args, **call_kwargs(llm, kwargs)) + + llm_type.generate, llm_type.agenerate = generate, agenerate + try: + yield + finally: + llm_type.generate, llm_type.agenerate = original_generate, original_agenerate + + +def run_worker(): os.umask(0o077) if not Path("/.dockerenv").exists() or os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": raise RuntimeError("worker_requires_owned_container") @@ -35,24 +72,13 @@ def main(): from openhands.tools.file_editor import FileEditorTool from openhands.tools.terminal import TerminalTool - class GatewayLLM(LLM): - """Per logical SDK call, including condenser calls; retries reuse the key.""" - - def generate(self, *args, **kwargs): - kwargs["extra_headers"] = headers_for_call(self.extra_headers) - return super().generate(*args, **kwargs) - - async def agenerate(self, *args, **kwargs): - kwargs["extra_headers"] = headers_for_call(self.extra_headers) - return await super().agenerate(*args, **kwargs) - cfg = read_json(HERE / "config/worker.json") policy = read_json(HERE / "config/mcp-policy.json") pins = read_json(HERE / "pins.json") versions = {name: importlib.metadata.version(name) for name in ("openhands-sdk", "openhands-tools")} if any(version != pins["version"] for version in versions.values()): raise RuntimeError("installed_version_mismatch") - llm = GatewayLLM( + llm = LLM( **llm_config(cfg, os.environ.get(cfg["model_env"])), usage_id="agent", extra_headers={"x-omniroute-session": str(uuid.uuid4())}, @@ -61,8 +87,8 @@ async def agenerate(self, *args, **kwargs): condenser = LLMSummarizingCondenser( llm=llm.model_copy(update={"usage_id": "condenser"}), **condenser_fields, ) - _, _, skills = load_skills_from_dir("/skills") - expected = {entry["name"] for entry in read_json(HERE / "config/skills.lock.json")["skills"]} + _, _, skills = load_skills_from_dir("/workspace/.agents/skills") + expected = set(read_json("/run-input/skills.json")["names"]) if set(skills) != expected: raise RuntimeError("skill_discovery_mismatch") context = AgentContext( @@ -97,7 +123,7 @@ async def agenerate(self, *args, **kwargs): def record_event(event): # Native event fields, not claimed tool use inferred from a prompt/config. # Full upstream events remain in private SDK persistence. Export only the - # fields needed for file-scope/test ordering and tool/skill observations. + # fields needed for native tool/skill observations. kind = type(event).__name__ selected = {} for key in ("tool_name", "tool_call_id", "action", "observation"): @@ -105,23 +131,29 @@ def record_event(event): if value is not None: selected[key] = value.model_dump(mode="json") if hasattr(value, "model_dump") else value if selected: - selected.update({"kind": kind, "workspace": inventory("/workspace")}) + selected.update({"kind": kind}) with (output / "events.jsonl").open("a") as stream: stream.write(json.dumps(selected) + "\n") + # Discovery is recorded before the first call; activation still requires + # the SDK's successful InvokeSkillObservation in its own trace. + (output / "skills-startup.json").write_text(json.dumps({ + "listed_skills": sorted(skills), "loader": "openhands.sdk.skills.load_skills_from_dir", + }) + "\n") conversation = Conversation( agent=agent, workspace="/workspace", persistence_dir="/run-output/conversations", callbacks=[record_event], token_callbacks=[lambda *_: None], hook_config=hooks, max_iteration_per_run=cfg["runtime"]["max_iteration_per_run"], visualizer=None, ) try: - conversation.send_message((HERE / "e2e/task.txt").read_text()) + conversation.send_message(Path("/run-input/task.txt").read_text()) conversation.run() state = conversation.state summary = { "versions": versions, "execution_status": str(state.execution_status), "invoked_skills": list(state.invoked_skills), + "listed_skills": sorted(skills), "native_metrics": conversation.conversation_stats.get_combined_metrics().model_dump(mode="json"), } (output / "native-summary.json").write_text(json.dumps(summary, indent=2) + "\n") @@ -130,5 +162,13 @@ def record_event(event): return 0 +def main(): + if not Path("/.dockerenv").exists() or os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": + raise RuntimeError("worker_requires_owned_container") + from openhands.sdk import LLM + with gateway_transport(LLM): + return run_worker() + + if __name__ == "__main__": raise SystemExit(main()) diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index bda55a82f..80f7fa663 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -6,6 +6,7 @@ structural/fixture checks, not upstream SDK or live gateway acceptance. """ +import asyncio import json import importlib.util from pathlib import Path @@ -110,19 +111,6 @@ def test_no_public_binds_or_secret_material(self): self.assertNotIn("BEGIN PRIVATE KEY", text) self.assertIsNone(re.search(r"sk-[A-Za-z0-9]{20,}", text)) - def test_checker_rejects_an_empty_result(self): - checker = RECIPE / "e2e/check.py" - self.assertTrue(checker.is_file()) - with tempfile.TemporaryDirectory() as tmp: - result = subprocess.run( - [sys.executable, str(checker), "--result", tmp], - capture_output=True, text=True, check=False, - ) - self.assertEqual(result.returncode, 1, result.stderr) - report = json.loads(result.stdout) - self.assertFalse(report["passed"]) - self.assertIn("missing_result", report["failures"]) - def test_fresh_call_headers_keep_session_affinity(self): worker = load_recipe_module("worker.py") original = {"x-omniroute-session": "fixture-session"} @@ -132,10 +120,40 @@ def test_fresh_call_headers_keep_session_affinity(self): self.assertEqual(first["x-omniroute-session"], second["x-omniroute-session"]) self.assertEqual(original, {"x-omniroute-session": "fixture-session"}) + def test_header_transport_preserves_exact_native_llm_type_and_restores_methods(self): + worker = load_recipe_module("worker.py") + + class NativeLLM: + extra_headers = {"x-omniroute-session": "control-conversation"} + + def generate(self, **kwargs): + return kwargs + + async def agenerate(self, **kwargs): + return kwargs + + original_sync, original_async = NativeLLM.generate, NativeLLM.agenerate + agent, condenser = NativeLLM(), NativeLLM() + # The real SDK uses "type(obj) is LLM" for native usage/context binding. + with worker.gateway_transport(NativeLLM): + self.assertIs(type(agent), NativeLLM) + self.assertIs(type(condenser), NativeLLM) + calls = [agent.generate(), condenser.generate(), asyncio.run(agent.agenerate())] + keys = {call["extra_headers"]["Idempotency-Key"] for call in calls} + self.assertEqual(len(keys), 3) + self.assertEqual({call["extra_headers"]["x-omniroute-session"] for call in calls}, + {"control-conversation"}) + for kwargs in ({"response_format": {"type": "json_object"}}, + {"text": {"format": {"type": "json_schema"}}}, {"temperature": 0.1}): + with self.subTest(kwargs=kwargs), self.assertRaises(ValueError): + agent.generate(**kwargs) + self.assertIs(NativeLLM.generate, original_sync) + self.assertIs(NativeLLM.agenerate, original_async) + def test_runtime_model_override_and_native_tool_filter(self): helpers = load_recipe_module("recipe.py") - result = helpers.llm_config(self.read_json("config/worker.json"), "cx/future-model-max") - self.assertEqual(result["model"], "openai/cx/future-model-max") + result = helpers.llm_config(self.read_json("config/worker.json"), "cx/gpt-6-sol-max") + self.assertEqual(result["model"], "openai/cx/gpt-6-sol-max") self.assertEqual(result["base_url"], "http://10.0.2.2:20128/v1") pattern = re.compile(helpers.tool_filter(self.read_json("config/mcp-policy.json"))) for name in ("terminal", "file_editor", "context-mode_ctx_execute", "serena_find_symbol", "ai-memory_memory_query", "qmd_query", "socraticode_codebase_health", "jcodemunch_order"): @@ -143,6 +161,26 @@ def test_runtime_model_override_and_native_tool_filter(self): for name in ("context-mode_ctx_upgrade", "context-mode_ctx_purge", "ai-memory_memory_write", "socraticode_codebase_index", "jcodemunch_delete_index", "headroom_headroom_compress", "memory_query", "qmd_query_evil"): self.assertIsNone(pattern.fullmatch(name), name) + def test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes(self): + helpers = load_recipe_module("recipe.py") + for model in ("claude-opus-5-5", "cx/claude-opus-5-5", "cx/gpt-5", "openai/cx/gpt-6-astra-max"): + with self.subTest(model=model), self.assertRaises(ValueError): + helpers.llm_config(self.read_json("config/worker.json"), model) + for change in ({"temperature": 0.1}, {"response_format": {"type": "json_object"}}, {"native_tool_calling": False}): + config = self.read_json("config/worker.json") + config["llm"].update(change) + with self.subTest(change=change), self.assertRaises(ValueError): + helpers.llm_config(config) + + def test_every_container_is_owned_and_has_no_published_port(self): + host = load_recipe_module("host.py") + args = host.docker_args(self.read_json("pins.json"), "rw-openhands-control") + self.assertIn("com.native-agent-stack.owner=gpt6-omniroute-framework-integration", args) + self.assertNotIn("-p", args) + self.assertNotIn("--publish", args) + with self.assertRaises(ValueError): + host.docker_args(self.read_json("pins.json"), "unowned") + def test_qmd_guard_requires_explicit_scope_and_lexical_query(self): guard = load_recipe_module("mcp_guard.py") params = {"collections": ["foundation-docs"], "searches": [{"type": "lex", "query": "fixture"}], "rerank": False} @@ -161,78 +199,200 @@ def test_serena_activates_plain_fixture_and_keeps_metadata_outside_it(self): self.assertNotIn("$projectDir/.serena", native) -class OpenHandsFrozenOracleTests(unittest.TestCase): - # Synthetic positive control for the oracle only. This implementation is - # outside the mounted recipe; a real E2E always starts with the broken copy. - FIX = '''def compress_ranges(values): - ranges = [] - for value in sorted(set(values)): - if ranges and value == ranges[-1][1] + 1: - ranges[-1] = (ranges[-1][0], value) - else: - ranges.append((value, value)) - return ranges -''' +class OpenHandsUpstreamAdapterTests(unittest.TestCase): + """Synthetic transport controls from SWE-bench 4.1.0 reporting.py. + + These exercise our parser only; they are not upstream grader executions. + """ + INSTANCE = "django__django-11333" def setUp(self): + self.checker = load_recipe_module("e2e/check.py") self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) self.result = Path(self.tmp.name) - self.checker = load_recipe_module("e2e/check.py") - self.helpers = load_recipe_module("recipe.py") - shutil.copytree(RECIPE / "e2e/fixture-repo", self.result / "workspace") - (self.result / "worker").mkdir() - baseline = self.checker.run_tests(self.result / "workspace") - self.assertEqual((baseline["exit_code"], baseline["tests_run"], baseline["failures"], baseline["errors"]), (1, 1, 1, 0)) - (self.result / "baseline.json").write_text(json.dumps(baseline)) - pristine = self.helpers.inventory(RECIPE / "e2e/fixture-repo") - self.events = [ - {"kind": "ActionEvent", "tool_name": "terminal", "tool_call_id": "synthetic", "action": {"command": "python -B -m unittest discover -s tests -v"}, "workspace": pristine}, - {"kind": "ObservationEvent", "tool_name": "terminal", "tool_call_id": "synthetic", "observation": {"exit_code": 1, "text": "test_ranges_follow_the_spec FAILED (failures=1)"}, "workspace": pristine}, - ] - self.write_events() - (self.result / "workspace/range_utils.py").write_text(self.FIX) - - def write_events(self): - (self.result / "worker/events.jsonl").write_text("\n".join(map(json.dumps, self.events)) + "\n") - - def test_fixed_copy_passes_and_frozen_source_stays_broken(self): - verdict = self.checker.check(self.result) - self.assertTrue(verdict["passed"], verdict) - self.assertEqual(verdict["changed_files"], ["range_utils.py"]) - self.assertEqual(verdict["tests"]["tests_run"], 1) - - def test_no_native_red_observation_cannot_pass(self): - self.events = self.events[:1] - self.write_events() - verdict = self.checker.check(self.result) - self.assertIn("no_native_test_failure_before_edit", verdict["failures"]) - - def test_red_after_edit_cannot_pass(self): - self.events[0]["workspace"] = self.helpers.inventory(self.result / "workspace") - self.write_events() - self.assertFalse(self.checker.check(self.result)["passed"]) - - def test_deleted_or_skipped_test_and_extra_files_are_rejected(self): - test = self.result / "workspace/tests/test_ranges.py" - original = test.read_text() - for content in ("", "import unittest\n" + original.replace(" def test_ranges", " @unittest.skip('removed')\n def test_ranges")): - test.write_text(content) - self.assertIn("diff_outside_allowed_files_or_empty", self.checker.check(self.result)["failures"]) - test.write_text(original) - (self.result / "workspace/extra.txt").write_text("outside contract") - self.assertFalse(self.checker.check(self.result)["passed"]) - - def test_broken_answer_and_symlink_cannot_pass(self): - source = self.result / "workspace/range_utils.py" - source.write_text("def compress_ranges(values):\n return []\n") - self.assertIn("tests_not_passing_unskipped", self.checker.check(self.result)["failures"]) - source.unlink() - source.symlink_to(RECIPE / "e2e/fixture-repo/range_utils.py") - self.assertIn("nonregular_result", self.checker.check(self.result)["failures"]) + + def report(self, bucket): + data = {key: [] for key in ("resolved_ids", "unresolved_ids", "error_ids", "empty_patch_ids", "incomplete_ids", "submitted_ids")} + data["submitted_ids"] = [self.INSTANCE] + data["schema_version"] = 2 + data[bucket] = [self.INSTANCE] + data["total_instances"] = 500 + # The upstream report can include unsubmitted IDs from the full split. + if bucket != "incomplete_ids": + data["incomplete_ids"] = ["unsubmitted-other-task"] + return data + + def invoke(self, data): + path = self.result / "report.json" + path.write_text(json.dumps(data)) + return subprocess.run([sys.executable, str(RECIPE / "e2e/check.py"), + "--report", str(path), "--instance-id", self.INSTANCE], + capture_output=True, text=True) + + def test_known_pass_relays_upstream_resolution(self): + completed = self.invoke(self.report("resolved_ids")) + self.assertEqual(completed.returncode, 0, completed.stderr) + result = json.loads(completed.stdout) + self.assertIs(result["upstream_resolved"], True) + self.assertEqual(result["verdict_source"], "swebench==4.1.0") + + def test_known_fail_is_not_a_successful_process_verdict(self): + for bucket in ("unresolved_ids", "empty_patch_ids", "error_ids", "incomplete_ids"): + with self.subTest(bucket=bucket): + completed = self.invoke(self.report(bucket)) + self.assertEqual(completed.returncode, 1, completed.stderr) + self.assertIs(json.loads(completed.stdout)["upstream_resolved"], False) + + def test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed(self): + conflict = self.report("resolved_ids") + conflict["unresolved_ids"] = [self.INSTANCE] + wrong = self.report("resolved_ids") + wrong["submitted_ids"] = ["wrong-task"] + for value in ({}, [], {"resolved_ids": "true"}, conflict, wrong): + with self.subTest(value=value): + self.assertEqual(self.invoke(value).returncode, 2) + path = self.result / "broken.json" + path.write_text('{"resolved_ids": [') + with self.assertRaises(ValueError): + self.checker.read_report(path, self.INSTANCE) + + def test_input_sanity_does_not_grade_the_patch(self): + path = self.result / "output.jsonl" + for patch_text in ("", "diff --git a/a.py b/a.py\n--- a/a.py\n+++ b/a.py\n@@ -1 +1 @@\n-broken\n+fixed\n"): + path.write_text(json.dumps({"instance_id": self.INSTANCE, "test_result": {"git_patch": patch_text}}) + "\n") + checked = self.checker.check_input(path, self.INSTANCE) + self.assertIs(checked["ready_to_grade"], True) + self.assertNotIn("passed", checked) + for content in ("", "{", json.dumps({"instance_id": self.INSTANCE}), + json.dumps({"instance_id": self.INSTANCE, "test_result": {"git_patch": 1}})): + path.write_text(content) + with self.subTest(content=content), self.assertRaises(ValueError): + self.checker.check_input(path, self.INSTANCE) + row = json.dumps({"instance_id": self.INSTANCE, "test_result": {"git_patch": ""}}) + path.write_text(row + "\n" + row + "\n") + with self.assertRaises(ValueError): + self.checker.check_input(path, self.INSTANCE) + + def test_shared_skills_installer_is_the_only_install_path(self): + host = load_recipe_module("host.py") + workspace = self.result / "workspace" + (workspace / ".git/info").mkdir(parents=True) + with patch.object(host.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run: + host.install_workspace_skills(ROOT, workspace) + self.assertEqual(run.call_args.args[0], [ + sys.executable, str(ROOT / "tools/adoption/install_skills.py"), + "--manifest", "blueprints/runtime-workers/skills/manifest.json", + "--project-dir", str(workspace), "--agent", "universal", + ]) + self.assertEqual(run.call_args.kwargs["cwd"], ROOT) + source = (RECIPE / "host.py").read_text() + self.assertNotIn('Path.home() / ".agents/skills"', source) + self.assertNotIn("link.symlink_to", source) + + def test_project_skills_bookkeeping_is_excluded_from_worker_patch(self): + host = load_recipe_module("host.py") + workspace = self.result / "workspace" + subprocess.run(["git", "init", "--quiet", str(workspace)], check=True, capture_output=True) + + def installed(*args, **kwargs): + (workspace / ".agents/skills/tdd").mkdir(parents=True) + (workspace / ".agents/skills/tdd/SKILL.md").write_text("synthetic skill control\n") + (workspace / "skills-lock.json").write_text("{}\n") + return subprocess.CompletedProcess([], 0) + + with patch.object(host.subprocess, "run", side_effect=installed): + host.install_workspace_skills(ROOT, workspace) + # Vercel skills 1.7.0 writes exactly these root setup paths. Ask Git + # which would be ignored; never stage or commit this synthetic checkout. + checked = subprocess.run(["git", "-C", str(workspace), "check-ignore", "--no-index", "--stdin"], + input=".agents/skills/tdd/SKILL.md\nskills-lock.json\nnested/skills-lock.json\n", + capture_output=True, text=True) + self.assertEqual(checked.stdout.splitlines(), [".agents/skills/tdd/SKILL.md", "skills-lock.json"]) + + def test_project_skills_install_refuses_task_path_conflicts(self): + host = load_recipe_module("host.py") + for reserved in (".agents", "skills-lock.json"): + with self.subTest(reserved=reserved): + workspace = self.result / reserved.replace(".", "_") + workspace.mkdir() + (workspace / reserved).write_text("preexisting task content\n") + with patch.object(host.subprocess, "run") as run, self.assertRaises(ValueError): + host.install_workspace_skills(ROOT, workspace) + run.assert_not_called() + self.assertEqual((workspace / reserved).read_text(), "preexisting task content\n") + + def test_official_grader_container_transport_preserves_upstream_settings(self): + module = load_recipe_module("e2e/docker_grader.py") + original = {"name": "sweb.eval.django__django-11333.attempt", "image": "swebench/example:latest", + "platform": "linux/amd64", "detach": True} + changed = module.owned_container_options(original) + self.assertEqual(changed["name"], "rw-openhands-sweb.eval.django__django-11333.attempt") + self.assertEqual(changed["labels"]["com.native-agent-stack.owner"], + "gpt6-omniroute-framework-integration") + self.assertEqual(changed["image"], original["image"]) + self.assertEqual(original["name"], "sweb.eval.django__django-11333.attempt") + for update in ({"ports": {"8000/tcp": 3710}}, {"name": "unowned"}, {"volumes": {"unowned": {}}}): + with self.subTest(update=update), self.assertRaises(ValueError): + module.owned_container_options({**original, **update}) + + def test_task_adapter_hides_oracle_and_requires_frozen_original_bytes(self): + module = load_recipe_module("e2e/task.py") + row = {"instance_id": self.INSTANCE, "repo": "django/django", "base_commit": "a" * 40, + "problem_statement": "Fix the selected issue.", "version": "1.7", + "patch": "gold-hidden", "test_patch": "test-hidden", + "FAIL_TO_PASS": '["private-test"]', "PASS_TO_PASS": "[]"} + path = self.result / "task.json" + path.write_text(json.dumps([row])) + import hashlib + digest = hashlib.sha256(path.read_bytes()).hexdigest() + task = module.load_task(path, digest) + prompt = module.worker_instruction(task) + self.assertIn(row["problem_statement"], prompt) + for secret in ("gold-hidden", "test-hidden", "private-test"): + self.assertNotIn(secret, prompt) + with self.assertRaises(ValueError): + module.load_task(path, "0" * 64) + path.write_text(json.dumps([row, row])) + with self.assertRaises(ValueError): + module.load_task(path, hashlib.sha256(path.read_bytes()).hexdigest()) class OpenHandsReceiptTests(unittest.TestCase): + def test_local_pass_field_is_never_a_worker_verdict(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp) + (result / "window.json").write_text(json.dumps({ + "started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:02Z", + "worker_exit_code": 0, + })) + (result / "check.json").write_text(json.dumps({"passed": True, "exit_code": 0})) + receipt = module.create_receipt(result, database=result / "absent.sqlite") + self.assertFalse(receipt["task_passed"]) + self.assertIsNone(receipt["upstream_grader"]["upstream_resolved"]) + + def test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp) + instance = "django__django-11333" + (result / "window.json").write_text(json.dumps({ + "started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:02Z", + "worker_exit_code": 0, "run_id": "rw-openhands-control", "instance_id": instance, + })) + (result / "check.json").write_text(json.dumps({ + "upstream_resolved": True, "grader_exit_code": 0, "conversion_exit_code": 0, + })) + data = {"schema_version": 2, "submitted_ids": [instance], "resolved_ids": [], + "unresolved_ids": [instance], "empty_patch_ids": [], "error_ids": [], "incomplete_ids": []} + report = result / "OpenHands.rw-openhands-control.json" + report.write_text(json.dumps(data)) + self.assertFalse(module.create_receipt(result, database=result / "absent.sqlite")["task_passed"]) + data.update(resolved_ids=[instance], unresolved_ids=[]) + report.write_text(json.dumps(data)) + self.assertTrue(module.create_receipt(result, database=result / "absent.sqlite")["task_passed"]) + def test_truncated_native_summary_retains_failed_attempt_receipt(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: @@ -287,7 +447,7 @@ def test_skills_and_mcp_require_observations(self): observed = {"kind": "ObservationEvent", "tool_name": "ai-memory_memory_query", "observation": {"is_error": False}} skill = {"kind": "ObservationEvent", "tool_name": "invoke_skill", "observation": {"skill_name": "tdd", "is_error": False}} self.assertEqual(module.observations([action])["mcp_calls_observed"], {}) - report = module.observations([action, observed, skill]) + report = module.observations([action, observed, skill], ["tdd"]) self.assertEqual(report["mcp_calls_observed"], {"ai-memory_memory_query": 1}) self.assertEqual(report["skills_observed"], {"tdd": 1}) From e83866951c4ed720f88dfd30a8471e2de42bad02 Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 17:17:31 -0400 Subject: [PATCH 03/22] OpenHands runtime worker: round-3 repair, REST dispatch and both gateway arms GPT-6 repair builder (effort max) against the Claude review (C1-C8), the security review (S2-S12) and the common round-3 requirements: - arms: control cx/gpt-6-astra-max on 20128; engines-on sharedgw/gpt-6-astra-max (one slash) on 20129; arm URL and model carried into the native request and the receipt; - usage only from each arm's entry gateway (unknown stays unknown), compression counted separately; - native agent-server REST start/wait/result on loopback 127.0.0.1:3730 with deterministic paths, early status, a deadline and distinct exits (setup 3, official negative 1, incomplete evidence 2); - SWE-bench checkout via the pinned REPO_BASE_COMMIT_BRANCH; no local grader resource limits; - security: remote MCPs disabled with fail-closed network evidence gates, verified upstream lock and hashed build tools, a resolved mount allowlist, non-root model containers with dropped capabilities, no-new-privileges and read-only roots, bounded no-follow reads of worker files. Declined with residuals: host firewall enforcement (needs host probes) and containerized grader builds; the receipt keeps evidence_complete=false. Coordinator fixes: build-requirements.lock moves setuptools 80.9.0 to 84.0.0 (PyPI wheel sha256 51a52592...), clearing GHSA-h35f-9h28-mq5c (fixed in 83); the four SDK build systems at 43376f1 require only setuptools>=61, and a hashed --only-binary install of the lock succeeds. pins.json carries the new lock digest. The OSV inventory lists requirements.lock and build-requirements.lock. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/osv-scanner-lockfiles.json | 8 + .../runtime-workers/openhands/README.md | 664 ++++++++-------- .../openhands/build-requirements.lock | 2 +- .../openhands/config/host.example.json | 10 + .../openhands/config/mcp.template.json | 2 + .../openhands/config/worker.json | 4 +- .../runtime-workers/openhands/dispatch.py | 291 +++++++ .../runtime-workers/openhands/e2e/check.py | 2 + .../openhands/e2e/docker_grader.py | 18 +- .../openhands/evidence/round3-commands.json | 258 ++++++ .../openhands/evidence/round3-verification.md | 734 ++++++++++++++++++ blueprints/runtime-workers/openhands/host.py | 365 +++++++-- .../openhands/install-grader.sh | 21 +- .../runtime-workers/openhands/install.sh | 2 +- .../runtime-workers/openhands/pins.json | 10 +- .../runtime-workers/openhands/receipt.py | 121 ++- .../runtime-workers/openhands/recipe.py | 36 +- .../runtime-workers/openhands/research.md | 77 ++ .../runtime-workers/openhands/run-e2e.sh | 2 +- .../openhands/server_transport.py | 17 + .../runtime-workers/openhands/worker.py | 100 ++- tests/test_runtime_worker_openhands.py | 549 ++++++++++++- 22 files changed, 2806 insertions(+), 487 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/dispatch.py create mode 100644 blueprints/runtime-workers/openhands/evidence/round3-commands.json create mode 100644 blueprints/runtime-workers/openhands/evidence/round3-verification.md create mode 100644 blueprints/runtime-workers/openhands/server_transport.py diff --git a/.github/osv-scanner-lockfiles.json b/.github/osv-scanner-lockfiles.json index 2244b4c2f..0673b1665 100644 --- a/.github/osv-scanner-lockfiles.json +++ b/.github/osv-scanner-lockfiles.json @@ -41,6 +41,14 @@ "path": "blueprints/us-equities/data/requirements.lock", "parser": "requirements.txt" }, + { + "path": "blueprints/runtime-workers/openhands/requirements.lock", + "parser": "requirements.txt" + }, + { + "path": "blueprints/runtime-workers/openhands/build-requirements.lock", + "parser": "requirements.txt" + }, { "path": "blueprints/us-equities/engine/patches/alpaca.packages.lock.json", "parser": "packages.lock.json" diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 6487b67ab..755fe644c 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -1,325 +1,353 @@ # OpenHands coding runtime worker -This recipe uses OpenHands SDK **1.49.6** for one frozen SWE-bench coding task -through OmniRoute. **Official SWE-bench 4.1.0 supplies the task verdict**, using -the conversion and grading pipeline selected by OpenHands/benchmarks at -405bae7140d7e961a75f4910a0b2e7069731db96. [check.py](e2e/check.py) only validates -transport and relays the official report. Local contract tests cannot establish -an upstream E2E pass. - -Round 2 is **awaiting host execution and the coordinator's skills PR**. No -package was installed, container/service started, model called, or upstream -grader executed in this build. The SDK owns the agent loop, tools, skills, -native trace and condensation. All agent/LLM/MCP execution stays in an owned -container; the grader uses separate official test containers. The worker never -receives this checkout, the frozen oracle row, or a Docker socket. - -## Sources, pins and installation - -[pins.json](pins.json) keeps the worker's existing artifact pins and the new -grader pins. [research.md](research.md) records research and corrections. - -| Component | Exact selected pin | Role | +Round 3 provides a source-backed **native agent-server REST dispatch** for a +frozen SWE-bench task with two explicit gateway arms. SDK/server **1.49.6** owns +the agent loop. **SWE-bench 4.1.0** alone supplies the task verdict through the +pinned OpenHands/benchmarks converter and official Docker harness. This repair +ran offline contract tests only. No installation, container, model, gateway +request or official grading run was performed. + +**Host acceptance is still open.** A verified firewall is required before model +execution. SDK trace, skill and version files remain worker-reported because the +terminal shares the server's UID. They cannot establish independent acceptance; +`evidence_complete` deliberately stays false until an independent observer is +qualified. A resolved official task therefore currently exits 2, not 0. This is +an explicit acceptance gap, not a failed model task. + +[research.md](research.md) records primary-source checks and corrections. +[round3-verification.md](evidence/round3-verification.md) records every applicable +finding and the exact offline commands, outputs and exit codes. Earlier rounds +remain historical evidence; their standalone/no-listener descriptions are +superseded by this round. + +## Sources and installation + +| Component | Pin | Purpose | | --- | --- | --- | -| OpenHands Software Agent SDK | v1.49.6 / fcc102a697874d54a357e36004e02c95040dbdc0 | Standalone coding worker | -| OpenHands/benchmarks | 405bae7140d7e961a75f4910a0b2e7069731db96 | Native output conversion and official grading integration | -| Benchmark SDK submodule | 43376f1868ffd702746080714a59c16d3f69ec12 | Preserved in the benchmark's separate environment | -| SWE-bench | 4.1.0 | Official Docker tests and resolved_ids verdict | -| Harbor, runner-up | v0.23.0 | Not selected: Terminal-Bench has a different task contract | - -The two SDK revisions are intentionally separate. The grader environment keeps -the report's exact submodule; it does not silently upgrade to the worker SDK. -The worker's inference is an explicitly local adaptation of the [standalone -example][hello] and the benchmark's [issue prompt and patch export][benchmark-infer]. -It is not an unchanged swebench-infer run. The official grader's patch -application, tests, scoring and report logic remain unchanged. - -The primary fits this coding task because its converter accepts actual -OpenHands patches and invokes the official SWE-bench harness. -[Harbor's OpenHands adapter][harbor] is a maintained alternative; selecting its -Terminal-Bench tasks would change the oracle. Stars and installation counts -were discovery signals only. This choice follows the supplied eval-framework -reports, particularly report2's W8a mapping. - -[install.sh](install.sh) preserves the round-1 worker installation: the -[official package path][getting-started], pinned published SDK/tools wheels, -[requirements.lock](requirements.lock), [build-requirements.lock](build-requirements.lock), -and a content-addressed agent-server image, used as a Python runtime with its -server entrypoint replaced. [install-container.sh](install-container.sh) runs -inside that image. The source archive, upstream lock, requirements and image -configuration hashes are verified before use. The image is: - -~~~text -ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 -~~~ - -It selects Linux amd64 manifest -ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12 -and image config 39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f. -The image metadata records Python 3.13.15 and uv 0.12.15. No image was pulled -here. The retained [wheel/source comparison](evidence/wheel-source-comparison.json) -is artifact evidence only. - -The worker install follows the upstream lock's dependency export plus hashed -published wheels. It bootstraps locked build tools before the hash-required -install without build isolation; func-timeout is an sdist. Installation checks -package versions/imports, not model behavior. SDK source builds, published binary -images and importable wheels are different installation paths; this recipe -retains the qualified artifact choices from round 1. - -[install-grader.sh](install-grader.sh), called by the host installer, clones -OpenHands/benchmarks into the owned prefix, checks out the exact commit, -initializes its submodules and runs **make build**, the [documented install][benchmark-install]. -The [pinned Makefile][benchmark-make] runs uv sync --dev and installs its own -development hooks in that isolated checkout. The script verifies the submodule -before and after installation, refuses tracked modifications, checks that -uv.lock/pyproject.toml did not drift, and verifies swebench==4.1.0. Grader source -and SDK state remain separate from this repository. No alternative grader is -installed as fallback. - -The prefix is $HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6; -private state is $HOME/.local/state/native-agent-stack/runtime-workers/openhands. -Every trial gets a fresh attempt directory. Failed attempts remain available. -The grader source install has its own isolated venv and cache. The worker venv's -interpreter link resolves inside the pinned image, not necessarily on the host. - -## Model and gateway contract - -[config/worker.json](config/worker.json) selects **cx/gpt-6-astra-max** for the -coding/judgment loop and condenser. OPENHANDS_MODEL can select another cx/gpt-6 -family alias; [recipe.py](recipe.py) rejects Claude and other model families. -OmniRoute does not serve claude-opus-5-5. The official SWE-bench grader is -deterministic and makes no LLM judgment request. - -| Setting | Native SDK interface | Recipe behavior | -| --- | --- | --- | -| Gateway | LLM.base_url and api_key | Host reference http://127.0.0.1:20128/v1; actual rootless-container URL http://10.0.2.2:20128/v1; keyless placeholder local-loopback | -| Model | LLM.model, model_canonical_name | LiteLLM openai/ provider prefix is added before cx/gpt-6-astra-max | -| Effort/API | LLM.api_mode, reasoning_effort | Responses, max; no automatic chat fallback | -| Structured actions | ToolDefinition.to_responses_tool | Native function tools, strict:false; no JSON response format | -| Sampling | LLM.temperature | Omitted, including summaries; values at or below 0.1 are rejected | -| Affinity | LLM.extra_headers | Stable x-omniroute-session for one conversation and its condenser | -| Idempotency | Public generate/agenerate kwargs | Fresh Idempotency-Key per logical call; upstream internal retries retain it | -| Context | LLMSummarizingCondenser | 80 events / 60,000 input-token trigger, keep_first=2 | -| Bounds | Native SDK run/request fields | 40 iterations, 1,200-second worker deadline, 16,384 output tokens, two retries, 180-second request timeout | - -The gateway owner's cognee 1.6.1 measurements establish that JSON-object mode -needs the word “json” in input messages; system text moves to instructions. -Strict JSON schemas need additionalProperties:false on every object. -This recipe chooses **tool calling** for structured actions, following -[the pinned serializer][tool-serialization]. It does not issue JSON-object or -JSON-schema calls. Its [native condenser][condenser] consumes ordinary summary -text. No gateway/schema acceptance is inferred from these offline checks. - -[worker.py](worker.py) changes only the public LLM generation boundary to supply -headers; the upstream serialization, retries and condenser remain native. -The adapter scopes its method wrapping to one worker process and restores the -methods afterward. Agent and condenser keep exact native LLM objects: -[SDK registration][llm-discovery] explicitly checks type(obj) is LLM, and -[conversation setup][llm-registration] uses that registry for native metrics -and condenser context. A subclass would silently skip this registration. -[SDK header merging][header-merging] preserves per-call headers. Actual -wire-level header stability, fresh logical call keys, max effort, streaming and -tool results still require host observation. Gateway log columns available to -this recipe cannot independently prove header/session attribution. - -## MCP and skills lifecycle - -[config/mcp.template.json](config/mcp.template.json) retains the native command -selection from the repository's adopted client templates. -[recipe.py](recipe.py) translates tool limits into Agent.filter_tools_regex; -Codex-only approval/allowlist keys are not passed as SDK configuration. -The locked FastMCP uses the server_name_tool_name namespace and the worker -requires multiple servers. See [MCP config][mcp-config] and [agent filtering][agent-base]. - -| Server | Scope | -| --- | --- | -| context-mode | Native stdio; ctx_upgrade and ctx_purge excluded | -| serena | Native stdio, explicit /workspace, dashboard off, external attempt-owned metadata | -| jcodemunch | Native stdio; route, menu and order only | -| qmd | Native stdio; query/get/multi_get/status over exactly four named catalog/document collections | -| ai-memory | Existing HTTP endpoint; query/read_page/recent/status/briefing only | -| socraticode | Native stdio; search/status/list_projects/health; watcher manual; existing local embedder | -| headroom | Configured but disabled; no measured remaining gap justifies it for this trial | - -Serena state and QMD indices live under the attempt's MCP state. -[qmd-setup.py](qmd-setup.py) uses native collection add/update only on the four -mounted document directories. [mcp_guard.py](mcp_guard.py) requires explicit -collections, lexical searches and rerank:false. There is no OmniRoute embedder. -[config/host.example.json](config/host.example.json) describes the private, -mode-0600 paths file; no credentials or authentication-store mounts are needed. - -**Skills installation has exactly one path**, executed from OPENHANDS_STACK_ROOT: - -~~~sh -PYTHONDONTWRITEBYTECODE=1 python3 tools/adoption/install_skills.py \ - --manifest blueprints/runtime-workers/skills/manifest.json \ - --project-dir "$WORKER_WORKSPACE" --agent universal -~~~ - -The new project-dir/agent options and runtime-worker manifest are **pending the -coordinator's skills PR** in this worktree. The call and regression test are -present now. Missing manifest/options fail the trial; there is no global, -copy, symlink or alternate installer fallback. The old -[config/skills.lock.json](config/skills.lock.json) records round-1 provenance only -and is no longer read for installation, loading or receipt decisions. - -The expected manifest contract is a skills array with name and -skill_md_sha256, including tdd and verification-before-completion. The installer -owns the project-local .agents/skills tree. The host verifies those bytes and -mounts .agents read-only; [native load_skills_from_dir][skills-loader] discovers -them and AgentContext.skills exposes them progressively. Public, home and project -auto-discovery stays disabled. Discovery mismatches fail startup. -The [upstream project installer][skills-local-lock] also writes skills-lock.json -at the workspace root. Both installer paths are excluded from the worker patch -and mounted read-only when present. A frozen task already containing .agents or -skills-lock.json is rejected before installation so its own files stay intact. -Additional artifacts from the pending shared installer must be reviewed at -integration; the recipe does not assume that extension has been accepted. - -Each attempt records the **actual names listed at start** in -worker/skills-startup.json and input/skills.json, rather than claiming a fixed -28-skill list in advance of the pending manifest. The E2E requires successful -native **ObservationEvent / invoke_skill / skill_name** results for tdd and -verification-before-completion, corroborated by the SDK's persisted trace. -[InvokeSkillTool][invoke-skill] returns the rendered skill content and records -the invocation. A name in config, startup listing or the task prompt alone -does not prove activation. No activation was observed in this build. - -## Frozen task and official verdict - -Before host acceptance, select one original SWE-bench Verified row from a -recorded dataset revision; retain the complete JSON-list or JSONL row, its -source revision and SHA256 outside every worktree. The [official loader][swe-dataset] -accepts both formats. OPENHANDS_TASK_FILE and OPENHANDS_TASK_SHA256 identify -these frozen bytes. A concrete instance/revision is a remaining host input; -the recipe does not invent a replacement benchmark task. - -[e2e/task.py](e2e/task.py) validates the hash, single-instance scope, repository, -base commit and native test fields. Only the issue, repository and base commit -enter the worker's prompt. Gold patch, test patch and test-name oracle fields -remain outside every worker mount. The worker gets a checkout at the frozen -base commit with protected Git/skill metadata. This bare-checkout adaptation -does not claim the benchmark's preinstalled Conda testbed; dependency and native -test availability must be established for the selected task. - -After inference, [host.py](host.py) exports the worker's staged diff against -that base commit using the benchmark's [native patch pattern][benchmark-infer] -into output.jsonl. The adapter rejects missing, malformed, duplicate or -wrong-instance predictions before upstream conversion. Empty patches remain -valid submissions that the official grader can fail. - -The primary pipeline is split at the official converter/grader boundary: - -~~~sh -# In the pinned benchmark environment; paths below are private attempt paths. -swebench-eval "$OUTPUT_JSONL" --dataset "$FROZEN_DATASET" \ - --run-id "$RUN_ID" --workers 1 --no-modal --skip-evaluation -python /path/to/recipe/e2e/docker_grader.py \ - --dataset_name "$FROZEN_DATASET" --predictions_path "$PREDICTIONS_JSONL" \ - --instance_ids "$INSTANCE_ID" --run_id "$RUN_ID" --max_workers 1 \ - --split test --timeout 1800 --namespace swebench \ - --cache_level instance --clean false --modal false -~~~ - -The first command is the unchanged [OpenHands converter][benchmark-eval]. -The second executes the **unchanged swebench.harness.run_evaluation CLI**, -the same module invoked by swebench-eval. [docker_grader.py](e2e/docker_grader.py) -adapts only the [Docker creation boundary][swe-container] for names, ownership -labels and resource limits. The split allows the native cache/cleanup flags, -which swebench-eval does not expose. There is no local test grader. - -The benchmark converter intentionally removes edits to pyproject.toml, -tox.ini and setup.py. Both original output.jsonl and converted -output.swebench.jsonl are retained with hashes; host acceptance must examine -any filtered changes. SWE-bench writes OpenHands.RUN_ID.json plus per-instance -report.json and test output in its native logs. [receipt.py](receipt.py) -re-reads that official report; resolved_ids supplies the verdict. A grader -exit code of zero alone is insufficient. Missing/conflicting/malformed report -fields leave the verdict unknown. [check.py](e2e/check.py) returns 0 for an -upstream-resolved ID, 1 for an upstream negative outcome, and 2 for invalid -transport; input-only mode reports ready_to_grade without a correctness claim. - -The old e2e/fixture-repo, frozen.json and task.txt are retained solely as -historical round-1 artifacts. They are not mounted or executed in this E2E. - -## Ownership, ports and coordinator execution - -Every created container has prefix **rw-openhands-** and label -**com.native-agent-stack.owner=gpt6-omniroute-framework-integration**. -No named volume or custom network is created; worker persistence uses owned -bind mounts. The grader rejects new volume/network/build operations and uses -official prebuilt SWE-bench images. Their actual resolved digests must be -recorded during host acceptance; the native per-instance image tags are not -claimed here to be immutable artifacts. - -| Port/resource | Mapping | +| OpenHands/software-agent-sdk | `fcc102a697874d54a357e36004e02c95040dbdc0`, v1.49.6 | Native SDK request types and agent-server | +| Agent-server image | `ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37` | Linux amd64 binary server and Python runtime | +| OpenHands/benchmarks | `405bae7140d7e961a75f4910a0b2e7069731db96` | Official conversion and grading commands | +| Benchmark SDK submodule | `43376f1868ffd702746080714a59c16d3f69ec12` | Unchanged separate grader dependency | +| SWE-bench | v4.1.0 | Official tests and `resolved_ids` | +| uv for host grader installation | 0.12.17 | Existing adopted executable; no automatic installation | + +The native server uses the image's **own ENTRYPOINT**. The published binary target +is `openhands-agent-server/openhands/agent_server/docker/Dockerfile:580-590`. +The source target at lines 570-576 has a +different ENTRYPOINT. Do not assume the image contains the source-target venv. +Request generation uses the separately installed SDK/tools wheels in the same +image, with network disabled. [pins.json](pins.json) preserves the image, wheel +and runtime lock hashes. No framework version changed in round 3. + +[install.sh](install.sh) installs the worker through the existing hash-required +wheel/runtime locks and preinstalled hashed build tools. The install container +can write only the worker venv and owned cache; the recipe snapshot is mounted +read-only with no writable parent alias. The installer uses container root for +these owned writes; model processes use upstream UID/GID 10001. + +[install-grader.sh](install-grader.sh) retains the pinned host checkout/submodule +and uses the upstream Makefile's `uv sync --dev` operation with supported +`--locked --no-build-isolation --inexact` flags. It first installs hashed build +requirements, verifies the unchanged upstream `uv.lock` hash, checks uv 0.12.17, +sets `UV_PYTHON_DOWNLOADS=never`, explicitly selects the seeded venv interpreter +with `--python`, and checks the final SWE-bench version. The upstream +`.python-version:1` selects 3.12; the explicit interpreter keeps the recipe's +3.13 environment and its hashed build tools together. The +Makefile's unrestricted sync and development-hook installation are not invoked. +Sources: OpenHands/benchmarks@405bae7 `Makefile:33-42`, `pyproject.toml:97-99`; +SDK@43376f1 package build-system sections listed in pins.json; installed +`uv 0.12.17 sync --help`. Host build execution remains a disclosed residual; +containerizing this host-side grader without changing its interpreter/Docker +transport has not been qualified. + +Prefix: `$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6`. +Private state: `$HOME/.local/state/native-agent-stack/runtime-workers/openhands`. +Failed attempts are retained. The coordinator runs installation and acceptance. + +## Arms + +`OPENHANDS_ARM=control|engines-on` defaults to `control`. `--arm` overrides it. +`OPENHANDS_MODEL` and `OPENHANDS_BASE_URL` are optional explicit overrides that +must match the selected arm. The control arm retains the existing `cx/gpt-6-*` +variant support; the engines-on arm admits only the exact one-slash route below. +LiteLLM prepends `openai/` for its provider selection. + +| Arm | Container base URL | Requested model | Compression header | +| --- | --- | --- | --- | +| control | `http://10.0.2.2:20128/v1` | `cx/gpt-6-astra-max` | absent | +| engines-on | `http://10.0.2.2:20129/v1` | `sharedgw/gpt-6-astra-max` | `x-omniroute-compression: allow-lossy` | + +Both arms use Responses, `reasoning_effort=max`, native function tools, omitted +temperature, a 40-iteration limit and a 1,200-second conversation deadline. +Configured effort other than max, a cross-arm URL/model pairing, Claude routes, +two-slash gateway slugs and JSON response-format modes are refused. + +The exact switches are: + +```sh +export OPENHANDS_ARM=control +export OPENHANDS_MODEL=cx/gpt-6-astra-max +export OPENHANDS_BASE_URL=http://10.0.2.2:20128/v1 +``` + +```sh +export OPENHANDS_ARM=engines-on +export OPENHANDS_MODEL=sharedgw/gpt-6-astra-max +export OPENHANDS_BASE_URL=http://10.0.2.2:20129/v1 +``` + +No separate compression toggle is needed. The arm adds/removes the header. +The receipt stores the arm, base URL, requested/routed model, expected path and +**header names only**. A stable `x-omniroute-session` binds the conversation and +condenser. `server_transport.py` loads through the native `--import-modules` +option and supplies a fresh Idempotency-Key per logical `generate/agenerate` +call, retaining upstream retries and exact native LLM types. It also captures +returned correlation IDs privately where LiteLLM exposes response headers. +Sources: SDK@fcc102a `llm/llm.py:442-446,1130-1138,1588-1642`, +`llm/options/common.py:26-46`, `agent/base.py:739-775`, +`agent_server/__main__.py:74-135,240-273`; LiteLLM@v1.93.0 +`litellm/llms/openai/responses/transformation.py:262-272`. + +## Workflow dispatch + +The coordinator first installs the recipe, supplies the frozen task and image +pin, completes the network gate described below, and prepares **one arm at a +time**. Preparation clones the task at its SWE-bench base-commit branch, +installs the shared project skills, builds the scoped lexical QMD index, +serializes the native request and starts the authenticated native server. +Preparation can be long and belongs in a background Bash task. The skills +manifest/installer PR must be available; there is no global-skill fallback. + +Set these private inputs outside all worktrees: + +- `OPENHANDS_HOST_FILE`: filled mode-0600 [host template](config/host.example.json). +- `OPENHANDS_STACK_ROOT`: stack checkout with the shared skill installer/manifest. +- `OPENHANDS_TASK_FILE`, `OPENHANDS_TASK_SHA256`: frozen original SWE-bench row. +- `OPENHANDS_GRADER_IMAGE_FILE`, `OPENHANDS_GRADER_IMAGE_SHA256`: frozen image pin. +- `OPENHANDS_SERVER_ENV`: mode-0600 file containing only this attempt's + `OH_SESSION_API_KEYS_0` setting. No shared service/provider secrets. +- `OPENHANDS_HEADERS`: mode-0600 file containing the corresponding + `X-Session-API-Key` header. Commands use curl's `-H @file`; no inline key. + +```sh +RECIPE="$PWD/blueprints/runtime-workers/openhands" +export OPENHANDS_RUN_ID=rw-openhands-trial-001 +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" prepare \ + --prefix "$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" \ + --state "$HOME/.local/state/native-agent-stack/runtime-workers/openhands" \ + --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +``` + +The workflow child runs exactly these three Bash commands after preparation: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/dispatch.py" start --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/dispatch.py" wait --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/dispatch.py" result --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +``` + +Run `wait` and `result` through the child's background Bash facility; the grader +may take longer than a foreground tool timeout. Observe native completion or +read `status.json`. Each command prints one JSON object including `run_id`, +`arm`, `receipt`, `failure_stage`, `task_passed`, and `evidence_complete`. +The deterministic host paths are: + +```text +$HOME/.local/state/native-agent-stack/runtime-workers/openhands/runs///status.json +$HOME/.local/state/native-agent-stack/runtime-workers/openhands/runs///receipt.json +``` + +The adapter uses curl to call these exact upstream routes on +`http://127.0.0.1:3730`: POST `/api/conversations`, GET +`/api/conversations/`, then GET +`/api/conversations//agent_final_response`. Start is asynchronous. The +native SDK constructs `StartConversationRequest` and calls +`model_dump(exclude_defaults=True, mode="json", context={"expose_secrets": True})`; +only the fixed keyless gateway placeholder is exposed. It includes workspace, +`initial_message.run=true`, native agent/condenser/MCP/hook configuration, +`max_iterations=40`, and lowercase source/dispatch/arm tags. Preparation checks +`/health`, `/docs`, and the request fields in live `/openapi.json`. +Sources: SDK@fcc102a `conversation_router.py:72-86,163-228,257-287`, +`sdk/conversation/request.py:64-72,105-147,212-228,317-347`, +`sdk/utils/pydantic_secrets.py:24-37,48-68`. + +Polling treats only finished/error/stuck as terminal; idle is not completion. +At the deadline it calls the native interrupt route and removes the exact owned +server. Result captures the native final response privately, confirms container +removal, then exports the candidate patch and invokes official grading. +A native final message alone is never a task verdict. Sources: SDK@fcc102a +`conversation_router.py:304-321`; SWE-bench@v4.1.0 `reporting.py:127-157`. + +| Final exit code | Meaning | | --- | --- | -| Worker, install, QMD and grader containers | No host ports published; no worker HTTP service configured | -| Permitted future worker listener allocation | 127.0.0.1:3730–3799 only | -| Existing gateway dependency | Host 127.0.0.1:20128; container access 10.0.2.2:20128 | -| Existing ai-memory / local embedder dependencies | 10.0.2.2:49474 / 10.0.2.2:18232; not created by this recipe | -| S3 memory arms and cognee | 3710–3729, 5433–5439 and 3800–3819 remain unallocated by this worker | - -On the coordinator's host, after the shared skills PR and a frozen native task: - -~~~sh -export OPENHANDS_HOST_FILE=/absolute/private/openhands-host.json -export OPENHANDS_STACK_ROOT=/absolute/native-agent-stack -export OPENHANDS_TASK_FILE=/absolute/private/frozen-instance.jsonl -export OPENHANDS_TASK_SHA256='' -bash blueprints/runtime-workers/openhands/install.sh -OPENHANDS_MODEL=cx/gpt-6-astra-max bash blueprints/runtime-workers/openhands/run-e2e.sh -~~~ - -The existing rootless Docker context is required. The worker uses container -UID 0 mapped to the host user, dropped capabilities, no-new-privileges, -a read-only image and temporary /root and /tmp. The worker and grader have -4 CPU, 8 GiB and 384 PID limits; the grader uses the existing rootless socket -from the host, never a worker socket mount. Its process deadline is 2,400 -seconds in addition to the upstream per-instance test timeout. - -Cleanup removes only each exact literal container name, then independently -checks absence. There is no global prune or upstream broad cleanup helper. -The grader's instance cache/clean=false setting retains images. Native logs -and failures stay in private attempt state. A hard-killed host may still need -literal-name cleanup; reboot/crash recovery has not been accepted. -Inspect only the owner label and the relevant rw-openhands attempt before -recovering. Do not remove shared Docker, gateway, native sign-ins or MCP state. - -## Evidence and remaining gates - -[Round-2 verification](evidence/round2-verification.md) records fail-first and -final commands, exits and returned output. The unit suite provides **local -synthetic transport controls**, including known-pass, known-fail, malformed, -missing, duplicate and conflicting results. It tests the shared installer -invocation, GPT-6 routing and resource adapter. None is reported as an unchanged -upstream test or a live model evaluation. - -The read-only gateway receipt still reads only the nine authorized call_logs -columns and never sums window rows into an exclusive worker cost. Native -metrics and full traces remain private. Unknown usage stays unknown. -Attribution, byte/token counts, cache subsets and provider spending are separate. - -Host checks remaining: install/import compatibility in both pinned environments; -frozen dataset source/instance and testbed compatibility; official pass/fail -controls using retained gold/empty patches before a real worker patch; actual -grader image digests; container ownership/cleanup; native MCP startup and tool -limits; exact skill listing and both activation observations; gateway headers, -Responses/tool behavior and requested/upstream max effort. Native MCP aggregate -startup is 30 seconds even when individual server timeouts are longer. -No matched A/B, skill ablation, token savings, full upstream SDK suite or -crash-recovery acceptance has been performed. A future comparison must freeze -the same task/oracle, environment, models, tools and cache policy across arms. - -The coordinator owns shared manifests. Changed round-1 evidence hashes require -re-registration in manifests/evidence.json after integration; this builder edits -only this recipe and its assigned test. Publication validation is still run and -its actual result is retained even when that shared-file boundary prevents a -clean hash check. - -[release]: https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.49.6 -[sdk-readme]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/README.md#L49-L81 +| 0 | Official task resolved and all required evidence complete | +| 1 | Official unresolved or empty-patch verdict | +| 2 | Incomplete evidence, including a resolved task with untrusted trace evidence | +| 3 | Setup/infrastructure failure, deadline, grading error/incomplete bucket, or transport failure | + +Preparation/start/wait exit 0 means that operation succeeded, not that the task +passed. Duplicate starts do not resubmit a native POST. Retrying a collected +result returns its retained receipt without another API or grader call. A host reservation +serializes dispatches from this recipe. It does not exclude unrelated gateway +clients. Interrupted attempts retain their files; cleanup removes named owned +containers only, never uses prune, and records whether removal was confirmed. +The coordinator must reclaim a stale reservation only after confirming its +recorded container is gone. Port 3730 must be available; another recipe using +that port must be stopped before this one is prepared. + +Invocation counts use existing pipelines: on Claude, child usage and OTel Bash +events whose command contains `dispatch.py`, the `start` subcommand and +`--run-id` (allow shell quoting and whitespace); on OpenHands, JSON access logs for +POST `/api/conversations`, plus native count/search endpoints. Run IDs also +travel in native tags. Poll/result calls are separate from invocation counts. +Native `server.log` is captured on result/cleanup and stays private. Sources: +SDK@fcc102a `conversation_router.py:105-162`, `logging_config.py:55-106`; +repository `adoption/templates/claude.settings.template.json:15` and +`observability/collector/collector.yaml:74`. No collector change is required. + +## Usage accounting + +The only usage authority is the selected arm's **entry** gateway, opened with +SQLite `?mode=ro` and `PRAGMA query_only=ON`: + +| Arm | Entry database | Expected entry row model/path | +| --- | --- | --- | +| control | `$HOME/.local/share/omniroute/storage.sqlite` | `gpt-6-astra-max`, `/v1/responses` | +| engines-on | `$HOME/.local/share/omniroute-fw/storage.sqlite` | `gpt-6-astra-max`, `/v1/responses` | + +For a retained control variant the routed model is the suffix after `cx/`. +The coordinator must verify both database paths and the actual entry row model +and path on the host. A missing/mismatched database produces unknown usage. +Never add forwarded 20128 rows to engines-on 20129 rows. + +Reads are restricted to timestamp, path, status, model, tokens_in, +tokens_cache_read, tokens_reasoning and correlation_id, plus the two explicitly +required effort columns. IDs are discarded before returning rows. The private +native capture may lack headers on streaming paths and shares the model's trust +boundary, so receipts conservatively use **time window + model + path** at the +entry gateway. They state the concurrent-caller limitation. Per-arm totals sum +each matched row once, including failed calls; if any requested counter is +missing, totals remain null. Cache-read tokens are an input subset, not an +additional cost. No output-token or complete-provider-cost total is invented. + +Rows with positive returned reasoning tokens expose their logged effort; both +requested and upstream effort must be max for `effort_verified`. Zero and +unknown reasoning counts are reported separately. Null effort columns are not +evidence that effort was omitted: OmniRoute@a58000c +`src/lib/usage/callLogs.ts:646-653` fills them only for encrypted reasoning. +Correlation response headers are defined in the same pin's +`src/sse/handlers/chatHelpers.ts:1172-1184`. + +For engines-on, the host records before/after +`GET /api/analytics/compression?since=all` snapshots and reports only their +`totalRequests` and `totalTokensSaved` deltas, separately from call-log usage. +Optional `OPENHANDS_COMPRESSION_HEADERS` points to a mode-0600 management-header +file if that endpoint requires authentication. Missing counters, auth failure +or reset keep the delta null. These global analytics can include other callers; +they do not establish measured semantic quality or provider-token savings. +Sources: OmniRoute@a58000c `src/app/api/analytics/compression/route.ts:13-24`, +`src/lib/db/compressionAnalytics.ts:52-55`. + +## Security posture + +All model-controlled shell, file and stdio MCP execution stays in the rootless +container, under non-root UID/GID 10001, with capabilities dropped, +no-new-privileges, read-only root, and an attempt-local temporary home. The model +has no Docker socket, host authentication directory, oracle dataset or grader +checkout. The task's Git metadata and installed skills are read-only mounts. +Host patch export disables system/global Git config, external diff and textconv +so candidate attributes cannot select host-global executable filters. +Sources: SDK@fcc102a `agent_server/docker/Dockerfile:7-9,301-305,343`; +Git@v2.43.0 `Documentation/git.txt:708-724`, `diff-options.txt:830-840`. + +MCP allowlists are not network security. ai-memory HTTP and socraticode are +**disabled** because their host services would bypass the framework's tool +filter. context-mode, Serena, jCodeMunch and lexical QMD remain container-local; +headroom remains disabled. The host mount allowlist accepts only selected +adoption tool roots and the four document roots, resolves symlinks before +checking, and rejects authentication stores and every ancestor of the host home. +No tool can execute directly on the host through this registration. + +Model launches require `rw-openhands-egress-control` or +`rw-openhands-egress-engines-on`. The supported policy mechanism is Docker's +**DOCKER-USER iptables chain**, with explicit destination/port rules before +Docker accepts forwarding. Host INPUT must also deny traffic from the worker +bridge; IPv6 is disabled. Only the selected gateway endpoint is allowed. Plain +`--internal` alone is insufficient because an internal bridge may still reach +host listeners. Source: docker/docs@4e9a5751518ed8223a8dcde53693badddd72604f +`content/manuals/engine/network/firewall-iptables.md:22-24,48-95` and +`port-publishing.md:186-192`. + +The coordinator must provision and **independently probe** that policy in the +rootless daemon's network namespace. The repair does not change daemon-wide +firewalls. `model_network` fails closed without a fresh, host-owned mode-0600 +policy receipt whose network ID matches Docker inspect and whose allowed TCP +endpoint is exactly the selected arm. The host template has per-arm receipt +paths. The receipt schema is: + +```json +{ + "mechanism": "docker-user-iptables", + "network_id": "", + "verified_at": "", + "allowed_tcp": ["10.0.2.2:20128"], + "default_deny": true, + "host_input_denied": true, + "ipv6_disabled": true, + "gateway_reachable": true, + "other_host_ports_denied": true +} +``` + +For engines-on only the endpoint changes to port 20129. This schema is an +operator evidence gate, not a firewall implementation or proof by itself. Keep +the actual rule dump and probe outputs privately; reject access to the other +arm, ai-memory, Qdrant, embedder and other worker listeners before any model run. +Docker was not found on this repair sandbox's PATH, so rootless rule placement, +packet behavior and post-restart policy persistence remain unverified. This is +a host setup gate, not an instruction to accept loopback exposure. Official +grader containers retain upstream options; the coordinator must separately +qualify their host-service isolation without misrepresenting altered test +conditions as an unchanged environment. + +Every authoritative status/window/verdict file is outside model mounts. Files +read from worker output use bounded regular-file reads with O_NOFOLLOW on every +path component, and are labelled worker-reported. The remaining shared-UID +trace limitation is explicit at the top of this document; counterfeit events +cannot cause `evidence_complete=true`. This round does not invent an isolation +mechanism unsupported by the pin. + +## Official grading and remaining host gates + +The task comes from a frozen original SWE-bench row with an externally supplied +SHA256. Clone branch selection uses the pinned `REPO_BASE_COMMIT_BRANCH` mapping +(SWE-bench@v4.1.0 `test_spec/python.py:271-277`). Oracle patches and private tests +are never mounted in the worker. The retained bare checkout and 40 iterations +remain inference adaptations; this is not unchanged `swebench-infer`. + +The image bundle contains `instance_id`, the exact upstream instance `tag`, an +immutable `ref` (`docker.io/swebench/@sha256:`), and its +registry `source`. The coordinator freezes its SHA256 and pre-pulls the digest. +Before grading, host code verifies RepoDigests/platform/Id, retags it locally to +the upstream name and passes the checked image ID to the adapter. Creation +rechecks that identity; mutable pulls and builds are refused. Sources: +SWE-bench@v4.1.0 `test_spec/test_spec.py:106-120`, `docker_build.py:516-524`; +docker/docker-py@7.1.0 `docker/models/resource.py:28-33`. + +`e2e/docker_grader.py` adapts names and ownership labels only. It adds **no memory, +CPU, PID, capability or user changes** to the official grading container. Gold +patch and known-negative official controls must run under the frozen image +before a worker verdict is accepted. `e2e/check.py` relays the actual report; an +error/incomplete bucket is infrastructure, not a model-negative tally. + +Host work remaining: native installation and import acceptance; shared skills +PR; frozen task/image selection; rootless firewall proof and port availability; +native server health/auth/OpenAPI/preload startup; actual tool names/skills/MCP +use; response-header capture for streaming and condenser calls; entry-database +matching and max effort; official positive/negative grading controls; independent +trace observation; cancellation/cleanup recovery. No A/B or savings result is +claimed until these observations exist. + [hello]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/01_standalone_sdk/01_hello_world.py#L10-L29 [makefile]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/Makefile#L34-L41 [getting-started]: https://docs.openhands.dev/sdk/getting-started diff --git a/blueprints/runtime-workers/openhands/build-requirements.lock b/blueprints/runtime-workers/openhands/build-requirements.lock index 66607e35a..2dd2f77d1 100644 --- a/blueprints/runtime-workers/openhands/build-requirements.lock +++ b/blueprints/runtime-workers/openhands/build-requirements.lock @@ -1,3 +1,3 @@ packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c -setuptools==80.9.0 --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 +setuptools==84.0.0 --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 wheel==0.46.3 --hash=sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d diff --git a/blueprints/runtime-workers/openhands/config/host.example.json b/blueprints/runtime-workers/openhands/config/host.example.json index dd5781861..76f5f3553 100644 --- a/blueprints/runtime-workers/openhands/config/host.example.json +++ b/blueprints/runtime-workers/openhands/config/host.example.json @@ -10,6 +10,16 @@ "mcp_readonly_mounts": [ "" ], + "egress": { + "control": { + "network": "rw-openhands-egress-control", + "policy_receipt": "" + }, + "engines-on": { + "network": "rw-openhands-egress-engines-on", + "policy_receipt": "" + } + }, "qmd_collections": { "foundation-docs": "", "foundation-adoption": "", diff --git a/blueprints/runtime-workers/openhands/config/mcp.template.json b/blueprints/runtime-workers/openhands/config/mcp.template.json index a19ae7440..7bc25023f 100644 --- a/blueprints/runtime-workers/openhands/config/mcp.template.json +++ b/blueprints/runtime-workers/openhands/config/mcp.template.json @@ -65,12 +65,14 @@ "env": {} }, "ai-memory": { + "enabled": false, "url": "http://${AI_MEMORY_URL}/mcp", "transport": "http", "timeout": 120, "env": {} }, "socraticode": { + "enabled": false, "command": "${ECO_ROOT}/bin/node", "args": [ "${ECO_ROOT}/tools/socraticode-1.14.0/lib/node_modules/socraticode/dist/index.js" diff --git a/blueprints/runtime-workers/openhands/config/worker.json b/blueprints/runtime-workers/openhands/config/worker.json index 8ccd8bbef..eaa3f5e16 100644 --- a/blueprints/runtime-workers/openhands/config/worker.json +++ b/blueprints/runtime-workers/openhands/config/worker.json @@ -1,6 +1,8 @@ { "schema_version": 1, "model_env": "OPENHANDS_MODEL", + "arm_env": "OPENHANDS_ARM", + "base_url_env": "OPENHANDS_BASE_URL", "llm": { "model": "cx/gpt-6-astra-max", "model_canonical_name": "openai/gpt-6", @@ -24,7 +26,7 @@ "llm_calls": "container", "stdio_mcp": "container", "gateway_base_url": "http://10.0.2.2:20128/v1", - "published_ports": [], + "published_ports": ["127.0.0.1:3730:8000"], "max_iteration_per_run": 40, "timeout_seconds": 1200, "cpus": "4", diff --git a/blueprints/runtime-workers/openhands/dispatch.py b/blueprints/runtime-workers/openhands/dispatch.py new file mode 100644 index 000000000..2816b561f --- /dev/null +++ b/blueprints/runtime-workers/openhands/dispatch.py @@ -0,0 +1,291 @@ +"""Host-owned start/wait/result files around the native agent-server REST API. + +OpenHands/software-agent-sdk@fcc102a conversation_router.py:163-228,257-287, +304-321. This adapter does not implement an agent loop or an HTTP server. +""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path +import re +import signal +import stat +import subprocess +import tempfile +import time +import uuid + +from recipe import HERE, arm_config, read_json +from host import DOCKER, cleanup_container, docker_args, grade, logged_command, result_exit, utc_now, write_json +from receipt import create_receipt, read_bounded + + +SERVER_URL = "http://127.0.0.1:3730" +TERMINAL = {"finished", "error", "stuck"} + + +def private_file(path): + path = Path(path).absolute() + info = path.stat() + if (path.resolve() != path or not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or stat.S_IMODE(info.st_mode) != 0o600 or path.is_relative_to(HERE.parents[2])): + raise ValueError("owned_mode_0600_file_outside_checkout_required") + return path + + +def curl_json(url, *, method="GET", headers=None, body=None): + args = ["curl", "--silent", "--show-error", "--fail", "--noproxy", "*", + "--connect-timeout", "5", "--max-time", "30", "--max-filesize", "4194304", "-X", method] + if headers: + args += ["-H", "@" + str(private_file(headers))] + if body: + args += ["-H", "Content-Type: application/json", "--data-binary", "@" + str(body)] + # No credential values in argv, OTel or printed errors; response is bounded. + with tempfile.TemporaryFile() as output: + subprocess.run([*args, url], stdout=output, stderr=subprocess.DEVNULL, check=True, timeout=35) + output.seek(0) + raw = output.read(4 * 1024 * 1024 + 1) + if len(raw) > 4 * 1024 * 1024: + raise ValueError("native_response_too_large") + return json.loads(raw) + + +def api_request(method, path, *, body=None): + if not re.fullmatch(r"/api/conversations(?:/[a-f0-9-]{36}(?:/(?:agent_final_response|interrupt))?)?", path): + raise ValueError("unexpected_native_route") + return curl_json(SERVER_URL + path, method=method, headers=os.environ["OPENHANDS_HEADERS"], body=body) + + +def compression_snapshot(): + # OmniRoute@a58000c compression/route.ts:13-24, compressionAnalytics.ts:52-55. + try: + value = curl_json("http://127.0.0.1:20129/api/analytics/compression?since=all", + headers=os.environ.get("OPENHANDS_COMPRESSION_HEADERS")) + selected = {k: value[k] for k in ("totalRequests", "totalTokensSaved")} + if all(isinstance(v, int) and not isinstance(v, bool) and v >= 0 for v in selected.values()): + return selected + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + pass + return None + + +def compression_delta(before, after): + keys = ("totalRequests", "totalTokensSaved") + if (not isinstance(before, dict) or not isinstance(after, dict) + or any(not isinstance(snapshot.get(k), int) or isinstance(snapshot[k], bool) or snapshot[k] < 0 + for snapshot in (before, after) for k in keys) + or any(after[k] < before[k] for k in keys)): + return {"delta": None, "status": "unavailable_or_counter_reset"} + return {"delta": {k: after[k] - before[k] for k in keys}, "status": "observed", + "basis": "entry gateway global analytics delta; separate from call_logs usage; concurrent callers may contribute"} + + +def server_command(pins, name, mounts, network, env_file): + # Keep the image ENTRYPOINT. Its binary target is Dockerfile:580-590, not + # the source target at :571. Supported preload: __main__.py:74-135,240-273. + return [*docker_args(pins, name, network=network), *mounts, "--detach", + "--publish", "127.0.0.1:3730:8000", "--env-file", str(env_file), + "--env", "OH_ENABLE_VSCODE=0", "--env", "OH_CONVERSATIONS_PATH=/state/server/conversations", + "--env", "OH_WORKSPACE_PATH=/workspace", "--env", "OH_BASH_EVENTS_DIR=/state/server/bash_events", + "--env", "OPENHANDS_OWNED_CONTAINER=1", "--workdir", "/workspace", + pins["image"]["ref"], "--extra-python-path", "/recipe", "--import-modules", "server_transport"] + + +def check_server(body): + """Native health/docs gate, then cross-check SDK output against live OpenAPI.""" + deadline = time.monotonic() + 60 + while True: + try: + for path in ("/health", "/docs"): + subprocess.run(["curl", "-fsS", "--noproxy", "*", "--max-time", "5", "-o", os.devnull, + SERVER_URL + path], check=True, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=6) + break + except (OSError, subprocess.SubprocessError): + if time.monotonic() >= deadline: + raise RuntimeError("native_server_health_failed") + time.sleep(1) + schema = curl_json(SERVER_URL + "/openapi.json") + request = schema["paths"]["/api/conversations"]["post"]["requestBody"]["content"]["application/json"]["schema"] + if "$ref" in request: + request = schema["components"]["schemas"][request["$ref"].split("/")[-1]] + if (not set(request.get("required", [])) <= set(body) + or not set(body) <= set(request["properties"])): + raise ValueError("live_openapi_request_mismatch") + + +def stop_server(result, status): + name = status.get("server_name") + if not isinstance(name, str) or not re.fullmatch(r"rw-openhands-[a-z0-9-]+-server", name): + return False + try: + logged_command(DOCKER + ["logs", name], result / "server.log", cwd=result, timeout=30) + finally: + cleanup_container(name, result / "server.log") + return read_json(result / "server.log.cleanup.json").get("confirmed_removed") is True + + +def finish_result(result, status, window): + removed = stop_server(result, status) + window["worker_exit_code"] = 0 if status.get("execution_status") == "finished" else 1 + if window["worker_exit_code"]: + window["failure_stage"] = "agent" + checked = {"upstream_resolved": None, "grader_exit_code": None} + stage = "export" + try: + if not removed: + raise RuntimeError("worker_removal_not_confirmed") + if not window["worker_exit_code"]: + task = read_json(result / "task-identity.json") + workspace = result / "workspace" + # git/git@v2.43.0 Documentation/git.txt (GIT_CONFIG_GLOBAL/NOSYSTEM) + # and diff-options.txt (--no-ext-diff/--no-textconv). A candidate's + # .gitattributes must not activate a host-global executable filter. + git_env = {**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"} + if logged_command(["git", "-C", str(workspace), "add", "-A"], + result / "stage-patch.log", cwd=result, timeout=60, env=git_env): + raise RuntimeError("patch_export_failed") + patch_text = subprocess.check_output([ + "git", "-C", str(workspace), "--no-pager", "diff", "--no-color", "--no-ext-diff", "--no-textconv", + "--cached", task["base_commit"]], text=True, timeout=60, env=git_env) + (result / "output.jsonl").write_text(json.dumps({"instance_id": task["instance_id"], + "test_result": {"git_patch": patch_text}, "error": None}) + "\n") + datasets = list(result.glob("dataset.*")) + if len(datasets) != 1: + raise ValueError("one_frozen_dataset_required") + prefix = Path.home() / ".local/share/codex-ecosystem/tools/openhands-1.49.6" + stage = "grader" + checked = grade(prefix, result, datasets[0], task["instance_id"], window["run_id"]) + except (Exception, KeyboardInterrupt): + window["failure_stage"] = stage + write_json(result / "window.json", window) + write_json(result / "check.json", checked) + return create_receipt(result) + + +def execute(action, state, run_id, arm): + """Exactly one native POST, bounded polling, deterministic host receipt.""" + if not re.fullmatch(r"rw-openhands-[a-z0-9-]{1,64}", run_id) or arm not in {"control", "engines-on"}: + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": None, "failure_stage": "preflight", + "task_passed": False, "evidence_complete": False})) + return 3 + result = state / "runs" / run_id / arm + receipt_path = result / "receipt.json" + status, window = {}, {"run_id": run_id, "arm": arm} + stage, code, receipt = action, 0, {"task_passed": False, "evidence_complete": False, "failure_stage": None} + lock = state / "active-dispatch.json" + owns_lock = False + try: + if result.resolve() != result.absolute(): + raise ValueError("owned_result_must_not_follow_symlinks") + status = json.loads(read_bounded(result / "status.json")) + window = json.loads(read_bounded(result / "window.json")) + if window.get("run_id") != run_id or window.get("arm") != arm: + raise ValueError("attempt_identity_mismatch") + if action == "start": + if status.get("status") != "prepared": + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), + "failure_stage": "start", "task_passed": False, "evidence_complete": False})) + return 3 # Do not mutate the running attempt or release its lock. + try: + fd = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + except FileExistsError: + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), + "failure_stage": "start", "task_passed": False, "evidence_complete": False})) + return 3 # Another invocation owns the serial reservation. + with os.fdopen(fd, "w") as stream: + json.dump({"run_id": run_id, "arm": arm}, stream) + owns_lock = True + status.update(status="starting", deadline=time.time() + 1200) + write_json(result / "status.json", status) + window["compression_before"] = compression_snapshot() if arm == "engines-on" else None + window["started_at"] = utc_now() + window["finished_at"] = None + write_json(result / "window.json", window) + response = api_request("POST", "/api/conversations", body=result / "start.json") + conversation_id = str(uuid.UUID(response["id"])) + status.update(status="running", conversation_id=conversation_id) + elif action == "wait": + if status.get("status") != "running": + raise ValueError("running_attempt_required") + conversation_id = str(uuid.UUID(status["conversation_id"])) + while True: + if time.time() >= status["deadline"]: + stage = "deadline" + try: + api_request("POST", f"/api/conversations/{conversation_id}/interrupt") + finally: + raise TimeoutError("native_conversation_deadline") + response = api_request("GET", f"/api/conversations/{conversation_id}") + execution = response["execution_status"] + if execution in TERMINAL: + status.update(status="terminal", execution_status=execution) + window["finished_at"] = utc_now() + if arm == "engines-on": + window["compression"] = compression_delta(window.pop("compression_before", None), compression_snapshot()) + break + if execution not in {"idle", "running", "paused", "waiting_for_confirmation"}: + raise ValueError("unknown_native_execution_status") + time.sleep(min(15, max(0, status["deadline"] - time.time()))) + elif action == "result" and status.get("status") == "collected": + # Native result is a GET; keep host collection repeatable after the + # server has been removed, without regrading or overwriting evidence. + receipt = json.loads(read_bounded(receipt_path)) + code = result_exit(receipt) + elif action == "result": + if status.get("status") != "terminal": + raise ValueError("terminal_attempt_required") + conversation_id = str(uuid.UUID(status["conversation_id"])) + response = api_request("GET", f"/api/conversations/{conversation_id}/agent_final_response") + if not isinstance(response.get("response"), str): + raise ValueError("native_final_response_contract") + write_json(result / "final-response.json", response) + receipt = finish_result(result, status, window) + write_json(receipt_path, receipt) + code = result_exit(receipt) + status.update(status="collected") + else: + raise ValueError("unknown_dispatch_action") + write_json(result / "window.json", window) + write_json(result / "status.json", status) + except (Exception, KeyboardInterrupt) as exc: + # Never export raw exceptions (curl paths, header material, server text). + code = 3 + receipt = {"arm": arm, "failure_stage": stage, "failure_type": type(exc).__name__, + "task_passed": False, "evidence_complete": False} + if result.is_dir() and result.resolve() == result.absolute(): + write_json(receipt_path, receipt) + write_json(result / "status.json", {**status, "status": "failed", "failure_stage": stage}) + if owns_lock or action in {"wait", "result"}: + try: + stop_server(result, status) + except (OSError, ValueError, subprocess.SubprocessError): + pass # Keep primary failure; cleanup receipt records uncertainty. + finally: + if (code == 3 or action == "result") and lock.is_file(): + try: + if json.loads(read_bounded(lock)) == {"run_id": run_id, "arm": arm}: + lock.unlink() + except (OSError, ValueError): + pass + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), + **{k: receipt.get(k) for k in ("failure_stage", "task_passed", "evidence_complete")}})) + return code + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=("start", "wait", "result")) + parser.add_argument("--run-id", default=os.environ.get("OPENHANDS_RUN_ID"), required="OPENHANDS_RUN_ID" not in os.environ) + parser.add_argument("--arm", choices=("control", "engines-on"), default=os.environ.get("OPENHANDS_ARM", "control")) + args = parser.parse_args() + os.umask(0o077) + signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) + state = Path.home() / ".local/state/native-agent-stack/runtime-workers/openhands" + return execute(args.action, state, args.run_id, args.arm) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/blueprints/runtime-workers/openhands/e2e/check.py b/blueprints/runtime-workers/openhands/e2e/check.py index ea5b56a29..2e212147d 100644 --- a/blueprints/runtime-workers/openhands/e2e/check.py +++ b/blueprints/runtime-workers/openhands/e2e/check.py @@ -76,6 +76,8 @@ def main(): print(json.dumps({"transport_error": "missing_or_malformed_output", "upstream_resolved": None})) return 2 print(json.dumps(result)) + if result.get("upstream_bucket") in {"error_ids", "incomplete_ids"}: + return 3 return 0 if args.input or result["upstream_resolved"] else 1 diff --git a/blueprints/runtime-workers/openhands/e2e/docker_grader.py b/blueprints/runtime-workers/openhands/e2e/docker_grader.py index 439b6f6eb..6635108f0 100644 --- a/blueprints/runtime-workers/openhands/e2e/docker_grader.py +++ b/blueprints/runtime-workers/openhands/e2e/docker_grader.py @@ -1,13 +1,14 @@ """Run the unchanged official SWE-bench CLI with owned Docker transport. Reference: swebench==4.1.0 harness/docker_build.py:516 and -harness/run_evaluation.py:580. Only Docker creation metadata/limits are adapted. +harness/run_evaluation.py:580. Only Docker creation names/labels are adapted. No tests, scoring, patch application or report-writing logic is replaced. """ from __future__ import annotations import importlib.metadata import json +import os import re import runpy from pathlib import Path @@ -17,6 +18,16 @@ OWNER = "gpt6-omniroute-framework-integration" +def checked_image(collection, image, expected_id): + """Docker SDK Image.id binds the mutable upstream tag to a pre-pulled digest.""" + if not isinstance(expected_id, str) or not re.fullmatch(r"sha256:[a-f0-9]{64}", expected_id): + raise ValueError("pinned_grader_image_required") + actual = collection.client.images.get(image).id + if actual != expected_id: + raise ValueError("grader_image_identity_mismatch") + return actual + + def owned_container_options(options): result = dict(options) name = result.get("name", "") @@ -26,7 +37,6 @@ def owned_container_options(options): raise ValueError("grader_requires_no_ports_volumes_or_custom_networks") result["name"] = "rw-openhands-" + name result["labels"] = {**result.get("labels", {}), OWNER_KEY: OWNER} - result.update(mem_limit="8g", nano_cpus=4_000_000_000, pids_limit=384) return result @@ -36,12 +46,15 @@ def main(): from docker.models.containers import ContainerCollection from docker.models.networks import NetworkCollection from docker.models.volumes import VolumeCollection + from docker.models.images import ImageCollection from docker.api.client import APIClient create = ContainerCollection.create def owned_create(collection, *args, **kwargs): kwargs = owned_container_options(kwargs) + image = kwargs.get("image", args[0] if args else None) + checked_image(collection, image, os.environ.get("OPENHANDS_GRADER_IMAGE_ID")) # A separate host supervisor retains these exact names for cleanup. with Path("docker-created.jsonl").open("a") as stream: stream.write(json.dumps({"name": kwargs["name"]}) + "\n") @@ -54,6 +67,7 @@ def forbidden(*args, **kwargs): # Namespace swebench uses prebuilt images. Never create unlabelled build # intermediates or additional resources if the selected upstream changes. APIClient.build = forbidden + ImageCollection.pull = forbidden VolumeCollection.create = forbidden NetworkCollection.create = forbidden runpy.run_module("swebench.harness.run_evaluation", run_name="__main__") diff --git a/blueprints/runtime-workers/openhands/evidence/round3-commands.json b/blueprints/runtime-workers/openhands/evidence/round3-commands.json new file mode 100644 index 000000000..2a137a7d3 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round3-commands.json @@ -0,0 +1,258 @@ +{ + "evidence_class": "Offline synthetic and source-contract checks, not native runtime or model acceptance.", + "sanitization": "Private worktree/interpreter prefixes, the obsolete container-home prefix and the synthetic conversation identifier were replaced with labelled placeholders. Returned timings, errors, output and exit codes otherwise preserved.", + "commands": [ + { + "id": "01-arms-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate -v", + "exit_code": 1, + "output": "test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... \n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (arm='control') ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (arm='engines-on') ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'model': 'sharedgw/cx/gpt-6-astra-max'}) ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'model': 'sharedgw/claude-opus-5-5'}) ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'control', 'model': 'sharedgw/gpt-6-astra-max'}) ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'base_url': 'http://10.0.2.2:20128/v1'}) ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'control', 'base_url': 'https://example.invalid/v1'}) ... ERROR\n test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'other'}) ... ERROR\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... FAIL\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (arm='control')\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 52, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n selected = helpers.arm_config(arm)\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_recipe' has no attribute 'arm_config'. Did you mean: 'llm_config'?\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (arm='engines-on')\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 52, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n selected = helpers.arm_config(arm)\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_recipe' has no attribute 'arm_config'. Did you mean: 'llm_config'?\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'model': 'sharedgw/cx/gpt-6-astra-max'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'model': 'sharedgw/claude-opus-5-5'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'control', 'model': 'sharedgw/gpt-6-astra-max'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'engines-on', 'base_url': 'http://10.0.2.2:20128/v1'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'control', 'base_url': 'https://example.invalid/v1'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nERROR: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) (kwargs={'arm': 'other'})\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 69, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n helpers.llm_config(cfg, **kwargs)\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nTypeError: llm_config() got an unexpected keyword argument 'arm'\n\n======================================================================\nFAIL: test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 71, in test_explicit_arms_keep_one_slash_routes_and_compression_separate\n with self.assertRaises(ValueError):\n ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^\nAssertionError: ValueError not raised\n\n----------------------------------------------------------------------\nRan 1 test in 0.005s\n\nFAILED (failures=1, errors=8)\n" + }, + { + "id": "01-arms-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate -v", + "exit_code": 0, + "output": "test_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\n\n----------------------------------------------------------------------\nRan 1 test in 0.003s\n\nOK\n" + }, + { + "id": "02-receipts-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsReceiptTests -v", + "exit_code": 1, + "output": "test_cleanup_timeout_does_not_erase_primary_exit (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ERROR\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_skills_and_mcp_require_observations (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ERROR\n\n======================================================================\nERROR: test_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 387, in test_entry_gateway_usage_and_effort_are_counted_once_without_ids\n self.assertEqual(module.gateway_database(\"engines-on\"),\n ^^^^^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_receipt' has no attribute 'gateway_database'\n\n======================================================================\nERROR: test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 420, in test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected\n module.read_bounded(root / \"link\")\n ^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_receipt' has no attribute 'read_bounded'\n\n----------------------------------------------------------------------\nRan 8 tests in 0.103s\n\nFAILED (errors=2)\n" + }, + { + "id": "02-receipts-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsReceiptTests -v", + "exit_code": 0, + "output": "test_cleanup_timeout_does_not_erase_primary_exit (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_skills_and_mcp_require_observations (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\n\n----------------------------------------------------------------------\nRan 8 tests in 0.166s\n\nOK\n" + }, + { + "id": "03-worker-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent -v", + "exit_code": 1, + "output": "test_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ERROR\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ERROR\n\n======================================================================\nERROR: test_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 158, in test_worker_arm_headers_and_response_correlation_capture\n cfg = worker.worker_llm_config({\"OPENHANDS_ARM\": arm}, \"dispatch-fixture\")\n ^^^^^^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_worker' has no attribute 'worker_llm_config'\n\n======================================================================\nERROR: test_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 199, in test_native_start_request_uses_sdk_serialization_and_shared_agent\n with patch.dict(sys.modules, modules), patch.object(worker, \"build_agent\", return_value=(\"native-agent\", {}, [])), \\\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/lib/python3.13/unittest/mock.py\", line 1503, in __enter__\n original, local = self.get_original()\n ~~~~~~~~~~~~~~~~~^^\n File \"/lib/python3.13/unittest/mock.py\", line 1473, in get_original\n raise AttributeError(\n \"%s does not have the attribute %r\" % (target, name)\n )\nAttributeError: /blueprints/runtime-workers/openhands/worker.py'> does not have the attribute 'build_agent'\n\n----------------------------------------------------------------------\nRan 2 tests in 0.011s\n\nFAILED (errors=2)\n" + }, + { + "id": "03-worker-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests -v", + "exit_code": 0, + "output": "test_every_container_is_owned_and_has_no_published_port (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_immutable_artifacts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_runtime_model_override_and_native_tool_filter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\n\n----------------------------------------------------------------------\nRan 15 tests in 0.036s\n\nOK\n" + }, + { + "id": "04-security-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings -v", + "exit_code": 1, + "output": "test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... FAIL\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ERROR\ntest_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ERROR\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... FAIL\n\n======================================================================\nERROR: test_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 292, in test_mount_allowlist_checks_resolved_targets_and_secret_ancestors\n self.assertEqual(host.checked_mount(good, [good]), good.resolve())\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'checked_mount'\n\n======================================================================\nERROR: test_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 309, in test_remote_mcp_services_are_not_registered_in_model_network\n self.assertFalse(mcp[\"ai-memory\"][\"enabled\"])\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^\nKeyError: 'enabled'\n\n======================================================================\nFAIL: test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 276, in test_model_container_is_nonroot_and_network_is_explicit\n self.assertEqual(args[args.index(\"--user\") + 1], \"10001:10001\")\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: '0:0' != '10001:10001'\n- 0:0\n+ 10001:10001\n\n\n======================================================================\nFAIL: test_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 453, in test_official_grader_container_transport_preserves_upstream_settings\n self.assertEqual(set(changed) - set(original), {\"labels\"})\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: Items in the first set but not the second:\n'nano_cpus'\n'pids_limit'\n'mem_limit'\n\n----------------------------------------------------------------------\nRan 4 tests in 0.043s\n\nFAILED (failures=2, errors=2)\n" + }, + { + "id": "04-security-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings -v", + "exit_code": 0, + "output": "test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\n\n----------------------------------------------------------------------\nRan 4 tests in 0.035s\n\nOK\n" + }, + { + "id": "05-host-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v", + "exit_code": 1, + "output": "test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ERROR\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ERROR\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ERROR\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ERROR\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n======================================================================\nERROR: test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 509, in test_clone_uses_pinned_swebench_branch_mapping\n argv = host.clone_command(self.result, task, self.result / \"workspace\")\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'clone_command'. Did you mean: 'logged_command'?\n\n======================================================================\nERROR: test_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 525, in test_egress_policy_missing_or_wrong_arm_fails_before_model_start\n host.model_network({}, \"control\")\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'model_network'\n\n======================================================================\nERROR: test_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 516, in test_installer_mounts_exclude_snapshot_from_writable_paths\n mounts = host.install_mounts(prefix, state)\n ^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'install_mounts'\n\n======================================================================\nERROR: test_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 488, in test_preflight_failure_still_has_status_receipt_and_setup_exit\n code = host.run(self.result / \"prefix\", self.result, run_id=run_id, arm=\"engines-on\")\nTypeError: run() got an unexpected keyword argument 'run_id'\n\n----------------------------------------------------------------------\nRan 13 tests in 0.354s\n\nFAILED (errors=4)\n" + }, + { + "id": "05-host-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v", + "exit_code": 1, + "output": "test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ERROR\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n======================================================================\nERROR: test_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 488, in test_preflight_failure_still_has_status_receipt_and_setup_exit\n code = host.run(self.result / \"prefix\", self.result, run_id=run_id, arm=\"engines-on\")\n File \"/blueprints/runtime-workers/openhands/host.py\", line 429, in run\n from e2e.task import load_task, worker_instruction\nModuleNotFoundError: No module named 'e2e'\n\n----------------------------------------------------------------------\nRan 13 tests in 0.374s\n\nFAILED (errors=1)\n" + }, + { + "id": "05-host-green-final", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v", + "exit_code": 0, + "output": "test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 13 tests in 0.377s\n\nOK\n" + }, + { + "id": "06-install-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags -v", + "exit_code": 1, + "output": "test_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... FAIL\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ERROR\n\n======================================================================\nERROR: test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 545, in test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags\n self.assertEqual(module.checked_image(collection, \"swebench/example:latest\", \"sha256:\" + \"a\" * 64),\n ^^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_e2e_docker_grader' has no attribute 'checked_image'\n\n======================================================================\nFAIL: test_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 532, in test_install_actually_uses_narrow_mounts_and_grader_lock\n self.assertNotIn(f\"type=bind,src={prefix},dst={prefix}\", args)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'type=bind,src=/.round3-tmp/tmp7qbbp_7y/prefix,dst=/.round3-tmp/tmp7qbbp_7y/prefix' unexpectedly found in ['docker', '--context', 'rootless', 'run', '--rm', '--pull=never', '--name', 'rw-openhands-install-fca5e9b53288', '--platform', 'linux/amd64', '--label', 'com.native-agent-stack.owner=gpt6-omniroute-framework-integration', '--user', '10001:10001', '--cap-drop=ALL', '--security-opt=no-new-privileges', '--read-only', '--tmpfs', '/tmp:rw,nosuid,nodev,mode=1777', '--tmpfs', ':rw,nosuid,nodev,uid=10001,gid=10001,mode=0700', '--env', 'HOME=', '--env', 'PYTHONDONTWRITEBYTECODE=1', '--env', 'UV_PYTHON_DOWNLOADS=never', '--cpus', '4', '--memory', '8g', '--pids-limit', '384', '--network=none', '--mount', 'type=bind,src=/.round3-tmp/tmp7qbbp_7y/prefix,dst=/.round3-tmp/tmp7qbbp_7y/prefix', '--mount', 'type=bind,src=/.round3-tmp/tmp7qbbp_7y/state,dst=/state', '--mount', 'type=bind,src=/.round3-tmp/tmp7qbbp_7y/prefix/recipe,dst=/recipe,readonly', '--entrypoint', '/bin/bash', 'ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37', '/recipe/install-container.sh', '/.round3-tmp/tmp7qbbp_7y/prefix']\n\n----------------------------------------------------------------------\nRan 2 tests in 0.042s\n\nFAILED (failures=1, errors=1)\n" + }, + { + "id": "06-install-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags -v", + "exit_code": 0, + "output": "test_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\n\n----------------------------------------------------------------------\nRan 2 tests in 0.040s\n\nOK\n" + }, + { + "id": "07-dispatch-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 1, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ERROR\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ERROR\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ERROR\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ERROR\n\n======================================================================\nERROR: test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 759, in test_compression_delta_is_separate_and_missing_or_reset_is_unknown\n dispatch = load_recipe_module(\"dispatch.py\")\n File \"/tests/test_runtime_worker_openhands.py\", line 36, in load_recipe_module\n spec.loader.exec_module(module)\n ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n File \"\", line 1019, in exec_module\n File \"\", line 1156, in get_code\n File \"\", line 1214, in get_data\nFileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/dispatch.py'\n\n======================================================================\nERROR: test_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 749, in test_server_preserves_native_entrypoint_and_uses_supported_preload\n dispatch = load_recipe_module(\"dispatch.py\")\n File \"/tests/test_runtime_worker_openhands.py\", line 36, in load_recipe_module\n spec.loader.exec_module(module)\n ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n File \"\", line 1019, in exec_module\n File \"\", line 1156, in get_code\n File \"\", line 1214, in get_data\nFileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/dispatch.py'\n\n======================================================================\nERROR: test_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 716, in test_start_wait_result_use_native_routes_and_deterministic_host_files\n dispatch = load_recipe_module(\"dispatch.py\")\n File \"/tests/test_runtime_worker_openhands.py\", line 36, in load_recipe_module\n spec.loader.exec_module(module)\n ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n File \"\", line 1019, in exec_module\n File \"\", line 1156, in get_code\n File \"\", line 1214, in get_data\nFileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/dispatch.py'\n\n======================================================================\nERROR: test_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 739, in test_wait_deadline_interrupts_and_emits_setup_failure\n dispatch = load_recipe_module(\"dispatch.py\")\n File \"/tests/test_runtime_worker_openhands.py\", line 36, in load_recipe_module\n spec.loader.exec_module(module)\n ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n File \"\", line 1019, in exec_module\n File \"\", line 1156, in get_code\n File \"\", line 1214, in get_data\nFileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/dispatch.py'\n\n----------------------------------------------------------------------\nRan 4 tests in 0.009s\n\nFAILED (errors=4)\n" + }, + { + "id": "07-dispatch-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\n\n----------------------------------------------------------------------\nRan 4 tests in 0.049s\n\nOK\n" + }, + { + "id": "08-native-launch-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 1, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ERROR\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ERROR\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ERROR\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\n\n======================================================================\nERROR: test_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 806, in test_model_mount_permissions_do_not_follow_symlinks\n host.model_visible(workspace, writable=True)\n ^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'model_visible'\n\n======================================================================\nERROR: test_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 819, in test_preload_installs_dynamic_call_adapter_through_upstream_hook\n load_recipe_module(\"server_transport.py\")\n ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^\n File \"/tests/test_runtime_worker_openhands.py\", line 37, in load_recipe_module\n spec.loader.exec_module(module)\n ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^\n File \"\", line 1019, in exec_module\n File \"\", line 1156, in get_code\n File \"\", line 1214, in get_data\nFileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/server_transport.py'\n\n======================================================================\nERROR: test_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 784, in test_prepare_serializes_body_offline_then_launches_native_server\n patch.object(dispatch, \"check_server\", return_value=None), \\\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/lib/python3.13/unittest/mock.py\", line 1503, in __enter__\n original, local = self.get_original()\n ~~~~~~~~~~~~~~~~~^^\n File \"/lib/python3.13/unittest/mock.py\", line 1473, in get_original\n raise AttributeError(\n \"%s does not have the attribute %r\" % (target, name)\n )\nAttributeError: /blueprints/runtime-workers/openhands/dispatch.py'> does not have the attribute 'check_server'\n\n----------------------------------------------------------------------\nRan 7 tests in 0.062s\n\nFAILED (errors=3)\n" + }, + { + "id": "08-native-launch-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\n\n----------------------------------------------------------------------\nRan 7 tests in 0.072s\n\nOK\n" + }, + { + "id": "09-grader-boundary-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v", + "exit_code": 1, + "output": "test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ERROR\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... \n test_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='error_ids') ... FAIL\n test_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='incomplete_ids') ... FAIL\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ERROR\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... FAIL\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n======================================================================\nERROR: test_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 572, in test_frozen_grader_digest_is_checked_before_retagging\n selected = host.prepare_grader_image(self.result, self.INSTANCE)\n ^^^^^^^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'prepare_grader_image'\n\n======================================================================\nERROR: test_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 582, in test_mount_roots_match_selected_adoption_layout\n roots = host.selected_mcp_roots(Path(\"/ecosystem\"))\n ^^^^^^^^^^^^^^^^^^^^^^^\nAttributeError: module 'openhands_recipe_test_host' has no attribute 'selected_mcp_roots'\n\n======================================================================\nFAIL: test_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='error_ids')\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 375, in test_known_fail_is_not_a_successful_process_verdict\n self.assertEqual(completed.returncode, 3 if bucket in {\"error_ids\", \"incomplete_ids\"} else 1, completed.stderr)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 1 != 3 : \n\n======================================================================\nFAIL: test_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) (bucket='incomplete_ids')\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 375, in test_known_fail_is_not_a_successful_process_verdict\n self.assertEqual(completed.returncode, 3 if bucket in {\"error_ids\", \"incomplete_ids\"} else 1, completed.stderr)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 1 != 3 : \n\n======================================================================\nFAIL: test_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 588, in test_shell_entrypoints_forward_stable_dispatch_flags\n self.assertIn('\"$@\"', (RECIPE / \"run-e2e.sh\").read_text())\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: '\"$@\"' not found in '#!/usr/bin/env bash\\n# Real execution is explicitly deferred to the coordinator; no auto-install.\\nset -euo pipefail\\numask 077\\nexport PYTHONDONTWRITEBYTECODE=1\\nrecipe_dir=\"$(cd -- \"$(dirname -- \"${BASH_SOURCE[0]}\")\" && pwd)\"\\nprefix=\"$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6\"\\nstate=\"$HOME/.local/state/native-agent-stack/runtime-workers/openhands\"\\nexec python3 \"$recipe_dir/host.py\" run --prefix \"$prefix\" --state \"$state\"\\n'\n\n----------------------------------------------------------------------\nRan 18 tests in 0.399s\n\nFAILED (failures=3, errors=2)\n" + }, + { + "id": "09-grader-boundary-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v", + "exit_code": 0, + "output": "test_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 18 tests in 0.431s\n\nOK\n" + }, + { + "id": "10-lifecycle-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology -v", + "exit_code": 1, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... FAIL\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... FAIL\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... FAIL\n\n======================================================================\nFAIL: test_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 869, in test_duplicate_start_keeps_running_status_and_serial_lock\n self.assertTrue(lock.exists())\n ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^\nAssertionError: False is not true\n\n======================================================================\nFAIL: test_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 881, in test_result_refuses_export_until_container_removal_is_confirmed\n self.assertEqual(report[\"failure_stage\"], \"export\")\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'grader' != 'export'\n- grader\n+ export\n\n\n======================================================================\nFAIL: test_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 89, in test_gateway_model_and_container_topology\n self.assertEqual(config[\"runtime\"][\"published_ports\"], [\"127.0.0.1:3730:8000\"])\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: Lists differ: [] != ['127.0.0.1:3730:8000']\n\nSecond list contains 1 additional elements.\nFirst extra element 0:\n'127.0.0.1:3730:8000'\n\n- []\n+ ['127.0.0.1:3730:8000']\n\n----------------------------------------------------------------------\nRan 11 tests in 0.093s\n\nFAILED (failures=3)\n" + }, + { + "id": "10-lifecycle-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\n\n----------------------------------------------------------------------\nRan 11 tests in 0.088s\n\nOK\n" + }, + { + "id": "11-export-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff -v", + "exit_code": 1, + "output": "test_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... FAIL\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ERROR\n\n======================================================================\nERROR: test_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 907, in test_patch_export_cannot_use_host_git_filters_or_external_diff\n self.assertEqual(command.call_args.kwargs[\"env\"][\"GIT_CONFIG_GLOBAL\"], os.devnull)\n ~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^\nKeyError: 'env'\n\n======================================================================\nFAIL: test_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 507, in test_preflight_failure_still_has_status_receipt_and_setup_exit\n self.assertEqual(host.result_exit(result), expected)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 1 != 3\n\n----------------------------------------------------------------------\nRan 2 tests in 0.045s\n\nFAILED (failures=1, errors=1)\n" + }, + { + "id": "11-export-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff -v", + "exit_code": 0, + "output": "test_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\n\n----------------------------------------------------------------------\nRan 2 tests in 0.042s\n\nOK\n" + }, + { + "id": "12-host-template-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts -v", + "exit_code": 1, + "output": "test_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ERROR\n\n======================================================================\nERROR: test_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 319, in test_host_template_names_the_two_required_network_policy_receipts\n self.assertEqual(set(host[\"egress\"]), {\"control\", \"engines-on\"})\n ~~~~^^^^^^^^^^\nKeyError: 'egress'\n\n----------------------------------------------------------------------\nRan 1 test in 0.001s\n\nFAILED (errors=1)\n" + }, + { + "id": "12-host-template-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts -v", + "exit_code": 0, + "output": "test_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\n\n----------------------------------------------------------------------\nRan 1 test in 0.000s\n\nOK\n" + }, + { + "id": "13-container-home-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit -v", + "exit_code": 1, + "output": "test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... FAIL\n\n======================================================================\nFAIL: test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 289, in test_model_container_is_nonroot_and_network_is_explicit\n self.assertIn(\"HOME=/state/home\", args)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 'HOME=/state/home' not found in ['docker', '--context', 'rootless', 'run', '--rm', '--pull=never', '--name', 'rw-openhands-agent', '--platform', 'linux/amd64', '--label', 'com.native-agent-stack.owner=gpt6-omniroute-framework-integration', '--user', '10001:10001', '--cap-drop=ALL', '--security-opt=no-new-privileges', '--read-only', '--tmpfs', '/tmp:rw,nosuid,nodev,mode=1777', '--tmpfs', ':rw,nosuid,nodev,uid=10001,gid=10001,mode=0700', '--env', 'HOME=', '--env', 'PYTHONDONTWRITEBYTECODE=1', '--env', 'UV_PYTHON_DOWNLOADS=never', '--cpus', '4', '--memory', '8g', '--pids-limit', '384', '--network=rw-openhands-egress-control']\n\n----------------------------------------------------------------------\nRan 1 test in 0.022s\n\nFAILED (failures=1)\n" + }, + { + "id": "13-container-home-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit -v", + "exit_code": 0, + "output": "test_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\n\n----------------------------------------------------------------------\nRan 1 test in 0.021s\n\nOK\n" + }, + { + "id": "acceptance-unit", + "command": "python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_cleanup_timeout_does_not_erase_primary_exit (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_skills_and_mcp_require_observations (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\ntest_every_container_is_owned_and_has_no_published_port (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\ntest_immutable_artifacts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_runtime_model_override_and_native_tool_filter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\ntest_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 56 tests in 0.688s\n\nOK\n" + }, + { + "id": "acceptance-validation-initial", + "command": "python3 scripts/validate.py", + "exit_code": 1, + "output": "Publication validation failed:\nblueprints/runtime-workers/openhands/README.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/README.md: byte count mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/worker.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/worker.json: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: byte count mismatch\nblueprints/runtime-workers/openhands/host.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/host.py: byte count mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: byte count mismatch\nblueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install.sh: byte count mismatch\nblueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/pins.json: byte count mismatch\nblueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/receipt.py: byte count mismatch\nblueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/recipe.py: byte count mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch\nblueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/worker.py: byte count mismatch\ntests/test_runtime_worker_openhands.py: SHA-256 mismatch\ntests/test_runtime_worker_openhands.py: byte count mismatch\ntests/test_runtime_worker_openhands.py: contains possible local session identifier\n" + }, + { + "id": "acceptance-diff", + "command": "git diff --check", + "exit_code": 0, + "output": "" + }, + { + "id": "14-publication-fixture-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy -v", + "exit_code": 1, + "output": "test_dispatch_fixtures_pass_publication_identifier_policy (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... FAIL\n\n======================================================================\nFAIL: test_dispatch_fixtures_pass_publication_identifier_policy (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 52, in test_dispatch_fixtures_pass_publication_identifier_policy\n self.assertIsNone(pattern.search(Path(__file__).read_text()))\n ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: is not None\n\n----------------------------------------------------------------------\nRan 1 test in 0.009s\n\nFAILED (failures=1)\n" + }, + { + "id": "14-publication-fixture-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 0, + "output": "test_dispatch_fixtures_pass_publication_identifier_policy (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... ok\ntest_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\n\n----------------------------------------------------------------------\nRan 12 tests in 0.119s\n\nOK\n" + }, + { + "id": "15-grader-interpreter-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter -v", + "exit_code": 1, + "output": "test_grader_sync_keeps_the_hash_seeded_interpreter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... FAIL\n\n======================================================================\nFAIL: test_grader_sync_keeps_the_hash_seeded_interpreter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 61, in test_grader_sync_keeps_the_hash_seeded_interpreter\n self.assertIn('--python \"$grader_dir/.venv/bin/python\"', sync)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: '--python \"$grader_dir/.venv/bin/python\"' not found in 'uv sync --project \"$grader_dir\" --locked --dev --no-build-isolation --inexact'\n\n----------------------------------------------------------------------\nRan 1 test in 0.002s\n\nFAILED (failures=1)\n" + }, + { + "id": "15-grader-interpreter-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock -v", + "exit_code": 0, + "output": "test_grader_sync_keeps_the_hash_seeded_interpreter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\n\n----------------------------------------------------------------------\nRan 2 tests in 0.047s\n\nOK\n" + }, + { + "id": "acceptance-unit-before-result-retry", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_cleanup_timeout_does_not_erase_primary_exit (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_skills_and_mcp_require_observations (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\ntest_dispatch_fixtures_pass_publication_identifier_policy (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... ok\ntest_every_container_is_owned_and_has_no_published_port (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_grader_sync_keeps_the_hash_seeded_interpreter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\ntest_immutable_artifacts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_runtime_model_override_and_native_tool_filter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\ntest_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 58 tests in 0.662s\n\nOK\n" + }, + { + "id": "acceptance-validation-before-result-retry", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py", + "exit_code": 1, + "output": "Publication validation failed:\nblueprints/runtime-workers/openhands/README.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/README.md: byte count mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/worker.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/worker.json: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: byte count mismatch\nblueprints/runtime-workers/openhands/host.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/host.py: byte count mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: byte count mismatch\nblueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install.sh: byte count mismatch\nblueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/pins.json: byte count mismatch\nblueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/receipt.py: byte count mismatch\nblueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/recipe.py: byte count mismatch\nblueprints/runtime-workers/openhands/research.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/research.md: byte count mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch\nblueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/worker.py: byte count mismatch\ntests/test_runtime_worker_openhands.py: SHA-256 mismatch\ntests/test_runtime_worker_openhands.py: byte count mismatch\n" + }, + { + "id": "acceptance-static", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'\nimport ast\nimport subprocess\nfrom pathlib import Path\nfrom scripts.validate import PRIVATE_CONTENT\nroot=Path('blueprints/runtime-workers/openhands')\npaths=list(root.rglob('*.py'))+[Path('tests/test_runtime_worker_openhands.py')]\nfor path in paths: ast.parse(path.read_text(), filename=str(path))\nscripts=list(root.rglob('*.sh'))\nfor path in scripts: subprocess.run(['rtk','bash','-n',str(path)], check=True)\npaths=[root/'evidence/round3-commands.json',root/'evidence/round3-verification.md',root/'research.md',root/'README.md']\nhits=[f'{p}: {name}' for p in paths for name,pattern in PRIVATE_CONTENT if pattern.search(p.read_text())]\nprint(f'Parsed Python sources: {len(list(root.rglob(\"*.py\")))+1}; shell syntax checks: {len(scripts)}')\nprint('Publication text scan: '+ ('; '.join(hits) if hits else 'no private-content matches'))\nraise SystemExit(bool(hits))\nPY", + "exit_code": 0, + "output": "Parsed Python sources: 14; shell syntax checks: 4\nPublication text scan: no private-content matches\n" + }, + { + "id": "16-result-retry-red", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt -v", + "exit_code": 1, + "output": "test_result_retry_preserves_collected_official_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt) ... FAIL\n\n======================================================================\nFAIL: test_result_retry_preserves_collected_official_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt)\n----------------------------------------------------------------------\nTraceback (most recent call last):\n File \"/tests/test_runtime_worker_openhands.py\", line 909, in test_result_retry_preserves_collected_official_receipt\n self.assertEqual(dispatch.execute(\"result\", self.state, self.run_id, self.arm), 2)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nAssertionError: 3 != 2\n\n----------------------------------------------------------------------\nRan 1 test in 0.032s\n\nFAILED (failures=1)\n" + }, + { + "id": "16-result-retry-green", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_result_retry_preserves_collected_official_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\n\n----------------------------------------------------------------------\nRan 12 tests in 0.101s\n\nOK\n" + }, + { + "id": "acceptance-unit-final", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR=\"$PWD/.round3-tmp\" python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "output": "test_compression_delta_is_separate_and_missing_or_reset_is_unknown (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_result_retry_preserves_collected_official_receipt (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_cleanup_timeout_does_not_erase_primary_exit (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_skills_and_mcp_require_observations (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\ntest_dispatch_fixtures_pass_publication_identifier_policy (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... ok\ntest_every_container_is_owned_and_has_no_published_port (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_grader_sync_keeps_the_hash_seeded_interpreter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_host_template_names_the_two_required_network_policy_receipts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\ntest_immutable_artifacts (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_model_container_is_nonroot_and_network_is_explicit (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_runtime_model_override_and_native_tool_filter (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\ntest_clone_uses_pinned_swebench_branch_mapping (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_mount_roots_match_selected_adoption_layout (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_shared_skills_installer_is_the_only_install_path (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 59 tests in 0.647s\n\nOK\n" + }, + { + "id": "acceptance-validation-final", + "command": "rtk env PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py", + "exit_code": 1, + "output": "Publication validation failed:\nblueprints/runtime-workers/openhands/README.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/README.md: byte count mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/host.example.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/mcp.template.json: byte count mismatch\nblueprints/runtime-workers/openhands/config/worker.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/config/worker.json: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/e2e/docker_grader.py: byte count mismatch\nblueprints/runtime-workers/openhands/host.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/host.py: byte count mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install-grader.sh: byte count mismatch\nblueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/install.sh: byte count mismatch\nblueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch\nblueprints/runtime-workers/openhands/pins.json: byte count mismatch\nblueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/receipt.py: byte count mismatch\nblueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/recipe.py: byte count mismatch\nblueprints/runtime-workers/openhands/research.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/research.md: byte count mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch\nblueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch\nblueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/worker.py: byte count mismatch\ntests/test_runtime_worker_openhands.py: SHA-256 mismatch\ntests/test_runtime_worker_openhands.py: byte count mismatch\n" + }, + { + "id": "acceptance-diff-final", + "command": "rtk env GIT_OPTIONAL_LOCKS=0 git diff --check", + "exit_code": 0, + "output": "" + } + ] +} diff --git a/blueprints/runtime-workers/openhands/evidence/round3-verification.md b/blueprints/runtime-workers/openhands/evidence/round3-verification.md new file mode 100644 index 000000000..8e4cbee48 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/round3-verification.md @@ -0,0 +1,734 @@ +# OpenHands round-3 repair verification — 2026-09-27 + +This is the single repair round for draft PR #425, baseline 17460572, branch +claude/runtime-worker-openhands-20260927. The coordinator owns commits and +manifests/evidence.json. This builder changed only this recipe and its unit test; +no Git metadata operations, installations, containers, model calls or gateway +requests were performed. All Python commands set PYTHONDONTWRITEBYTECODE=1. + +The review inputs were rw3-openhands-review.json, rw3-security.json, +rw3-openhands-integrate.json and rw3-common.md. These are coordinator inputs, +not upstream acceptance evidence. The common requirements override the older +two-slash engines-on route and downstream-20128 accounting suggestion. + +**Evidence class:** offline adapter contracts, synthetic fixtures, source review +and publication checks. No unchanged upstream suite, native server execution, +official grading, host network acceptance, A/B result or token saving is claimed. +The independent trace gate remains deliberately incomplete: evidence_complete +is false, so a resolved task currently exits 2. + +[Source index and correction log](../research.md#source-index-used-by-the-repair) +names every pinned upstream implementation used below. R1–R18 refer to that +index. [README](../README.md) contains the full operational contract and the +required Workflow dispatch, Arms, Usage accounting and Security posture sections. + +## Findings and disposition + +IDs C1–C8 and S1–S12 are one-based positions in the Claude and security arrays. +A disposition of fixed means the local defect is repaired with offline evidence; +it does not imply that a native host run has passed. + +| ID | Severity | Disposition | Evidence and reason | +| --- | --- | --- | --- | +| C1 | major | Fixed | recipe.py arm_config accepts sharedgw/gpt-6-astra-max, rejects two slashes/other families, and retains control variants. Round 01. Common arm contract; R5 provider config. | +| C2 | major | Fixed | OPENHANDS_BASE_URL is validated per arm and propagated to native request/window/receipt with model and header names. R2/R5; rounds 01,03,08. | +| C3 | major | Fixed | receipt.py selects exactly one entry database, matches expected model/path, sums each returned row once and preserves unknown totals. Separate reasoning/no-reasoning counts and compression delta. R13/R14; rounds 02,07. The common contract overrides the review's 20128 engines-on advice. Entry model/database matching remains a host check. | +| C4 | major | Fixed | Native start/wait/result adapter, stable run/arm directory, early status, deterministic JSON receipt path, deadlines and distinct exits. R1–R3; rounds 05,07,08,10,11. | +| C5 | major | Declined: coordinator-owned | The user expressly forbids editing manifests/evidence.json. Run and retain validate.py; coordinator re-registers changed hashes/new evidence and obtains a clean publication pass. No hashes were bypassed. | +| C6 | minor | Fixed | Setup/preflight failures write status/receipt and return 3; official negatives return 1; incomplete evidence returns 2. R10 report contract; rounds 05,09,11. | +| C7 | minor | Fixed | clone_command queries pinned SWE-bench REPO_BASE_COMMIT_BRANCH before clone/reset. R10 python.py:271-277; round 05. | +| C8 | minor | Fixed | Official grader transport adds names/labels and identity gates, without local CPU/memory/PID limits. R10 docker_build.py:516-524; rounds 04,09. | +| S2 | major | Declined: host enforcement unverified | Remote MCPs disabled; resolved mount allowlist, scoped per-arm networks and a fresh host policy/probe receipt now fail closed. R12 supports DOCKER-USER filtering, but its rootless namespace placement/effects were not observed; no Docker or host mutation is authorized here. The operator assertion is not an implemented firewall. Grader host-service isolation also needs separate qualification while retaining official grading options. Round 04 and round 12 prove gates only. | +| S4/S5 | major, cross-cutting | Fixed for this recipe | Upstream lock hash, uv 0.12.17, hashed build tools, --locked --no-build-isolation --inexact and the explicitly seeded interpreter; automatic Python downloads disabled. R8/R9 and installed uv help; rounds 06,15. Other recipes are outside owned scope. | +| S6 | minor | Fixed | Resolve symlinks before exact tool/document allowlisting; reject home ancestors and auth-store intersections. R17 selected adoption layout; round 04. | +| S7 | minor | Declined: host build isolation | Lock/build inputs and image digests are secured, mutable pulls denied, pre-pulled identity rechecked (R8–R11; rounds 06,09,15). The grader build still executes pinned upstream code on the host. A container-built host venv/interpreter and Docker transport have not been qualified; installation/container experiments are prohibited this round. This residual is documented, not claimed eliminated. | +| S9 | minor, cross-cutting | Fixed for model containers | UID/GID 10001, capability drop, no-new-privileges, read-only root and scoped temporary home. Installer remains root only for owned offline writes. R4; rounds 04,13. Official grading options stay upstream per the common contract. | +| S10 | minor | Fixed trust vulnerability; observer deferred | Bounded descriptor-relative no-follow regular-file reads reject symlink/FIFO/oversize input. Worker trace/versions/skills are explicitly untrusted and cannot make evidence_complete true. R16/R18; round 02. An independent trace observer is not implemented or qualified; canonical REST dispatch does not export legacy standalone callback events. | +| S12 | minor | Fixed | Installer sees only worker venv/cache writable; no writable parent alias reaches the recipe snapshot. Round 06; existing hashed wheel install retained. | +| Local correction | minor | Fixed | Fixture identifiers are constructed at runtime; publication policy is reused unchanged. R17 and round 14. | + +S1/S3/S8/S11 identify other recipes and were not changed. The required +cross-cutting network, installation and hardening requirements are accounted +for above. + +## Test-first record + +For each implementation slice the test was extended before running red, then +the source was changed and green run. The test seam is the user-requested +tests.test_runtime_worker_openhands public recipe/host/dispatch boundary. +No test installs packages, starts Docker, sends a model request or uses Git to +create/stage/commit a repository. External operations are mocked. Static +installer command checks are explicitly source-contract checks. + +[round3-commands.json](round3-commands.json) retains full returned outputs and +exit codes. Only private worktree/interpreter prefixes, the obsolete container +home prefix and the synthetic conversation identifier are replaced by labelled +placeholders. Timings, failures and intermediate failed runs are preserved. +The first host implementation still failed with an import error (exit 1); +the corrected host implementation passed (exit 0). This failed condition is +retained rather than replaced by the later pass. + +| Slice | Fail first | Final pass | +| --- | --- | --- | +| 01 Arms | 1: Ran 1 test in 0.005s; FAILED (failures=1, errors=8) | 0: Ran 1 test in 0.003s; OK | +| 02 Entry-gateway accounting and untrusted inputs | 1: Ran 8 tests in 0.103s; FAILED (errors=2) | 0: Ran 8 tests in 0.166s; OK | +| 03 Native request and response headers | 1: Ran 2 tests in 0.011s; FAILED (errors=2) | 0: Ran 15 tests in 0.036s; OK | +| 04 Mounts, non-root model container, official limits | 1: Ran 4 tests in 0.043s; FAILED (failures=2, errors=2) | 0: Ran 4 tests in 0.035s; OK | +| 05 Host failure paths, clone mapping and exits | 1: Ran 13 tests in 0.354s; FAILED (errors=4) | 0: Ran 13 tests in 0.377s; OK | +| 06 Installer mounts, locks and image identity | 1: Ran 2 tests in 0.042s; FAILED (failures=1, errors=1) | 0: Ran 2 tests in 0.040s; OK | +| 07 REST dispatch | 1: Ran 4 tests in 0.009s; FAILED (errors=4) | 0: Ran 4 tests in 0.049s; OK | +| 08 Native launch and module preload | 1: Ran 7 tests in 0.062s; FAILED (errors=3) | 0: Ran 7 tests in 0.072s; OK | +| 09 Grader boundary | 1: Ran 18 tests in 0.399s; FAILED (failures=3, errors=2) | 0: Ran 18 tests in 0.431s; OK | +| 10 Lifecycle and removal | 1: Ran 11 tests in 0.093s; FAILED (failures=3) | 0: Ran 11 tests in 0.088s; OK | +| 11 Host patch export | 1: Ran 2 tests in 0.045s; FAILED (failures=1, errors=1) | 0: Ran 2 tests in 0.042s; OK | +| 12 Host network template | 1: Ran 1 test in 0.001s; FAILED (errors=1) | 0: Ran 1 test in 0.000s; OK | +| 13 Container home | 1: Ran 1 test in 0.022s; FAILED (failures=1) | 0: Ran 1 test in 0.021s; OK | +| 14 Publication-safe synthetic fixture | 1: Ran 1 test in 0.009s; FAILED (failures=1) | 0: Ran 12 tests in 0.119s; OK | +| 15 Grader interpreter selection | 1: Ran 1 test in 0.002s; FAILED (failures=1) | 0: Ran 2 tests in 0.047s; OK | +| 16 Collected result retry | 1: Ran 1 test in 0.032s; FAILED (failures=1) | 0: Ran 12 tests in 0.101s; OK | + +The initial required unit run passed 56 tests, and the initial publication run +failed with changed-file hashes plus the literal-fixture identifier. Round 14 +corrected that publication issue. Round 15 explicitly keeps uv sync on the +hash-seeded interpreter. A subsequent full run passed 58 tests; final inspection +then reproduced and repaired the collected-result retry bug in round 16. +Final acceptance results follow at the end of this +record. + +## Exact red and green commands + +All commands ran from the owned worktree. The temporary test directory is +inside the worktree and removed after verification. + +### 01: Arms + +**01-arms-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.005s + +FAILED (failures=1, errors=8) +``` + +**01-arms-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.003s + +OK +``` + +### 02: Entry-gateway accounting and untrusted inputs + +**02-receipts-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsReceiptTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 8 tests in 0.103s + +FAILED (errors=2) +``` + +**02-receipts-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsReceiptTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 8 tests in 0.166s + +OK +``` + +### 03: Native request and response headers + +**03-worker-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.011s + +FAILED (errors=2) +``` + +**03-worker-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 15 tests in 0.036s + +OK +``` + +### 04: Mounts, non-root model container, official limits + +**04-security-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 4 tests in 0.043s + +FAILED (failures=2, errors=2) +``` + +**04-security-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 4 tests in 0.035s + +OK +``` + +### 05: Host failure paths, clone mapping and exits + +**05-host-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 13 tests in 0.354s + +FAILED (errors=4) +``` + +**05-host-green**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 13 tests in 0.374s + +FAILED (errors=1) +``` + +**05-host-green-final**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 13 tests in 0.377s + +OK +``` + +### 06: Installer mounts, locks and image identity + +**06-install-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.042s + +FAILED (failures=1, errors=1) +``` + +**06-install-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.040s + +OK +``` + +### 07: REST dispatch + +**07-dispatch-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 4 tests in 0.009s + +FAILED (errors=4) +``` + +**07-dispatch-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 4 tests in 0.049s + +OK +``` + +### 08: Native launch and module preload + +**08-native-launch-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 7 tests in 0.062s + +FAILED (errors=3) +``` + +**08-native-launch-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 7 tests in 0.072s + +OK +``` + +### 09: Grader boundary + +**09-grader-boundary-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 18 tests in 0.399s + +FAILED (failures=3, errors=2) +``` + +**09-grader-boundary-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 18 tests in 0.431s + +OK +``` + +### 10: Lifecycle and removal + +**10-lifecycle-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 11 tests in 0.093s + +FAILED (failures=3) +``` + +**10-lifecycle-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_gateway_model_and_container_topology -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 11 tests in 0.088s + +OK +``` + +### 11: Host patch export + +**11-export-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.045s + +FAILED (failures=1, errors=1) +``` + +**11-export-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.042s + +OK +``` + +### 12: Host network template + +**12-host-template-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.001s + +FAILED (errors=1) +``` + +**12-host-template-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.000s + +OK +``` + +### 13: Container home + +**13-container-home-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.022s + +FAILED (failures=1) +``` + +**13-container-home-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.021s + +OK +``` + +### 14: Publication-safe synthetic fixture + +**14-publication-fixture-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.009s + +FAILED (failures=1) +``` + +**14-publication-fixture-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 12 tests in 0.119s + +OK +``` + +### 15: Grader interpreter selection + +**15-grader-interpreter-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.002s + +FAILED (failures=1) +``` + +**15-grader-interpreter-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock -v +``` + +Returned summary (complete output retained in the JSON command record): + +```text +---------------------------------------------------------------------- +Ran 2 tests in 0.047s + +OK +``` + +### 16: Preserve collected results on retry + +**16-result-retry-red**, exit 1: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt -v +``` + +Returned summary (full failure retained in round3-commands.json): + +```text +---------------------------------------------------------------------- +Ran 1 test in 0.032s + +FAILED (failures=1) +``` + +**16-result-retry-green**, exit 0: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsDispatchTests -v +``` + +Returned summary: + +```text +---------------------------------------------------------------------- +Ran 12 tests in 0.101s + +OK +``` + + +## Host verification still required + +- Coordinator re-registers changed hashes/new files; no local evidence claim + overrides the failed publication exit. +- Perform native hash-locked worker/grader installation, image entrypoint and + preload import checks, authenticated health/docs/OpenAPI and native request + acceptance. Confirm non-root access to all selected tool mounts. +- Supply the shared skills PR, frozen task/hash and per-instance image digest; + run gold-patch and known-negative official grader controls unchanged. +- Provision and probe rootless per-port filtering, including other host + services, the opposite arm and post-restart policy behavior. Qualify + candidate execution in the official grader without hiding environment + deviations. Keep rule dumps/probe output; do not synthesize a proof receipt. +- Verify each entry database/model/path and max effort, raw response header + capture including condenser/streaming behavior, missing counters, compression + snapshot auth/deltas and serial attribution. No request was sent here. +- Qualify an observer outside the model's trust boundary before enabling a + complete-evidence verdict. The present code intentionally cannot return a + final pass for a resolved task. +- Confirm invocation counts in Claude child-usage/OTel and native server logs, + actual tool/skill behavior, background wait/result, cancellation/removal and + stale-reservation recovery. + +## Final acceptance outputs + +The final required commands returned: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 TMPDIR="$PWD/.round3-tmp" python3 -m unittest tests.test_runtime_worker_openhands -v +``` + +Exit 0; returned summary (all 59 verbose test results are retained in +round3-commands.json): + +```text +---------------------------------------------------------------------- +Ran 59 tests in 0.647s + +OK +``` + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py +``` + +Exit 1; complete returned output: + +```text +Publication validation failed: +blueprints/runtime-workers/openhands/README.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/README.md: byte count mismatch +blueprints/runtime-workers/openhands/config/host.example.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/config/host.example.json: byte count mismatch +blueprints/runtime-workers/openhands/config/mcp.template.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/config/mcp.template.json: byte count mismatch +blueprints/runtime-workers/openhands/config/worker.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/config/worker.json: byte count mismatch +blueprints/runtime-workers/openhands/e2e/check.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/e2e/check.py: byte count mismatch +blueprints/runtime-workers/openhands/e2e/docker_grader.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/e2e/docker_grader.py: byte count mismatch +blueprints/runtime-workers/openhands/host.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/host.py: byte count mismatch +blueprints/runtime-workers/openhands/install-grader.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/install-grader.sh: byte count mismatch +blueprints/runtime-workers/openhands/install.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/install.sh: byte count mismatch +blueprints/runtime-workers/openhands/pins.json: SHA-256 mismatch +blueprints/runtime-workers/openhands/pins.json: byte count mismatch +blueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/receipt.py: byte count mismatch +blueprints/runtime-workers/openhands/recipe.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/recipe.py: byte count mismatch +blueprints/runtime-workers/openhands/research.md: SHA-256 mismatch +blueprints/runtime-workers/openhands/research.md: byte count mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: SHA-256 mismatch +blueprints/runtime-workers/openhands/run-e2e.sh: byte count mismatch +blueprints/runtime-workers/openhands/worker.py: SHA-256 mismatch +blueprints/runtime-workers/openhands/worker.py: byte count mismatch +tests/test_runtime_worker_openhands.py: SHA-256 mismatch +tests/test_runtime_worker_openhands.py: byte count mismatch +``` + +These are SHA256/byte-count mismatches for the 16 tracked files changed in this +repair. There are no remaining identifier, schema, reference or other +publication errors in this returned output. This is not a publication pass; +the coordinator must register hashes and new evidence, then rerun the check. + +```sh +rtk env GIT_OPTIONAL_LOCKS=0 git diff --check +``` + +Exit 0; returned output was empty. + +Additional verification returned exit 0: + +```text +Parsed Python sources: 14; shell syntax checks: 4 +Publication text scan: no private-content matches +``` + +The whole worktree cache scan found no bytecode/cache directories; the empty +owned test temporary directory was removed. No commit, staging, Git metadata +write or manifests/evidence.json edit was performed. diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index f7d1bbd1d..ae208a55e 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -8,8 +8,11 @@ from __future__ import annotations import argparse +import contextlib +import hashlib from datetime import datetime, timezone import json +import io import os from pathlib import Path import re @@ -22,7 +25,9 @@ import urllib.request import uuid -from recipe import HERE, digest, read_json, render_mcp +from recipe import HERE, arm_config, digest, llm_config, read_json, render_mcp +from e2e.task import load_task, worker_instruction +from receipt import create_receipt, read_bounded, time_value DOCKER = ["docker", "--context", "rootless"] @@ -33,7 +38,10 @@ def utc_now(): def write_json(path, value): - Path(path).write_text(json.dumps(value, indent=2) + "\n") + path = Path(path) + temporary = path.with_suffix(path.suffix + ".tmp") + temporary.write_text(json.dumps(value, indent=2) + "\n") + temporary.replace(path) def private_directory(path): @@ -56,6 +64,33 @@ def check_rootless(): raise ValueError("rootless_docker_required") +def checked_mount(source, allowed): + """Resolve before checking; only selected tool/collection roots may be bound.""" + candidate = Path(source) + if not candidate.is_absolute() or not candidate.is_dir(): + raise ValueError("existing_absolute_directory_required") + candidate = candidate.resolve() + home = Path.home().resolve() + protected = [home / part for part in (".config", ".ssh", ".codex", ".claude", ".aws", ".gnupg", + ".local/share/omniroute", ".local/share/omniroute-fw")] + protected.append(Path(os.environ.get("XDG_CONFIG_HOME", str(home / ".config"))).resolve()) + if (candidate == home or candidate in home.parents + or any(candidate == p or candidate.is_relative_to(p) or p.is_relative_to(candidate) for p in protected) + or any(part in {"credentials", ".ssh", ".codex", ".claude", ".aws", ".gnupg"} for part in candidate.parts)): + raise ValueError("broad_or_authentication_mount_refused") + if candidate not in {Path(path).resolve() for path in allowed}: + raise ValueError("mount_outside_selected_roots") + return candidate + + +def selected_mcp_roots(eco): + # adoption/bootstrap-linux.sh:240,439,457 and pins-linux-x86_64.json. + return [eco / name for name in ( + "bin", "tools/context-mode-1.0.169", "tools/qmd-2.8.3", "tools/node-24.21.0", + "python-tools/serena", "python-tools/jcodemunch-mcp", + )] + + def preflight(prefix, state): pins = read_json(HERE / "pins.json") if digest(HERE / "requirements.lock") != pins["requirements_sha256"]: @@ -93,13 +128,13 @@ def preflight(prefix, state): required = set(read_json(HERE / "config/mcp-policy.json")["qmd"]["collections"]) if set(host["qmd_collections"]) != required: raise ValueError("exact_qmd_collections_required") - for value in [*host["mcp_readonly_mounts"], *host["qmd_collections"].values()]: - candidate = Path(value) - if not candidate.is_absolute() or not candidate.is_dir(): - raise ValueError("existing_absolute_directory_required") - forbidden = (Path("/"), Path.home(), Path("/run"), Path("/var/run"), Path.home() / ".local") - if candidate.resolve() in forbidden or any(part in {".ssh", ".codex", ".claude", "credentials"} for part in candidate.parts): - raise ValueError("broad_or_authentication_mount_refused") + eco = Path(variables["ECO_ROOT"]) + for value in host["mcp_readonly_mounts"]: + checked_mount(value, selected_mcp_roots(eco)) + stack = Path(os.environ.get("OPENHANDS_STACK_ROOT", str(HERE.parents[2]))).resolve() + documents = [stack / name for name in ("docs", "adoption", "blueprints/us-equities", "catalogs/us-equities")] + for value in host["qmd_collections"].values(): + checked_mount(value, documents) check_rootless() return pins, host, mcp @@ -137,21 +172,27 @@ def workspace_skills(stack_root, workspace): return {"names": sorted(names), "manifest_sha256": digest(manifest)} -def docker_args(pins, name, *, network=True): +def docker_args(pins, name, *, network="none", installer=False): if not re.fullmatch(r"rw-openhands-[a-z0-9-]+", name): raise ValueError("owned_container_name_required") limits = read_json(HERE / "config/worker.json")["runtime"] + if network is False: + network = "none" + if network not in {"none", "rw-openhands-egress-control", "rw-openhands-egress-engines-on"}: + if not (installer and network == "bridge" and "-install-" in name): + raise ValueError("explicit_scoped_model_network_required") args = DOCKER + [ "run", "--rm", "--pull=never", "--name", name, "--platform", pins["image"]["platform"], "--label", "com.native-agent-stack.owner=gpt6-omniroute-framework-integration", - "--user", "0:0", "--cap-drop=ALL", "--security-opt=no-new-privileges", - "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", "--tmpfs", "/root:rw,nosuid,nodev,mode=0700", + "--user", "0:0" if installer else "10001:10001", "--cap-drop=ALL", "--security-opt=no-new-privileges", + "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", + "--tmpfs", "/state/home:rw,nosuid,nodev,mode=0700," + ("uid=0,gid=0" if installer else "uid=10001,gid=10001"), + "--env", "HOME=/state/home", "--env", "PYTHONDONTWRITEBYTECODE=1", "--env", "UV_PYTHON_DOWNLOADS=never", "--cpus", limits["cpus"], "--memory", limits["memory"], "--pids-limit", str(limits["pids_limit"]), ] - if not network: - args += ["--network=none"] - # No HTTP listener and no port publication; image EXPOSE is not publishing. + args += ["--network=" + network] + # Base launches publish nothing; dispatch adds only the loopback mapping. return args @@ -205,6 +246,38 @@ def logged_command(argv, logfile, *, cwd, timeout, env=None): return record["exit_code"] +def prepare_grader_image(result, instance_id): + """Bind the upstream instance tag to a coordinator-frozen registry digest. + + SWE-bench@v4.1.0 test_spec/test_spec.py:106-115 defines this tag. Docker + SDK@7.1.0 models/resource.py:28-33 provides the independently checked Id. + The coordinator pre-pulls the digest; grading cannot pull mutable tags. + """ + raw = read_bounded(os.environ["OPENHANDS_GRADER_IMAGE_FILE"], limit=1024 * 1024) + expected = os.environ["OPENHANDS_GRADER_IMAGE_SHA256"] + if hashlib.sha256(raw.encode()).hexdigest() != expected: + raise ValueError("frozen_grader_image_pin_hash_mismatch") + pin = json.loads(raw) + tag = ("swebench/sweb.eval.x86_64." + instance_id.lower() + ":latest").replace("__", "_1776_") + ref = pin.get("ref", "") + canonical = ref.removeprefix("docker.io/") + if (pin.get("instance_id") != instance_id or pin.get("tag") != tag or not pin.get("source") + or not re.fullmatch(re.escape(tag.removesuffix(":latest")) + r"@sha256:[a-f0-9]{64}", canonical)): + raise ValueError("frozen_instance_image_required") + images = json.loads(subprocess.check_output(DOCKER + ["image", "inspect", ref], text=True, timeout=30)) + if len(images) != 1: + raise ValueError("one_prepulled_image_required") + info = images[0] + if (canonical not in [v.removeprefix("docker.io/") for v in info.get("RepoDigests", [])] + or info.get("Architecture") != "amd64" or info.get("Os") != "linux" + or not re.fullmatch(r"sha256:[a-f0-9]{64}", info.get("Id", ""))): + raise ValueError("prepulled_image_identity_mismatch") + subprocess.run(DOCKER + ["tag", ref, tag], check=True, timeout=30) + selected = {"ref": ref, "tag": tag, "image_id": info["Id"], "pin_sha256": expected} + write_json(result / "grader-image.json", selected) + return selected + + def grade(prefix, result, dataset, instance_id, run_id): """Upstream conversion, then its unchanged official Docker grading CLI.""" from e2e.check import check_input, read_report @@ -220,13 +293,14 @@ def grade(prefix, result, dataset, instance_id, run_id): raise ValueError("grader_checkout_modified") source = result / "output.jsonl" check_input(source, instance_id) + image = prepare_grader_image(result, instance_id) # The SDK Docker API uses DOCKER_HOST, not the CLI's --context option. endpoint = subprocess.check_output(DOCKER + ["context", "inspect", "rootless", "--format", "{{.Endpoints.docker.Host}}"], text=True, timeout=30).strip() if not endpoint.startswith("unix://"): raise ValueError("rootless_local_docker_endpoint_required") environment = {**os.environ, "DOCKER_HOST": endpoint, "PYTHONDONTWRITEBYTECODE": "1", - "HF_HOME": str(result / "grader-cache")} + "HF_HOME": str(result / "grader-cache"), "OPENHANDS_GRADER_IMAGE_ID": image["image_id"]} convert = [str(grader / ".venv/bin/swebench-eval"), str(source), "--dataset", str(dataset), "--run-id", run_id, "--workers", "1", "--no-modal", "--skip-evaluation"] @@ -276,7 +350,7 @@ def download_verified(artifact, target): def install(prefix, state): pins, _, _ = preflight(prefix, state) - for directory in (prefix, state, state / "cache"): + for directory in (prefix, prefix / "venv", state, state / "cache"): private_directory(directory) # Source bytes are retained for reproducibility, not executed or patched. download_verified(pins["source_archive"], state / "cache/upstream.tar.gz") @@ -290,8 +364,8 @@ def install(prefix, state): shutil.copytree(HERE, prefix / "recipe", dirs_exist_ok=True, ignore=shutil.ignore_patterns("__pycache__", "*.pyc")) name = "rw-openhands-install-" + uuid.uuid4().hex[:12] - args = docker_args(pins, name) - args += mount(prefix, prefix, False) + mount(state, "/state", False) + mount(prefix / "recipe", "/recipe") + args = docker_args(pins, name, installer=True, network="bridge") + args += install_mounts(prefix, state) args += ["--entrypoint", "/bin/bash", pins["image"]["ref"], "/recipe/install-container.sh", str(prefix)] private_directory(state / "install-attempts") attempt = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "install-attempts")) @@ -300,6 +374,7 @@ def install(prefix, state): "requirements_sha256": pins["requirements_sha256"], "exit_code": code}) if code: raise RuntimeError("container_install_failed_see_private_install_log") + model_visible(prefix / "venv", writable=False) subprocess.run(["bash", str(HERE / "install-grader.sh"), str(prefix / "benchmarks"), str(state / "cache")], check=True, timeout=1200) print("Installed SDK and pinned upstream grader; model/MCP/E2E acceptance remains pending.") @@ -314,31 +389,171 @@ def runtime_mounts(host): return args -def run(prefix, state): - from e2e.task import load_task, worker_instruction - from recipe import llm_config - pins, host, mcp = preflight(prefix, state) - installed = read_json(state / "installation.json") - if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: - raise ValueError("matching_successful_installation_required") - if not os.path.lexists(prefix / "venv/bin/python"): - raise ValueError("owned_sdk_venv_missing") - cfg = read_json(HERE / "config/worker.json") - selected_model = os.environ.get(cfg["model_env"], cfg["llm"]["model"]) - llm_config(cfg, selected_model) - private_directory(state / "runs") - result = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "runs")) +def install_mounts(prefix, state): + return (mount(prefix / "venv", prefix / "venv", False) + mount(state / "cache", "/state/cache", False) + + mount(prefix / "recipe", "/recipe")) + + +def model_network(host, arm): + """Require coordinator network-policy evidence before granting any egress. + + Docker docs@4e9a575 firewall-iptables.md:22-24,48-95 defines DOCKER-USER. + Rootless namespace placement and actual rule effects require host probes. + A network name or MCP allowlist alone is never network acceptance. + """ + selection = arm_config(arm) + entry = host.get("egress", {}).get(arm, {}) + name = "rw-openhands-egress-" + arm + if entry.get("network") != name or not entry.get("policy_receipt"): + raise ValueError("verified_per_port_egress_policy_required") + path = Path(entry["policy_receipt"]) + if path.stat().st_uid != os.getuid() or stat.S_IMODE(path.stat().st_mode) != 0o600: + raise ValueError("private_host_policy_receipt_required") + proof = json.loads(read_bounded(path)) + age = datetime.now(timezone.utc) - time_value(proof["verified_at"]) + if not 0 <= age.total_seconds() <= 900: + raise ValueError("fresh_egress_verification_required") + if (proof.get("mechanism") != "docker-user-iptables" + or proof.get("allowed_tcp") != [f"10.0.2.2:{selection['gateway_port']}"] + or any(proof.get(k) is not True for k in ( + "default_deny", "host_input_denied", "ipv6_disabled", "gateway_reachable", "other_host_ports_denied"))): + raise ValueError("egress_policy_contract_mismatch") + info = json.loads(subprocess.check_output(DOCKER + ["network", "inspect", name], text=True, timeout=30))[0] + if (info.get("Id") != proof.get("network_id") or info.get("Driver") != "bridge" + or info.get("EnableIPv6") or info.get("Labels", {}).get("com.native-agent-stack.owner") + != "gpt6-omniroute-framework-integration"): + raise ValueError("egress_network_identity_mismatch") + return name + + +def clone_command(prefix, task, workspace): + # SWE-bench@v4.1.0 test_spec/python.py:271-277, including branch exceptions. + code = ("import json,sys; from importlib.metadata import version; " + "assert version('swebench') == '4.1.0'; " + "from swebench.harness.test_spec.python import REPO_BASE_COMMIT_BRANCH; " + "print(json.dumps(REPO_BASE_COMMIT_BRANCH.get(sys.argv[1], {}).get(sys.argv[2], '')))") + branch = json.loads(subprocess.check_output([ + str(prefix / "benchmarks/.venv/bin/python"), "-c", code, task["repo"], task["base_commit"]], + text=True, timeout=30)) + if not isinstance(branch, str) or (branch and not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._/-]*", branch)): + raise ValueError("invalid_upstream_branch_mapping") + return ["git", "clone", "--no-checkout", "--single-branch", *(["--branch", branch] if branch else []), + "--", "https://github.com/" + task["repo"] + ".git", str(workspace)] + + +def result_exit(receipt): + if receipt.get("failure_stage"): + return 3 + verdict = receipt.get("upstream_grader", {}) + if any(verdict.get(key) not in (None, 0) for key in ("grader_exit_code", "conversion_exit_code")): + return 3 + if verdict.get("upstream_resolved") is False: + return 1 if verdict.get("upstream_bucket") in {"unresolved_ids", "empty_patch_ids"} else 3 + if receipt.get("task_passed") and receipt.get("evidence_complete"): + return 0 + return 2 + + +def begin_attempt(state, run_id, arm): + if not re.fullmatch(r"rw-openhands-[a-z0-9-]{1,64}", run_id): + raise ValueError("owned_run_id_required") + arm_config(arm) + result = state / "runs" / run_id / arm + if result.exists(): + raise ValueError("run_id_arm_already_exists") + private_directory(result) for directory in (result / "input", result / "worker", result / "mcp"): private_directory(directory) - window = {"started_at": utc_now(), "finished_at": None, "worker_exit_code": None, "model": selected_model} + write_json(result / "status.json", {"run_id": run_id, "arm": arm, "status": "starting", + "receipt": str(result / "receipt.json")}) + return result + + +def model_visible(root, *, writable): + """Owned mount contents only; private mode-0700 attempt parent stays private. + + Rootless Docker's UID mapping is not host UID equality. The upstream image + user is 10001 (Dockerfile@fcc102a:7-9,343). Never chmod symlink targets. + """ + root = Path(root) + for path in (root, *root.rglob("*")): + if path.is_symlink(): + continue + mode = path.stat().st_mode + if stat.S_ISDIR(mode): + path.chmod(0o777 if writable else 0o755) + elif stat.S_ISREG(mode): + path.chmod((0o666 if writable else 0o644) | (0o111 if mode & 0o111 else 0)) + else: + raise ValueError("special_file_in_model_mount") + + +def prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id): + from dispatch import check_server, private_file, server_command + stem = run_id + "-" + selection["arm"] + env_file = private_file(os.environ["OPENHANDS_SERVER_ENV"]) + private_file(os.environ["OPENHANDS_HEADERS"]) + environment = ["--env", "OPENHANDS_OWNED_CONTAINER=1", + "--env", "OPENHANDS_RUN_ID=" + run_id, "--env", "OPENHANDS_ARM=" + selection["arm"], + "--env", "OPENHANDS_MODEL=" + selection["requested_model"], + "--env", "OPENHANDS_BASE_URL=" + selection["base_url"], + "--env", f"PATH={prefix}/venv/bin:" + host["variables"]["HOST_PATH"]] + render = docker_args(pins, stem + "-request") + base + mount(result / "worker", "/run-output", False) + render += environment + ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/worker.py", "--request"] + if execute_container(render, stem + "-request", result / "request.log", 120): + raise RuntimeError("native_request_serialization_failed") + # Host copy made before a model can run; later worker edits cannot change it. + body = json.loads(read_bounded(result / "worker/start.json")) + write_json(result / "start.json", body) + private_directory(result / "server") + model_visible(result / "server", writable=True) + mounts = base + environment + mount(result / "server", "/state/server", False) + mount(result / "worker", "/run-output", False) + name = stem + "-server" + try: + if logged_command(server_command(pins, name, mounts, network, env_file), + result / "server-start.log", cwd=result, timeout=60): + raise RuntimeError("native_server_launch_failed") + check_server(body) + except (Exception, KeyboardInterrupt): + cleanup_container(name, result / "server.log") + raise + write_json(result / "status.json", {"run_id": run_id, "arm": selection["arm"], "status": "prepared", + "server_name": name, "receipt": str(result / "receipt.json")}) + + +def run(prefix, state, *, run_id=None, arm=None, prepare_only=False): + run_id = run_id or os.environ.get("OPENHANDS_RUN_ID", "rw-openhands-e2e-" + uuid.uuid4().hex[:12]) + arm = arm or os.environ.get("OPENHANDS_ARM", "control") + try: + result = begin_attempt(state, run_id, arm) + except (OSError, ValueError): + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": None, "failure_stage": "preflight", + "task_passed": False, "evidence_complete": False})) + return 3 + window = {"started_at": utc_now(), "finished_at": None, "worker_exit_code": None, "arm": arm, "run_id": run_id} checked = {"upstream_resolved": None, "grader_exit_code": None} - stem = "rw-openhands-e2e-" + uuid.uuid4().hex[:12] - stage = "prepare" + stem = run_id + "-" + arm + stage = "preflight" + prepared = False + native_receipt = None try: + pins, host, mcp = preflight(prefix, state) + installed = read_json(state / "installation.json") + if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: + raise ValueError("matching_successful_installation_required") + if not os.path.lexists(prefix / "venv/bin/python"): + raise ValueError("owned_sdk_venv_missing") + cfg = read_json(HERE / "config/worker.json") + selection = arm_config(arm, os.environ.get("OPENHANDS_MODEL"), os.environ.get("OPENHANDS_BASE_URL")) + llm_config(cfg, selection["requested_model"], arm=arm, base_url=selection["base_url"]) + window.update({k: v for k, v in selection.items() if k != "headers"}) + window["header_names"] = sorted([*selection["headers"], "x-omniroute-session", "X-Correlation-Id", "Idempotency-Key"]) + network = model_network(host, arm) + stage = "prepare" original = Path(os.environ["OPENHANDS_TASK_FILE"]).resolve() task_sha = os.environ["OPENHANDS_TASK_SHA256"] task = load_task(original, task_sha) - window.update(instance_id=task["instance_id"], run_id=stem) + window.update(instance_id=task["instance_id"]) # Frozen oracle bytes are outside every worker mount. dataset = result / ("dataset" + original.suffix) shutil.copyfile(original, dataset) @@ -348,8 +563,7 @@ def run(prefix, state): "base_commit": task["base_commit"], "dataset_sha256": task_sha, }) workspace = result / "workspace" - code = logged_command(["git", "clone", "--no-checkout", "--single-branch", "--", "https://github.com/" + task["repo"] + ".git", - str(workspace)], result / "clone.log", cwd=result, timeout=300) + code = logged_command(clone_command(prefix, task, workspace), result / "clone.log", cwd=result, timeout=300) if code: raise RuntimeError("task_clone_failed") code = logged_command(["git", "-C", str(workspace), "reset", "--hard", task["base_commit"]], @@ -391,6 +605,9 @@ def run(prefix, state): serena_home = result / "mcp/serena/home" private_directory(serena_home) shutil.copyfile(HERE / "config/serena_config.yml", serena_home / "serena_config.yml") + for directory in (workspace, result / "mcp", result / "worker"): + model_visible(directory, writable=True) + model_visible(result / "input", writable=False) # Only the worker venv, not the grader checkout/environment, is mounted. base = mount(prefix / "venv", prefix / "venv") + mount(HERE, "/recipe") base += mount(result / "input", "/run-input") + mount(result / "mcp", "/state/mcp", False) @@ -403,54 +620,46 @@ def run(prefix, state): args += ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/qmd-setup.py"] if execute_container(args, stem + "-qmd", result / "qmd-setup.log", 900): raise RuntimeError("qmd_setup_failed") - stage = "agent" - args = docker_args(pins, stem + "-agent") + base + mount(result / "worker", "/run-output", False) - args += ["--workdir", "/workspace", "--env", "OPENHANDS_OWNED_CONTAINER=1", - "--env", f"PATH={prefix}/venv/bin:" + host["variables"]["HOST_PATH"], - "--env", "XDG_CACHE_HOME=/run-output/cache", "--env", "XDG_STATE_HOME=/run-output/state", - "--env", "XDG_DATA_HOME=/run-output/data", "--env", "XDG_CONFIG_HOME=/run-output/config", - "--env", cfg["model_env"] + "=" + selected_model, - "--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/worker.py"] - window["started_at"] = utc_now() - window["worker_exit_code"] = execute_container(args, stem + "-agent", result / "worker.log", - cfg["runtime"]["timeout_seconds"]) - window["finished_at"] = utc_now() - if window["worker_exit_code"]: - raise RuntimeError("worker_failed") - stage = "export" - # Native benchmarks/run_infer.py get_patch uses git add -A then cached - # diff for unfinished/uncommitted work. No candidate Python runs here. - code = logged_command(["git", "-C", str(workspace), "add", "-A"], - result / "stage-patch.log", cwd=result, timeout=60) - if code: - raise RuntimeError("patch_export_failed") - patch_text = subprocess.check_output( - ["git", "-C", str(workspace), "--no-pager", "diff", "--no-color", "--cached", task["base_commit"]], - text=True, timeout=60) - (result / "output.jsonl").write_text(json.dumps({ - "instance_id": task["instance_id"], "test_result": {"git_patch": patch_text}, "error": None, - }) + "\n") - stage = "grader" - checked = grade(prefix, result, dataset, task["instance_id"], stem) + stage = "start" + prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id) + prepared = True + write_json(result / "window.json", window) + if not prepare_only: + from dispatch import execute + with contextlib.redirect_stdout(io.StringIO()): + for action in ("start", "wait", "result"): + if execute(action, state, run_id, arm): + break + native_receipt = read_json(result / "receipt.json") except (Exception, KeyboardInterrupt) as exc: window["failure_stage"] = stage window["failure_type"] = type(exc).__name__ finally: - window["finished_at"] = window["finished_at"] or utc_now() - write_json(result / "window.json", window) - write_json(result / "check.json", checked) - from receipt import create_receipt - receipt = create_receipt(result) - write_json(result / "receipt.json", receipt) - print(json.dumps({"task_passed": receipt["task_passed"], "evidence_complete": receipt["evidence_complete"]})) - return 0 if receipt["task_passed"] and receipt["evidence_complete"] else 1 + if native_receipt is not None: + receipt = native_receipt + else: + window["finished_at"] = window["finished_at"] or utc_now() + write_json(result / "window.json", window) + write_json(result / "check.json", checked) + receipt = create_receipt(result) + write_json(result / "receipt.json", receipt) + output = {"run_id": run_id, "arm": arm, "receipt": str(result / "receipt.json"), + **{k: receipt[k] for k in ("failure_stage", "task_passed", "evidence_complete")}} + if not prepared: + write_json(result / "status.json", {**output, "status": "failed" if receipt["failure_stage"] else "finished"}) + print(json.dumps(output)) + if prepare_only and prepared: + return 0 + return result_exit(receipt) def main(): parser = argparse.ArgumentParser() - parser.add_argument("action", choices=("install", "run")) + parser.add_argument("action", choices=("install", "run", "prepare")) parser.add_argument("--prefix", required=True, type=Path) parser.add_argument("--state", required=True, type=Path) + parser.add_argument("--run-id") + parser.add_argument("--arm", choices=("control", "engines-on")) args = parser.parse_args() os.umask(0o077) # SIGTERM goes through container cleanup and the failed-attempt receipt path. @@ -458,7 +667,7 @@ def main(): if args.action == "install": install(args.prefix, args.state) return 0 - return run(args.prefix, args.state) + return run(args.prefix, args.state, run_id=args.run_id, arm=args.arm, prepare_only=args.action == "prepare") if __name__ == "__main__": diff --git a/blueprints/runtime-workers/openhands/install-grader.sh b/blueprints/runtime-workers/openhands/install-grader.sh index 7f36bdcc1..df91efc57 100644 --- a/blueprints/runtime-workers/openhands/install-grader.sh +++ b/blueprints/runtime-workers/openhands/install-grader.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# OpenHands/benchmarks 405bae7 README + Makefile: supported make build path. +# OpenHands/benchmarks@405bae7 Makefile:33-42 native uv sync; locked adaptation. # This is a coordinator host recipe; never invoked by offline contract tests. set -euo pipefail umask 077 @@ -8,6 +8,10 @@ cache_dir="$2" benchmark_pin=405bae7140d7e961a75f4910a0b2e7069731db96 sdk_pin=43376f1868ffd702746080714a59c16d3f69ec12 export PYTHONDONTWRITEBYTECODE=1 +export UV_PYTHON_DOWNLOADS=never +export UV_NO_CONFIG=1 +recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +[[ "$(uv --version | cut -d' ' -f2)" == 0.12.17 ]] export UV_CACHE_DIR="$cache_dir/grader-uv" export PRE_COMMIT_HOME="$cache_dir/grader-pre-commit" export XDG_CACHE_HOME="$cache_dir/grader" @@ -19,9 +23,18 @@ fi [[ -z "$(git -C "$grader_dir" status --porcelain --untracked-files=no)" ]] git -C "$grader_dir" submodule update --init --recursive [[ "$(git -C "$grader_dir/vendor/software-agent-sdk" rev-parse HEAD)" == "$sdk_pin" ]] -# Makefile runs uv sync --dev against its workspace/submodule and installs its -# own development hooks in this isolated checkout, never in native-agent-stack. -make -C "$grader_dir" build +# Hash of the unchanged upstream lock, read via GitHub contents at this commit. +printf '%s %s\n' 287a42d2157d044ca0f5e723fe3a05c1e4bdf6413049370360226374db6d7e8c "$grader_dir/uv.lock" | sha256sum -c - +if [[ ! -x "$grader_dir/.venv/bin/python" ]]; then + uv venv --python python3.13 "$grader_dir/.venv" +fi +# All four SDK workspace build systems at 43376f1 require setuptools>=61 + wheel. +# Hash-pinned build tools, no isolated downloads; --inexact retains those tools. +uv pip install --python "$grader_dir/.venv/bin/python" --require-hashes --no-deps \ + --only-binary :all: -r "$recipe_dir/build-requirements.lock" +# Override upstream .python-version:1 (3.12) with the hash-seeded environment; +# installed uv 0.12.17 sync --help documents --python for this selection. +uv sync --project "$grader_dir" --python "$grader_dir/.venv/bin/python" --locked --dev --no-build-isolation --inexact git -C "$grader_dir" diff --exit-code -- uv.lock pyproject.toml [[ "$(git -C "$grader_dir/vendor/software-agent-sdk" rev-parse HEAD)" == "$sdk_pin" ]] "$grader_dir/.venv/bin/python" -c 'from importlib.metadata import version; assert version("swebench") == "4.1.0"; print("swebench==4.1.0 installed; grader not executed")' diff --git a/blueprints/runtime-workers/openhands/install.sh b/blueprints/runtime-workers/openhands/install.sh index c407ba975..dd4d6ac50 100755 --- a/blueprints/runtime-workers/openhands/install.sh +++ b/blueprints/runtime-workers/openhands/install.sh @@ -5,7 +5,7 @@ umask 077 recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" -# host.py checks rootless Docker, source/requirement sha256, private host values +# host.py checks rootless Docker, the locked grader, source/requirement sha256, private host values # before downloading or installing. Project skills use the shared installer # when a task workspace is created. No sudo, no service changes. export PYTHONDONTWRITEBYTECODE=1 diff --git a/blueprints/runtime-workers/openhands/pins.json b/blueprints/runtime-workers/openhands/pins.json index d53f742ef..a888af0be 100644 --- a/blueprints/runtime-workers/openhands/pins.json +++ b/blueprints/runtime-workers/openhands/pins.json @@ -33,15 +33,21 @@ } }, "requirements_sha256": "c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56", - "build_requirements_sha256": "4485df6339240d8551488bb18f27e644e8bd55d7d50bf439ca9d9da03ab7318a", + "build_requirements_sha256": "32cff7dc47659683a19936944a47123160731a2bdb711aee78a7d63fbab205e2", "grader": { "repository": "https://github.com/OpenHands/benchmarks", "commit": "405bae7140d7e961a75f4910a0b2e7069731db96", "sdk_submodule_commit": "43376f1868ffd702746080714a59c16d3f69ec12", "swebench_version": "4.1.0", "swebench_wheel_sha256": "1243776f720047cc9e20a427f7a52b75c13a07abda6154fb60fe77f82ec8af57", - "installation": "make build", + "installation": "uv sync --locked --dev --no-build-isolation --inexact with hashed build-requirements.lock", + "uv_lock_sha256": "287a42d2157d044ca0f5e723fe3a05c1e4bdf6413049370360226374db6d7e8c", + "lock_source": "OpenHands/benchmarks@405bae7140d7e961a75f4910a0b2e7069731db96 uv.lock; Makefile:33-42", + "uv_version": "0.12.17", + "uv_source": "adoption/pins-linux-x86_64.json uv archive; astral-sh/uv@0.12.17", + "build_source": "OpenHands/software-agent-sdk@43376f1868ffd702746080714a59c16d3f69ec12 openhands-sdk/pyproject.toml:36-38; tools:26-28; agent-server:27-29; workspace:19-21", "verdict": "swebench.harness.run_evaluation official Docker tests and resolved_ids", + "evaluation_image": "Required per-instance digest in frozen OPENHANDS_GRADER_IMAGE_FILE; no task or digest selected in this worktree", "runner_up": { "repository": "https://github.com/harbor-framework/harbor", "tag": "v0.23.0", diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index b45f220f0..e69fa1c33 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -1,8 +1,8 @@ """Sanitized evidence from native SDK events, independent check and gateway rows. Uses Python's sqlite3 URI mode=ro + query_only. The only SQL SELECT reads the -nine user-authorized columns from call_logs. No schema introspection, other -tables, credentials, prompts, responses, request identifiers or client config. +authorized usage/effort columns from call_logs. Correlation IDs may be used +in memory for matching, but are never returned in receipts. """ from __future__ import annotations @@ -10,21 +10,73 @@ from collections import Counter from datetime import datetime, timezone import json +import os from pathlib import Path import re import sqlite3 +import stat -from recipe import HERE, read_json, tool_filter +from recipe import HERE, arm_config, read_json, tool_filter from e2e.check import read_report COLUMNS = ( "timestamp", "path", "status", "model", "reasoning_effort_requested", - "reasoning_effort_upstream", "tokens_in", "tokens_cache_read", "tokens_reasoning", + "reasoning_effort_upstream", "tokens_in", "tokens_cache_read", "tokens_reasoning", "correlation_id", ) EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh", "max", "auto"} +def read_bounded(path, limit=4 * 1024 * 1024): + """CPython@v3.13.15 os.open dir_fd/O_NOFOLLOW; refuse every symlink hop. + + Worker files are untrusted even if their content parses. O_NONBLOCK avoids + hanging on a malicious FIFO; fstat checks the opened object, not its name. + """ + path = Path(path).absolute() + directory = os.open(path.anchor, os.O_RDONLY | os.O_DIRECTORY) + try: + for part in path.parts[1:-1]: + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory) + os.close(directory) + directory = child + fd = os.open(path.name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + finally: + os.close(directory) + with os.fdopen(fd, "rb") as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_size > limit: + raise ValueError("receipt_input_not_bounded_regular_file") + data = stream.read(limit + 1) + if len(data) > limit: + raise ValueError("receipt_input_too_large") + return data.decode("utf-8") + + +def gateway_database(arm): + arm_config(arm) + store = "omniroute" if arm == "control" else "omniroute-fw" + return Path.home() / ".local/share" / store / "storage.sqlite" + + +def summarize_gateway(rows, selection): + matched = [row for row in rows if row.get("model") == selection["gateway_model"] + and row.get("path") == selection["gateway_path"]] + counters = ("tokens_in", "tokens_cache_read", "tokens_reasoning") + known = bool(matched) and all(isinstance(row.get(k), int) and not isinstance(row[k], bool) + and row[k] >= 0 for row in matched for k in counters) + reasoning = [row for row in matched if (row.get("tokens_reasoning") or 0) > 0] + return {"rows": [{k: row.get(k) for k in COLUMNS if k != "correlation_id"} for row in matched], + "totals": {k: sum(row[k] for row in matched) for k in counters} if known else None, + "reasoning_rows": len(reasoning), + "no_returned_reasoning_rows": sum(row.get("tokens_reasoning") == 0 for row in matched), + "unknown_reasoning_rows": sum(row.get("tokens_reasoning") is None for row in matched), + "effort_verified": bool(reasoning) and all( + row.get("reasoning_effort_requested") == row.get("reasoning_effort_upstream") == "max" + for row in reasoning), + "successful_rows": sum(row.get("status") in (200, "success", "ok") for row in matched)} + + def time_value(value): if isinstance(value, (int, float)): return datetime.fromtimestamp(value / 1000 if value > 100_000_000_000 else value, timezone.utc) @@ -34,11 +86,14 @@ def time_value(value): raise ValueError("invalid_timestamp") -def gateway_rows(database, started_at, finished_at): - """Time-window evidence only: the allowed columns cannot correlate sessions.""" +def gateway_rows(database, started_at, finished_at, correlation_ids=None): + """Read only the arm entry gateway; discard IDs after optional exact matching.""" start = time_value(started_at) end = time_value(finished_at) - uri = Path(database).resolve().as_uri() + "?mode=ro" + database = Path(database) + if not database.is_file() or database.is_symlink(): + raise ValueError("gateway_database_must_be_existing_regular_file") + uri = database.resolve().as_uri() + "?mode=ro" connection = sqlite3.connect(uri, uri=True, timeout=3) try: connection.execute("PRAGMA query_only = ON") @@ -53,7 +108,10 @@ def gateway_rows(database, started_at, finished_at): start.timestamp() * 1000, end.timestamp() * 1000)).fetchall() finally: connection.close() - return [sanitize_row(dict(zip(COLUMNS, row))) for row in rows] + records = [dict(zip(COLUMNS, row)) for row in rows] + if correlation_ids is not None: + records = [row for row in records if row["correlation_id"] in correlation_ids] + return [sanitize_row(row) for row in records] def sanitize_row(row): @@ -84,10 +142,12 @@ def observations(events, known_skills=()): known_skills = set(known_skills) completed, failed, skills = Counter(), Counter(), Counter() for event in events: - if event.get("kind") != "ObservationEvent": + if not isinstance(event, dict) or event.get("kind") != "ObservationEvent": continue name = event.get("tool_name", "") observation = event.get("observation", {}) + if not isinstance(name, str) or not isinstance(observation, dict): + continue if any(name.startswith(server + "_") for server in policy) and pattern.fullmatch(name): completed[name] += 1 if observation.get("is_error"): @@ -109,11 +169,13 @@ def create_receipt(result, database=None): trace_status = "missing" if events_path.is_file(): try: - events = [json.loads(line) for line in events_path.read_text().splitlines() if line] + events = [json.loads(line) for line in read_bounded(events_path).splitlines() if line] trace_status = "observed" if events else "empty" except (ValueError, OSError): trace_status = "unreadable" - db = database or Path.home() / ".local/share/omniroute/storage.sqlite" + selection = arm_config(window.get("arm", "control"), window.get("requested_model", window.get("model")), + window.get("base_url")) + db = database or gateway_database(selection["arm"]) rows, db_status = [], "unavailable" try: rows = gateway_rows(db, window["started_at"], window["finished_at"]) @@ -125,7 +187,7 @@ def create_receipt(result, database=None): summary = result / "worker/native-summary.json" if summary.is_file(): try: - native = read_json(summary) + native = json.loads(read_bounded(summary)) candidate = native.get("versions", {}) if isinstance(native, dict) else {} if isinstance(candidate, dict) and all(candidate.get(name) == pins["version"] for name in ("openhands-sdk", "openhands-tools")): versions = {name: candidate[name] for name in ("openhands-sdk", "openhands-tools")} @@ -145,7 +207,7 @@ def create_receipt(result, database=None): listed_skills, skills_match = [], False try: expected = read_json(result / "input/skills.json")["names"] - listed = read_json(result / "worker/skills-startup.json")["listed_skills"] + listed = json.loads(read_bounded(result / "worker/skills-startup.json"))["listed_skills"] if (isinstance(expected, list) and isinstance(listed, list) and expected and all(isinstance(name, str) and re.fullmatch(r"[a-z0-9-]+", name) for name in expected) and sorted(expected) == sorted(listed)): @@ -153,11 +215,7 @@ def create_receipt(result, database=None): except (OSError, ValueError, TypeError, KeyError): pass observed = observations(events, listed_skills) - expected_model = window.get("model") - matching_gateway_rows = [row for row in rows if ( - row["model"] == expected_model and row["path"] == "/v1/responses" - and row["status"] in (200, "success", "ok") and row["reasoning_effort_upstream"] == "max" - )] + usage = summarize_gateway(rows, selection) cleanup = [] for path in sorted(result.glob("*.log.cleanup.json")): try: @@ -165,27 +223,30 @@ def create_receipt(result, database=None): except (OSError, ValueError, AttributeError): cleanup.append(False) return { - "schema_version": 2, "evidence_class": "standalone SDK inference adapter with official SWE-bench grading", + "schema_version": 3, "evidence_class": "SDK inference adapter with official SWE-bench grading", + **{k: selection[k] for k in ("arm", "base_url", "requested_model", "gateway_model", "gateway_path")}, + "header_names": window.get("header_names", sorted(selection["headers"])), "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], "observed_versions": versions, "image": pins["image"]["ref"], "source_commit": pins["commit"], "window": {key: window[key] for key in ("started_at", "finished_at")}, "worker_exit_code": window.get("worker_exit_code"), - "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"prepare", "skills", "qmd_setup", "agent", "export", "grader"} else None, + "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"preflight", "prepare", "skills", "qmd_setup", "agent", "export", "grader", "start", "wait", "result", "deadline"} else None, "task_passed": task_passed, - "evidence_complete": bool(matching_gateway_rows) and trace_status == "observed" - and versions is not None and skills_match - and all(observed["skills_observed"].get(name, 0) > 0 for name in ("tdd", "verification-before-completion")) - and checked.get("grader_exit_code") == 0 and verdict["upstream_resolved"] is not None - and bool(cleanup) and all(cleanup), + # The terminal shares the SDK's UID and writable persistence. Source: + # SDK@fcc102a tools/terminal/terminal/subprocess_terminal.py. Without a + # separate observer these files cannot prove skill use or SDK identity. + "evidence_complete": False, + "worker_evidence_trust": "worker-reported; not independent acceptance", + "independent_trace_required": True, "skills_listed_at_start": listed_skills, "skill_listing_matches_manifest": skills_match, "container_cleanup": {"attempts": len(cleanup), "confirmed_removed": sum(cleanup), "complete": bool(cleanup) and all(cleanup)}, "gateway": { - "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, "rows": rows, - "matching_model_responses_max_rows": len(matching_gateway_rows), - "attribution": "Time window only; may include concurrent callers. Session/request identifiers are forbidden by this receipt contract.", - "totals": None, - "usage_note": "Rows are not summed or claimed as exclusive worker cost. Cache-read is a subset of input; missing counters remain null.", + "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, **usage, + "entry_port": selection["gateway_port"], + "attribution": "Time window + model + path at the arm entry gateway; concurrent callers cannot be excluded by the recipe lock.", + "usage_note": "Sum each entry row once, including failed calls; never add 20128 to 20129. Cache-read is an input subset. Missing counters keep totals null.", }, + "compression": window.get("compression", {"delta": None, "status": "unavailable"}), "trace_status": trace_status, **observed, "upstream_grader": { **{key: verdict.get(key) for key in ("upstream_resolved", "upstream_bucket", "report_sha256")}, diff --git a/blueprints/runtime-workers/openhands/recipe.py b/blueprints/runtime-workers/openhands/recipe.py index 5f5ee93a6..34ae7e989 100644 --- a/blueprints/runtime-workers/openhands/recipe.py +++ b/blueprints/runtime-workers/openhands/recipe.py @@ -69,18 +69,42 @@ def expand(value): return config -def llm_config(config, model=None): +def arm_config(arm="control", model=None, base_url=None): + """Round-3 common contract; provider prefix belongs to LiteLLM only. + + SDK@fcc102a llm/llm.py:442-446; coordinator's 2026-09-27 one-slash probe. + Entry-gateway model matching is explicit: OmniRoute logs the routed model. + """ + if arm not in {"control", "engines-on"}: + raise ValueError("unknown_arm") + port = 20128 if arm == "control" else 20129 + selected = model or ("cx/gpt-6-astra-max" if arm == "control" else "sharedgw/gpt-6-astra-max") + valid = (isinstance(selected, str) and re.fullmatch(r"cx/gpt-6(?:-[a-z0-9]+)*", selected) + if arm == "control" else selected == "sharedgw/gpt-6-astra-max") + if not valid: + raise ValueError("gateway_requires_gpt6_route_for_selected_arm") + expected_url = f"http://10.0.2.2:{port}/v1" + if base_url is not None and base_url != expected_url: + raise ValueError("base_url_must_match_arm") + return {"arm": arm, "requested_model": selected, "base_url": expected_url, + "gateway_port": port, "gateway_model": selected.split("/", 1)[1], + "gateway_path": "/v1/responses", + "headers": {"x-omniroute-compression": "allow-lossy"} if arm == "engines-on" else {}} + + +def llm_config(config, model=None, *, arm="control", base_url=None): result = dict(config["llm"]) - selected = model or result["model"] - if not isinstance(selected, str) or not re.fullmatch(r"cx/gpt-6(?:-[a-z0-9]+)*", selected): - raise ValueError("gateway_requires_gpt6_route") + selected = arm_config(arm, model, base_url) + if result.get("reasoning_effort") != "max": + raise ValueError("max_reasoning_effort_required") if result.get("temperature") is not None and result["temperature"] <= 0.1: raise ValueError("gateway_temperature_must_exceed_0_1_or_be_omitted") if result.get("response_format") is not None or result.get("native_tool_calling") is not True: raise ValueError("use_native_tool_calling_for_structured_output") # LiteLLM strips its openai provider prefix; the gateway receives selected. - result["model"] = "openai/" + selected - result["base_url"] = config["runtime"]["gateway_base_url"] + result["model"] = "openai/" + selected["requested_model"] + result["base_url"] = selected["base_url"] + result["extra_headers"] = selected["headers"] return result diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index cf50afa53..4051c95bc 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -142,3 +142,80 @@ The pending shared installer PR must still confirm its exact output contract. These new corrections remain recipe-local under the assigned file ownership; the coordinator can promote general entries into docs/harness-defaults.md. + +## Round 3: native dispatch, arm repair and trust boundaries (2026-09-27) + +Inputs were the coordinator's rw3 OpenHands review, security review, integration +research and common requirements. The common requirements supersede the older +two-slash route and downstream-20128 counting suggestions. Eight Claude findings +were present (five major, three minor); no blocker was listed. Security finding +numbers in the verification record are their one-based positions in the review. + +The installed client was inspected first: Python 3.13.15, uv 0.12.17, +curl 8.5.0, gh 2.101.0 and Git 2.43.0. Docker and OpenHands were not found on +PATH; the current interpreter had no OpenHands SDK package. These are scoped +observations, not claims that the host lacks them. The GitHub v1.49.6 release +was read, followed by pinned source files using read-only gh api calls. +Shell-network gh calls failed; Context Mode's read-only gh transport succeeded. +The skills.sh leaderboard was inspected for discovery. The installed tdd, +verification-before-completion, search-first and find-skills instructions were +used subject to the user's no-delegation/no-install rule. No package or skill +was installed, no container was started, and neither gateway was contacted. + +The existing maintained SDK/server pin remains the selected source. No new +agent loop, HTTP server, grader, model client or verdict implementation was +introduced. The new dispatch adapter performs the recommended native REST +start/get/final-response operations and retains host status, cancellation and +official grading around them. It is an integration adaptation, not unchanged +upstream execution. + +### Source index used by the repair + +Each numbered source names the repository, pin and exact file/line scope. +Abbreviated pins elsewhere in this recipe resolve to these entries. + +| ID | Primary source | Behavior used | +| --- | --- | --- | +| R1 | [OpenHands/software-agent-sdk@fcc102a697874d54a357e36004e02c95040dbdc0 openhands-agent-server/openhands/agent_server/conversation_router.py:72-86,105-228,257-321](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/conversation_router.py#L72-L321) | Native serialization example, count/search, get/final response, start and interrupt. | +| R2 | [Same SDK pin, openhands-sdk/openhands/sdk/conversation/request.py:64-72,105-147,212-228,317-347](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/conversation/request.py#L64-L347) | run=true, required workspace, bound iterations, tags/hooks and Agent validation. | +| R3 | [Same SDK pin, openhands-agent-server/openhands/agent_server/__main__.py:74-135,240-285](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/__main__.py#L74-L135) | Supported module preloading, including PyInstaller; authenticated bind default. | +| R4 | [Same SDK pin, openhands-agent-server/openhands/agent_server/docker/Dockerfile:7-9,301-305,343,570-590](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-agent-server/openhands/agent_server/docker/Dockerfile#L570-L590) | UID/GID 10001 and distinct source/binary ENTRYPOINTs. | +| R5 | [Same SDK pin, openhands-sdk/openhands/sdk/llm/llm.py:442-446,1130-1138,1588-1642](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/llm.py#L1130-L1138), [llm/options/common.py:26-46](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/llm/options/common.py#L26-L46), [agent/base.py:739-775](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/agent/base.py#L739-L775) | Raw response, public call kwargs, header merging and exact native LLM registration. | +| R6 | [Same SDK pin, openhands-sdk/openhands/sdk/utils/pydantic_secrets.py:24-37,48-68](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-sdk/openhands/sdk/utils/pydantic_secrets.py#L24-L68) | Explicit serialization context for the fixed keyless gateway placeholder. | +| R7 | [BerriAI/litellm@v1.93.0 litellm/llms/openai/responses/transformation.py:262-272](https://github.com/BerriAI/litellm/blob/v1.93.0/litellm/llms/openai/responses/transformation.py#L262-L272) | Returned headers in raw_response._hidden_params. | +| R8 | [OpenHands/benchmarks@405bae7140d7e961a75f4910a0b2e7069731db96 Makefile:33-42](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/Makefile#L33-L42), [pyproject.toml:6,97-99](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/pyproject.toml#L97-L99), [.python-version:1](https://github.com/OpenHands/benchmarks/blob/405bae7140d7e961a75f4910a0b2e7069731db96/.python-version#L1), uv.lock | Native sync, build requirements, Python selection; unchanged lock SHA256 287a42d2157d044ca0f5e723fe3a05c1e4bdf6413049370360226374db6d7e8c. Installed uv 0.12.17 sync --help confirms --python, --locked, --no-build-isolation and --inexact. | +| R9 | [OpenHands/software-agent-sdk@43376f1868ffd702746080714a59c16d3f69ec12 openhands-sdk/pyproject.toml:36-38](https://github.com/OpenHands/software-agent-sdk/blob/43376f1868ffd702746080714a59c16d3f69ec12/openhands-sdk/pyproject.toml#L36-L38); openhands-tools/pyproject.toml:26-28; openhands-agent-server/pyproject.toml:27-29; openhands-workspace/pyproject.toml:19-21 | The separate benchmark submodule requires setuptools>=61 and wheel. Retain its pin and the recipe's hashed build tools. | +| R10 | [SWE-bench/SWE-bench@v4.1.0 swebench/harness/test_spec/python.py:271-292](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/test_spec/python.py#L271-L292), [test_spec/test_spec.py:106-120](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/test_spec/test_spec.py#L106-L120), [docker_build.py:516-524](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/docker_build.py#L516-L524), [reporting.py:127-157](https://github.com/SWE-bench/SWE-bench/blob/v4.1.0/swebench/harness/reporting.py#L127-L157) | Branch exceptions, history boundary, instance tag/name, unchanged container resources and official verdict report. | +| R11 | [docker/docker-py@7.1.0 docker/models/resource.py:28-33](https://github.com/docker/docker-py/blob/7.1.0/docker/models/resource.py#L28-L33) | Image.id identity check before official container creation. | +| R12 | [docker/docs@4e9a5751518ed8223a8dcde53693badddd72604f content/manuals/engine/network/firewall-iptables.md:22-24,48-95](https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/firewall-iptables.md#L22-L95), [port-publishing.md:186-192](https://github.com/docker/docs/blob/4e9a5751518ed8223a8dcde53693badddd72604f/content/manuals/engine/network/port-publishing.md#L186-L192) | DOCKER-USER policy and internal-bridge host reachability. Rootless namespace placement and effective filtering are host gates, not established here. | +| R13 | [diegosouzapw/OmniRoute@a58000c7685f4091c7a6fd8ddf3ebce7d2ec67c3 src/lib/usage/callLogs.ts:646-653](https://github.com/diegosouzapw/OmniRoute/blob/a58000c7685f4091c7a6fd8ddf3ebce7d2ec67c3/src/lib/usage/callLogs.ts#L646-L653), [src/sse/handlers/chatHelpers.ts:1172-1184](https://github.com/diegosouzapw/OmniRoute/blob/a58000c7685f4091c7a6fd8ddf3ebce7d2ec67c3/src/sse/handlers/chatHelpers.ts#L1172-L1184) | Encrypted-reasoning effort fields and X-Correlation-Id response header. Entry database/model values follow the common contract and require verification against the host's patched build. | +| R14 | [Same OmniRoute pin, src/app/api/analytics/compression/route.ts:13-24](https://github.com/diegosouzapw/OmniRoute/blob/a58000c7685f4091c7a6fd8ddf3ebce7d2ec67c3/src/app/api/analytics/compression/route.ts#L13-L24), [src/lib/db/compressionAnalytics.ts:52-55](https://github.com/diegosouzapw/OmniRoute/blob/a58000c7685f4091c7a6fd8ddf3ebce7d2ec67c3/src/lib/db/compressionAnalytics.ts#L52-L55) | Authenticated since=all snapshot; separate totalRequests/totalTokensSaved delta. | +| R15 | [git/git@v2.43.0 Documentation/git.txt:708-724](https://github.com/git/git/blob/v2.43.0/Documentation/git.txt#L708-L724), [Documentation/diff-options.txt:830-840](https://github.com/git/git/blob/v2.43.0/Documentation/diff-options.txt#L830-L840) | Disable system/global config and external diff/textconv during host patch export. | +| R16 | [SDK@fcc102a697874d54a357e36004e02c95040dbdc0 openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:144-170](https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py#L144-L170) | Terminal inherits process environment and UID. A writable SDK trace is not independent evidence. | +| R17 | Repository baseline 17460572, adoption/bootstrap-linux.sh:240,439,457 and adoption/pins-linux-x86_64.json; scripts/validate.py:25-29 | Selected node/python-tool mount layout and publication identifier policy. No other lane's files changed. | +| R18 | [python/cpython@v3.13.15 Doc/library/os.rst:1087-1092,1272-1296,1317-1373](https://github.com/python/cpython/blob/v3.13.15/Doc/library/os.rst#L1272-L1373) | Descriptor-relative opens, platform no-follow/directory flags and descriptor stat used by the bounded reader. Linux symlink/FIFO rejection is exercised locally. | + +### Round-3 corrections and remaining decisions + +| Finding or mistaken assumption | Correction and verification path | +| --- | --- | +| Older review used sharedgw/cx/... and proposed counting engines-on at 20128. | The common contract overrides both: one slash, entry 20129 only for engines-on. Round 01/02 red-green tests; host matching remains unobserved. | +| Integration research assumed the source image ENTRYPOINT and found no per-call extension. | Pinned R3/R4 show the binary ENTRYPOINT and supported module preload. Preserve ENTRYPOINT; preload the transport wrapper. No native startup claim. | +| Default model_dump was assumed to serialize the dummy gateway key. | R6 masks secrets by default. Explicit expose_secrets serializes only the fixed placeholder; server auth is not in the request body. | +| A guessed Dockerfile path returned not-found. | Resolved the pinned repository tree to R4; the failed path was not absence evidence. | +| Guessed node22 and uv-tools paths did not match adoption. | R17 requires node24.21.0 and python-tools. Resolved-path allowlist tests cover the corrected selected roots. | +| An initial host-green run failed on a lazy e2e import after the test loader restored sys.path. | Moved the shared import to module load. Preserve both failed and passing runs in round3-commands.json. | +| Duplicate start could release the running attempt's reservation. | A red-green lifecycle control now retains status/reservation and never resubmits the POST. | +| Retrying result after collection could replace the official receipt with a transport failure. | Round 16 reproduced the overwritten-verdict path; collected results now return the retained host receipt without another API/grader call. Native result retrieval is a GET (R1). | +| Result could proceed without confirmed server removal. | Red-green export control now stops before Git/grading until the exact owned server is gone. | +| Literal synthetic UUIDs triggered the publication session-data rule. | Reuse that rule in the unit test; construct the fixture UUID at runtime. Round 14 red/green retains the original failure with the synthetic value redacted. The shared validator is unchanged. | +| Upstream .python-version selects 3.12 while the recipe seeds build tools into 3.13. | Explicit --python keeps uv sync on the seeded interpreter (R8, installed help); round 15 red/green catches omission. This prevents ambiguous selection; no interpreter replacement was executed or observed here. | +| Trace persistence shares the model terminal's UID (R16). | Bounded no-follow reads, untrusted labels and evidence_complete=false. Separate observer qualification is declined in this round because no tested source-backed deployment is available. Native dispatch does not currently export the legacy standalone callback trace. | +| Host-loopback protection cannot be inferred from rootless mode, internal networks or MCP filters. | Disable remote MCPs, require scoped networks plus fresh external probe evidence. DOCKER-USER is source-backed (R12); its host rootless realization is deferred. A JSON assertion alone is not firewall proof. | +| A pinned top-level grader is not a dependency/build lock. | Check upstream uv.lock bytes; hash-seed build tools; locked nonisolated sync with pinned uv; no pre-commit hook install. Grader host build execution remains a disclosed declined part of S7. | +| Official grading options had local memory/CPU/PID changes. | Remove them per R10/common requirements. Digest identity gates/names/labels are transport adaptations; no grading-equivalence claim until official controls run. | + +The evidence remains offline adapter/synthetic evidence. Full upstream acceptance, +independent trace qualification, rootless filtering, installed imports and model +behavior are not inferred from these tests. General anti-pattern promotion and +evidence hash registration remain coordinator-owned. diff --git a/blueprints/runtime-workers/openhands/run-e2e.sh b/blueprints/runtime-workers/openhands/run-e2e.sh index f0e70c927..d0e3aa464 100755 --- a/blueprints/runtime-workers/openhands/run-e2e.sh +++ b/blueprints/runtime-workers/openhands/run-e2e.sh @@ -6,4 +6,4 @@ export PYTHONDONTWRITEBYTECODE=1 recipe_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" prefix="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" state="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" -exec python3 "$recipe_dir/host.py" run --prefix "$prefix" --state "$state" +exec python3 "$recipe_dir/host.py" run --prefix "$prefix" --state "$state" "$@" diff --git a/blueprints/runtime-workers/openhands/server_transport.py b/blueprints/runtime-workers/openhands/server_transport.py new file mode 100644 index 000000000..0006db6da --- /dev/null +++ b/blueprints/runtime-workers/openhands/server_transport.py @@ -0,0 +1,17 @@ +"""Loaded by the native server's supported --import-modules startup option. + +SDK@fcc102a agent_server/__main__.py:74-135,240-273. Keep its ENTRYPOINT, +native LLM types, retries and serialization; reuse the per-call header adapter. +""" +import atexit +import os + +from openhands.sdk import LLM +from worker import capture_correlation, gateway_transport + +if os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": + raise RuntimeError("server_transport_requires_owned_container") + +_transport = gateway_transport(LLM, correlation_callback=capture_correlation) +_transport.__enter__() +atexit.register(_transport.__exit__, None, None, None) diff --git a/blueprints/runtime-workers/openhands/worker.py b/blueprints/runtime-workers/openhands/worker.py index cb8d32bc7..5a20542e0 100644 --- a/blueprints/runtime-workers/openhands/worker.py +++ b/blueprints/runtime-workers/openhands/worker.py @@ -24,8 +24,31 @@ def headers_for_call(static_headers): return {**(static_headers or {}), "Idempotency-Key": str(uuid.uuid4())} +def worker_llm_config(environment, dispatch_id): + cfg = read_json(HERE / "config/worker.json") + fields = llm_config(cfg, environment.get("OPENHANDS_MODEL"), + arm=environment.get("OPENHANDS_ARM", "control"), + base_url=environment.get("OPENHANDS_BASE_URL")) + fields["extra_headers"].update({"x-omniroute-session": dispatch_id, "X-Correlation-Id": dispatch_id}) + fields["usage_id"] = "agent" + return fields + + +def response_correlation(response): + # SDK@fcc102a llm.py:1130-1138 preserves raw_response. LiteLLM@v1.93.0 + # llms/openai/responses/transformation.py:262-272 preserves raw headers. + raw = getattr(response, "raw_response", None) + hidden = getattr(raw, "_hidden_params", {}) + headers = hidden.get("headers", {}) if isinstance(hidden, dict) else {} + if isinstance(headers, dict): + for key, value in headers.items(): + if key.lower() == "x-correlation-id" and isinstance(value, str) and 0 < len(value) <= 256: + return value + return None + + @contextmanager -def gateway_transport(llm_type): +def gateway_transport(llm_type, correlation_callback=None): """Scope header transport to this single worker process, preserving LLM type. SDK agent/base.py:739-775 registers only exact LLM objects. A subclass skips @@ -46,11 +69,17 @@ def call_kwargs(llm, kwargs): @wraps(original_generate) def generate(llm, *args, **kwargs): - return original_generate(llm, *args, **call_kwargs(llm, kwargs)) + response = original_generate(llm, *args, **call_kwargs(llm, kwargs)) + if correlation_callback: + correlation_callback(response_correlation(response)) + return response @wraps(original_agenerate) async def agenerate(llm, *args, **kwargs): - return await original_agenerate(llm, *args, **call_kwargs(llm, kwargs)) + response = await original_agenerate(llm, *args, **call_kwargs(llm, kwargs)) + if correlation_callback: + correlation_callback(response_correlation(response)) + return response llm_type.generate, llm_type.agenerate = generate, agenerate try: @@ -59,14 +88,10 @@ async def agenerate(llm, *args, **kwargs): llm_type.generate, llm_type.agenerate = original_generate, original_agenerate -def run_worker(): - os.umask(0o077) - if not Path("/.dockerenv").exists() or os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": - raise RuntimeError("worker_requires_owned_container") +def build_agent(environment, dispatch_id): # Imports belong inside this entry point; offline recipe tests need no SDK. - from openhands.sdk import Agent, AgentContext, Conversation, LLM, Tool + from openhands.sdk import Agent, AgentContext, LLM, Tool from openhands.sdk.context.condenser import LLMSummarizingCondenser - from openhands.sdk.hooks import HookConfig, HookDefinition, HookMatcher from openhands.sdk.mcp.config import coerce_mcp_config from openhands.sdk.skills import load_skills_from_dir from openhands.tools.file_editor import FileEditorTool @@ -78,11 +103,7 @@ def run_worker(): versions = {name: importlib.metadata.version(name) for name in ("openhands-sdk", "openhands-tools")} if any(version != pins["version"] for version in versions.values()): raise RuntimeError("installed_version_mismatch") - llm = LLM( - **llm_config(cfg, os.environ.get(cfg["model_env"])), - usage_id="agent", - extra_headers={"x-omniroute-session": str(uuid.uuid4())}, - ) + llm = LLM(**worker_llm_config(environment, dispatch_id)) condenser_fields = {k: v for k, v in cfg["condenser"].items() if k != "kind"} condenser = LLMSummarizingCondenser( llm=llm.model_copy(update={"usage_id": "condenser"}), **condenser_fields, @@ -112,14 +133,50 @@ def run_worker(): include_default_tools=["FinishTool"], mcp_config=mcp_config, filter_tools_regex=tool_filter(policy), ) - output = Path("/run-output") - output.mkdir(exist_ok=True) - hooks = HookConfig(pre_tool_use=[HookMatcher( + return agent, versions, sorted(skills) + + +def worker_hooks(): + from openhands.sdk.hooks import HookConfig, HookDefinition, HookMatcher + return HookConfig(pre_tool_use=[HookMatcher( matcher="qmd_query", hooks=[HookDefinition( command=f"{shlex.quote(sys.executable)} {shlex.quote(str(HERE / 'mcp_guard.py'))}", timeout=10, )], )]) + +def start_request(task, run_id, arm): + """Native request example: SDK@fcc102a conversation_router.py:72-86. + + expose_secrets serializes only the fixed keyless placeholder; the server + auth key is never part of this body. pydantic_secrets.py:24-37,48-68. + """ + from openhands.sdk import TextContent + from openhands.sdk.workspace import LocalWorkspace + from openhands.sdk.conversation.request import StartConversationRequest, SendMessageRequest + agent, _, _ = build_agent({**os.environ, "OPENHANDS_ARM": arm}, run_id) + request = StartConversationRequest( + agent=agent, workspace=LocalWorkspace(working_dir="/workspace"), + initial_message=SendMessageRequest(role="user", content=[TextContent(text=task)], run=True), + max_iterations=read_json(HERE / "config/worker.json")["runtime"]["max_iteration_per_run"], + hook_config=worker_hooks(), tags={"source": "ultracode", "dispatch": run_id, "arm": arm}, + ) + return request.model_dump(exclude_defaults=True, mode="json", context={"expose_secrets": True}) + + +def capture_correlation(value): + # Private, explicitly worker-reported. Never copy identifiers into receipts. + with Path("/run-output/correlation.jsonl").open("a") as stream: + stream.write(json.dumps({"correlation_id": value}) + "\n") + + +def run_worker(): + os.umask(0o077) + from openhands.sdk import Conversation + cfg = read_json(HERE / "config/worker.json") + agent, versions, skills = build_agent(os.environ, os.environ.get("OPENHANDS_RUN_ID", str(uuid.uuid4()))) + output = Path("/run-output") + output.mkdir(exist_ok=True) def record_event(event): # Native event fields, not claimed tool use inferred from a prompt/config. # Full upstream events remain in private SDK persistence. Export only the @@ -142,7 +199,7 @@ def record_event(event): }) + "\n") conversation = Conversation( agent=agent, workspace="/workspace", persistence_dir="/run-output/conversations", - callbacks=[record_event], token_callbacks=[lambda *_: None], hook_config=hooks, + callbacks=[record_event], token_callbacks=[lambda *_: None], hook_config=worker_hooks(), max_iteration_per_run=cfg["runtime"]["max_iteration_per_run"], visualizer=None, ) try: @@ -165,8 +222,13 @@ def record_event(event): def main(): if not Path("/.dockerenv").exists() or os.environ.get("OPENHANDS_OWNED_CONTAINER") != "1": raise RuntimeError("worker_requires_owned_container") + if sys.argv[1:] == ["--request"]: + body = start_request(Path("/run-input/task.txt").read_text(), + os.environ["OPENHANDS_RUN_ID"], os.environ["OPENHANDS_ARM"]) + Path("/run-output/start.json").write_text(json.dumps(body) + "\n") + return 0 from openhands.sdk import LLM - with gateway_transport(LLM): + with gateway_transport(LLM, correlation_callback=capture_correlation): return run_worker() diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 80f7fa663..bd02728c7 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -7,7 +7,10 @@ """ import asyncio +import contextlib +import io import json +import os import importlib.util from pathlib import Path import re @@ -16,12 +19,15 @@ import subprocess import sys import tempfile +from types import SimpleNamespace import unittest -from unittest.mock import patch +import uuid +from unittest.mock import patch, mock_open ROOT = Path(__file__).resolve().parents[1] RECIPE = ROOT / "blueprints/runtime-workers/openhands" +FIXTURE_CONVERSATION_ID = str(uuid.UUID(int=1)) def load_recipe_module(filename): @@ -40,6 +46,51 @@ class OpenHandsRecipeTests(unittest.TestCase): def read_json(self, relative): return json.loads((RECIPE / relative).read_text()) + def test_dispatch_fixtures_pass_publication_identifier_policy(self): + # Reuse the repository policy; synthetic native IDs must be constructed + # at runtime so published source cannot be mistaken for session state. + from scripts.validate import PRIVATE_CONTENT + pattern = dict(PRIVATE_CONTENT)["local session identifier"] + self.assertIsNone(pattern.search(Path(__file__).read_text())) + + def test_grader_sync_keeps_the_hash_seeded_interpreter(self): + # benchmarks@405bae7 .python-version:1 requests 3.12; our hashed + # build tools were seeded into 3.13. uv must retain that interpreter. + script = (RECIPE / "install-grader.sh").read_text() + sync = next(line for line in script.splitlines() if line.startswith("uv sync ")) + self.assertIn('--python "$grader_dir/.venv/bin/python"', sync) + + def test_explicit_arms_keep_one_slash_routes_and_compression_separate(self): + helpers = load_recipe_module("recipe.py") + cfg = self.read_json("config/worker.json") + for arm, model, port, headers in ( + ("control", "cx/gpt-6-astra-max", 20128, {}), + ("engines-on", "sharedgw/gpt-6-astra-max", 20129, + {"x-omniroute-compression": "allow-lossy"}), + ): + with self.subTest(arm=arm): + selected = helpers.arm_config(arm) + self.assertEqual(selected["requested_model"], model) + self.assertEqual(selected["base_url"], f"http://10.0.2.2:{port}/v1") + self.assertEqual(selected["headers"], headers) + llm = helpers.llm_config(cfg, arm=arm) + self.assertEqual(llm["model"], "openai/" + model) + self.assertEqual(llm["reasoning_effort"], "max") + self.assertEqual(llm["extra_headers"], headers) + for kwargs in ( + {"arm": "engines-on", "model": "sharedgw/cx/gpt-6-astra-max"}, + {"arm": "engines-on", "model": "sharedgw/claude-opus-5-5"}, + {"arm": "control", "model": "sharedgw/gpt-6-astra-max"}, + {"arm": "engines-on", "base_url": "http://10.0.2.2:20128/v1"}, + {"arm": "control", "base_url": "https://example.invalid/v1"}, + {"arm": "other"}, + ): + with self.subTest(kwargs=kwargs), self.assertRaises(ValueError): + helpers.llm_config(cfg, **kwargs) + cfg["llm"]["reasoning_effort"] = "high" + with self.assertRaises(ValueError): + helpers.llm_config(cfg) + def test_gateway_model_and_container_topology(self): config = self.read_json("config/worker.json") self.assertEqual(config["llm"]["base_url"], "http://127.0.0.1:20128/v1") @@ -51,7 +102,9 @@ def test_gateway_model_and_container_topology(self): self.assertNotEqual(config["llm"].get("reasoning_effort"), "auto") self.assertEqual(config["runtime"]["gateway_base_url"], "http://10.0.2.2:20128/v1") self.assertEqual(config["runtime"]["agent_loop"], "container") - self.assertEqual(config["runtime"]["published_ports"], []) + self.assertEqual(config["runtime"]["published_ports"], ["127.0.0.1:3730:8000"]) + self.assertEqual(config["arm_env"], "OPENHANDS_ARM") + self.assertEqual(config["base_url_env"], "OPENHANDS_BASE_URL") self.assertEqual(config["condenser"]["kind"], "LLMSummarizingCondenser") self.assertGreater(config["condenser"]["max_tokens"], 0) @@ -120,6 +173,62 @@ def test_fresh_call_headers_keep_session_affinity(self): self.assertEqual(first["x-omniroute-session"], second["x-omniroute-session"]) self.assertEqual(original, {"x-omniroute-session": "fixture-session"}) + def test_worker_arm_headers_and_response_correlation_capture(self): + worker = load_recipe_module("worker.py") + for arm in ("control", "engines-on"): + cfg = worker.worker_llm_config({"OPENHANDS_ARM": arm}, "dispatch-fixture") + self.assertEqual(cfg["extra_headers"]["x-omniroute-session"], "dispatch-fixture") + self.assertEqual("x-omniroute-compression" in cfg["extra_headers"], arm == "engines-on") + response = SimpleNamespace(raw_response=SimpleNamespace( + _hidden_params={"headers": {"X-Correlation-Id": "opaque-response-id"}})) + + class NativeLLM: + extra_headers = {"x-omniroute-session": "fixture"} + + def generate(self, **kwargs): + return response + + async def agenerate(self, **kwargs): + return response + + captured = [] + with worker.gateway_transport(NativeLLM, correlation_callback=captured.append): + self.assertIs(NativeLLM().generate(), response) + self.assertIs(asyncio.run(NativeLLM().agenerate()), response) + self.assertEqual(captured, ["opaque-response-id", "opaque-response-id"]) + self.assertIsNone(worker.response_correlation(SimpleNamespace(raw_response=object()))) + + def test_native_start_request_uses_sdk_serialization_and_shared_agent(self): + worker = load_recipe_module("worker.py") + created = [] + + class NativeModel: + def __init__(self, **kwargs): + self.fields = kwargs + created.append(self) + + def model_dump(self, **kwargs): + self.dump_options = kwargs + return self.fields + + modules = { + "openhands.sdk": SimpleNamespace(TextContent=NativeModel), + "openhands.sdk.workspace": SimpleNamespace(LocalWorkspace=NativeModel), + "openhands.sdk.conversation.request": SimpleNamespace( + StartConversationRequest=NativeModel, SendMessageRequest=NativeModel), + } + with patch.dict(sys.modules, modules), patch.object(worker, "build_agent", return_value=("native-agent", {}, [])), \ + patch.object(worker, "worker_hooks", return_value="native-hooks"): + body = worker.start_request("Fix the issue", "rw-openhands-fixture", "engines-on") + self.assertEqual(body["agent"], "native-agent") + self.assertEqual(body["workspace"].fields["working_dir"], "/workspace") + self.assertTrue(body["initial_message"].fields["run"]) + self.assertEqual(body["max_iterations"], 40) + self.assertEqual(body["tags"], {"source": "ultracode", "dispatch": "rw-openhands-fixture", "arm": "engines-on"}) + self.assertEqual(body["hook_config"], "native-hooks") + self.assertEqual(created[-1].dump_options, + {"exclude_defaults": True, "mode": "json", "context": {"expose_secrets": True}}) + def test_header_transport_preserves_exact_native_llm_type_and_restores_methods(self): worker = load_recipe_module("worker.py") @@ -181,6 +290,54 @@ def test_every_container_is_owned_and_has_no_published_port(self): with self.assertRaises(ValueError): host.docker_args(self.read_json("pins.json"), "unowned") + def test_model_container_is_nonroot_and_network_is_explicit(self): + host = load_recipe_module("host.py") + pins = self.read_json("pins.json") + args = host.docker_args(pins, "rw-openhands-fixture") + self.assertEqual(args[args.index("--user") + 1], "10001:10001") + self.assertIn("--network=none", args) + with self.assertRaises(ValueError): + host.docker_args(pins, "rw-openhands-agent", network="bridge") + args = host.docker_args(pins, "rw-openhands-agent", network="rw-openhands-egress-control") + self.assertIn("--network=rw-openhands-egress-control", args) + self.assertIn("--cap-drop=ALL", args) + self.assertIn("--security-opt=no-new-privileges", args) + self.assertIn("HOME=/state/home", args) + + def test_mount_allowlist_checks_resolved_targets_and_secret_ancestors(self): + host = load_recipe_module("host.py") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + eco = root / "ecosystem" + good = eco / "bin" + good.mkdir(parents=True) + self.assertEqual(host.checked_mount(good, [good]), good.resolve()) + home = root / "home" + secret = home / ".config/native-agent-stack" + secret.mkdir(parents=True) + alias = root / "alias" + alias.symlink_to(secret, target_is_directory=True) + with patch.object(Path, "home", return_value=home): + for path in (home.parent, home, home / ".config", alias, secret): + with self.subTest(path=path), self.assertRaises(ValueError): + host.checked_mount(path, [path]) + with self.assertRaises(ValueError): + host.checked_mount(eco, [good]) + + def test_remote_mcp_services_are_not_registered_in_model_network(self): + helper = load_recipe_module("recipe.py") + variables = self.read_json("config/host.example.json")["variables"] + mcp = helper.render_mcp(variables) + self.assertFalse(mcp["ai-memory"]["enabled"]) + self.assertFalse(mcp["socraticode"]["enabled"]) + + def test_host_template_names_the_two_required_network_policy_receipts(self): + host = self.read_json("config/host.example.json") + self.assertEqual(set(host["egress"]), {"control", "engines-on"}) + for arm, entry in host["egress"].items(): + self.assertEqual(entry["network"], "rw-openhands-egress-" + arm) + self.assertIn("policy_receipt", entry) + def test_qmd_guard_requires_explicit_scope_and_lexical_query(self): guard = load_recipe_module("mcp_guard.py") params = {"collections": ["foundation-docs"], "searches": [{"type": "lex", "query": "fixture"}], "rerank": False} @@ -241,7 +398,7 @@ def test_known_fail_is_not_a_successful_process_verdict(self): for bucket in ("unresolved_ids", "empty_patch_ids", "error_ids", "incomplete_ids"): with self.subTest(bucket=bucket): completed = self.invoke(self.report(bucket)) - self.assertEqual(completed.returncode, 1, completed.stderr) + self.assertEqual(completed.returncode, 3 if bucket in {"error_ids", "incomplete_ids"} else 1, completed.stderr) self.assertIs(json.loads(completed.stdout)["upstream_resolved"], False) def test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed(self): @@ -277,8 +434,8 @@ def test_input_sanity_does_not_grade_the_patch(self): def test_shared_skills_installer_is_the_only_install_path(self): host = load_recipe_module("host.py") workspace = self.result / "workspace" - (workspace / ".git/info").mkdir(parents=True) - with patch.object(host.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run: + with patch.object(host.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)) as run, \ + patch.object(Path, "open", mock_open()): host.install_workspace_skills(ROOT, workspace) self.assertEqual(run.call_args.args[0], [ sys.executable, str(ROOT / "tools/adoption/install_skills.py"), @@ -293,22 +450,13 @@ def test_shared_skills_installer_is_the_only_install_path(self): def test_project_skills_bookkeeping_is_excluded_from_worker_patch(self): host = load_recipe_module("host.py") workspace = self.result / "workspace" - subprocess.run(["git", "init", "--quiet", str(workspace)], check=True, capture_output=True) - - def installed(*args, **kwargs): - (workspace / ".agents/skills/tdd").mkdir(parents=True) - (workspace / ".agents/skills/tdd/SKILL.md").write_text("synthetic skill control\n") - (workspace / "skills-lock.json").write_text("{}\n") - return subprocess.CompletedProcess([], 0) - - with patch.object(host.subprocess, "run", side_effect=installed): + # Round 3 forbids writing Git metadata, even in synthetic checkouts. + # Verify the exported exclusion contract with an in-memory stream. + output = mock_open() + with patch.object(host.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)), \ + patch.object(Path, "open", output): host.install_workspace_skills(ROOT, workspace) - # Vercel skills 1.7.0 writes exactly these root setup paths. Ask Git - # which would be ignored; never stage or commit this synthetic checkout. - checked = subprocess.run(["git", "-C", str(workspace), "check-ignore", "--no-index", "--stdin"], - input=".agents/skills/tdd/SKILL.md\nskills-lock.json\nnested/skills-lock.json\n", - capture_output=True, text=True) - self.assertEqual(checked.stdout.splitlines(), [".agents/skills/tdd/SKILL.md", "skills-lock.json"]) + output().write.assert_called_once_with("\n/.agents/\n/skills-lock.json\n") def test_project_skills_install_refuses_task_path_conflicts(self): host = load_recipe_module("host.py") @@ -331,6 +479,7 @@ def test_official_grader_container_transport_preserves_upstream_settings(self): self.assertEqual(changed["labels"]["com.native-agent-stack.owner"], "gpt6-omniroute-framework-integration") self.assertEqual(changed["image"], original["image"]) + self.assertEqual(set(changed) - set(original), {"labels"}) self.assertEqual(original["name"], "sweb.eval.django__django-11333.attempt") for update in ({"ports": {"8000/tcp": 3710}}, {"name": "unowned"}, {"volumes": {"unowned": {}}}): with self.subTest(update=update), self.assertRaises(ValueError): @@ -357,8 +506,166 @@ def test_task_adapter_hides_oracle_and_requires_frozen_original_bytes(self): with self.assertRaises(ValueError): module.load_task(path, hashlib.sha256(path.read_bytes()).hexdigest()) + def test_preflight_failure_still_has_status_receipt_and_setup_exit(self): + host = load_recipe_module("host.py") + output = io.StringIO() + run_id = "rw-openhands-fixture" + with patch.object(host, "preflight", side_effect=ValueError("synthetic_setup_failure")), \ + contextlib.redirect_stdout(output): + code = host.run(self.result / "prefix", self.result, run_id=run_id, arm="engines-on") + self.assertEqual(code, 3) + returned = json.loads(output.getvalue()) + path = self.result / "runs" / run_id / "engines-on" + self.assertEqual(returned["receipt"], str(path / "receipt.json")) + self.assertEqual(returned["run_id"], run_id) + self.assertEqual(returned["arm"], "engines-on") + self.assertEqual(returned["failure_stage"], "preflight") + self.assertFalse(returned["task_passed"]) + self.assertEqual(json.loads((path / "status.json").read_text())["status"], "failed") + self.assertTrue((path / "receipt.json").is_file()) + for result, expected in (({"task_passed": True, "evidence_complete": True}, 0), + ({"task_passed": True, "evidence_complete": False}, 2), + ({"upstream_grader": {"upstream_resolved": False, "upstream_bucket": "unresolved_ids"}}, 1), + ({"upstream_grader": {"upstream_resolved": False, "upstream_bucket": "unresolved_ids", "grader_exit_code": 124}}, 3), + ({"failure_stage": "preflight"}, 3)): + self.assertEqual(host.result_exit(result), expected) + + def test_clone_uses_pinned_swebench_branch_mapping(self): + host = load_recipe_module("host.py") + task = {"repo": "pytest-dev/pytest", "base_commit": "a" * 40} + with patch.object(host.subprocess, "check_output", return_value='"4.6.x"\n') as call: + argv = host.clone_command(self.result, task, self.result / "workspace") + self.assertEqual(argv[argv.index("--branch") + 1], "4.6.x") + self.assertIn("REPO_BASE_COMMIT_BRANCH", call.call_args.args[0][2]) + + def test_installer_mounts_exclude_snapshot_from_writable_paths(self): + host = load_recipe_module("host.py") + prefix, state = self.result / "prefix", self.result / "state" + mounts = host.install_mounts(prefix, state) + writable = [v for v in mounts if v.startswith("type=bind") and not v.endswith(",readonly")] + self.assertEqual(len(writable), 2) + self.assertTrue(any(f"src={prefix}/venv," in v for v in writable)) + self.assertFalse(any(f"src={prefix}," in v for v in writable)) + + def test_install_actually_uses_narrow_mounts_and_grader_lock(self): + host = load_recipe_module("host.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + prefix, state = self.result / "prefix", self.result / "state" + with patch.object(host, "preflight", return_value=(pins, {}, {})), \ + patch.object(host, "download_verified"), patch.object(host.subprocess, "run"), \ + patch.object(host.subprocess, "check_output", return_value="sha256:" + pins["image"]["config_sha256"]), \ + patch.object(host, "execute_container", return_value=0) as execute, contextlib.redirect_stdout(io.StringIO()): + host.install(prefix, state) + args = execute.call_args.args[0] + self.assertNotIn(f"type=bind,src={prefix},dst={prefix}", args) + self.assertIn(f"type=bind,src={prefix}/venv,dst={prefix}/venv", args) + self.assertIn("--network=bridge", args) + script = (RECIPE / "install-grader.sh").read_text() + for flag in ("--locked", "--no-build-isolation", "--require-hashes", "UV_PYTHON_DOWNLOADS=never"): + self.assertIn(flag, script) + self.assertNotIn('make -C "$grader_dir" build', script) + self.assertRegex(pins["grader"]["uv_lock_sha256"], r"^[a-f0-9]{64}$") + + def test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags(self): + module = load_recipe_module("e2e/docker_grader.py") + collection = SimpleNamespace(client=SimpleNamespace(images=SimpleNamespace( + get=lambda image: SimpleNamespace(id="sha256:" + "a" * 64)))) + self.assertEqual(module.checked_image(collection, "swebench/example:latest", "sha256:" + "a" * 64), + "sha256:" + "a" * 64) + with self.assertRaises(ValueError): + module.checked_image(collection, "swebench/example:latest", "sha256:" + "b" * 64) + with self.assertRaises(ValueError): + module.checked_image(collection, "swebench/example:latest", None) + self.assertIn("ImageCollection.pull = forbidden", (RECIPE / "e2e/docker_grader.py").read_text()) + + def test_egress_policy_missing_or_wrong_arm_fails_before_model_start(self): + host = load_recipe_module("host.py") + with self.assertRaises(ValueError): + host.model_network({}, "control") + with self.assertRaises(ValueError): + host.model_network({"egress": {"control": {"network": "bridge"}}}, "control") + + def test_frozen_grader_digest_is_checked_before_retagging(self): + host = load_recipe_module("host.py") + ref = "docker.io/swebench/sweb.eval.x86_64.django_1776_django-11333@sha256:" + "a" * 64 + tag = "swebench/sweb.eval.x86_64.django_1776_django-11333:latest" + pinfile = self.result / "image.json" + pinfile.write_text(json.dumps({"instance_id": self.INSTANCE, "ref": ref, "tag": tag, + "source": "frozen registry digest fixture"})) + environment = {"OPENHANDS_GRADER_IMAGE_FILE": str(pinfile), "OPENHANDS_GRADER_IMAGE_SHA256": host.digest(pinfile)} + info = [{"Id": "sha256:" + "b" * 64, "RepoDigests": [ref], "Architecture": "amd64", "Os": "linux"}] + with patch.dict(os.environ, environment), patch.object(host.subprocess, "check_output", return_value=json.dumps(info)), \ + patch.object(host.subprocess, "run") as run: + selected = host.prepare_grader_image(self.result, self.INSTANCE) + self.assertEqual(selected["image_id"], info[0]["Id"]) + self.assertEqual(run.call_args.args[0][-3:], ["tag", ref, tag]) + with patch.dict(os.environ, environment), patch.object(host.subprocess, "check_output", return_value='[]'), \ + patch.object(host.subprocess, "run") as run, self.assertRaises(ValueError): + host.prepare_grader_image(self.result, self.INSTANCE) + run.assert_not_called() + + def test_mount_roots_match_selected_adoption_layout(self): + host = load_recipe_module("host.py") + roots = host.selected_mcp_roots(Path("/ecosystem")) + self.assertIn(Path("/ecosystem/tools/node-24.21.0"), roots) + self.assertIn(Path("/ecosystem/python-tools/serena"), roots) + self.assertNotIn(Path("/ecosystem"), roots) + + def test_shell_entrypoints_forward_stable_dispatch_flags(self): + self.assertIn('"$@"', (RECIPE / "run-e2e.sh").read_text()) + self.assertIn("PYTHONDONTWRITEBYTECODE=1", (RECIPE / "install.sh").read_text()) + self.assertIn("locked grader", (RECIPE / "install.sh").read_text()) + class OpenHandsReceiptTests(unittest.TestCase): + def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): + module = load_recipe_module("receipt.py") + helpers = load_recipe_module("recipe.py") + self.assertEqual(module.gateway_database("engines-on"), + Path.home() / ".local/share/omniroute-fw/storage.sqlite") + rows = [dict(timestamp="2026-09-27T18:00:01Z", path="/v1/responses", status=200, + model="gpt-6-astra-max", tokens_in=30, tokens_cache_read=20, + tokens_reasoning=4, reasoning_effort_requested="max", + reasoning_effort_upstream="max", correlation_id="private-id"), + dict(timestamp="2026-09-27T18:00:02Z", path="/v1/responses", status=200, + model="gpt-6-astra-max", tokens_in=10, tokens_cache_read=0, + tokens_reasoning=0, reasoning_effort_requested=None, + reasoning_effort_upstream=None, correlation_id="another-private-id")] + for arm in ("control", "engines-on"): + selection = helpers.arm_config(arm) + summary = module.summarize_gateway(rows, selection) + self.assertEqual(summary["totals"], {"tokens_in": 40, "tokens_cache_read": 20, "tokens_reasoning": 4}) + self.assertEqual(summary["reasoning_rows"], 1) + self.assertEqual(summary["no_returned_reasoning_rows"], 1) + self.assertTrue(summary["effort_verified"]) + self.assertNotIn("private-id", json.dumps(summary)) + rows[0]["tokens_in"] = None + self.assertIsNone(module.summarize_gateway(rows, selection)["totals"]) + rows[0]["tokens_in"] = 30 + rows[0]["reasoning_effort_upstream"] = "high" + self.assertFalse(module.summarize_gateway(rows, selection)["effort_verified"]) + rows[0]["reasoning_effort_upstream"] = "max" + self.assertIsNone(module.summarize_gateway([], selection)["totals"]) + + def test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "target").write_text("sensitive") + (root / "link").symlink_to(root / "target") + with self.assertRaises((ValueError, OSError)): + module.read_bounded(root / "link") + (root / "parent").symlink_to(root, target_is_directory=True) + with self.assertRaises((ValueError, OSError)): + module.read_bounded(root / "parent/target") + with self.assertRaises(ValueError): + module.read_bounded(root / "target", limit=2) + (root / "window.json").write_text(json.dumps({"started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:01:00Z"})) + (root / "check.json").write_text("{}") + report = module.create_receipt(root, database=root / "missing.sqlite") + self.assertEqual(report["worker_evidence_trust"], "worker-reported; not independent acceptance") + self.assertFalse(report["evidence_complete"]) + def test_local_pass_field_is_never_a_worker_verdict(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: @@ -422,17 +729,17 @@ def test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage(self) with tempfile.TemporaryDirectory() as tmp: db = Path(tmp) / "fixture.sqlite" connection = sqlite3.connect(db) - connection.execute("CREATE TABLE call_logs (timestamp, path, status, model, reasoning_effort_requested, reasoning_effort_upstream, tokens_in, tokens_cache_read, tokens_reasoning, forbidden_prompt)") + connection.execute("CREATE TABLE call_logs (timestamp, path, status, model, reasoning_effort_requested, reasoning_effort_upstream, tokens_in, tokens_cache_read, tokens_reasoning, correlation_id, forbidden_prompt)") connection.execute("CREATE TABLE forbidden_credentials (secret)") - row = ("2026-09-27T18:00:01Z", "/v1/responses", 200, "cx/gpt-6-astra-max", "max", "max", 30, 20, None, "private") - connection.execute("INSERT INTO call_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", row) + row = ("2026-09-27T18:00:01Z", "/v1/responses", 200, "cx/gpt-6-astra-max", "max", "max", 30, 20, None, "private-id", "private") + connection.execute("INSERT INTO call_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", row) connection.commit() connection.close() before = db.read_bytes() rows = module.gateway_rows(db, "2026-09-27T18:00:00Z", "2026-09-27T18:00:02Z") self.assertEqual(db.read_bytes(), before) self.assertEqual(len(rows), 1) - self.assertEqual(set(rows[0]), set(module.COLUMNS)) + self.assertEqual(set(rows[0]), set(module.COLUMNS) - {"correlation_id"}) self.assertIsNone(rows[0]["tokens_reasoning"]) self.assertNotIn("private", json.dumps(rows)) self.assertEqual(module.gateway_rows(db, "2026-09-27T19:00:00Z", "2026-09-27T19:00:01Z"), []) @@ -452,5 +759,199 @@ def test_skills_and_mcp_require_observations(self): self.assertEqual(report["skills_observed"], {"tdd": 1}) +class OpenHandsDispatchTests(unittest.TestCase): + """Synthetic native REST transport, not an agent-server/model execution.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.state = Path(self.tmp.name) + self.run_id = "rw-openhands-fixture" + self.arm = "engines-on" + self.result = self.state / "runs" / self.run_id / self.arm + self.result.mkdir(parents=True) + + def test_start_wait_result_use_native_routes_and_deterministic_host_files(self): + dispatch = load_recipe_module("dispatch.py") + (self.result / "status.json").write_text(json.dumps({"status": "prepared", "server_name": "rw-openhands-fixture-engines-on-server"})) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + (self.result / "start.json").write_text("{}") + replies = [{"id": FIXTURE_CONVERSATION_ID}, {"execution_status": "finished"}, {"response": "Done"}] + output = io.StringIO() + with patch.object(dispatch, "api_request", side_effect=replies) as api, \ + patch.object(dispatch, "compression_snapshot", return_value={"totalRequests": 5, "totalTokensSaved": 12}), \ + patch.object(dispatch, "finish_result", return_value={"task_passed": True, "evidence_complete": False, "failure_stage": None}), \ + contextlib.redirect_stdout(output): + self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 0) + self.assertEqual(dispatch.execute("wait", self.state, self.run_id, self.arm), 0) + self.assertEqual(dispatch.execute("result", self.state, self.run_id, self.arm), 2) + routes = [call.args[:2] for call in api.call_args_list] + self.assertEqual(routes, [("POST", "/api/conversations"), + ("GET", f"/api/conversations/{FIXTURE_CONVERSATION_ID}"), + ("GET", f"/api/conversations/{FIXTURE_CONVERSATION_ID}/agent_final_response")]) + for row in output.getvalue().splitlines(): + self.assertEqual(json.loads(row)["receipt"], str(self.result / "receipt.json")) + self.assertEqual(json.loads((self.result / "status.json").read_text())["status"], "collected") + self.assertNotIn(FIXTURE_CONVERSATION_ID, (self.result / "receipt.json").read_text()) + + def test_wait_deadline_interrupts_and_emits_setup_failure(self): + dispatch = load_recipe_module("dispatch.py") + (self.result / "status.json").write_text(json.dumps({"status": "running", "conversation_id": FIXTURE_CONVERSATION_ID, "deadline": 0})) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + with patch.object(dispatch, "api_request", return_value={}) as api, \ + patch.object(dispatch, "stop_server"), contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(dispatch.execute("wait", self.state, self.run_id, self.arm), 3) + self.assertEqual(api.call_args.args[:2], ("POST", f"/api/conversations/{FIXTURE_CONVERSATION_ID}/interrupt")) + self.assertEqual(json.loads((self.result / "receipt.json").read_text())["failure_stage"], "deadline") + + def test_server_preserves_native_entrypoint_and_uses_supported_preload(self): + dispatch = load_recipe_module("dispatch.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + args = dispatch.server_command(pins, "rw-openhands-fixture-server", [], + "rw-openhands-egress-control", "/private/server.env") + self.assertNotIn("--entrypoint", args) + self.assertIn("127.0.0.1:3730:8000", args) + self.assertEqual(args[-4:], ["--extra-python-path", "/recipe", "--import-modules", "server_transport"]) + self.assertIn("--env-file", args) + + def test_compression_delta_is_separate_and_missing_or_reset_is_unknown(self): + dispatch = load_recipe_module("dispatch.py") + before = {"totalRequests": 3, "totalTokensSaved": 10} + after = {"totalRequests": 5, "totalTokensSaved": 25} + self.assertEqual(dispatch.compression_delta(before, after)["delta"], {"totalRequests": 2, "totalTokensSaved": 15}) + self.assertIsNone(dispatch.compression_delta(None, after)["delta"]) + self.assertIsNone(dispatch.compression_delta(after, before)["delta"]) + + def test_prepare_serializes_body_offline_then_launches_native_server(self): + host = load_recipe_module("host.py") + dispatch = load_recipe_module("dispatch.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + (self.result / "worker").mkdir() + selection = load_recipe_module("recipe.py").arm_config("engines-on") + calls = [] + + def rendered(args, name, log, timeout): + calls.append(args) + (self.result / "worker/start.json").write_text(json.dumps({"agent": {"llm": {}}, "workspace": {}, "initial_message": {}})) + return 0 + + with patch.dict(sys.modules, {"dispatch": dispatch}), \ + patch.object(host, "execute_container", side_effect=rendered), \ + patch.object(host, "logged_command", return_value=0) as launch, \ + patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ + patch.object(dispatch, "check_server", return_value=None), \ + patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}): + host.prepare_native_dispatch(self.result, self.state / "prefix", pins, [], + "rw-openhands-egress-engines-on", {"variables": {"HOST_PATH": "/usr/bin"}}, + selection, self.run_id) + self.assertIn("--network=none", calls[0]) + self.assertIn("OPENHANDS_ARM=engines-on", calls[0]) + self.assertEqual(calls[0][-1], "--request") + launched = launch.call_args.args[0] + self.assertNotIn("--entrypoint", launched) + self.assertTrue((self.result / "start.json").exists()) + self.assertEqual(json.loads((self.result / "status.json").read_text())["status"], "prepared") + + def test_model_mount_permissions_do_not_follow_symlinks(self): + host = load_recipe_module("host.py") + workspace = self.result / "workspace" + workspace.mkdir(mode=0o700) + (workspace / "owned").write_text("fixture") + private = self.result / "private" + private.write_text("private fixture") + private.chmod(0o600) + (workspace / "link").symlink_to(private) + host.model_visible(workspace, writable=True) + self.assertEqual(workspace.stat().st_mode & 0o777, 0o777) + self.assertEqual((workspace / "owned").stat().st_mode & 0o777, 0o666) + self.assertEqual(private.stat().st_mode & 0o777, 0o600) + + def test_preload_installs_dynamic_call_adapter_through_upstream_hook(self): + from unittest.mock import MagicMock + context = MagicMock() + factory = MagicMock(return_value=context) + modules = {"openhands.sdk": SimpleNamespace(LLM="native-llm"), + "worker": SimpleNamespace(gateway_transport=factory, capture_correlation="callback")} + with patch.dict(sys.modules, modules), patch.dict(os.environ, {"OPENHANDS_OWNED_CONTAINER": "1"}), \ + patch("atexit.register") as register: + load_recipe_module("server_transport.py") + factory.assert_called_once_with("native-llm", correlation_callback="callback") + context.__enter__.assert_called_once() + register.assert_called_once() + + def test_duplicate_start_keeps_running_status_and_serial_lock(self): + dispatch = load_recipe_module("dispatch.py") + original = {"status": "running", "conversation_id": FIXTURE_CONVERSATION_ID} + (self.result / "status.json").write_text(json.dumps(original)) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + lock = self.state / "active-dispatch.json" + lock.write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + with patch.object(dispatch, "api_request") as api, patch.object(dispatch, "stop_server") as stop, \ + contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 3) + api.assert_not_called() + stop.assert_not_called() + self.assertTrue(lock.exists()) + self.assertEqual(json.loads((self.result / "status.json").read_text()), original) + + def test_result_retry_preserves_collected_official_receipt(self): + # Native result retrieval is a GET (SDK@fcc102a conversation_router.py: + # 202-228). Retrying the host collection must not erase its verdict. + dispatch = load_recipe_module("dispatch.py") + (self.result / "status.json").write_text(json.dumps({"status": "collected"})) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + receipt = {"task_passed": True, "evidence_complete": False, "failure_stage": None, + "upstream_grader": {"upstream_resolved": True, "grader_exit_code": 0}} + (self.result / "receipt.json").write_text(json.dumps(receipt)) + output = io.StringIO() + with patch.object(dispatch, "api_request") as api, patch.object(dispatch, "finish_result") as finish, \ + patch.object(dispatch, "stop_server") as stop, contextlib.redirect_stdout(output): + self.assertEqual(dispatch.execute("result", self.state, self.run_id, self.arm), 2) + api.assert_not_called() + finish.assert_not_called() + stop.assert_not_called() + self.assertEqual(json.loads((self.result / "receipt.json").read_text()), receipt) + self.assertTrue(json.loads(output.getvalue())["task_passed"]) + + def test_result_refuses_export_until_container_removal_is_confirmed(self): + dispatch = load_recipe_module("dispatch.py") + window = {"arm": self.arm, "run_id": self.run_id, "started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:01Z"} + with patch.object(dispatch, "stop_server", return_value=False), \ + patch.object(dispatch, "logged_command") as command, patch.object(dispatch, "grade") as grade, \ + patch.object(dispatch, "create_receipt", side_effect=lambda result: json.loads((result / "window.json").read_text())): + report = dispatch.finish_result(self.result, {"execution_status": "finished"}, window) + command.assert_not_called() + grade.assert_not_called() + self.assertEqual(report["failure_stage"], "export") + + def test_request_serialization_failure_cannot_leave_prepared_receipt(self): + host = load_recipe_module("host.py") + dispatch = load_recipe_module("dispatch.py") + with patch.dict(sys.modules, {"dispatch": dispatch}), \ + patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ + patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}), \ + patch.object(host, "execute_container", return_value=7), \ + patch.object(host, "logged_command") as launch, self.assertRaises(RuntimeError): + host.prepare_native_dispatch(self.result, self.state / "prefix", json.loads((RECIPE / "pins.json").read_text()), [], + "rw-openhands-egress-engines-on", {"variables": {"HOST_PATH": "/usr/bin"}}, + load_recipe_module("recipe.py").arm_config(self.arm), self.run_id) + launch.assert_not_called() + + def test_patch_export_cannot_use_host_git_filters_or_external_diff(self): + dispatch = load_recipe_module("dispatch.py") + (self.result / "task-identity.json").write_text(json.dumps({"instance_id": "django__django-11333", "base_commit": "a" * 40})) + (self.result / "dataset.json").write_text("[]") + window = {"arm": self.arm, "run_id": self.run_id} + with patch.object(dispatch, "stop_server", return_value=True), \ + patch.object(dispatch, "logged_command", return_value=0) as command, \ + patch.object(dispatch.subprocess, "check_output", return_value="patch") as diff, \ + patch.object(dispatch, "grade", return_value={}), patch.object(dispatch, "create_receipt", return_value={}): + dispatch.finish_result(self.result, {"execution_status": "finished"}, window) + self.assertEqual(command.call_args.kwargs["env"]["GIT_CONFIG_GLOBAL"], os.devnull) + self.assertEqual(command.call_args.kwargs["env"]["GIT_CONFIG_NOSYSTEM"], "1") + self.assertIn("--no-ext-diff", diff.call_args.args[0]) + self.assertIn("--no-textconv", diff.call_args.args[0]) + + if __name__ == "__main__": unittest.main() From eb716631120a548c2e4c3ff73a54600ef68c75c2 Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 17:44:18 -0400 Subject: [PATCH 04/22] test_runtime_worker_openhands: resolve temp roots for macOS validate-macos (run 36351247715) failed 6 tests with NotADirectoryError: 'var'. read_bounded refuses every symlink hop from "/" by design, and macOS temp dirs sit under the /var -> /private/var symlink. The test module now realpath-resolves all nine temp roots and keeps the production rule unchanged. Reproduced on Linux with TMPDIR set to a symlinked directory: failures=2, errors=4 before, the same counts as macOS. After the change: 59 tests OK with a symlinked or a plain TMPDIR. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_runtime_worker_openhands.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index bd02728c7..858fbfca8 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -307,7 +307,7 @@ def test_model_container_is_nonroot_and_network_is_explicit(self): def test_mount_allowlist_checks_resolved_targets_and_secret_ancestors(self): host = load_recipe_module("host.py") with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) + root = Path(tmp).resolve() eco = root / "ecosystem" good = eco / "bin" good.mkdir(parents=True) @@ -363,11 +363,13 @@ class OpenHandsUpstreamAdapterTests(unittest.TestCase): """ INSTANCE = "django__django-11333" + # Temp roots are realpath-resolved throughout: read_bounded refuses every symlink hop from "/", and macOS temp + # dirs sit under the /var -> /private/var symlink. def setUp(self): self.checker = load_recipe_module("e2e/check.py") self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) - self.result = Path(self.tmp.name) + self.result = Path(self.tmp.name).resolve() def report(self, bucket): data = {key: [] for key in ("resolved_ids", "unresolved_ids", "error_ids", "empty_patch_ids", "incomplete_ids", "submitted_ids")} @@ -650,7 +652,7 @@ def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): def test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: - root = Path(tmp) + root = Path(tmp).resolve() (root / "target").write_text("sensitive") (root / "link").symlink_to(root / "target") with self.assertRaises((ValueError, OSError)): @@ -669,7 +671,7 @@ def test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected(self) def test_local_pass_field_is_never_a_worker_verdict(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: - result = Path(tmp) + result = Path(tmp).resolve() (result / "window.json").write_text(json.dumps({ "started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:02Z", "worker_exit_code": 0, @@ -682,7 +684,7 @@ def test_local_pass_field_is_never_a_worker_verdict(self): def test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: - result = Path(tmp) + result = Path(tmp).resolve() instance = "django__django-11333" (result / "window.json").write_text(json.dumps({ "started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:02Z", @@ -703,7 +705,7 @@ def test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass(self def test_truncated_native_summary_retains_failed_attempt_receipt(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: - result = Path(tmp) + result = Path(tmp).resolve() (result / "worker").mkdir() (result / "window.json").write_text(json.dumps({"started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:00:01Z", "worker_exit_code": 124})) (result / "check.json").write_text(json.dumps({"passed": False, "exit_code": 1, "failures": ["missing_result"]})) @@ -717,7 +719,7 @@ def test_truncated_native_summary_retains_failed_attempt_receipt(self): def test_cleanup_timeout_does_not_erase_primary_exit(self): host = load_recipe_module("host.py") with tempfile.TemporaryDirectory() as tmp: - log = Path(tmp) / "worker.log" + log = Path(tmp).resolve() / "worker.log" effects = [subprocess.CompletedProcess(["synthetic"], 7), subprocess.TimeoutExpired(["synthetic-cleanup"], 30)] with patch.object(host.subprocess, "run", side_effect=effects): self.assertEqual(host.execute_container(["synthetic"], "fixture", log, 1), 7) @@ -727,7 +729,7 @@ def test_cleanup_timeout_does_not_erase_primary_exit(self): def test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage(self): module = load_recipe_module("receipt.py") with tempfile.TemporaryDirectory() as tmp: - db = Path(tmp) / "fixture.sqlite" + db = Path(tmp).resolve() / "fixture.sqlite" connection = sqlite3.connect(db) connection.execute("CREATE TABLE call_logs (timestamp, path, status, model, reasoning_effort_requested, reasoning_effort_upstream, tokens_in, tokens_cache_read, tokens_reasoning, correlation_id, forbidden_prompt)") connection.execute("CREATE TABLE forbidden_credentials (secret)") @@ -765,7 +767,7 @@ class OpenHandsDispatchTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.addCleanup(self.tmp.cleanup) - self.state = Path(self.tmp.name) + self.state = Path(self.tmp.name).resolve() self.run_id = "rw-openhands-fixture" self.arm = "engines-on" self.result = self.state / "runs" / self.run_id / self.arm From 3a740b6eca3ec63b77aeb2149b8e3e2f20c61017 Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 18:59:41 -0400 Subject: [PATCH 05/22] OpenHands runtime lock: linux/amd64 environments restriction plus four security upgrades OSV-Scanner 2.6.0 flagged 14 advisories in five packages of the previous lock (CI run 36351247754). v1.49.6 is still upstream's latest release, and its main branch pins the same versions. - Restrict the resolution to the recipe's only install target with uv's documented `environments` setting (pins.json image.platform linux/amd64). - Upgrade anyio 4.14.2, click 8.5.0, pypdf 6.19.0 and soupsieve 2.9.2 (version-pinned `uv lock --upgrade-package`). - cryptography needs no upgrade: the restriction drops the darwin x86_64 48.0.1 line, and the linux 50.0.0 already fixes all three of its advisories. The coordinator reproduced the relock from a fresh copy of the unchanged upstream workspace: uv.lock 30e608b1 and requirements.lock 02d0a7f0, byte for byte. `uv lock --check` exits 0. The classification reports 0 violations (no linux x86_64 wheel or sdist hash removed, no marker change that alters linux x86_64 applicability). The CI osv command exits 0 with "No issues found". The install-container.sh sequence, `uv pip check` and the offline import check all exit 0. research.md and the evidence file (section F) describe the final lock. A superseded five-package variant (cryptography 50.0.1) stays in sections C to E. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 5 +- .../openhands/evidence/relock-2026-09-27.txt | 584 ++++ .../runtime-workers/openhands/pins.json | 2 +- .../openhands/requirements.lock | 2625 +++-------------- .../runtime-workers/openhands/research.md | 130 + 5 files changed, 1168 insertions(+), 2178 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/evidence/relock-2026-09-27.txt diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 755fe644c..bc5460614 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -37,7 +37,10 @@ The source target at lines 570-576 has a different ENTRYPOINT. Do not assume the image contains the source-target venv. Request generation uses the separately installed SDK/tools wheels in the same image, with network disabled. [pins.json](pins.json) preserves the image, wheel -and runtime lock hashes. No framework version changed in round 3. +and runtime lock hashes. No framework version changed in round 3. The runtime +lock is a linux/amd64 security relock of the upstream export with four +dependency upgrades, reproduced from the unchanged upstream workspace +([research.md](research.md#runtime-lock-security-relock-2026-09-27)). [install.sh](install.sh) installs the worker through the existing hash-required wheel/runtime locks and preinstalled hashed build tools. The install container diff --git a/blueprints/runtime-workers/openhands/evidence/relock-2026-09-27.txt b/blueprints/runtime-workers/openhands/evidence/relock-2026-09-27.txt new file mode 100644 index 000000000..c3834a225 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/relock-2026-09-27.txt @@ -0,0 +1,584 @@ +Runtime lock security relock, 2026-09-27. Local scratch evidence only: no +container, image installation, model, gateway or official grading run. + is a private scratch directory and the recipe checkout. + is an unchanged copy of +OpenHands/software-agent-sdk@fcc102a697874d54a357e36004e02c95040dbdc0; +and are further copies used for relocks. uv is the installed +0.12.17. The final lock is section F: the environments restriction plus four +upgrades, with cryptography left at 50.0.0. Section C (five upgrades, including +cryptography 50.0.1) and its checks in sections D and E are superseded and kept +as returned. Section B keeps the rejected, declined and superseded attempts. + +== A. Upstream source and reproduction of the previous lock + +Command: curl -fsSL -o /sdk-fcc102a6.tar.gz https://codeload.github.com/OpenHands/software-agent-sdk/tar.gz/fcc102a697874d54a357e36004e02c95040dbdc0; sha256sum of the archive and of its extracted uv.lock +Exit code: 0 +Returned output: +2d3006de86eaec690a82e497fcd4459816637afe6e53f7da87783183792148a8 sdk-fcc102a6.tar.gz +d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910 /uv.lock +(Both equal pins.json source_archive.sha256 and uv_lock.sha256.) + +Command (in ): uv lock --check +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 419 packages in 4ms + +Command (in ): uv export --frozen --format requirements.txt --no-header --no-annotate --package openhands-tools --no-emit-workspace -o +Exit code: 0 +It ran three times: with UV_NO_CONFIG=1, with UV_NO_CONFIG=1 plus --no-dev, and +without UV_NO_CONFIG. Each output was compared by cmp with the previous +requirements.lock minus its last four lines. +Exit codes: 0, 0, 0 +Returned output: (empty) + +Command: python3 - (append " @ \\\n --hash=sha256:\n" for each pins.json wheels entry, in file order, to the first export; compare with the previous requirements.lock) +Exit code: 0 +Returned output: +reconstructed sha256 c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56 +current sha256 c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56 +identical: True + +== B. Rejected, declined and superseded attempts (universal resolution) + +B1, rejected. Command (in , with UV_NO_CONFIG=1): uv lock --upgrade-package anyio --upgrade-package click --upgrade-package cryptography --upgrade-package pypdf --upgrade-package soupsieve +Exit code: 0 +Returned output: +Using CPython 3.14.7 +Resolving despite existing lockfile due to removal of global exclude newer +Resolved 418 packages in 2.15s +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated cryptography v48.0.1, v50.0.0 -> v48.0.1 +Updated pypdf v6.14.2 -> v6.19.0 +Updated soupsieve v2.8.4 -> v2.10 +Reason rejected: UV_NO_CONFIG disables pyproject.toml discovery, so the +workspace's seven-day exclude-newer was dropped (soupsieve 2.10 was uploaded +2026-09-24T02:36:33Z). + +B2, rejected. Command (in a fresh , workspace configuration honored): uv lock --upgrade-package anyio --upgrade-package click --upgrade-package cryptography --upgrade-package pypdf --upgrade-package soupsieve +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 418 packages in 232ms +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated cryptography v48.0.1, v50.0.0 -> v48.0.1 +Updated pypdf v6.14.2 -> v6.19.0 +Updated soupsieve v2.8.4 -> v2.9.2 +Reason rejected: the cryptography fork collapsed to 48.0.1 under +pyproject.toml:18, "cryptography<49; sys_platform == 'darwin' and +platform_machine == 'x86_64'", which would downgrade Linux from 50.0.0. + +B3, superseded intermediate. Command (in a fresh , started 2026-09-27T21:49:57Z): uv lock --upgrade-package anyio --upgrade-package click --upgrade-package pypdf --upgrade-package soupsieve +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 419 packages in 321ms +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated pypdf v6.14.2 -> v6.19.0 +Updated soupsieve v2.8.4 -> v2.9.2 +Resulting uv.lock SHA256: 531e84bb0955a9da8be6adfb37f9c2e7ecf7a002417ce13c97387b6ffa7d1ec6 +The version-pinned replay (anyio==4.14.2, click==8.5.0, pypdf==6.19.0, +soupsieve==2.9.2) printed the same four updates and produced the same uv.lock +(cmp exit 0). The export and wheel append gave requirements SHA256 +da0d550f1e03d42ef2d1f4fe31a608eb8abdb796ecb9354d30457875f2172d71, four changed +three-line blocks. Section D2 shows its remaining advisories. + +Probe, not applied. Command (in a fresh copy of ): uv lock --dry-run --upgrade-package anyio --upgrade-package typing-extensions +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 419 packages in 225ms +Update anyio v4.11.0 -> v4.15.1 +Update typing-extensions v4.15.0 -> v4.16.0 +(uv.lock unchanged. PyPI metadata for anyio 4.15.0 and 4.15.1 lists +'typing_extensions>=4.16.0; python_version < "3.15"'.) + +B4, declined. Command (in a copy of the B3 ): delete pyproject.toml line 18, then uv lock --upgrade-package cryptography +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 418 packages in 256ms +Updated cryptography v48.0.1, v50.0.0 -> v50.0.1 +The coordinator declined deleting the upstream constraint and selected the +environments restriction in section C. + +== C. Superseded by section F: linux x86_64 environments restriction plus five upgrades + +Change (in , a fresh copy of ; git diff --no-index -U0 of pyproject.toml): +@@ -6,0 +7 @@ members = ["openhands-sdk", "openhands-tools", "openhands-workspace", "openhands ++environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"] +Resulting pyproject.toml SHA256: 9abe06798fbc2c42454e0c5685991fab682725fb3ba4363e5078b754777db2e9 + +Command (in , started 2026-09-27T22:08:28Z): uv lock --upgrade-package anyio --upgrade-package click --upgrade-package cryptography --upgrade-package pypdf --upgrade-package soupsieve +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 412 packages in 1.86s +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated cryptography v48.0.1, v50.0.0 -> v50.0.1 +Removed cython v3.1.4 +Removed macholib v1.16.3 +Removed pefile v2023.2.7 +Removed pyobjc-framework-pubsub v11.1 +Updated pypdf v6.14.2 -> v6.19.0 +Removed pywin32 v311 +Removed pywin32-ctypes v0.2.3 +Updated soupsieve v2.8.4 -> v2.9.2 +Resulting uv.lock SHA256: b3e8a6f4c32ce5d558a8152b63993f5cbaf852b0ff6a667c14069b47d5443d91 +Then: uv lock --check +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 412 packages in 2ms + +Version-pinned replay. Command (in a fresh copy of with the same pyproject.toml): uv lock --upgrade-package anyio==4.14.2 --upgrade-package click==8.5.0 --upgrade-package cryptography==50.0.1 --upgrade-package pypdf==6.19.0 --upgrade-package soupsieve==2.9.2 +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 412 packages in 232ms +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated cryptography v48.0.1, v50.0.0 -> v50.0.1 +Removed cython v3.1.4 +Removed macholib v1.16.3 +Removed pefile v2023.2.7 +Removed pyobjc-framework-pubsub v11.1 +Updated pypdf v6.14.2 -> v6.19.0 +Removed pywin32 v311 +Removed pywin32-ctypes v0.2.3 +Updated soupsieve v2.8.4 -> v2.9.2 +Resulting uv.lock SHA256: b3e8a6f4c32ce5d558a8152b63993f5cbaf852b0ff6a667c14069b47d5443d91 (cmp exit 0 against the run above) + +Command (in ): the section A export command with -o /blueprints/runtime-workers/openhands/requirements.lock +Exit code: 0 +The four pins.json wheel lines were then appended with the editor. A separate +copy was built in from the same export plus the section A append. +Then: sha256sum, wc -l and cmp of the worktree lock against that copy +Returned output: f215ac9f6446b3b7c3068695f23f9d96556b0dda36b83134b701aae2ddefaacf +for both files; 1133 lines; cmp exit 0. + +Classification of every change against the previous lock. Local check, not +upstream acceptance: a scratch Python script using packaging 26.3 (installed +from build-requirements.lock with hashes) parsed both requirement files. It +mapped every removed or added hash to its file through the upstream and relocked +uv.lock files, checked each removed wheel's platform tags for linux x86_64 +(manylinux, musllinux or linux x86_64, or any), and evaluated the old and new +markers with Marker.evaluate on eight environments: sys_platform linux, +platform_system Linux, platform_machine x86_64, CPython and PyPy, Python 3.12, +3.13, 3.14 and 3.15. +Exit code: 0 +Returned output (the 329 per-entry marker lines and the 159 remaining pyobjc +lines are summarized by count): +requirement entries: old 342, new 336 +environment grid: linux x86_64 x (CPython, PyPy) x (3.12, 3.13, 3.14, 3.15) = 8 + +== Packages removed entirely (old marker; applies on linux x86_64 grid?) + colorama==0.4.6 ; sys_platform == 'win32' -> applies on linux x86_64: False + cython==3.1.4 ; platform_system != 'darwin' and sys_platform == 'darwin' -> applies on linux x86_64: False + pyobjc-framework-pubsub==11.1 ; platform_release >= '9' and platform_release < '18' and platform_system == 'darwin' and sys_platform == 'darwin' -> applies on linux x86_64: False + pywin32==311 ; sys_platform == 'win32' -> applies on linux x86_64: False + pywin32-ctypes==0.2.3 ; sys_platform == 'win32' -> applies on linux x86_64: False + +== Packages added + +== Version changes + anyio: 4.11.0 -> 4.14.2 [one of the five] + click: 8.1.8 -> 8.5.0 [one of the five] + cryptography: 48.0.1, 50.0.0 -> 50.0.1 [one of the five] + pypdf: 6.14.2 -> 6.19.0 [one of the five] + soupsieve: 2.8.4 -> 2.9.2 [one of the five] + +== Marker changes on unchanged (name, version) entries + [329 lines. 162 read "==: None -> \"platform_machine == 'x86_64' and sys_platform == 'linux'\"". + The other 167 (159 pyobjc entries, cffi, google-api-core 2.26.0 and 2.31.0, + hf-xet, jeepney, pycparser, screeninfo and secretstorage) combine that marker + with the previous one, for example + cffi==2.0.0: "platform_python_implementation != 'PyPy'" -> "platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'". + Every line ends "same linux x86_64 applicability: True"; none ends False.] + total marker changes: 329 + +== Hash lines removed from unchanged (name, version) entries, by wheel platform family + android arm64_v8a: 2 + android x86_64: 2 + ios ios_13_0_arm64_iphoneos: 5 + ios ios_13_0_arm64_iphonesimulator: 5 + linux armv7l: 3 + macosx arm64: 102 + macosx universal2: 533 + macosx x86_64: 96 + manylinux aarch64: 121 + manylinux armv7l: 54 + manylinux i686: 53 + manylinux ppc64le: 71 + manylinux riscv64: 20 + manylinux s390x: 68 + musllinux aarch64: 102 + musllinux armv7l: 42 + musllinux i686: 31 + musllinux ppc64le: 38 + musllinux riscv64: 10 + musllinux s390x: 32 + win amd64: 108 + win arm64: 75 + win win32: 94 + total removed hashes: 1667 across 128 packages + hashes added to unchanged entries: 0 + +== The five: old -> new, hash counts + anyio: 4.11.0 (None) 2 hashes -> 4.14.2 (platform_machine == 'x86_64' and sys_platform == 'linux') 2 hashes + click: 8.1.8 (None) 2 hashes -> 8.5.0 (platform_machine == 'x86_64' and sys_platform == 'linux') 2 hashes + cryptography: 48.0.1 (platform_machine == 'x86_64' and sys_platform == 'darwin') 4 hashes; 50.0.0 (platform_machine != 'x86_64' or sys_platform != 'darwin') 40 hashes -> 50.0.1 (platform_machine == 'x86_64' and sys_platform == 'linux') 13 hashes + pypdf: 6.14.2 (None) 2 hashes -> 6.19.0 (platform_machine == 'x86_64' and sys_platform == 'linux') 2 hashes + soupsieve: 2.8.4 (None) 2 hashes -> 2.9.2 (platform_machine == 'x86_64' and sys_platform == 'linux') 2 hashes + +== Remaining entries whose marker never applies on linux x86_64 + [159 lines, all pyobjc, pyobjc-core or pyobjc-framework-* entries] + +VIOLATIONS: 0 + +The 13 cryptography 50.0.1 hashes map through the relocked uv.lock to +cryptography-50.0.1.tar.gz and twelve x86_64 wheels: cp39-abi3, cp311-abi3 and +cp314-cp314t, each as manylinux2014/manylinux_2_17, manylinux_2_28, +manylinux_2_34 and musllinux_1_2. + +== D. OSV-Scanner 2.6.0 (D3 and D4 scanned the superseded section C lock; section F repeats D4) + +Binary SHA256 ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108, +as pinned in .github/workflows/security-scan.yml; osv-scanner --version: 2.6.0. +Single-file scans ran in /exp with --config +/.github/osv-scanner.toml --no-resolve. Their notice that the two +Lumibot ignores were unused, and the scan log lines, are omitted below. + +D1. Command: osv-scanner scan source ... --lockfile=requirements.txt:old-requirements.lock (the previous lock, from git HEAD) +Exit code: 1 +Returned output: +Total 5 packages affected by 14 known vulnerabilities (1 Critical, 3 High, 10 Medium, 0 Low, 0 Unknown) from 1 ecosystem. +14 vulnerabilities can be fixed. + ++-------------------------------------+------+-----------+--------------+---------+---------------+-----------------------+ +| OSV URL | CVSS | ECOSYSTEM | PACKAGE | VERSION | FIXED VERSION | SOURCE | ++-------------------------------------+------+-----------+--------------+---------+---------------+-----------------------+ +| https://osv.dev/GHSA-5p39-cfhj-2xmp | 6.8 | PyPI | anyio | 4.11.0 | 4.14.2 | old-requirements.lock | +| https://osv.dev/GHSA-82r6-8w77-94w6 | 9.3 | PyPI | anyio | 4.11.0 | 4.14.2 | old-requirements.lock | +| https://osv.dev/PYSEC-2026-2132 | 7.2 | PyPI | click | 8.1.8 | 8.3.3 | old-requirements.lock | +| https://osv.dev/PYSEC-2026-3552 | 8.2 | PyPI | cryptography | 48.0.1 | 50.0.0 | old-requirements.lock | +| https://osv.dev/GHSA-g6cj-pr64-35w5 | | | | | | | +| https://osv.dev/PYSEC-2026-3553 | 8.7 | PyPI | cryptography | 48.0.1 | 49.0.0 | old-requirements.lock | +| https://osv.dev/GHSA-jwv3-5hgf-82ww | | | | | | | +| https://osv.dev/PYSEC-2026-3554 | 6.9 | PyPI | cryptography | 48.0.1 | 49.0.0 | old-requirements.lock | +| https://osv.dev/GHSA-m2h6-j472-rp4c | | | | | | | +| https://osv.dev/PYSEC-2026-3655 | 4.8 | PyPI | pypdf | 6.14.2 | 6.15.0 | old-requirements.lock | +| https://osv.dev/GHSA-fp3f-mc75-235c | | | | | | | +| https://osv.dev/PYSEC-2026-3656 | 4.8 | PyPI | pypdf | 6.14.2 | 6.15.0 | old-requirements.lock | +| https://osv.dev/GHSA-fwg2-594c-jp42 | | | | | | | +| https://osv.dev/PYSEC-2026-3910 | 4.8 | PyPI | pypdf | 6.14.2 | 6.16.1 | old-requirements.lock | +| https://osv.dev/GHSA-23w6-3w8w-8484 | | | | | | | +| https://osv.dev/PYSEC-2026-3911 | 4.8 | PyPI | pypdf | 6.14.2 | 6.16.1 | old-requirements.lock | +| https://osv.dev/GHSA-763m-79hh-57f2 | | | | | | | +| https://osv.dev/PYSEC-2026-3912 | 6.9 | PyPI | pypdf | 6.14.2 | 6.15.0 | old-requirements.lock | +| https://osv.dev/GHSA-fc8x-2rww-xw9m | | | | | | | +| https://osv.dev/PYSEC-2026-3913 | 6.9 | PyPI | pypdf | 6.14.2 | 6.16.0 | old-requirements.lock | +| https://osv.dev/GHSA-jp53-mhqp-8xcg | | | | | | | +| https://osv.dev/GHSA-gjv8-xp57-g29c | 5.3 | PyPI | soupsieve | 2.8.4 | 2.9.0 | old-requirements.lock | +| https://osv.dev/GHSA-j934-xhv5-fg8f | 5.3 | PyPI | soupsieve | 2.8.4 | 2.9.0 | old-requirements.lock | ++-------------------------------------+------+-----------+--------------+---------+---------------+-----------------------+ + +D2. Command: osv-scanner scan source ... --lockfile=requirements.txt:new-requirements.lock (the superseded B3 lock) +Exit code: 1 +Returned output (the Total line and table rows; table borders, header and the +"3 vulnerabilities can be fixed." line omitted): +Total 1 package affected by 3 known vulnerabilities (0 Critical, 2 High, 1 Medium, 0 Low, 0 Unknown) from 1 ecosystem. +| https://osv.dev/PYSEC-2026-3552 | 8.2 | PyPI | cryptography | 48.0.1 | 50.0.0 | new-requirements.lock | +| https://osv.dev/GHSA-g6cj-pr64-35w5 | | | | | | | +| https://osv.dev/PYSEC-2026-3553 | 8.7 | PyPI | cryptography | 48.0.1 | 49.0.0 | new-requirements.lock | +| https://osv.dev/GHSA-jwv3-5hgf-82ww | | | | | | | +| https://osv.dev/PYSEC-2026-3554 | 6.9 | PyPI | cryptography | 48.0.1 | 49.0.0 | new-requirements.lock | +| https://osv.dev/GHSA-m2h6-j472-rp4c | | | | | | | + +D3. Command: osv-scanner scan source ... --lockfile=requirements.txt:env-requirements.lock (the section C lock) +Exit code: 0 +Returned output: +Scanned /exp/env-requirements.lock file and found 334 packages +No issues found + +D4. Command (from after the change): mapfile -t lockfiles < <(jq -r '.lockfiles[] | "--lockfile=\(.parser // ""):\(.path)"' .github/osv-scanner-lockfiles.json); osv-scanner scan source --config .github/osv-scanner.toml --no-resolve "${lockfiles[@]}" +Exit code: 0 +Returned output (49 lockfile arguments; scan log lines omitted; the filtered +lines were identical in the scan before this change): +Filtered 175 local/unscannable package/s from the scan. +GHSA-8mgp-746c-j5xp and 2 aliases have been filtered out because: nltk 3.10.3 [Lumibot ignore reason as in .github/osv-scanner.toml] +GHSA-h35f-9h28-mq5c and 3 aliases have been filtered out because: setuptools 80.10.2 [Lumibot ignore reason as in .github/osv-scanner.toml] +Filtered 3 vulnerabilities from output + +No issues found +The scan log includes: Scanned /blueprints/runtime-workers/openhands/requirements.lock file and found 334 packages + +== E. Consistency check on the section C lock (superseded; see section F) + +Environment: uv venv --python python3.13 /venv (CPython 3.13.15), with +TMPDIR, UV_CACHE_DIR and the venv under , PYTHONDONTWRITEBYTECODE=1, +UV_NO_CONFIG=1 and UV_PYTHON_DOWNLOADS=never. + +Command: curl -fsSL https://pypi.org/pypi/func-timeout/json (file types per 4.3.x release) +Exit code: 0 +Returned output: +latest 4.3.5 +{'4.3.0': ['sdist'], '4.3.1': ['sdist'], '4.3.2': ['sdist'], '4.3.3': ['sdist'], '4.3.4': ['sdist'], '4.3.5': ['sdist']} + +Command: uv pip install --python /venv/bin/python --require-hashes --only-binary=:all: --no-deps -r requirements.lock +Exit code: 1 +Returned output: +Using Python 3.13.15 environment at: /venv +error: No solution found when resolving dependencies + cause: Because func-timeout{platform_machine == 'x86_64' and sys_platform == 'linux'}==4.3.5 has no usable wheels and you require func-timeout{platform_machine == 'x86_64' and sys_platform == 'linux'}==4.3.5, we can conclude that your requirements are unsatisfiable. + +hint: Wheels are required for `func-timeout` because building from source is disabled for all packages (i.e., with `--no-build`) + +Command (install-container.sh step 1): uv pip install --python /venv/bin/python --require-hashes --no-deps --only-binary :all: --index-url https://pypi.org/simple -r build-requirements.lock +Exit code: 0 +Returned output: +Using Python 3.13.15 environment at: /venv +Resolved 3 packages in 1ms +Installed 3 packages in 31ms + + packaging==26.3 + + setuptools==84.0.0 + + wheel==0.46.3 + +Command (install-container.sh step 2): uv pip install --python /venv/bin/python --require-hashes --no-deps --no-build-isolation --index-url https://pypi.org/simple -r requirements.lock +Exit code: 0 +Returned output (excerpt: every line except the Downloading/Downloaded lines and +175 "+" lines, then selected "+" lines): +Using Python 3.13.15 environment at: /venv +Resolved 176 packages in 18ms + Building func-timeout==4.3.5 + Built func-timeout==4.3.5 +Prepared 175 packages in 1.30s +Installed 175 packages in 522ms + + anyio==4.14.2 + + cffi==2.0.0 + + click==8.5.0 + + cryptography==50.0.1 + + func-timeout==4.3.5 + + google-api-core==2.31.0 + + jeepney==0.9.0 + + openhands-sdk==1.49.6 (from https://files.pythonhosted.org/packages/e9/ac/d27c5becc3b59b8cc46a1614ac478cc94eb73e626ab15831906fcdaafb29/openhands_sdk-1.49.6-py3-none-any.whl) + + openhands-tools==1.49.6 (from https://files.pythonhosted.org/packages/68/20/27d6d20cc8f3465ec861c450bc0113c8646285aa6c8f4291b62366dcb8a2/openhands_tools-1.49.6-py3-none-any.whl) + + pypdf==6.19.0 + + soupsieve==2.9.2 + +Command (install-container.sh step 3): uv pip check --python /venv/bin/python +Exit code: 0 +Returned output: +Using Python 3.13.15 environment at: /venv +Checked 178 packages in 3ms +All installed packages are compatible + +Command: diff of the sorted "+" lines from step 2 on the B3 lock (same commands, earlier venv) and on the section C lock +Exit code: 1 +Returned output: +29c29 +< + cryptography==50.0.0 +--- +> + cryptography==50.0.1 +(The B3 run's steps 1 to 3 also exited 0, 0 and 0, with "Installed 175 packages" +and "All installed packages are compatible".) + +Command (install-container.sh import check): bwrap --unshare-net --die-with-parent --ro-bind / / --bind --dev /dev --proc /proc --tmpfs /tmp --setenv HOME /home --setenv TMPDIR --setenv PYTHONDONTWRITEBYTECODE 1 -- /venv/bin/python -c '' +Exit code: 0 +Returned output (ANSI colour codes removed): +18:11:16 - LiteLLM:WARNING: get_model_cost_map.py:290 - LiteLLM: Failed to fetch remote model cost map from https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json: [Errno -3] Temporary failure in name resolution. Falling back to local backup. ++----------------------------------------------------------------------+ +| OpenHands SDK v1.49.6 | +| | +| Report a bug: github.com/OpenHands/software-agent-sdk/issues | +| Get help: openhands.dev/joinslack | +| Scale up: openhands.dev/product/sdk | +| | +| Set OPENHANDS_SUPPRESS_BANNER=1 to hide this message | ++----------------------------------------------------------------------+ + +SDK/tools 1.49.6 imports passed; no model request + +== F. Final lock: linux x86_64 environments restriction plus four upgrades + +Coordinator decision, 2026-09-27T22:06:57Z: cryptography is not upgraded. The +linux x86_64 resolution already locks cryptography 50.0.0, and PYSEC-2026-3552, +PYSEC-2026-3553 and PYSEC-2026-3554 are fixed in 50.0.0, 49.0.0 and 49.0.0 +(section D1). The section C lock (cryptography 50.0.1; uv.lock b3e8a6f4..., +requirements.lock f215ac9f...) is superseded. + +The relock builder produced this lock; its session ended before it recorded the +run here. The coordinator then reproduced it independently from a fresh copy + of , with uv 0.12.17 and UV_CACHE_DIR under . + +Command: sha256sum /uv.lock (before any change) +Exit code: 0 +Returned output: +d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910 uv.lock + +Change (diff /pyproject.toml /pyproject.toml), the same line as section C: +6a7 +> environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"] +Resulting pyproject.toml SHA256: 9abe06798fbc2c42454e0c5685991fab682725fb3ba4363e5078b754777db2e9 (as in section C) + +Command (in ): uv lock --upgrade-package anyio==4.14.2 --upgrade-package click==8.5.0 --upgrade-package pypdf==6.19.0 --upgrade-package soupsieve==2.9.2 +Exit code: 0 +Returned output (Resolved and download lines filtered by the command): +Using CPython 3.13.15 +Updated anyio v4.11.0 -> v4.14.2 +Updated click v8.1.8 -> v8.5.0 +Updated cryptography v48.0.1, v50.0.0 -> v50.0.0 +Removed cython v3.1.4 +Removed macholib v1.16.3 +Removed pefile v2023.2.7 +Removed pyobjc-framework-pubsub v11.1 +Updated pypdf v6.14.2 -> v6.19.0 +Removed pywin32 v311 +Removed pywin32-ctypes v0.2.3 +Updated soupsieve v2.8.4 -> v2.9.2 +Resulting uv.lock SHA256: 30e608b1fdb091db1609e8261a787020df12e0350911079cc2177c772a9ccffd +(cmp: byte-identical to the relock builder's four-package workspace. The +cryptography line is the darwin x86_64 fork leaving the resolved environment; +linux keeps 50.0.0.) + +Command (in ): uv lock --check +Exit code: 0 +Returned output: +Using CPython 3.13.15 +Resolved 412 packages in 2ms + +Command (in ): uv export --frozen --format requirements.txt --no-header --no-annotate --package openhands-tools --no-emit-workspace, then the pins.json wheels appended as in section A (` @ \` and ` --hash=sha256:`) +Exit code: 0 +Returned output (sha256sum; `cmp` against /blueprints/runtime-workers/openhands/requirements.lock exited 0): +02d0a7f058d08d28d0fb3f7344ed9b042bf5a317607454faf4bbb210af7fa394 +(A first append without the trailing ` \` differed from the worktree lock only +on the two URL lines, 1130 and 1132.) + +Command: /toolvenv/bin/python /analyze_lock_diff.py /uv.lock /uv.lock +Exit code: 0 +Returned output (long package lists cut at 250 characters): +requirement entries: old 342, new 336 +environment grid: linux x86_64 x (CPython, PyPy) x (3.12, 3.13, 3.14, 3.15) = 8 + +== Old entries without a same-version new entry + anyio==4.11.0 ; None -> upgraded (allowed) + click==8.1.8 ; None -> upgraded (allowed) + colorama==0.4.6 ; sys_platform == 'win32' -> removed; never applies on linux x86_64 + cryptography==48.0.1 ; platform_machine == 'x86_64' and sys_platform == 'darwin' -> removed; never applies on linux x86_64 + cython==3.1.4 ; platform_system != 'darwin' and sys_platform == 'darwin' -> removed; never applies on linux x86_64 + pyobjc-framework-pubsub==11.1 ; platform_release >= '9' and platform_release < '18' and platform_system == 'darwin' and sys_platform == 'darwin' -> removed; never applies on linux x86_64 + pypdf==6.14.2 ; None -> upgraded (allowed) + pywin32==311 ; sys_platform == 'win32' -> removed; never applies on linux x86_64 + pywin32-ctypes==0.2.3 ; sys_platform == 'win32' -> removed; never applies on linux x86_64 + soupsieve==2.8.4 ; None -> upgraded (allowed) + +== New entries without a same-version old entry + anyio==4.14.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' (2 hashes) -> upgrade target (allowed) + click==8.5.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' (2 hashes) -> upgrade target (allowed) + pypdf==6.19.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' (2 hashes) -> upgrade target (allowed) + soupsieve==2.9.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' (2 hashes) -> upgrade target (allowed) + +== Marker changes on same-version entries: 330 +-- 162 entries had no marker and now carry "platform_machine == 'x86_64' and sys_platform == 'linux'": + agent-client-protocol==0.12.1, aiofile==3.9.0, aiofiles==25.1.0, aiohappyeyeballs==2.6.1, aiohttp==3.14.3, aiosignal==1.4.0, annotated-types==0.7.0, anthropic==0.75.0, attrs==25.3.0, authlib==1.6.12, backoff==2.2.1, beartype==0.22.9, beautifulsoup +-- 9 other non-pyobjc entries (old -> new): + cffi==2.0.0: "platform_python_implementation != 'PyPy'" -> "platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'" + cryptography==50.0.0: "platform_machine != 'x86_64' or sys_platform != 'darwin'" -> "platform_machine == 'x86_64' and sys_platform == 'linux'" + google-api-core==2.26.0: "python_full_version < '3.13'" -> "python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" + google-api-core==2.31.0: "python_full_version >= '3.13'" -> "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux'" + hf-xet==1.1.10: "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" -> "platform_machine == 'x86_64' and sys_platform == 'linux'" + jeepney==0.9.0: "sys_platform == 'linux'" -> "platform_machine == 'x86_64' and sys_platform == 'linux'" + pycparser==2.23: "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" -> "implementation_name != 'PyPy' and platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux'" + screeninfo==0.8.1: "platform_system != 'darwin'" -> "platform_machine == 'x86_64' and platform_system != 'darwin' and sys_platform == 'linux'" + secretstorage==3.5.0: "sys_platform == 'linux'" -> "platform_machine == 'x86_64' and sys_platform == 'linux'" +-- 159 pyobjc entries; 159 of them never apply on linux x86_64, before or after: + pyobjc==11.1, pyobjc-core==11.1, pyobjc-framework-accessibility==11.1, pyobjc-framework-accounts==11.1, pyobjc-framework-addressbook==11.1, pyobjc-framework-adservices==11.1, pyobjc-framework-adsupport==11.1, pyobjc-framework-applescriptkit==11.1, + example: pyobjc: "platform_system == 'darwin'" -> "platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux'" + Every marker change keeps linux x86_64 applicability: True + +== Hash lines removed from same-version entries, by wheel platform family + android arm64_v8a: 2 + android x86_64: 2 + ios ios_13_0_arm64_iphoneos: 5 + ios ios_13_0_arm64_iphonesimulator: 5 + linux armv7l: 3 + macosx arm64: 105 + macosx universal2: 533 + macosx x86_64: 96 + manylinux aarch64: 130 + manylinux armv7l: 57 + manylinux i686: 53 + manylinux ppc64le: 77 + manylinux riscv64: 20 + manylinux s390x: 68 + musllinux aarch64: 105 + musllinux armv7l: 42 + musllinux i686: 31 + musllinux ppc64le: 38 + musllinux riscv64: 10 + musllinux s390x: 32 + win amd64: 111 + win arm64: 75 + win win32: 94 + total removed hashes: 1694 across 129 entries + +VIOLATIONS: 0 + +Command (from , the section F lock in place): the section D4 command +Exit code: 0 +Returned output (scan log lines omitted, except the OpenHands lock line): +Scanned /blueprints/runtime-workers/openhands/requirements.lock file and found 334 packages +Filtered 175 local/unscannable package/s from the scan. +GHSA-8mgp-746c-j5xp and 2 aliases have been filtered out because: nltk 3.10.3 [Lumibot ignore reason as in .github/osv-scanner.toml] +GHSA-h35f-9h28-mq5c and 3 aliases have been filtered out because: setuptools 80.10.2 [Lumibot ignore reason as in .github/osv-scanner.toml] +Filtered 3 vulnerabilities from output +No issues found + +Consistency check (install-container.sh order) in , CPython 3.13.15 +from `uv venv --python 3.13`, UV_NO_CONFIG=1, working directory outside any +workspace. A first run without UV_NO_CONFIG=1 from inside exited 2 on +step 1: "error: In `--require-hashes` mode, all requirements must have their +versions pinned with `==`, but found: starlette>=0.49.1". That is the workspace's +`[tool.uv]` constraint-dependencies, which install-container.sh's UV_NO_CONFIG=1 +keeps out. + +Command (step 1): uv pip install --python /bin/python --require-hashes --no-deps --only-binary :all: --index-url https://pypi.org/simple -r build-requirements.lock +Exit code: 0 +Returned output: +Using Python 3.13.15 environment at: +Resolved 3 packages in 1ms +Installed 3 packages in 31ms + + packaging==26.3 + + setuptools==84.0.0 + + wheel==0.46.3 + +Command (step 2): uv pip install --python /bin/python --require-hashes --no-deps --no-build-isolation --index-url https://pypi.org/simple -r requirements.lock +Exit code: 0 +Returned output (summary lines; no build line was printed: func-timeout 4.3.5 was built earlier into the same uv cache): +Using Python 3.13.15 environment at: +Resolved 176 packages in 19ms +Installed 175 packages in 535ms + +Command (step 3): uv pip check --python /bin/python +Exit code: 0 +Returned output: +Checked 178 packages in 4ms +All installed packages are compatible + +Command: /bin/python -c (importlib.metadata versions of the five advisory packages) +Exit code: 0 +Returned output: +anyio 4.14.2; click 8.5.0; cryptography 50.0.0; pypdf 6.19.0; soupsieve 2.9.2 + +Command (install-container.sh import check): bwrap --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp --unshare-net -- /bin/python -c '' +Exit code: 0 +Returned output (last line; LiteLLM's cost-map fetch failed on name resolution, as in section E): +SDK/tools 1.49.6 imports passed; no model request + +Test suites on (TMPDIR outside the checkout, PYTHONDONTWRITEBYTECODE=1): +python3 -m unittest tests.test_runtime_worker_openhands -> Ran 59 tests ... OK +python3 -m unittest tests.test_osv_lockfile_coverage tests.test_blind_checkout tests.test_workflow_security_coverage -> Ran 57 tests ... OK diff --git a/blueprints/runtime-workers/openhands/pins.json b/blueprints/runtime-workers/openhands/pins.json index a888af0be..ae6898e1c 100644 --- a/blueprints/runtime-workers/openhands/pins.json +++ b/blueprints/runtime-workers/openhands/pins.json @@ -32,7 +32,7 @@ "filename": "openhands_tools-1.49.6-py3-none-any.whl" } }, - "requirements_sha256": "c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56", + "requirements_sha256": "02d0a7f058d08d28d0fb3f7344ed9b042bf5a317607454faf4bbb210af7fa394", "build_requirements_sha256": "32cff7dc47659683a19936944a47123160731a2bdb711aee78a7d63fbab205e2", "grader": { "repository": "https://github.com/OpenHands/benchmarks", diff --git a/blueprints/runtime-workers/openhands/requirements.lock b/blueprints/runtime-workers/openhands/requirements.lock index fd21cfd03..bcd85abd9 100644 --- a/blueprints/runtime-workers/openhands/requirements.lock +++ b/blueprints/runtime-workers/openhands/requirements.lock @@ -1,2857 +1,1130 @@ -agent-client-protocol==0.12.1 \ +agent-client-protocol==0.12.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:9963ccb590ed96d7dc542c9c023e90676fc76b1c6562e21a9ee112f4fbcb0095 \ --hash=sha256:aaf3cc301ed87d9e2c4ccb91adfe1cd58784355f6ce7e513f50e7ca2391862ef -aiofile==3.9.0 \ +aiofile==3.9.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa \ --hash=sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b -aiofiles==25.1.0 \ +aiofiles==25.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2 \ --hash=sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695 -aiohappyeyeballs==2.6.1 \ +aiohappyeyeballs==2.6.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 -aiohttp==3.14.3 \ - --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ - --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ - --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ - --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ - --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ +aiohttp==3.14.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ - --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ - --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ - --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ - --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ - --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ - --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ - --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ - --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ - --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ - --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ - --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ - --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ - --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ - --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ - --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ - --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ - --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ - --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ - --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ - --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ - --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ - --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ - --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ - --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ - --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ - --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ - --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ - --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ - --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ - --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ - --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ - --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ - --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ - --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ - --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ - --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ - --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ - --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ - --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ - --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ - --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ - --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ - --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ - --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ - --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ - --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ - --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ - --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ - --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ - --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ - --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ - --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ - --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ - --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ - --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ - --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ - --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ - --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ - --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ - --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ - --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ - --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ - --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ - --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ - --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ - --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ - --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ - --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 -aiosignal==1.4.0 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 +aiosignal==1.4.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 -annotated-types==0.7.0 \ +annotated-types==0.7.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 -anthropic==0.75.0 \ +anthropic==0.75.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb \ --hash=sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b -anyio==4.11.0 \ - --hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \ - --hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4 -attrs==25.3.0 \ +anyio==4.14.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f +attrs==25.3.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b -authlib==1.6.12 \ +authlib==1.6.12 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd \ --hash=sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab -backoff==2.2.1 \ +backoff==2.2.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba \ --hash=sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8 -beartype==0.22.9 \ +beartype==0.22.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 -beautifulsoup4==4.14.2 \ +beautifulsoup4==4.14.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e \ --hash=sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515 -binaryornot==0.4.4 \ +binaryornot==0.4.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ --hash=sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4 -blake3==1.0.9 \ - --hash=sha256:021309d760b390706fecf13498f9a25aa8f689bbb65a0896029b8fa223aae18b \ - --hash=sha256:09a69fcedf06785bb81d4d3d39f95ee65dbaf2cb246e174cfc9ff64d027f7551 \ - --hash=sha256:0aba416bb2e3ef0c65e74d5eba21062483c714cd78e7e303c9d03c547fc7d015 \ - --hash=sha256:0cdfeff65488089ef86f7587c76055ff72b28d28d10e427b547f5711477c376d \ - --hash=sha256:128a62136c9a39c7cb9fdaa5fb38471f2418853da7f5a89f31495735d0ba6f2c \ - --hash=sha256:15566065ff90ab3da46ec0be1417406f00507af902b6fb0fbc6563e77f02fc42 \ +blake3==1.0.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:172d44245a19dfec08ab771c1b7a506b97783163cdc65f559fe020007e403c99 \ - --hash=sha256:1bbdff61e049297ef3180867ce1f079cea7e5b372fd76953c3183da5b8124206 \ - --hash=sha256:1bd981dc318c05375c3160a99df493b7cc4c83fffa1a34d14b18a071b47b262b \ - --hash=sha256:249e5964fa9e768924bc7cc3d4efe75a425bb5dd3fb7671c3eda8eeddfa50591 \ - --hash=sha256:28404301de485e9546365d01b30f65eaa835520c4211d6ef61242975b6722b60 \ - --hash=sha256:2a800b87433955f37691b5f361ad29c7dd3ee089c9cd109adc5aea8e24bc4c1f \ - --hash=sha256:32bd4521ec2d477627ad93eb70f9ac4d01e12d1489024159bcaeff79466332f6 \ --hash=sha256:3cbe7f190164896dc3908e920716ee66bc31d40f1a0fb603ed59ac53290fb9cf \ - --hash=sha256:42609e4adc4b2d7423137f2cb35135bca598b925c5af09d2bc0a2c368b25aeb1 \ - --hash=sha256:42fa57bf462285ef16400601b0fd32214c248ba92505bbb94b1221ab9af5a092 \ - --hash=sha256:4865a8cfb2b3d7c0baf5267f2fa6816a3384e836cd1bd0caf359f406cb1e8fba \ - --hash=sha256:508ccaf8f9377cc47e6026c2897fdc37de61faeb1420dc023b6379cc2474eb65 \ - --hash=sha256:5b35abe24a66a7b3db423eb4f8668ed7be1a362aa9c0024ab6483ec0b2c16058 \ - --hash=sha256:5ea0c60dd9c1e3d05610606579e4bf80f562854c46ed55f9ee8545e18987a480 \ --hash=sha256:5fa374fa5070ca084368776c19b420157eb0f2d3f091343d6bc59189929d62e2 \ --hash=sha256:66c84dbc2a31eda88b55bbf5c5b711037bf0698eba0fd1faf06bdaf313c39048 \ - --hash=sha256:6879739e7904b9c42afbedbcc2e8c36cebe140fb3fc3f5c492993579cf5cd516 \ - --hash=sha256:689a7e4069de681d9c5d9445b8b6473ee880ad04d7960a6789c60bd788980250 \ - --hash=sha256:69ff5aebc7650954443aa701feff2028d7c7ea5b5e18ee265f15e2104e892328 \ - --hash=sha256:6edeb3d49a24c307995899b70dd47aa901d0e9ad51d2f8a79aba4f074f32d8c5 \ - --hash=sha256:72cdecf088a9d25e6ec79948a578995649b0dbee407e7a46c543a9ecc0f6f281 \ - --hash=sha256:73a48f7e9f0e047f51a445d9b0361ab1907bdc72b6857815a84dacd2e59556f8 \ - --hash=sha256:766f1555cbe614f14f399c2fbec0983568d20edb36837ba04040807eb9e1a609 \ - --hash=sha256:83b4a2336105af3800f7e17ac4b943f293a3927a2d66a6308d50dba944a6953e \ --hash=sha256:8a99f896e7718050ed033a888245098aab3d6a5338f91cc9450c563b53f90ad5 \ - --hash=sha256:8adb0b0032e53919ee95b3d4f911448d3268316c28cd7df232ff2a1e7c9a4ba4 \ - --hash=sha256:91db52a809b68b5bebe7c413ddcd230e1f759398e7fa7a873104595a4fa648b6 \ - --hash=sha256:95fc3545f80901b0dcd0508d16bc40f15ae39556709fa6cf86675f742d4f3c9c \ - --hash=sha256:9767f16199b99aa022b61ff825ac4dbd39864bf637ae712605a2ce1f8b6a55e0 \ - --hash=sha256:9cef6d4d07a7de0c44f5ba17f6383d55276d9efc8d601f75113538fcaa35008b \ - --hash=sha256:a288664d08dee154cc496e06e62517fc9e655ecec12b0d7db538d244ac79edf1 \ - --hash=sha256:a70c20542d5e7960983a0ff32999049a2b0e5ef1f22dbbbdfb51cf04828a4156 \ - --hash=sha256:ab0c030cf6644c30e786b0e785bde4e4596013ae9ea6ce9877e39d52383e25d7 \ - --hash=sha256:ae47c3d5729ff89baa6ddf6de47fcfcc915985d39eb1bfcd6db653331f3c6fcc \ - --hash=sha256:b25ccde5a64be070f20e5c7a81da70292db40b164b6c77588cbd6230856badbb \ - --hash=sha256:b27550ada40f839aca64c66127940e4318bb6ef3e291890ef913017f6f637448 \ - --hash=sha256:b5d5bf0f68cd77108a942c95db98e960d9c3d5643b95172f783822ce22667759 \ --hash=sha256:bcd56a7a972c4185070f7042ccc20166927eec3c0f98b8405f375d007b604a0b \ --hash=sha256:c7f648fa425138452d1e585ac625c7aefddb946d9765906c4c12d564a1523cd8 \ - --hash=sha256:ca7dfe8fb197ff8a3f5c915424183ccd52a99e8afb12680f51b2e1f4c9c6c97f \ - --hash=sha256:caded2806d2cbeed638c5e2517ed8b2a94165b3452fda35e72896142d22070e0 \ --hash=sha256:cfaa671b07eb73883162ca940442193868358b0b904cfa266e4b74131ce966da \ - --hash=sha256:d1ea0bf17b184b03444007646d902207d2b4d4f3e91a0cac3836552d83db74b9 \ - --hash=sha256:dab59b324aa65c09e937d6c43de5de85ec9581627f4e79dcc9806d85b54a1c34 \ - --hash=sha256:eca281fedcbe5c56655bd5a4176e6036eddbbe57df96114a03838fce08b1e0ca \ - --hash=sha256:f5c9d57f0dcb92243b6ae575c3065793edc9df9008d0ebd98d8245cdeb7c3f84 \ --hash=sha256:f65d77eb05331495485048f6804f53885b192b998acb7e6fe1487d941bf08435 -browser-use==0.11.9 \ +browser-use==0.11.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:a66accf4dded24d056eb2733e354e159fd7d82cab986d75dc0f14b395467c82c \ --hash=sha256:c3d8a420acf26d26c688c42916358ba546e7159e1a699c06064b899cb7927577 -browser-use-sdk==2.0.15 \ +browser-use-sdk==2.0.15 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0832ae0998736e6386457e6cf506e2820db0d626c4d9dbf0f567ea2b6c6888d3 \ --hash=sha256:5d8ba3836070c67a9774baac556b233e22f266823d3dee34295d98869bec7752 -bubus==1.5.6 \ +bubus==1.5.6 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1a5456f0a576e86613a7bd66e819891b677778320b6e291094e339b0d9df2e0d \ --hash=sha256:254ae37cd9299941f5e9d6afb11f8e3ce069f83e5b9476f88c6b2e32912f237d -cachetools==7.1.8 \ +cachetools==7.1.8 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1221d547a0b24b7f26fa891d40d488b5258beab9aebd8ed68c729be3af849c43 \ --hash=sha256:a81e3844acaa7355b6567f97bd67a94a14ec3a9bc2cbbdae45b9592cc036775b -caio==0.9.25 \ +caio==0.9.25 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40 \ --hash=sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6 \ --hash=sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7 \ --hash=sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10 \ - --hash=sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f \ --hash=sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77 \ - --hash=sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb \ - --hash=sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79 \ --hash=sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69 \ --hash=sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64 \ - --hash=sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7 \ - --hash=sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451 \ - --hash=sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db \ - --hash=sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044 -cdp-use==1.4.4 \ + --hash=sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7 +cdp-use==1.4.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:330a848b517006eb9ad1dc468aa6434d913cf0c6918610760c36c3fdfdba0fab \ --hash=sha256:e37e80e067db2653d6fdf953d4ff9e5d80d75daa27b7c6d48c0261cccbef73e1 -certifi==2025.8.3 \ +certifi==2025.8.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 -cffi==2.0.0 ; platform_python_implementation != 'PyPy' \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ +cffi==2.0.0 ; platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 -chardet==5.2.0 \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 +chardet==5.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ --hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970 -charset-normalizer==3.4.3 \ - --hash=sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154 \ - --hash=sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884 \ - --hash=sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe \ +charset-normalizer==3.4.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f \ --hash=sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc \ --hash=sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa \ - --hash=sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9 \ - --hash=sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d \ - --hash=sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92 \ - --hash=sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31 \ --hash=sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f \ - --hash=sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15 \ - --hash=sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392 \ --hash=sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f \ - --hash=sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8 \ - --hash=sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491 \ - --hash=sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0 \ - --hash=sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927 \ - --hash=sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce \ --hash=sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14 \ - --hash=sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c \ - --hash=sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc \ - --hash=sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096 \ - --hash=sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db \ - --hash=sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5 \ - --hash=sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce \ - --hash=sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018 \ - --hash=sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93 \ - --hash=sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049 \ --hash=sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a \ - --hash=sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef \ - --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 \ - --hash=sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1 \ - --hash=sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37 \ - --hash=sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9 -click==8.1.8 \ - --hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \ - --hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a -cloudpickle==3.1.2 \ + --hash=sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16 +click==8.5.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ + --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 +cloudpickle==3.1.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414 \ --hash=sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a -colorama==0.4.6 ; sys_platform == 'win32' \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 -cryptography==48.0.1 ; platform_machine == 'x86_64' and sys_platform == 'darwin' \ - --hash=sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a \ - --hash=sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1 \ - --hash=sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67 \ - --hash=sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345 -cryptography==50.0.0 ; platform_machine != 'x86_64' or sys_platform != 'darwin' \ - --hash=sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03 \ +cryptography==50.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7 \ - --hash=sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987 \ --hash=sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025 \ --hash=sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037 \ - --hash=sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269 \ - --hash=sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105 \ - --hash=sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc \ --hash=sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95 \ - --hash=sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b \ --hash=sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47 \ - --hash=sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c \ - --hash=sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41 \ --hash=sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7 \ --hash=sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c \ - --hash=sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708 \ --hash=sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef \ --hash=sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f \ --hash=sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f \ - --hash=sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a \ - --hash=sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f \ - --hash=sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a \ - --hash=sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3 \ - --hash=sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d \ - --hash=sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3 \ --hash=sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f \ --hash=sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae \ - --hash=sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30 \ - --hash=sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9 \ - --hash=sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07 \ - --hash=sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba \ - --hash=sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3 \ - --hash=sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f \ - --hash=sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533 \ - --hash=sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5 \ - --hash=sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11 \ - --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 \ - --hash=sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f \ - --hash=sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169 \ - --hash=sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645 -cyclopts==4.6.0 \ + --hash=sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9 +cyclopts==4.6.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0a891cb55bfd79a3cdce024db8987b33316aba11071e5258c21ac12a640ba9f2 \ --hash=sha256:483c4704b953ea6da742e8de15972f405d2e748d19a848a4d61595e8e5360ee5 -cython==3.1.4 ; platform_system != 'darwin' and sys_platform == 'darwin' \ - --hash=sha256:0d26af46505d0e54fe0f05e7ad089fd0eed8fa04f385f3ab88796f554467bcb9 \ - --hash=sha256:52eae5d9bcc515441a436dcae2cbadfd00c5063d4d7809bd0178931690c06a76 \ - --hash=sha256:66ac8bb5068156c92359e3f0eefa138c177d59d1a2e8a89467881fa7d06aba3b \ - --hash=sha256:9aefefe831331e2d66ab31799814eae4d0f8a2d246cbaaaa14d1be29ef777683 \ - --hash=sha256:ab549d0fc187804e0f14fc4759e4b5ad6485ffc01554b2f8b720cc44aeb929cd \ - --hash=sha256:b2d522ee8d3528035e247ee721fb40abe92e9ea852dc9e48802cec080d5de859 \ - --hash=sha256:c7258739d5560918741cb040bd85ba7cc2f09d868de9116a637e06714fec1f69 \ - --hash=sha256:d194d95e4fa029a3f6c7d46bdd16d973808c7ea4797586911fdb67cb98b1a2c6 -deprecation==2.1.0 \ +deprecation==2.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \ --hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a -distro==1.9.0 \ +distro==1.9.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 -dnspython==2.8.0 \ +dnspython==2.8.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af \ --hash=sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f -docstring-parser==0.17.0 \ +docstring-parser==0.17.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 -docutils==0.22.2 \ +docutils==0.22.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:9fdb771707c8784c8f2728b67cb2c691305933d68137ef95a75db5f4dfbc213d \ --hash=sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8 -email-validator==2.3.0 \ +email-validator==2.3.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 \ --hash=sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426 -exceptiongroup==1.3.0 \ +exceptiongroup==1.3.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \ --hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88 -fakeredis==2.34.1 \ +fakeredis==2.34.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12 \ --hash=sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b -fastmcp==3.2.0 \ +fastmcp==3.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef \ --hash=sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681 -fastuuid==0.14.0 \ - --hash=sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995 \ - --hash=sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796 \ - --hash=sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed \ +fastuuid==0.14.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab \ --hash=sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26 \ - --hash=sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75 \ - --hash=sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714 \ - --hash=sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b \ - --hash=sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8 \ --hash=sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87 \ --hash=sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8 \ - --hash=sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021 \ - --hash=sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a \ --hash=sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09 \ - --hash=sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176 \ - --hash=sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4 \ - --hash=sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc \ - --hash=sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad \ --hash=sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f \ - --hash=sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f \ - --hash=sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f \ - --hash=sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741 \ - --hash=sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5 \ - --hash=sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad \ - --hash=sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057 \ - --hash=sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b \ - --hash=sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d \ - --hash=sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022 \ - --hash=sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070 \ - --hash=sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397 \ - --hash=sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a \ - --hash=sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06 \ - --hash=sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8 \ - --hash=sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad -filelock==3.20.3 \ + --hash=sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8 +filelock==3.20.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1 \ --hash=sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 -frozenlist==1.8.0 \ - --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ - --hash=sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0 \ - --hash=sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121 \ - --hash=sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd \ +frozenlist==1.8.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7 \ - --hash=sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c \ --hash=sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d \ - --hash=sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b \ - --hash=sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79 \ - --hash=sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f \ - --hash=sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7 \ - --hash=sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef \ --hash=sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed \ - --hash=sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b \ - --hash=sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe \ - --hash=sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143 \ - --hash=sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e \ - --hash=sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930 \ - --hash=sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37 \ - --hash=sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128 \ - --hash=sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2 \ - --hash=sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f \ - --hash=sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746 \ - --hash=sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df \ - --hash=sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8 \ --hash=sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c \ - --hash=sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0 \ --hash=sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad \ - --hash=sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82 \ - --hash=sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29 \ - --hash=sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30 \ - --hash=sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf \ - --hash=sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62 \ --hash=sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 \ - --hash=sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c \ - --hash=sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52 \ - --hash=sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d \ - --hash=sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1 \ - --hash=sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a \ - --hash=sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714 \ - --hash=sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65 \ - --hash=sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506 \ - --hash=sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888 \ --hash=sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41 \ - --hash=sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608 \ --hash=sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa \ - --hash=sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8 \ - --hash=sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1 \ - --hash=sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed \ - --hash=sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231 \ - --hash=sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496 \ - --hash=sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a \ - --hash=sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3 \ - --hash=sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24 \ - --hash=sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7 \ - --hash=sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4 \ - --hash=sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e \ --hash=sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e \ - --hash=sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8 \ --hash=sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51 \ - --hash=sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8 \ - --hash=sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806 \ - --hash=sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042 \ - --hash=sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b \ - --hash=sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a \ --hash=sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2 \ - --hash=sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0 \ - --hash=sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e \ - --hash=sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b \ - --hash=sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d \ - --hash=sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52 \ - --hash=sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1 \ - --hash=sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94 \ - --hash=sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822 \ - --hash=sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a \ - --hash=sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11 \ - --hash=sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51 \ - --hash=sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40 \ - --hash=sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5 \ - --hash=sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4 \ - --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 \ - --hash=sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd -fsspec==2025.9.0 \ + --hash=sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027 +fsspec==2025.9.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:19fd429483d25d28b65ec68f9f4adc16c17ea2c7c7bf54ec61360d478fb19c19 \ --hash=sha256:530dc2a2af60a414a832059574df4a6e10cce927f6f4a78209390fe38955cfb7 -func-timeout==4.3.5 \ +func-timeout==4.3.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:74cd3c428ec94f4edfba81f9b2f14904846d5ffccc27c92433b8b5939b5575dd -google-api-core==2.26.0 ; python_full_version < '3.13' \ +google-api-core==2.26.0 ; python_full_version < '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2b204bd0da2c81f918e3582c48458e24c11771f987f6258e6e227212af78f3ed \ --hash=sha256:e6e6d78bd6cf757f4aee41dcc85b07f485fbb069d5daa3afb126defba1e91a62 -google-api-core==2.31.0 ; python_full_version >= '3.13' \ +google-api-core==2.31.0 ; python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2 \ --hash=sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab -google-api-python-client==2.184.0 \ +google-api-python-client==2.184.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:15a18d02f42de99416921c77be235d12ead474e474a1abc348b01a2b92633fa4 \ --hash=sha256:ef2a3330ad058cdfc8a558d199c051c3356f6ed012436c3ad3d08b67891b039f -google-auth==2.53.0 \ +google-auth==2.53.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:6e7449917c599b35126a99ec268ec6880301f2fea41dce198fe8fd83ff642b68 \ --hash=sha256:e7e6aa16f6bee7b2b264830fd04f08087a1d5a836df516251a5d15327b246c9c -google-auth-httplib2==0.2.0 \ +google-auth-httplib2==0.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \ --hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d -google-auth-oauthlib==1.2.2 \ +google-auth-oauthlib==1.2.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:11046fb8d3348b296302dd939ace8af0a724042e8029c1b872d87fabc9f41684 \ --hash=sha256:fd619506f4b3908b5df17b65f39ca8d66ea56986e5472eb5978fd8f3786f00a2 -google-genai==1.75.0 \ +google-genai==1.75.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf \ --hash=sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c -googleapis-common-protos==1.70.0 \ +googleapis-common-protos==1.70.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257 \ --hash=sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8 -groq==0.32.0 \ +groq==0.32.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0ed0be290042f8826f851f3a1defaac4f979dcfce86ec4a0681a23af00ec800b \ --hash=sha256:fb1ade61f47a06d1a1c1dc0fab690d269b799ebd57ad1dd867efaeaa7adeb2af -grpcio==1.76.0 \ - --hash=sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280 \ - --hash=sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd \ +grpcio==1.76.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc \ --hash=sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba \ - --hash=sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03 \ - --hash=sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2 \ - --hash=sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb \ - --hash=sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958 \ - --hash=sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468 \ - --hash=sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc \ - --hash=sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09 \ - --hash=sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980 \ - --hash=sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f \ --hash=sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882 \ --hash=sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae \ - --hash=sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77 \ - --hash=sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e \ --hash=sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73 \ - --hash=sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8 \ - --hash=sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3 \ - --hash=sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e \ --hash=sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42 \ - --hash=sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6 \ - --hash=sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11 \ - --hash=sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a \ - --hash=sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347 \ - --hash=sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4 \ - --hash=sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8 \ - --hash=sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8 \ - --hash=sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc \ - --hash=sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62 -h11==0.16.0 \ + --hash=sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6 +h11==0.16.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 -hf-xet==1.1.10 ; platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' \ - --hash=sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06 \ +hf-xet==1.1.10 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97 \ - --hash=sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045 \ - --hash=sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d \ --hash=sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435 \ - --hash=sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b \ - --hash=sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c \ --hash=sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f -httpcore==1.0.9 \ +httpcore==1.0.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 -httplib2==0.32.0 \ +httplib2==0.32.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025 \ --hash=sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816 -httpx==0.28.1 \ +httpx==0.28.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad -httpx-sse==0.4.3 \ +httpx-sse==0.4.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d -huggingface-hub==0.35.3 \ +huggingface-hub==0.35.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba \ --hash=sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a -idna==3.15 \ +idna==3.15 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8 \ --hash=sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc -importlib-metadata==8.5.0 \ +importlib-metadata==8.5.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \ --hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7 -inquirerpy==0.3.4 \ +inquirerpy==0.3.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:89d2ada0111f337483cb41ae31073108b2ec1e618a49d7110b0d7ade89fc197e \ --hash=sha256:c65fdfbac1fa00e3ee4fb10679f4d3ed7a012abf4833910e63c295827fe2a7d4 -jaraco-classes==3.4.0 \ +jaraco-classes==3.4.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \ --hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790 -jaraco-context==6.1.0 \ +jaraco-context==6.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f \ --hash=sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda -jaraco-functools==4.4.0 \ +jaraco-functools==4.4.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176 \ --hash=sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb -jeepney==0.9.0 ; sys_platform == 'linux' \ +jeepney==0.9.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \ --hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732 -jinja2==3.1.6 \ +jinja2==3.1.6 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 -jiter==0.11.0 \ - --hash=sha256:07630bb46ea2a6b9c6ed986c6e17e35b26148cce2c535454b26ee3f0e8dcaba1 \ - --hash=sha256:089f9df9f69532d1339e83142438668f52c97cd22ee2d1195551c2b1a9e6cf33 \ +jiter==0.11.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:11840d2324c9ab5162fc1abba23bc922124fedcff0d7b7f85fffa291e2f69206 \ - --hash=sha256:1d4a6c4a737d486f77f842aeb22807edecb4a9417e6700c7b981e16d34ba7c72 \ --hash=sha256:1d9637eaf8c1d6a63d6562f2a6e5ab3af946c66037eb1b894e8fad75422266e4 \ - --hash=sha256:25a5b1110cca7329fd0daf5060faa1234be5c11e988948e4f1a1923b6a457fe1 \ --hash=sha256:29ed1fe69a8c69bf0f2a962d8d706c7b89b50f1332cd6b9fbda014f60bd03a03 \ - --hash=sha256:29fff31190ab3a26de026da2f187814f4b9c6695361e20a9ac2123e4d4378a4c \ - --hash=sha256:2fb7b377688cc3850bbe5c192a6bd493562a0bc50cbc8b047316428fbae00ada \ - --hash=sha256:4441a91b80a80249f9a6452c14b2c24708f139f64de959943dfeaa6cb915e8eb \ - --hash=sha256:452d13e4fd59698408087235259cebe67d9d49173b4dacb3e8d35ce4acf385d6 \ - --hash=sha256:4ad8bd82165961867a10f52010590ce0b7a8c53da5ddd8bbb62fef68c181b921 \ --hash=sha256:4ee5821e3d66606b29ae5b497230b304f1376f38137d69e35f8d2bd5f310ff73 \ - --hash=sha256:4f01a744d24a5f2bb4a11657a1b27b61dc038ae2e674621a74020406e08f749b \ - --hash=sha256:53933a38ef7b551dd9c7f1064f9d7bb235bb3168d0fa5f14f0798d1b7ea0d9c5 \ --hash=sha256:6e2bbf24f16ba5ad4441a9845e40e4ea0cb9eed00e76ba94050664ef53ef4406 \ - --hash=sha256:7764f27d28cd4a9cbc61704dfcd80c903ce3aad106a37902d3270cd6673d17f4 \ - --hash=sha256:7b13a431dba4b059e9e43019d3022346d009baf5066c24dcdea321a303cde9f0 \ - --hash=sha256:8e36924dad32c48d3c5e188d169e71dc6e84d6cb8dedefea089de5739d1d2f80 \ - --hash=sha256:9a6dff27eca70930bdbe4cbb7c1a4ba8526e13b63dc808c0670083d2d51a4a72 \ - --hash=sha256:a1b7cbe3f25bd0d8abb468ba4302a5d45617ee61b2a7a638f63fee1dc086be99 \ - --hash=sha256:a4d71d7ea6ea8786291423fe209acf6f8d398a0759d03e7f24094acb8ab686ba \ - --hash=sha256:adcab442f4a099a358a7f562eaa54ed6456fb866e922c6545a717be51dbed7d7 \ - --hash=sha256:af62e84ca3889604ebb645df3b0a3f3bcf6b92babbff642bd214616f57abb93a \ - --hash=sha256:b1ae2a7593a62132c7d4c2abbee80bbbb94fdc6d157e2c6cc966250c564ef774 \ - --hash=sha256:b42c2cd74273455ce439fd9528db0c6e84b5623cb74572305bdd9f2f2961d3df \ --hash=sha256:bb948402821bc76d1f6ef0f9e19b816f9b09f8577844ba7140f0b6afe994bc64 \ - --hash=sha256:bf11807e802a214daf6c485037778843fadd3e2ec29377ae17e0706ec1a25758 \ - --hash=sha256:c0a7f0ec81d5b7588c5cade1eb1925b91436ae6726dc2df2348524aeabad5de6 \ - --hash=sha256:c2d13ba7567ca8799f17c76ed56b1d49be30df996eb7fa33e46b62800562a5e2 \ - --hash=sha256:c5e86126d64706fd28dfc46f910d496923c6f95b395138c02d0e252947f452bd \ - --hash=sha256:c6f3b32bb723246e6b351aecace52aba78adb8eeb4b2391630322dc30ff6c773 \ --hash=sha256:c9967c2ab338ee2b2c0102fd379ec2693c496abf71ffd47e4d791d1f593b68e2 \ - --hash=sha256:cdef53eda7d18e799625023e1e250dbc18fbc275153039b873ec74d7e8883e09 \ --hash=sha256:cf408d2a0abd919b60de8c2e7bc5eeab72d4dafd18784152acc7c9adc3291591 \ - --hash=sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166 \ - --hash=sha256:e7d0bed3b187af8b47a981d9742ddfc1d9b252a7235471ad6078e7e4e5fe75c2 \ - --hash=sha256:f0062dab98172dd0599fcdbf90214d0dcde070b1ff38a00cc1b90e111f071982 \ - --hash=sha256:f6fe0283e903ebc55f1a6cc569b8c1f3bf4abd026fed85e3ff8598a9e6f982f0 \ - --hash=sha256:fb4790497369d134a07fc763cc88888c46f734abdd66f9fdf7865038bf3a8f40 \ - --hash=sha256:ff85fc6d2a431251ad82dbd1ea953affb5a60376b62e7d6809c5cd058bb39471 -joserfc==1.7.5 \ + --hash=sha256:dbb57da40631c267861dd0090461222060960012d70fd6e4c799b0f62d0ba166 +joserfc==1.7.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:add2c2c84e8373b084d526a8b53daba5d7a513a118cd2dcd9fc9f979d0922159 \ --hash=sha256:d5ff536e658e17664f8c1b1ab60dc4aa62aa973fcef1edd33cc44bda45d6f5ea -json-repair==0.60.1 \ +json-repair==0.60.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:841661cdd2df507c9a4e189097f38ca6bc372e06d4b4e36d72e590f68176c290 \ --hash=sha256:ba6ff974f2a8bef2f7768144a7f03f870a816443f03da27a49cdd0ec31a78049 -jsonref==1.1.0 \ +jsonref==1.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 -jsonschema==4.23.0 \ +jsonschema==4.23.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4 \ --hash=sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566 -jsonschema-path==0.3.4 \ +jsonschema-path==0.3.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001 \ --hash=sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8 -jsonschema-specifications==2025.9.1 \ +jsonschema-specifications==2025.9.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d -keyring==25.7.0 \ +keyring==25.7.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \ --hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b -libtmux==0.53.0 \ +libtmux==0.53.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:024b7ae6a12aae55358e8feb914c8632b3ab9bd61c0987c53559643c6a58ee4f \ --hash=sha256:1d19af4cea0c19543954d7e7317c7025c0739b029cccbe3b843212fae238f1bd -litellm==1.93.0 \ +litellm==1.93.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0784172435de48f66ef7ad89d421604db1ad0db1321ef7f39dbcfe6b20111417 \ --hash=sha256:140bf215e264c71601bca9c06d2436c5451bb59e1e195ea23fc2d3d87b6929ec \ - --hash=sha256:1a476ebc340c070c982eab15b4673fa63a70f5936935f9f20e4d2acb5f35d23b \ - --hash=sha256:23b8eea4fb8b3b6ade05e7b6085ec9ca12daffcfb38d033d0bbce9c1d5894da5 \ - --hash=sha256:3daf5c5aceb07f5d68871071e0ecdc678caccebadca9143cc00bb76f5a8c54e8 \ --hash=sha256:98c15e84d32e922a821c105308bf9ddae8700de9ed396530bee2cbb1cacf4cbb \ --hash=sha256:cd70ccd4ba3ef1a395535c287bce72d30c7d937ecca4290c0db37a00738f7ada -lmnr==0.7.60 \ +lmnr==0.7.60 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:35076b302a8f4349824deb08f79d1c7e4c2486b5a14704c0e6efee9879942096 \ --hash=sha256:efec73de28eb8b66bcda6aaab7db0bda8247a26f982e63295a754412bee607f0 -lmnr-claude-code-proxy==0.1.23 \ - --hash=sha256:0147eecb2704a39eceaf01a23768be1de49c3f2f909d5ad5e5ad5a1c8a4b8d94 \ - --hash=sha256:0ca8b8badb3bf137fc58cf3e98542869d983691b91d431508c1f92d8c7b1267c \ +lmnr-claude-code-proxy==0.1.23 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0fd8e1bfeef82802c8a74533a6d27e7c7912c7db68358ca67976f1635311503f \ - --hash=sha256:1304486204ddc39a3f5639153df6ae4ee53152ea83063b0ce3331360f05ee026 \ - --hash=sha256:18147e58da5c3cd2901adfbbdf0851c5aef4a0feb0fed66732f4f0ef64549531 \ - --hash=sha256:1b04bca678761d37e60508494b8c04c3368bf23bd6569a4d215e93ca5daac89a \ - --hash=sha256:207f6d423467909a130308d2c33eba05b25232e6934c9c23b928b24037d51f3b \ - --hash=sha256:21fdef811f3f06fc507ffd795bca64a21b525faa7f3f6e2f33856910d0f96192 \ - --hash=sha256:29e19c7fb7d14e6df820a6c2e4a8ee62f13974fc23d966086a75f1d3d90bc68a \ - --hash=sha256:2d6c4bf49bd060146577022d2f45a9921ce5604289d7f52db357fd3d5aeac838 \ --hash=sha256:44b270a4c40daf3b11a5d96694357c4ff99842e82c514281b2910eae4a5d653a \ - --hash=sha256:48479d6205854919139c748cac2407a39061eca363ce00a2664e05623933cada \ - --hash=sha256:4a42afb4bc589f0d1d6947b88d8a33e3c44029620212d143320875a3d997331a \ - --hash=sha256:50a9659f4e06cdf783740cef04811e09017e091993da2d26aafef6e0cc1526a9 \ - --hash=sha256:53c6d47ad2bea70dda75a642a3d8fa7b4fbb68c718958d37a5f994ff6f98d210 \ - --hash=sha256:5853e6fa123af8611c53b3fcac86231a6944d0861fe7808cdc2281c8168d66bd \ - --hash=sha256:5b59f21e7baa11212e61a300736fca7c2a724950811f1301e605c7d08c886fd5 \ --hash=sha256:5bb8df798ad59b5ba096a7ce1c9a1a08fab5fcb9a6701b3e304bac6a060f109c \ - --hash=sha256:5e67485c946f716e6f4c7522c67ecb0a7925f3ed15a0d40bafd4632409c1acc6 \ - --hash=sha256:5f90acf340a618cfa445bcff29b0c942f231368fcaaca590755ca47960437373 \ - --hash=sha256:6704d8e779950f643c901d1c298428ed06eb331f6e86dd95758fd89ebb17f9cb \ --hash=sha256:68fdb6a26d30977b5d4e7ce4a117b2a427a16b1cc0d075f4cecd1839da12975a \ - --hash=sha256:6bfb54627cf1a400e18e4c51a7bcb312da1bac701ef56059c82f094d5edf75c7 \ - --hash=sha256:6d7fe54463ff075d557dc1941d7d271bc79e2663e2a90ab1ff45594804b6958c \ --hash=sha256:6d8fa044c2f5774a6b70de9025a91c13c74361d4349ae528b4b4fcca48219a13 \ - --hash=sha256:7164c71707717a6e242ef5fe0ddeb33095a69e8566a80bb979f4d81037e4dcf2 \ - --hash=sha256:7192a72122633ce7d92e2ae4d3df53854c2aa3dddccf4f8c5155d0dfe3e574ab \ --hash=sha256:724ac0371eef99c034762521e580a986fc2b012ffc8c650dd8d66f1765d9a102 \ - --hash=sha256:7b883a547e444a095f93eebbc42ed0d27e84346d986251097b7cb62fc2a0a492 \ - --hash=sha256:7c372c3678aa21d422644bef5955a68b4b53cafb40a0c0804b09eab418443fb8 \ - --hash=sha256:8646625c70cd2730799e6d3266cf42af86c47101e7eae7254e2c6b482c4db05f \ - --hash=sha256:88c39ab3a86fcafcad48297bda0f91bd1b2d0e4d9dbf4ac2a94e7ecace3f6165 \ - --hash=sha256:8b43a95fdf053fc4ebc0723363d9f88aa5060078733f9df6b3e991235c473a96 \ --hash=sha256:92233f1c19971678225ac884d26b77cbbe9e90ec0a154752f8feee9ffa3f21c2 \ - --hash=sha256:962aafb103e435461a66c3ca0a77bfddb7d2bde5abe56922f418076472eb27cc \ - --hash=sha256:97bd9997af125df8f2a0fb72ac135c0c0fe89ae67f497ac5da51b45ac142f5a4 \ - --hash=sha256:9cccd9219743a8539ab9711fe01e84eb91d95388b19a3bfaa0d215cc946d7bf2 \ --hash=sha256:9e7a58160b8c327e9fddbba828c86bb1084967519a160ae4f64d0ae01d65730b \ - --hash=sha256:a3769f25f3341ae04885f3b8668fd48214b8eb9b72faa97b9d7dbc9095c8a40d \ - --hash=sha256:a3a3ec1b6b015d8fb6fb373cedb55bfc90c1187333456eb9e6a4a8124b23ee1d \ - --hash=sha256:a44d51db3002164ea5795721e3d8d8927f7981a89ffe1ae9359b6b69ebe02836 \ - --hash=sha256:a5ace97624a755d5c79fcbf9d0ab6564ac17a9d1dc361747fcfa299a3a004249 \ - --hash=sha256:a77258d487989bf23ec6807e21cd2c334e9e457e9f854875a0c253cb04260387 \ - --hash=sha256:a99809eea013706b24b260a2f095551d2b82ddd6e0655e0b0ff2a5d468fdfbea \ - --hash=sha256:b4288560b0076ba68d6103065043d5258594eed4d1fe59631a74e20c6cddf9c2 \ - --hash=sha256:b5c64fcdd623ceec9f41cf0cd6761409a1c0add4e85409de294faf0647f5eb86 \ - --hash=sha256:bcd83941a84b89fc2f5dac40b10b7736a5602766f33afcd554545b0bf770fdc2 \ - --hash=sha256:cff0ef6dbe34324bada6d538a8dd71bae07184715f6e9ba8408ab85bf251b1ab \ - --hash=sha256:cffbd2436215675119b94de508d20a87c49e0bcf9de7b2dba1974f1af35f0bfc \ - --hash=sha256:d2b41beaf87b99e12943dfd4c28343b9fbf7771582ac68bb8dbb847eeb7b7446 \ - --hash=sha256:d575823c9002c55e67a4e8277ff0477739f84b419e3356a974a3dbedbda90b63 \ - --hash=sha256:d7ac385abe119b21aaf8ab1c1aee7f9dcdd8a03a3a00aad331f3326b6aac075c \ - --hash=sha256:e140b8e9c0b56e63ef86e8ccda0faf6fa9e171c3d0efa11467afd35853770daf \ - --hash=sha256:e5bf63f3f99f011200a6efcc3312a8a2c72cf4a7457580056654df71ef4cd87b \ - --hash=sha256:e747afab3475d07f0c8941e7d1bbb27d2e88284afef03b7824fc40175adb047c \ - --hash=sha256:e8ef14e4b592cfd27d057b47a25e9f62e6d900a95ed244c4acb3316b76f6034f \ - --hash=sha256:e9d9d94f552a1ae8e7f871b8af2440653b614ba818054c60080d457a6fefd07a \ - --hash=sha256:f05a1978c707ba5df0be531dd2cac0da8117c86c6172d84cb4f5180c8123fc49 \ - --hash=sha256:f06ff39da6c5bdedd7daadd4b33337aacc33b51ed57aa6a6a4f035de243ab59e \ - --hash=sha256:f82487d932680337e3335573a8d582cfd02cee5bc95a4e28cb4befa38b658a27 \ --hash=sha256:f8d636068b2b141f210dcadf433f86dc69f810245771f95f647c1a57d2a37dd9 -lupa==2.8 \ - --hash=sha256:1628371c6592a6d5650497a9e31fb2bb3a7e9883c1f301d1111265e484045af9 \ - --hash=sha256:24b4d8af5558e549b70daf1547f5c1c1d664ecea9fc790f83efe5d75e9a93797 \ - --hash=sha256:250e035fdaffe8c87093e3ebc206ac29a26131b1568ea711d780c26001ce96e7 \ - --hash=sha256:27044f3363047f946b3d3aab9157cbd172b3538ada9ec1baef43432bf7d03a78 \ - --hash=sha256:281bedc5deb92d31e649a3552edd662449365a635904fa4d5cb4509c7245e34e \ +lupa==2.8 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2e64acbbd47e9b82a64405a39e0d2b36a5a7dad8ab41c0f3437f572f7d282ba3 \ - --hash=sha256:348c3f8ecabb6324dcbc05c2740d762ef8fcec7b06c79e45262ab97a217684e3 \ - --hash=sha256:360056453a7a4eaa4ac5a204c31a5a014b1eb2ee5490603234d2ba831684f1f2 \ - --hash=sha256:3903c9cf628dae2f56405503247b77a61a3a61bd2dda470e336950c74776d55d \ - --hash=sha256:4203fa1659315e939a5304e75001b8cc14234fb3cbb3ed86c049b0cc5d90fcee \ - --hash=sha256:450650f91c48c2415b0d59ab3abfcfda3b6efb5b858205f4d4bda8ad141fa529 \ - --hash=sha256:45fc9da0145ecb0083ef5ff9975116cc784bd0258bdc2bd131ba15483ce18398 \ - --hash=sha256:4f7c553c1d8cfffbe85d81daef730d12cae4b6002d457542914da0ac8a1145b3 \ --hash=sha256:4f81a02806e7c7ad26d8c6fa222c8bef1b0c1b124347c879be880b41339d41e4 \ - --hash=sha256:4fe5d7a810b64ea8511eb885fc8cdde042ee5ff7b7d08ae78f32449756acb177 \ - --hash=sha256:54cff414f21f8cd8c6be4aae52541f3b9cd39602b59e3a3db9b5c9f9f674ff18 \ - --hash=sha256:58e18afed57955b41130e269c78f53d4123ab86e236b53816f4cbffa25cb5d30 \ - --hash=sha256:7f210d5a8353e510ea1199c42cf3cbdd630553bf2bc8fb4c00fea06fdec7c798 \ - --hash=sha256:81f2d843ce668b653146c007467570210ae44be51dac6926666c51d49536f307 \ - --hash=sha256:891f72e0bffbed1e4175f975aeb2a083956586a100066525e1be485f617f7b25 \ --hash=sha256:8cf4f064a0e5531afce2d7d750120c10c10f9529139af6ca6150d13151034398 \ - --hash=sha256:91d622777febda3ab1bed1d45295f2f32a4680c7b3d7caf8c669998ed5c44118 \ - --hash=sha256:951496471056061598a7d1729a6cdf48d662fec777a9f2d8aa5a1e62fd30e5a5 \ - --hash=sha256:9e0d11b8f3a8dac6413f704fef7161d048bb10c58bdac6cbffa5e60efa56e9a3 \ - --hash=sha256:9e304fb1c50cf23fd8882afbe1aa87525ef8a72667bcab3b37b2bbb2bc542269 \ - --hash=sha256:a295f87b5b7ebbfd5191932e8cb0e51df3c7769101ac6b6c7d7c9fb27bfd1307 \ --hash=sha256:a591b9947ca347b41a63370e121d6e2b1458fe6dde9ae065029ec10a37f25ff4 \ - --hash=sha256:ac6b6e8d0e617e26a98cbb44880bcd75de5d32b3ad7b3b3793583909292b47ed \ - --hash=sha256:b036738282a5acd2e71fdddb317c9df8b87c1673aa57f403d05fcc2be8abc4ba \ - --hash=sha256:b9bddb09acfffb4f828f790f444b11dc0cca591afea1a244d9329eea2d20c003 \ - --hash=sha256:ba3a7dd839f90c3d2e53bebe3c192b1f3f9fd720a6781256405123211fd0dce6 \ - --hash=sha256:bfc470012ef66ad064c7bd77416af03a3452ef630b04b9012595ea13f2e54518 \ - --hash=sha256:c2a5fd15dc62374e1661a55f01744c9ec1c56f291ba4a0749d3af2174556e78f \ - --hash=sha256:ce86dff1ee7f7cf45f5622065ae991949dd7bb1703581cbc58a630137bb7ccf9 \ - --hash=sha256:ce9404c661dbac65cc9bed351ad45e797af93d30d70be309a3fa8209ac86d93b \ - --hash=sha256:d3d0cde2c77588d1c60875a4f34f059513476c6e1775351897195b51e0f3df08 \ - --hash=sha256:d7edb13a7a5250b5c6c22d1495d9e842b5c9fc5081c8fe6b5efe2112fe3e41f9 \ --hash=sha256:d8022641b9ec8ecf2c5ecbe9f47e5a70e0b87c4b5ae921b92cb02a638e0acd08 \ - --hash=sha256:d8766aff03a78c80ad2d188a8bdb216de5ec838359cd87e05bbdfa56394a6105 \ - --hash=sha256:dc51250e76367a3e27fcd01dc769b9bfcbbc34f48df48dde53d6af6e75b7eaa5 \ - --hash=sha256:f4342f4de76ae7ce2ab0672d36003bdb7e1a33252f293b569298ddd792e70e33 \ - --hash=sha256:f4d01b2a08c70bbb883a9e082b6b36b89121ed5910b710f1ba11c73295ff4fba \ - --hash=sha256:f711a8ab0486b9ac6fdda94a22ddcfbc9f0d4a27e3a8cf1bf79c6e48b33017c1 \ --hash=sha256:f8a22088a552828958603323f0a5c4b3e11e03b75d0bf4c965ef879de9b60a8d \ --hash=sha256:fc47f536ac13a79cef47d29a2b205576a22841f042a2bcec1676b95806e7706a -lxml==6.1.0 \ - --hash=sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2 \ - --hash=sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773 \ - --hash=sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad \ - --hash=sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54 \ - --hash=sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c \ - --hash=sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69 \ - --hash=sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd \ - --hash=sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405 \ - --hash=sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b \ +lxml==6.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d \ - --hash=sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac \ - --hash=sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62 \ - --hash=sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f \ - --hash=sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad \ - --hash=sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292 \ --hash=sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491 \ - --hash=sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120 \ --hash=sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb \ - --hash=sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32 \ - --hash=sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2 \ - --hash=sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88 \ - --hash=sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43 \ - --hash=sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03 \ - --hash=sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485 \ - --hash=sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16 \ --hash=sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb \ - --hash=sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9 \ - --hash=sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f \ - --hash=sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105 \ - --hash=sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7 \ - --hash=sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5 \ - --hash=sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d \ - --hash=sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9 \ - --hash=sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d \ - --hash=sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb \ - --hash=sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f \ - --hash=sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb \ - --hash=sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e \ --hash=sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33 \ - --hash=sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5 \ --hash=sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585 \ - --hash=sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946 \ - --hash=sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e \ --hash=sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a \ - --hash=sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28 \ - --hash=sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c \ --hash=sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9 \ - --hash=sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495 \ - --hash=sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45 \ - --hash=sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc \ - --hash=sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8 \ - --hash=sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f \ - --hash=sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366 \ --hash=sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814 \ - --hash=sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690 \ --hash=sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13 \ - --hash=sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819 \ - --hash=sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c \ - --hash=sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86 \ --hash=sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180 \ - --hash=sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe \ - --hash=sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181 \ - --hash=sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d \ - --hash=sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24 \ - --hash=sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d \ - --hash=sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d \ - --hash=sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9 \ --hash=sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2 \ - --hash=sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f \ - --hash=sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93 \ - --hash=sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d \ - --hash=sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d \ --hash=sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086 -markdown-it-py==4.0.0 \ +markdown-it-py==4.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 -markdownify==1.2.0 \ +markdownify==1.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351 \ --hash=sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c -markupsafe==3.0.2 \ - --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ +markupsafe==3.0.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ - --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ - --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ - --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ - --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ - --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ - --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ - --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ - --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ - --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ - --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ - --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ - --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ - --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ - --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ - --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ - --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ - --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ - --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ - --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ - --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ - --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ - --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ - --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ - --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 -mcp==1.28.1 \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 +mcp==1.28.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df \ --hash=sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683 -mdurl==0.1.2 \ +mdurl==0.1.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba -more-itertools==10.8.0 \ +more-itertools==10.8.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b \ --hash=sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd -multidict==6.7.0 \ - --hash=sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec \ - --hash=sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd \ - --hash=sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b \ - --hash=sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb \ - --hash=sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f \ - --hash=sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7 \ - --hash=sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd \ +multidict==6.7.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d \ --hash=sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0 \ --hash=sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3 \ - --hash=sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d \ - --hash=sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa \ - --hash=sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10 \ - --hash=sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202 \ - --hash=sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718 \ - --hash=sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e \ - --hash=sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1 \ - --hash=sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2 \ - --hash=sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754 \ - --hash=sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c \ - --hash=sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390 \ - --hash=sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128 \ --hash=sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3 \ - --hash=sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f \ - --hash=sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842 \ - --hash=sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9 \ - --hash=sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6 \ --hash=sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8 \ - --hash=sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599 \ - --hash=sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec \ - --hash=sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34 \ - --hash=sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c \ - --hash=sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4 \ - --hash=sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38 \ - --hash=sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5 \ - --hash=sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1 \ - --hash=sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a \ - --hash=sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c \ - --hash=sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036 \ - --hash=sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38 \ - --hash=sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99 \ - --hash=sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64 \ - --hash=sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e \ - --hash=sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159 \ - --hash=sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12 \ - --hash=sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1 \ - --hash=sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0 \ - --hash=sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184 \ - --hash=sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b \ --hash=sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288 \ - --hash=sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd \ - --hash=sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45 \ - --hash=sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a \ - --hash=sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca \ - --hash=sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b \ --hash=sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f \ --hash=sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32 \ - --hash=sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5 \ - --hash=sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4 \ - --hash=sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17 \ - --hash=sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60 \ - --hash=sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394 \ - --hash=sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00 \ - --hash=sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304 \ - --hash=sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13 \ - --hash=sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e \ - --hash=sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e \ - --hash=sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792 \ - --hash=sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb \ - --hash=sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b \ - --hash=sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000 \ - --hash=sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6 \ - --hash=sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62 \ - --hash=sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63 \ --hash=sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5 \ - --hash=sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e \ --hash=sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c \ - --hash=sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827 \ - --hash=sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8 \ --hash=sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96 \ - --hash=sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad \ - --hash=sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6 \ - --hash=sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4 \ - --hash=sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064 \ - --hash=sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762 \ - --hash=sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e \ - --hash=sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546 \ - --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 \ - --hash=sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d \ - --hash=sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf \ - --hash=sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885 \ - --hash=sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7 -oauthlib==3.3.1 \ + --hash=sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6 +oauthlib==3.3.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9 \ --hash=sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1 -ollama==0.6.0 \ +ollama==0.6.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0 \ --hash=sha256:da2b2d846b5944cfbcee1ca1e6ee0585f6c9d45a2fe9467cbcd096a37383da2f -openai==2.33.0 \ +openai==2.33.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:03ac37d70e8c9e3a8124214e3afa785e2cbc12e627fbd98177a086ef2fd87ad5 \ --hash=sha256:f850c435e2a4685bba3295bd54912dd26315d9c1b7733068186134d6e0599f9a -openapi-pydantic==0.5.1 \ +openapi-pydantic==0.5.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146 \ --hash=sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d -opentelemetry-api==1.39.1 \ +opentelemetry-api==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950 \ --hash=sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c -opentelemetry-exporter-otlp-proto-common==1.39.1 \ +opentelemetry-exporter-otlp-proto-common==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde \ --hash=sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464 -opentelemetry-exporter-otlp-proto-grpc==1.39.1 \ +opentelemetry-exporter-otlp-proto-grpc==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad \ --hash=sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18 -opentelemetry-exporter-otlp-proto-http==1.39.1 \ +opentelemetry-exporter-otlp-proto-http==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb \ --hash=sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985 -opentelemetry-instrumentation==0.60b1 \ +opentelemetry-instrumentation==0.60b1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d \ --hash=sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a -opentelemetry-instrumentation-threading==0.60b1 \ +opentelemetry-instrumentation-threading==0.60b1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:20b18a68abe5801fa9474336b7c27487d4af3e00b66f6a8734e4fdd75c8b0b43 \ --hash=sha256:92a52a60fee5e32bc6aa8f5acd749b15691ad0bc4457a310f5736b76a6d9d1de -opentelemetry-proto==1.39.1 \ +opentelemetry-proto==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007 \ --hash=sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8 -opentelemetry-sdk==1.39.1 \ +opentelemetry-sdk==1.39.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c \ --hash=sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6 -opentelemetry-semantic-conventions==0.60b1 \ +opentelemetry-semantic-conventions==0.60b1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953 \ --hash=sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb -opentelemetry-semantic-conventions-ai==0.4.13 \ +opentelemetry-semantic-conventions-ai==0.4.13 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5 \ --hash=sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036 -orjson==3.11.7 \ - --hash=sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f \ - --hash=sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733 \ +orjson==3.11.7 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223 \ - --hash=sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650 \ - --hash=sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5 \ - --hash=sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1 \ - --hash=sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8 \ - --hash=sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3 \ - --hash=sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6 \ --hash=sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2 \ - --hash=sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d \ - --hash=sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a \ - --hash=sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5 \ - --hash=sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e \ - --hash=sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892 \ - --hash=sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3 \ - --hash=sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539 \ - --hash=sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785 \ - --hash=sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1 \ - --hash=sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab \ - --hash=sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576 \ - --hash=sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b \ - --hash=sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141 \ --hash=sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2 \ - --hash=sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b \ --hash=sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49 \ - --hash=sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705 \ --hash=sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace \ --hash=sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1 \ - --hash=sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157 \ - --hash=sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10 \ - --hash=sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757 \ - --hash=sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d \ - --hash=sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f \ - --hash=sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf \ - --hash=sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183 \ - --hash=sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74 \ - --hash=sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0 \ - --hash=sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539 \ - --hash=sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4 \ - --hash=sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0 \ - --hash=sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad \ - --hash=sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa \ - --hash=sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f \ - --hash=sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1 \ - --hash=sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867 -packaging==26.3 \ + --hash=sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad +packaging==26.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c -pathable==0.4.4 \ +pathable==0.4.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2 \ --hash=sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2 -pfzy==0.3.4 \ +pfzy==0.3.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:5f50d5b2b3207fa72e7ec0ef08372ef652685470974a107d0d4999fc5a903a96 \ --hash=sha256:717ea765dd10b63618e7298b2d98efd819e0b30cd5905c9707223dceeb94b3f1 -pillow==12.3.0 \ - --hash=sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59 \ - --hash=sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45 \ +pillow==12.3.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3 \ --hash=sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df \ - --hash=sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139 \ - --hash=sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39 \ - --hash=sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e \ - --hash=sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8 \ - --hash=sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1 \ - --hash=sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8 \ - --hash=sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89 \ --hash=sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130 \ - --hash=sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d \ --hash=sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b \ - --hash=sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace \ --hash=sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931 \ - --hash=sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510 \ --hash=sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce \ - --hash=sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385 \ - --hash=sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e \ --hash=sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c \ - --hash=sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7 \ - --hash=sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace \ - --hash=sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64 \ - --hash=sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a \ - --hash=sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827 \ - --hash=sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17 \ --hash=sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4 \ - --hash=sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701 \ --hash=sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e \ --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91 \ - --hash=sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66 \ - --hash=sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217 \ - --hash=sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658 \ - --hash=sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418 \ - --hash=sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c \ - --hash=sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330 \ - --hash=sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402 \ - --hash=sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09 \ --hash=sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930 \ - --hash=sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f \ --hash=sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec \ - --hash=sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a \ - --hash=sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b \ - --hash=sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965 \ - --hash=sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8 \ --hash=sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c \ --hash=sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777 \ - --hash=sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35 \ - --hash=sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9 \ - --hash=sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f \ - --hash=sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f \ - --hash=sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0 \ - --hash=sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c \ --hash=sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71 \ - --hash=sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838 \ - --hash=sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf \ - --hash=sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321 \ - --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 \ - --hash=sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65 \ - --hash=sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5 \ - --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d \ - --hash=sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198 \ - --hash=sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7 -platformdirs==4.4.0 \ + --hash=sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9 +platformdirs==4.4.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \ --hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf -portalocker==2.10.1 \ +portalocker==2.10.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf \ --hash=sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f -posthog==6.7.7 \ +posthog==6.7.7 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:bf8187ce8e6da33d5c122ee03a759efa8f181630380260526294454dab0c8bab \ --hash=sha256:ffb7bac388f66e5c9e2e3d6028572386ad3c98bba7a9a4669db05b10ac23357a -prompt-toolkit==3.0.52 \ +prompt-toolkit==3.0.52 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 -propcache==0.4.1 \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ +propcache==0.4.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af -proto-plus==1.26.1 \ + --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d +proto-plus==1.26.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66 \ --hash=sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012 -protobuf==6.33.5 \ - --hash=sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c \ +protobuf==6.33.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02 \ --hash=sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c \ - --hash=sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd \ - --hash=sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190 \ - --hash=sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5 \ - --hash=sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0 \ - --hash=sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b -psutil==7.2.2 \ + --hash=sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0 +psutil==7.2.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9 \ - --hash=sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841 \ --hash=sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63 \ - --hash=sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979 \ --hash=sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a \ - --hash=sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b \ - --hash=sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9 \ - --hash=sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee \ - --hash=sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312 \ - --hash=sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b \ - --hash=sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9 \ - --hash=sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e \ - --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc \ - --hash=sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1 \ - --hash=sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf \ - --hash=sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea \ - --hash=sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988 \ - --hash=sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486 \ - --hash=sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00 \ - --hash=sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8 -py-key-value-aio==0.4.4 \ + --hash=sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc +py-key-value-aio==0.4.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d \ --hash=sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55 -pyasn1==0.6.4 \ +pyasn1==0.6.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b -pyasn1-modules==0.4.2 \ +pyasn1-modules==0.4.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ --hash=sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6 -pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ +pycparser==2.23 ; implementation_name != 'PyPy' and platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux' \ --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 -pydantic==2.12.5 \ +pydantic==2.12.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d -pydantic-core==2.41.5 \ - --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ - --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ - --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ - --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ +pydantic-core==2.41.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ - --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ - --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ - --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ - --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ - --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ - --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ - --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ - --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ - --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ - --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ - --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ - --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ - --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ - --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ - --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ - --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ - --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ - --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ - --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ - --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ - --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ - --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ - --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ - --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ - --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ - --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ - --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ - --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ - --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ - --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ - --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ - --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ - --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ - --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ - --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ - --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ - --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ - --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ - --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ - --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ - --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ - --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ - --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ - --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ - --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ - --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 -pydantic-settings==2.11.0 \ + --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c +pydantic-settings==2.11.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180 \ --hash=sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c -pygments==2.20.0 \ +pygments==2.20.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 -pyjwt==2.13.0 \ +pyjwt==2.13.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 -pyobjc==11.1 ; platform_system == 'darwin' \ +pyobjc==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:903f822cba40be53d408b8eaf834514937ec0b4e6af1c5ecc24fcb652812dd85 \ --hash=sha256:a71b14389657811d658526ba4d5faba4ef7eadbddcf9fe8bf4fb3a6261effba3 -pyobjc-core==11.1 ; platform_system == 'darwin' or sys_platform == 'darwin' \ - --hash=sha256:18986f83998fbd5d3f56d8a8428b2f3e0754fd15cef3ef786ca0d29619024f2c \ - --hash=sha256:765b97dea6b87ec4612b3212258024d8496ea23517c95a1c5f0735f96b7fd529 \ - --hash=sha256:8849e78cfe6595c4911fbba29683decfb0bf57a350aed8a43316976ba6f659d2 \ - --hash=sha256:8cb9ed17a8d84a312a6e8b665dd22393d48336ea1d8277e7ad20c19a38edf731 \ - --hash=sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe \ - --hash=sha256:f2455683e807f8541f0d83fbba0f5d9a46128ab0d5cc83ea208f0bec759b7f96 -pyobjc-framework-accessibility==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:3496c55569a421ef3c98ea66fc0ebaf68c686ede5b26db0fdcb0b0ad4191a20b \ - --hash=sha256:7c4124981a5d84b71464babb4babfbeb5bfab145bc75b6f3577bd046a9579226 \ - --hash=sha256:a049b63b32514da68aaaeef0d6c00a125e0618e4042aa6dbe3867b74fb2a8b2b \ - --hash=sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656 \ - --hash=sha256:ea98239e339136e3d20d753afe7908006cf29567ba39b8e83ceda7c221e6aad1 \ - --hash=sha256:fd5a03b731d1a2bbb2bf706b58889a5e82df82ac69210ec3245c7dc69e42a63a -pyobjc-framework-accounts==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-core==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:b63d4d90c5df7e762f34739b39cc55bc63dbcf9fb2fb3f2671e528488c7a87fe +pyobjc-framework-accessibility==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656 +pyobjc-framework-accounts==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:384fec156e13ff75253bb094339013f4013464f6dfd47e2f7de3e2ae7441c030 \ --hash=sha256:9c3fe342be7b8e73cba735e5a38affbe349cf8bc19091aa4fd788eabf2074b72 -pyobjc-framework-addressbook==11.1 ; platform_system == 'darwin' \ - --hash=sha256:411adf4874cc4343f2928a26fe4cb3673d2f5f73365b45cd3650aa7304a45e24 \ - --hash=sha256:6735f297f0e5fd109fa77ca90cace57eb2e10eb65e3c15ccd249df2228030d3b \ - --hash=sha256:6bc42832e85f418a9f978b7e001e219faf52cbb279a0df185115cd4292c381cb \ - --hash=sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc \ - --hash=sha256:e4004bdf134a069c58d91b231cbeb9e0adad26a73d2689015baaf6a98c411c54 \ - --hash=sha256:fb3d0a710f8342a0c63a8e4caf64a044b4d7e42d6d242c8e1b54470238b938cb -pyobjc-framework-adservices==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-addressbook==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc +pyobjc-framework-adservices==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:1744f59a75b2375e139c39f3e85658e62cd10cc0f12b158a80421f18734e9ffc \ --hash=sha256:44c72f8163705c9aa41baca938fdb17dde257639e5797e6a5c3a2b2d8afdade9 -pyobjc-framework-adsupport==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-adsupport==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:78b9667c275785df96219d205bd4309731869c3298d0931e32aed83bede29096 \ --hash=sha256:c3e009612778948910d3a7135b9d77b9b7c06aab29d40957770834c083acf825 -pyobjc-framework-applescriptkit==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-applescriptkit==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:477707352eaa6cc4a5f8c593759dc3227a19d5958481b1482f0d59394a4601c3 \ --hash=sha256:e22cbc9d1a25a4a713f21aa94dd017c311186b02062fc7ffbde3009495fb0067 -pyobjc-framework-applescriptobjc==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-applescriptobjc==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:ac22526fd1f0a3b07ac1d77f90046b77f10ec9549182114f2428ee1e96d3de2b \ --hash=sha256:c8a0ec975b64411a4f16a1280c5ea8dbe949fd361e723edd343102f0f95aba6e -pyobjc-framework-applicationservices==11.1 ; platform_system == 'darwin' \ - --hash=sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf \ - --hash=sha256:385a89f4d0838c97a331e247519d9e9745aa3f7427169d18570e3c664076a63c \ - --hash=sha256:a0ce40a57a9b993793b6f72c4fd93f80618ef54a69d76a1da97b8360a2f3ffc5 \ - --hash=sha256:e8dee91c6a14fd042f98819dc0ac4a182e0e816282565534032f0e544bfab143 \ - --hash=sha256:f480fab20f3005e559c9d06c9a3874a1f1c60dde52c6d28a53ab59b45e79d55f \ - --hash=sha256:f4a85ccd78bab84f7f05ac65ff9be117839dfc09d48c39edd65c617ed73eb01c -pyobjc-framework-apptrackingtransparency==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-applicationservices==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf +pyobjc-framework-apptrackingtransparency==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:796cc5f83346c10973806cfb535d4200b894a5d2626ff2eeb1972d594d14fed4 \ --hash=sha256:e25c3eae25d24ee8b523b7ecc4d2b07af37c7733444b80c4964071dea7b0cb19 -pyobjc-framework-audiovideobridging==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991 \ - --hash=sha256:30a12be3784f41e1c6b5ef532c08e73bae7071d9a036b26b1e36b919ee5b6f57 \ - --hash=sha256:3bef4383dc9233dbd9efc3817ce9c8fe8670c61d21a94de3c149e7f460245792 \ - --hash=sha256:591e80ff6973ea51a12f7c1a2e3fd59496633a51d5a1bf73f4fb989a43e23681 \ - --hash=sha256:6159b94448af08c9b119eb6ecf3fdbc2b3348ad66fb99586f991939779e412ec \ - --hash=sha256:e466561bd9eb77be050aabead6ad7313a480d05389d9892e1db2cbc06ce1f475 -pyobjc-framework-authenticationservices==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:364035d265129192e6906f7a94cbdf714d737b6b9f20e56bfe74d0007c8761b1 \ - --hash=sha256:60bf585e561d885cc88a21713ef2db259baf6434ce7116f82265a0c727f29dba \ - --hash=sha256:6655dd53d9135ef85265a4297da5e7459ed7836973f2796027fdfbfd7f08e433 \ - --hash=sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a \ - --hash=sha256:e92bf7e829229fbecba4f7f649d3ae38760cf25aa9e909c0e737b1945f36b62d \ - --hash=sha256:f19ea757ecfda6ac929559c779c3afb001855dd5e41e4acc4c42343c7d912da6 -pyobjc-framework-automaticassessmentconfiguration==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:378d233879bb011ed9d0bcf1b0e3c048fb756023d0f6819e997f62acc2c32bc3 \ - --hash=sha256:4b11c33fb6f6092b9e1fb63747f2402f516b7ff0f815be4ece4625f2a2ec954f \ - --hash=sha256:55d1684dd676730fb1afbc7c67e0669e3a7159f18c126fea7453fe6182c098f9 \ - --hash=sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032 \ - --hash=sha256:e5fa297c7d4db225f75e5d11121fa68e0956c104e14b24250a52157a180e5f6c \ - --hash=sha256:fbcbe406c2a02d632885f6b23285c259b715f019b938d666cc554a66ecf5f9c3 -pyobjc-framework-automator==11.1 ; platform_system == 'darwin' \ - --hash=sha256:112815d2e1b6002b4f9bc644bdae6b02257d249145c79346d7b8bb11e6f76b03 \ - --hash=sha256:203b888152a78b39a8c67be663ff78a749ebff208ce993b4419fc4409faa1fda \ - --hash=sha256:3458f836671ea922ad0771f617c927e9c52841c0a6e71b4a5a9dbb438736c207 \ - --hash=sha256:651760236cb2d2481faa5afb66da97054850d34fdbebc5e4ee2f83a683a8be10 \ - --hash=sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4 \ - --hash=sha256:af5941f8d90167244209b352512b7779e5590d17dc1e703e087a6cfe79ee3d64 -pyobjc-framework-avfoundation==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1ab2108b652496b13b9758c295f0f6de53b6d12125cf574ddae84ce28044bce1 \ - --hash=sha256:5dd6ac6a57f86b7ed5ac0a965ce54328f6ce77816b4a1fbf0d85c06fb251867a \ - --hash=sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377 \ - --hash=sha256:94f065db4e87b1baebb5cf9f464cf9d82c5f903fff192001ebc974d9e3132c7e \ - --hash=sha256:dd3965aad0b236b8ac12f216d688c1a22b963f63e7e4fdb7107dd6790e80ee12 \ - --hash=sha256:e204d155a09c186601490e4402dcffb2845a5831079e389b47bd6a341fe5ee63 -pyobjc-framework-avkit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:023b1cdb78c3aa5873d8abe69697396872b47278208991ec5e5aea4464309b01 \ - --hash=sha256:3a5f22bc4f4b0b82c8039d37996882bf4a38f509963d1afa3275a45ddd4a0b00 \ - --hash=sha256:a6b418603fc270a8e63c2a5efffa753704fd14bf8bca0657901c49a7cc9b22b5 \ - --hash=sha256:c5810b349745078ef8b4a562e85afe40de3245127f633d8cabe98aeca765c7fc \ - --hash=sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263 \ - --hash=sha256:ef9cd9fe37c6199bfde7ee5cd6e76ede23a6797932882785c53ef3070e209afb -pyobjc-framework-avrouting==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:120c9d65d4f9047b9921f8dced0b4f26d799156bc08ff7e3974217cd036b1bfc \ - --hash=sha256:6a7b335161d327792f42054acb3ff415f7778e1492582df8e91b8609b4b02244 \ - --hash=sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0 \ - --hash=sha256:9aa9b0a7ae7ee5874e7d92bebefca4525d5cf1f0aa1f50e78e558984a39cad2e \ - --hash=sha256:dc309e175abf3961f933f8b341c0504b17f4717931242ebb121a83256b8b5c13 \ - --hash=sha256:f52f9d62a3c8485b5687187ea58d905d7edccac9941c444b4add8129841cd031 -pyobjc-framework-backgroundassets==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:13bf451c59b409b6ce1ac0e717a970a1b03bca7a944a7f19219da0d46ab7c561 \ - --hash=sha256:2484a2f9c87e8cae2fc375a39d68ea7ff02e4fb786e4afe88237c51fd5e78ec9 \ - --hash=sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2 \ - --hash=sha256:708466d847a479e1798f31c59fbc5307473d03fa1083f40cfcaa18fd31819c40 \ - --hash=sha256:a4db45048d1021900be5b03136b927773820bcbb40d623aeac54712e1c86d6f6 \ - --hash=sha256:a72536ed18cf2462085bbb2184d0a3eecf9b97669c0ef4db45418555a609b534 -pyobjc-framework-browserenginekit==11.1 ; platform_release >= '23.4' and platform_system == 'darwin' \ - --hash=sha256:1f4cce594a94d0bc0a020122153f8149c16578fa4761b0e27d868c013f76214c \ - --hash=sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479 \ - --hash=sha256:9b815b167533015d62832b956e9cfb962bd2026f5a4ccd66718cf3bb2e15ab27 \ - --hash=sha256:c3195c4fb3b84150fac6dd18ce318eaae17f246f98678825397ed80d6da3c371 \ - --hash=sha256:dfe469f8eb1313ea0cbe0616cd3bbc56f62bdd8a683c959819ef01d7e9ac0de7 \ - --hash=sha256:f3332ffa9ae74cc6633fd17f6d998ac77b8939abbe9ecf95ae56df200ee93853 -pyobjc-framework-businesschat==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-audiovideobridging==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991 +pyobjc-framework-authenticationservices==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a +pyobjc-framework-automaticassessmentconfiguration==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032 +pyobjc-framework-automator==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4 +pyobjc-framework-avfoundation==11.1 ; platform_machine == 'x86_64' and platform_release >= '11.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377 +pyobjc-framework-avkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263 +pyobjc-framework-avrouting==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0 +pyobjc-framework-backgroundassets==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2 +pyobjc-framework-browserenginekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '23.4' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479 +pyobjc-framework-businesschat==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:69589d2f0cb4e7892e5ecc6aed79b1abd1ec55c099a7faacae6a326bc921259d \ --hash=sha256:7fdc1219b988ce3ae896bffd01f547c06cec3b4e4b2d0aa04d251444d7f1c2db -pyobjc-framework-calendarstore==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-calendarstore==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:858ee00e6a380d9c086c2d7db82c116a6c406234038e0ec8fc2ad02e385dc437 \ --hash=sha256:bf066e17392c978becf17a61863eb81727bf593a2bfdab261177126072557e24 -pyobjc-framework-callkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:554e09ca3dab44d93a89927d9e300f004d2ef0db020b10425a4622b432e7b684 \ - --hash=sha256:8266ee797fdabb657f7cb4fa808404fc33fcf3f31d4bcab1ab3c53d272e1ff83 \ - --hash=sha256:b69b4262897f2701348ea0da36afe32d60f84e2a036baf13e258a97875b25a6c \ - --hash=sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94 \ - --hash=sha256:bc1d2349dab93f7a0d298b01893828d7f46aded9122a341469b835d977a0646d \ - --hash=sha256:fc5e638ddbc9dd3e9993205d2b077f5db41b6cd4e97b9c5592b7249575f23f04 -pyobjc-framework-carbon==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-callkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94 +pyobjc-framework-carbon==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:047f098535479efa3ab89da1ebdf3cf9ec0b439a33a4f32806193886e9fcea71 \ --hash=sha256:1bf66853e939315ad7ee968170b16dd12cb838c42b80dfcd5354687760998825 -pyobjc-framework-cfnetwork==11.1 ; platform_system == 'darwin' \ - --hash=sha256:2e9a4ce6b416bff881df499d9060c1096220ef8c20e519108a7b91692d1fd1d7 \ - --hash=sha256:4b998daa3e6ce253c48455365f004647b3b1da2f313fbc8a5a607e460b4d5567 \ - --hash=sha256:5dd866fcbe6870931373636d19144544344f0f89685f6720e4a45453957702dd \ - --hash=sha256:62ccc6dcaaa5877534d21f93a15861a3d8af95888123d659f9ff5383d1a2a1f4 \ - --hash=sha256:70beb8095df76e0e8eb7ab218be1e69ae180e01a4d77f7cad73c97b4eb7a296a \ +pyobjc-framework-cfnetwork==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:ad600163eeadb7bf71abc51a9b6f2b5462a018d3f9bb1510c5ce3fdf2f22959d -pyobjc-framework-cinematic==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-cinematic==11.1 ; platform_machine == 'x86_64' and platform_release >= '23.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:b62c024c1a9c7890481bc2fdfaf0cd3c251a4a08357d57dc1795d98920fcdbd1 \ --hash=sha256:efde39a6a2379e1738dbc5434b2470cd187cf3114ffb81390b3b1abda470b382 -pyobjc-framework-classkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:08000deb43004d16fb39ccd83b3de30e1e3b72639a79d05206d7d5c15f005b3a \ - --hash=sha256:161dcb9b718649e6331a5eab5a76c2b43a9b322b15b37b3f8f9c5faad12ee6d1 \ - --hash=sha256:9a1b9d31f9b23e05b92769bbdb4ef2167a59b3b24aefa6af86448f5087a2e105 \ - --hash=sha256:be279d91f10d68ad9a256e96d26d8975e35b9b1bb304c82491766d29ad252b0d \ - --hash=sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4 \ - --hash=sha256:ef28d042964b0f757569e72df737bb049b531c33b7d06a705ce2dcfa4e6e45d8 -pyobjc-framework-cloudkit==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-classkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4 +pyobjc-framework-cloudkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:40d2dc4bf28c5be9b836b01e4d267a15d847d756c2a65530e1fcd79b2825e86d \ --hash=sha256:c583e40c710cf85ebe34173d1d2995e832a20127edc8899b2f35b13f98498af1 -pyobjc-framework-cocoa==11.1 ; platform_system == 'darwin' or sys_platform == 'darwin' \ - --hash=sha256:1b5de4e1757bb65689d6dc1f8d8717de9ec8587eb0c4831c134f13aba29f9b71 \ - --hash=sha256:54e93e1d9b0fc41c032582a6f0834befe1d418d73893968f3f450281b11603da \ - --hash=sha256:806de56f06dfba8f301a244cce289d54877c36b4b19818e3b53150eb7c2424d0 \ - --hash=sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038 \ - --hash=sha256:aede53a1afc5433e1e7d66568cc52acceeb171b0a6005407a42e8e82580b4fc0 \ - --hash=sha256:fd5245ee1997d93e78b72703be1289d75d88ff6490af94462b564892e9266350 -pyobjc-framework-collaboration==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-cocoa==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038 +pyobjc-framework-collaboration==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:3629ea5b56c513fb330d43952afabb2df2a2ac2f9048b8ec6e8ab4486191390a \ --hash=sha256:4564e3931bfc51773623d4f57f2431b58a39b75cb964ae5c48d27ee4dde2f4ea -pyobjc-framework-colorsync==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-colorsync==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:7a346f71f34b2ccd1b020a34c219b85bf8b6f6e05283d503185aeb7767a269dd \ --hash=sha256:d19d6da2c7175a3896a63c9b40a8ab98ade0779a5b40062789681501c33efd5c -pyobjc-framework-contacts==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:09b873d2bd739fea63d744430defb04ce4b44af064aaf0b6bf558eea23f82bd7 \ - --hash=sha256:23312bb4bfc5aafecdac84ca402189e312e754e9dc0586d8f282d225c3952c00 \ - --hash=sha256:3409aba6e23cb179b3fe932c1a0a53d7b273ac8292d5adf1bf6849e925cc0955 \ - --hash=sha256:576ee4aec05d755444bff10b45833f73083b5b3d1b2740e133b92111f7765e54 \ - --hash=sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc \ - --hash=sha256:910f40a2e4d80a97f282bfdecba0f5ff95201b11844acd3f9cb9522db364ab57 -pyobjc-framework-contactsui==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f \ - --hash=sha256:666586174b306b33b791d2edee021cd979a8c970d444f906ed294e27583a6b54 \ - --hash=sha256:7901eed3c669ad52cca86089c443fd30820b21586bf758e03fb83696f435ba87 \ - --hash=sha256:8b03bd175095b4774c55bd5f38a01942e945b668bea15b9dc3b4f1a28b1a8696 \ - --hash=sha256:c34a6f27ef5aa4742cc44fd5b4d16fe1e1745ff839578b4c059faf2c58eee3ca \ - --hash=sha256:f3b4f0225645a26ed9e6c008c2e8c217035b4a50fa9cd6623c628a11c37924d0 -pyobjc-framework-coreaudio==11.1 ; platform_system == 'darwin' \ - --hash=sha256:2538d1242dab4e27efb346eafbad50594e7e95597fa7220f0bab2099c825da55 \ - --hash=sha256:67dae111b78d91c26c753dbfbccc3ea5498cfda3dfe83c6f3778628b435e1e7b \ - --hash=sha256:6ba8b67f185c0e3f26b17ae525cee3f411bc8d6e9c9a8bfd899a28f594623d2f \ - --hash=sha256:9527a16a2b88b37bace578d499f21229f9a33b9afdcdd35d4f44374cb8eb9ab6 \ - --hash=sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b \ - --hash=sha256:f73d996df1e721931d9f78050e1708735a173dbe3a76d9c71fb36e04f7208478 -pyobjc-framework-coreaudiokit==11.1 ; platform_system == 'darwin' \ - --hash=sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40 \ - --hash=sha256:11d42770dfbc6a8af8d5fa39a4f700f0067d7e6c7ba9335e6624d89de3c599a9 \ - --hash=sha256:20440a2926b1d91da8efc8bc060e77c7a195cb0443dbf3770eaca9e597276748 \ - --hash=sha256:6fea7c7ea5305e8cbd75808ec4edcde8e2320137f227b3d771266dd9a71e1fa5 \ - --hash=sha256:8d012561eb95877f0214aa0cd13043b1a2693add4a9534d1e6fb82f6d7183c7c \ - --hash=sha256:a71447196a48869b551a2e3b6ba92f39241cb64d0257120505c62ddb611aef0f -pyobjc-framework-corebluetooth==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190 \ - --hash=sha256:36bef95a822c68b72f505cf909913affd61a15b56eeaeafea7302d35a82f4f05 \ - --hash=sha256:4da1106265d7efd3f726bacdf13ba9528cc380fb534b5af38b22a397e6908291 \ - --hash=sha256:992404b03033ecf637e9174caed70cb22fd1be2a98c16faa699217678e62a5c7 \ - --hash=sha256:e84cbf52006a93d937b90421ada0bc4a146d6d348eb40ae10d5bd2256cc92206 \ - --hash=sha256:ebb8648f5e33d98446eb1d6c4654ba4fcc15d62bfcb47fa3bbd5596f6ecdb37c -pyobjc-framework-coredata==11.1 ; platform_system == 'darwin' \ - --hash=sha256:007160eb10bb8c789076f231e3d625d8875ca42eb5a806fdab5d0277c48866f8 \ - --hash=sha256:699ad568f98f58e88e642159c91ffff0c68ce3d1ec798e4af8333b27431fd058 \ - --hash=sha256:89dde863eff01ed6b5f8d88c764a08b154ef37078397c98c5f403e8798723b9d \ - --hash=sha256:c9b2374784e67694a18fc8c120a12f11b355a20b643c01f23ae2ce87330a75e0 \ - --hash=sha256:d84afaccbb4f18dbda4c557cd059b7adc2116436a065353e25e7cbc840d9f8b4 \ +pyobjc-framework-contacts==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc +pyobjc-framework-contactsui==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f +pyobjc-framework-coreaudio==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b +pyobjc-framework-coreaudiokit==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40 +pyobjc-framework-corebluetooth==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190 +pyobjc-framework-coredata==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:fe9fd985f8e06c70c0fb1e6bbea5b731461f9e76f8f8d8e89c7c72667cdc6adf -pyobjc-framework-corehaptics==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-corehaptics==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:8f8c47ccca5052d07f95d2f35e6e399c5ac1f2072ba9d9eaae902edf4e3a7af4 \ --hash=sha256:e5da3a97ed6aca9b7268c8c5196c0a339773a50baa72d1502d3435dc1a2a80f1 -pyobjc-framework-corelocation==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0f8182835429118a55ed65963c80f5b2892d190747b986e8395b1cd99f41a1d0 \ - --hash=sha256:17ce2530bd5a0dca9059eb11bc647d920490bcdd35b5cac1e160f51f0297bdc8 \ - --hash=sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206 \ - --hash=sha256:562e31124f80207becfd8df01868f73fa5aa70169cc4460e1209fb16916e4fb4 \ - --hash=sha256:a384d9fcba2c041d8f8115b51a07ef11c391bc30f72560aaea8b94db6b3b225c \ - --hash=sha256:bc3f27802415aa62330a2d2507adc3a9b98a89d6de7d1033ebe6b8c461610831 -pyobjc-framework-coremedia==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:7ecdb64c743ffe9fd3949c7cc9109891b9f399a0852717fcb969d33c4e7ba527 \ - --hash=sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3 \ - --hash=sha256:969ce357c616f6835f47e27d1e73964374cdb671476571dfd358894a8ced06f2 \ - --hash=sha256:aa942d9ad0cf5bc4d3ede8779c3fac2f04cf3857687f2fb8505bae3378d04b95 \ - --hash=sha256:bf1da05c297776c297ab3489ebf18d954efdff530acbdd6e70c32be811e20ec6 \ - --hash=sha256:ea5055298af91e463ffa7977d573530f9bada57b8f2968dcc76a75e339b9a598 -pyobjc-framework-coremediaio==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0a7ffded00a7dc6f0bf4a44a6832f0150d45a83886486148b71ccc67c70ef215 \ - --hash=sha256:39ad2518de9943c474e5ca0037e78f92423c3352deeee6c513a489016dac1266 \ - --hash=sha256:5a94f9e507b470ce7dcb887e79ccf19e98693a606ad34462d711004e3edd88c3 \ - --hash=sha256:5ff161025ef28d5e2eed90db0e8b828cb361281b799b16b1885711ca0addc1aa \ - --hash=sha256:9e0a079fe790ce8a69d11bea46b315c9a0d3f3999a2f09e2ef4fcc4430a47c42 \ +pyobjc-framework-corelocation==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206 +pyobjc-framework-coremedia==11.1 ; platform_machine == 'x86_64' and platform_release >= '11.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3 +pyobjc-framework-coremediaio==11.1 ; platform_machine == 'x86_64' and platform_release >= '11.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:bccd69712578b177144ded398f4695d71a765ef61204da51a21f0c90b4ad4c64 -pyobjc-framework-coremidi==11.1 ; platform_system == 'darwin' \ - --hash=sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376 \ - --hash=sha256:2ef1a10f6230fce82b931670470158404657d9fb9ac558a77b46b547e9978524 \ - --hash=sha256:3462a158214adb7ebc785fb6924e674c58dcd471888dbca5e2e77381f3f1bbdc \ - --hash=sha256:c2e1ab122501206ceae07123fdc433e91a5f1a97224f80ece0717b6f36ad2029 \ - --hash=sha256:deb9120478a831a898f22f68737fc683bb9b937e77556e78b75986aebd349c55 \ - --hash=sha256:f4b70864cae295f27b5d51817c0768fade7c1335a59410910146e5f2a54c475c -pyobjc-framework-coreml==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:10d51f8a5fe8d30c7ec70304a2324df76b48b9fbef30ee0f0c33b99a49ae8853 \ - --hash=sha256:287a2a059016d02d8c40e0d29e70226142a4969db97ad79cefc70ec9bf0ab29e \ - --hash=sha256:4df25ee233430f016ffcb4e88506b54c8e7b668c93197e6a1341761530a5922c \ - --hash=sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7 \ - --hash=sha256:a479c3d759aff3695f72c7915a78df6e92e0eca7027abaa8b4a07e876ba1dbfb \ - --hash=sha256:c768b03d72488b964d753392e9c587684961d8237b69cca848b3a5a00aea79c9 -pyobjc-framework-coremotion==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:42fb307b86999d078503ff79bdf8df4d1c27d38763db6b1c5c0f4054241f67a3 \ - --hash=sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e \ - --hash=sha256:708431c53f483bc6da199375227ffea1b4e8e7d8c81d162492db3fc36893fb53 \ - --hash=sha256:8c3b33228a170bf8495508a8923451ec600435c7bff93d7614f19c913baeafd1 \ - --hash=sha256:ac5302deaab99a7443cad63f125061a90040852d4f8efb58492542a612b2afe3 \ - --hash=sha256:d67413a56989154dab7bf1b69c14b0b2387d87d3a4c8e3c8a9fc0230f061e8ab -pyobjc-framework-coreservices==11.1 ; platform_system == 'darwin' \ - --hash=sha256:21e9e86192d719cd5c899cc0e931110733da0b5bbbf606681e5fccd4dd39c174 \ - --hash=sha256:4ffa188322ab9d05c6964926959dedba5cc04534232f1eff03aee5f09faa499e \ - --hash=sha256:6bd313ec326efd715b4b10c3ebcc9f054e3ee3178be407b97ea225cd871351d2 \ - --hash=sha256:74dcc295245f07754328bada9577b189e3abef71607d013e939751c1b5b55729 \ - --hash=sha256:8aee505dca56afc5363d8d0dff0b2d26583a8d0f3ac37674cef86f66c51a2934 \ +pyobjc-framework-coremidi==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376 +pyobjc-framework-coreml==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7 +pyobjc-framework-coremotion==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e +pyobjc-framework-coreservices==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:cf8eb5e272c60a96d025313eca26ff2487dcd02c47034cc9db39f6852d077873 -pyobjc-framework-corespotlight==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2 \ - --hash=sha256:546d0d9b101de4ca20449f3807d1f88e5c26de0345a8bfefc70f12f87efb8433 \ - --hash=sha256:7cedd3792fe1fe2a8dc65a8ff1f70baf12415a5dc9dc4d88f987059567d7e694 \ - --hash=sha256:bce3d84f97014228b244c734aea3ec03b257573b22c097dff4eb176a80cd29a9 \ - --hash=sha256:f562cc65865066f8e2e5d96c868fd7f463d8280f1ef01df85250fc1150feed0e \ - --hash=sha256:f59d0d2f0411db102d16490e47b457b994c613f1b980869fa3a151863da7aa4c -pyobjc-framework-coretext==11.1 ; platform_system == 'darwin' \ - --hash=sha256:15650ba99692d00953e91e53118c11636056a22c90d472020f7ba31500577bf5 \ - --hash=sha256:1597bf7234270ee1b9963bf112e9061050d5fb8e1384b3f50c11bde2fe2b1570 \ - --hash=sha256:37e051e8f12a0f47a81b8efc8c902156eb5bc3d8123c43e5bd4cebd24c222228 \ - --hash=sha256:56a3a02202e0d50be3c43e781c00f9f1859ab9b73a8342ff56260b908e911e37 \ - --hash=sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6 \ - --hash=sha256:fb27f66a56660c31bb956191d64b85b95bac99cfb833f6e99622ca0ac4b3ba12 -pyobjc-framework-corewlan==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:14c7af9135ba0a920192af4dc50219bbf6185fcbb5de7041f097e1a1c8509587 \ - --hash=sha256:3c66643a97fcf3aa797fda997a3afc28d8d9bba9727dd5c0e68a313899d780f7 \ - --hash=sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce \ - --hash=sha256:6dc28264b56b18096c8869cce3f85e519fd27936f19524bb77458572ccfd7518 \ - --hash=sha256:7bd0775d2466ad500aad4747d8a889993db3a14240239f30ef53c087745e9c8e \ - --hash=sha256:872de75409a710bb9a461e64e97185f8489d01898ec1b02c3e058c04606b61cf -pyobjc-framework-cryptotokenkit==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328 \ - --hash=sha256:6384cb1d86fc586e2da934a5a37900825bd789e3a5df97517691de9af354af0c \ - --hash=sha256:a55c0e57ab164aa5ce562e4d9e69026339067ecb4888638995690f1c43b79cfa \ - --hash=sha256:cb3e1bd344e794cb98343171b5501a1a3b75548ef5385bda3d5ec613c0b98045 \ - --hash=sha256:efd89e5b024475701f6e9bec4cf1c2563e1bab37e79288397e09d9ad4e53d174 \ - --hash=sha256:faab9493e36095c0257598e25ef81c50bcdb3afb5843a82e6dfad8c7d1f47bcf -pyobjc-framework-datadetection==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-corespotlight==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2 +pyobjc-framework-coretext==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6 +pyobjc-framework-corewlan==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce +pyobjc-framework-cryptotokenkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328 +pyobjc-framework-datadetection==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:5afd3dde7bba3324befb7a3133c9aeaa5088efd72dccc0804267a74799f4a12f \ --hash=sha256:cbe0080b51e09b2f91eaf2a9babec3dcf2883d7966bc0abd8393ef7abfcfc5db -pyobjc-framework-devicecheck==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-devicecheck==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:8b05973eb2673571144d81346336e749a21cec90bd7fcaade76ffd3b147a0741 \ --hash=sha256:8edb36329cdd5d55e2c2c57c379cb5ba1f500f74a08fe8d2612b1a69b7a26435 -pyobjc-framework-devicediscoveryextension==11.1 ; (platform_release >= '24.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '24' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-devicediscoveryextension==11.1 ; platform_machine == 'x86_64' and platform_release >= '24.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:96e5b13c718bd0e6c80fbd4e14b8073cffc88b3ab9bb1bbb4dab7893a62e4f11 \ --hash=sha256:ae160ea40f25d3ee5e7ce80ac9c1b315f94d0a4c7ccb86920396f71c6bf799a0 -pyobjc-framework-dictionaryservices==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-dictionaryservices==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:39c24452d0ddd037afeb73a1742614c94535f15b1c024a8a6cc7ff081e1d22e7 \ --hash=sha256:92f4871066653f18e2394ac93b0a2ab50588d60020f6b3bd93e97b67cd511326 -pyobjc-framework-discrecording==11.1 ; platform_system == 'darwin' \ - --hash=sha256:2d18158366d124852ad58291954611ebdcc43263a3bb75d7fd273408e67720e2 \ - --hash=sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87 \ - --hash=sha256:7c33421d6bed0993d9f1861dbf38b717b9a9e49dfb98fdf8b3cd8d558fdd50eb \ - --hash=sha256:9cb36715bebdbbe1ad95e3c17359c2f5d3f6479a26b527ea1032154ca7cf3e09 \ - --hash=sha256:b027eca3a0391196d4335fcbd50c03ef1e8f5ce095411ed51a081328b4945bf5 \ - --hash=sha256:e29bc8c3741ae52fae092f892de856dbab2363e71537a8ae6fd026ecb88e2252 -pyobjc-framework-discrecordingui==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-discrecording==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87 +pyobjc-framework-discrecordingui==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:33233b87d7b85ce277a51d27acca0f5b38485cf1d1dc8e28a065910047766ee2 \ --hash=sha256:a9f10e2e7ee19582c77f0755ae11a64e3d61c652cbd8a5bf52756f599be24797 -pyobjc-framework-diskarbitration==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-diskarbitration==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:6a8e551e54df481a9081abba6fd680f6633babe5c7735f649731b22896bb6f08 \ --hash=sha256:a933efc6624779a393fafe0313e43378bcae2b85d6d15cff95ac30048c1ef490 -pyobjc-framework-dvdplayback==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-dvdplayback==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:6094e4651ea29540ac817294b27e1596b9d1883d30e78fb5f9619daf94ed30cb \ --hash=sha256:b44c36a62c8479e649133216e22941859407cca5796b5f778815ef9340a838f4 -pyobjc-framework-eventkit==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-eventkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:5643150f584243681099c5e9435efa833a913e93fe9ca81f62007e287349b561 \ --hash=sha256:c303207610d9c742f4090799f60103cede466002f3c89cf66011c8bf1987750b -pyobjc-framework-exceptionhandling==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-exceptionhandling==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:31e6538160dfd7526ac0549bc0fce5d039932aea84c36abbe7b49c79ffc62437 \ --hash=sha256:e010f56bf60ab4e9e3225954ebb53e9d7135d37097043ac6dd2a3f35770d4efa -pyobjc-framework-executionpolicy==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-executionpolicy==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:3280ad2f4c5eaf45901f310cee0c52db940c0c63e959ad082efb8df41055d986 \ --hash=sha256:7d4141e572cb916e73bb34bb74f6f976a8aa0a396a0bffd1cf66e5505f7c76c8 -pyobjc-framework-extensionkit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2767635e57b277e051719fa53c7683396ebdbcf3d40d44c1296758978ca8c92a \ - --hash=sha256:3507f67dd06285c09bbdf5216a1148f5dd3a2f10eee7a9318dd14430bf6e67ee \ - --hash=sha256:5c2e203cb8134be1dd7df73d74c630adbaaf43d78eba04be451ea4f8bf582e22 \ - --hash=sha256:abbadbea5b18e4a6944c3c428753ee298a133cbf601c70e9586b14e3aebf649b \ - --hash=sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409 \ - --hash=sha256:d505a64617c9db4373eb386664d62a82ba9ffc909bffad42cb4da8ca8e244c66 -pyobjc-framework-externalaccessory==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:00caf75b959db5d14118d78c04085e2148255498839cdee735a0b9f6ef86b6a2 \ - --hash=sha256:318772e698c6363e8c3c81229d93b639f5066a02a742ba1ab10cfdef3101d88b \ - --hash=sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371 \ - --hash=sha256:50b796a4721db87863a28cd55668cb1547fcc28834afda2032e500cdab5b3d95 \ - --hash=sha256:569124b686569c48e3855fff128f438a2b46af06280eac2a516aaa214ad325de \ - --hash=sha256:d259724665617fc4f3e666d353b756a67cabb74e6f9d7b8f6f250a2d4bf05cb7 -pyobjc-framework-fileprovider==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0e48015bf50b3e56312c640ec6efde73cf3855e29b6d70d173a88957d9d74d27 \ - --hash=sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8 \ - --hash=sha256:95ed3a03741076a4479aabb616b1e3ea022025a0ad842147a1200c27709019e2 \ - --hash=sha256:9af41255df395a40a6e0b08c4410be5463f3ea91d8c9be61f6bd114252490ab2 \ - --hash=sha256:ce6092dfe74c78c0b2abc03bfc18a0f5d8ddc624fc6a1d8dfef26d7796653072 \ - --hash=sha256:d2720acdd582756ebda34418981e7646b7b85588b0b8fdafba7016eb657be6b8 -pyobjc-framework-fileproviderui==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-extensionkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409 +pyobjc-framework-externalaccessory==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371 +pyobjc-framework-fileprovider==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8 +pyobjc-framework-fileproviderui==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:162a23e67f59e1bb247e84dda88d513d7944d815144901a46be6fe051b6c7970 \ --hash=sha256:f2765f114c2f4356aa41fb45c621fa8f0a4fae0b6d3c6b1a274366f5fe7fe829 -pyobjc-framework-findersync==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-findersync==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:692364937f418f0e4e4abd395a09a7d4a0cdd55fd4e0184de85ee59642defb6e \ --hash=sha256:c72b0fd8b746b99cfa498da36c5bb333121b2080ad73fa8cbea05cd47db1fa82 -pyobjc-framework-fsevents==11.1 ; platform_system == 'darwin' \ - --hash=sha256:3289192f4d60e5b26f8ac88ae4049a11eff47caa6fb76ce34e3f7df405119905 \ - --hash=sha256:8b51d120b8f12a1ca94e28cf74113bf2bfd4c5aee7035b452e895518f4df7630 \ - --hash=sha256:9191ee2819f1d5dcae1559e4a66f19be03da3a103bccdc417e6888bcb5659f8f \ - --hash=sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d \ - --hash=sha256:fad5ada269f137afabd622b5fc04884c668ae1c7914a8791bab73b1d972f7713 \ - --hash=sha256:ff064cfa9d9cffb5d4ab476fb5091604568744d961c670aced037b2b6f0d0185 -pyobjc-framework-fskit==11.1 ; platform_release >= '24.4' and platform_system == 'darwin' \ - --hash=sha256:1d3793938e6d9b871483d4a6fad8f93d554bcbebd1fe7bed20e3f5d2feaa814b \ - --hash=sha256:1e50b8f949f1386fada73b408463c87eb81ef7fd0b3482bacf0c206a73723013 \ - --hash=sha256:44fe7b6781c8fd0552b13ab3d0ec21176cd7cd685a8a61d712f9e4e42eb2f736 \ - --hash=sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f \ - --hash=sha256:cc2390934a23b6407aa7802b11978374301444c3135835ad3373f7b4930c24eb \ - --hash=sha256:e38f9c449647109e5b14dc4a17f425efca10c7e539a3836ebdd1f9c0ef725a3b -pyobjc-framework-gamecenter==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2a2cb6471d4d4b19f124c7e91a32882a0fab6e326bb0415915fd8f3b91cfc311 \ - --hash=sha256:6ff8905a5a7bfd86cb2b95671b452be0836f79db065b8d8b3bb2a1a5750ffd0d \ - --hash=sha256:779cdf8f52348be7f64d16e3ea37fd621d5ee933c032db3a22a8ccad46d69c59 \ - --hash=sha256:90132bb32f5ed6607e13c6f39346ad621611cb92cea308ced661a6ba1305b94e \ - --hash=sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c \ - --hash=sha256:a73ca7027b2b827e26075b46551fe42425d4a68985022baa4413329a3a2c16ff -pyobjc-framework-gamecontroller==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:4866b25df05f583af06095e7103ddd2fbb2484b0ac2c78fd2cd825f995e524fa \ - --hash=sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b \ - --hash=sha256:98f3f7afcbbe473a53537da42b2cdc0363df2647289eb66e8c762e4b46c23e73 \ - --hash=sha256:afe9f3aed8c900ebe63ee4f6e53c73c2fef7e503f6388afd39f46b31487f84a3 \ - --hash=sha256:d2cbc0c6c7d9c63e6b5b0b124d0c2bad01bb4b136f3cbc305f27d31f8aab6083 \ - --hash=sha256:de3892b8d09a65a3413d85a2f0762eba092afda8d97cbf9cda0417689cfb7027 -pyobjc-framework-gamekit==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:10331a69282b9554ce7ae618dc9ff68e96451759f6cfc687e188c82ba6b0e2ff \ - --hash=sha256:555cb8d868fd2699ad70d4f9e7efccaa5df1995893050d05d478cb8f24dbf876 \ - --hash=sha256:8c7f2bf7ecf44ca678cfdf76f23b32d9c2d03006a0af9ad8e60d9114d6be640a \ - --hash=sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269 \ - --hash=sha256:a7c8fce8a2c4614e3dd88b002540e67423e3efd41aa26d576db2de0fc61651b9 \ - --hash=sha256:f945c7cfe53c4a349a03a1272f2736cc5cf88fe9e7a7a407abb03899635d860c -pyobjc-framework-gameplaykit==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0e4f34db8177b8b4d89fd22a2a882a6c9f6e50cb438ea2fbbf96845481bcd80d \ - --hash=sha256:30e15e4e8df9b1c0ca92bfabf79f6b12a286e544e67762b14dd3023c53e41978 \ - --hash=sha256:4dbea3471b5d4a82b37ddca41bfddd63380c31050de7392e2467fabebcd110b8 \ - --hash=sha256:51abecafc1b55fcc9a5d73c078ea2d5a75964e0facf2c867a25d7f4f40238331 \ - --hash=sha256:78c513bc53bafd996d896f6f4535f2700b4916013417f8b41f47045790c6208d \ +pyobjc-framework-fsevents==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d +pyobjc-framework-fskit==11.1 ; platform_machine == 'x86_64' and platform_release >= '24.4' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f +pyobjc-framework-gamecenter==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c +pyobjc-framework-gamecontroller==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b +pyobjc-framework-gamekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269 +pyobjc-framework-gameplaykit==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:9ae2bee69b0cc1afa0e210b4663c7cdbb3cc94be1374808df06f98f992e83639 -pyobjc-framework-healthkit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76 \ - --hash=sha256:2d1b76b04e9e33ac9441cafa695766938eac04f8c8c69f7efd93a6aceb6eca40 \ - --hash=sha256:547ac283f84b5024be75290f351863f86eb48a950ec61e3150760230e6eba773 \ - --hash=sha256:8b6c739e17362897f0b1ba4aa4dc395b3d0c3855b87423eaeb6a89f910adc43f \ - --hash=sha256:ab4350f9fe65909107dd7992b367a6c8aac7dc31ed3d5b52eeb2310367d0eb0b \ - --hash=sha256:c693725d8476b745232df90ef01487e75e1e1c448e599dd34adf3dce859de760 -pyobjc-framework-imagecapturecore==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:254ae4502d651526c500533b8e2aee77ae7939f9acfd7d706dba2d464417deba \ - --hash=sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624 \ - --hash=sha256:bab8ed798598ddaa53f5b39707b58e16a1b1152858c87fd3fa0d64081f0c0364 \ - --hash=sha256:ded8dc6a8c826a6ae1b6a6d0a31542bd1eb85345f86201689c54e51193b572dc \ - --hash=sha256:e01c29456d0560667f8fcd3ff2749e79ad51bf72512e699646ce32227f91b447 \ - --hash=sha256:ed296c23d3d8d1d9af96a6486d09fb8d294cc318e4a2152e6f134151c76065f8 -pyobjc-framework-inputmethodkit==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:013919a4d766a7e66045fa5dd5d819bfa0450ccb59baba2b89d7449bce637d6b \ - --hash=sha256:2228bf58369351767294fe1aa400e98ec61e397a74a178788c24c98a1cff97ee \ - --hash=sha256:5095005809a4108f362998b46994f99b5a57f9ba367c01141c1b9eaea311bc5b \ - --hash=sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496 \ - --hash=sha256:92b9ce788ce4b094e352a64508050ff8e24307b8670d33488304b941d118894e \ - --hash=sha256:dd0c591a9d26967018a781fa4638470147ef2a9af3ab4a28612f147573eeefba -pyobjc-framework-installerplugins==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-healthkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76 +pyobjc-framework-imagecapturecore==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624 +pyobjc-framework-inputmethodkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496 +pyobjc-framework-installerplugins==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:363e59c7e05553d881f0facd41884f17b489ff443d7856e33dd0312064c746d9 \ --hash=sha256:f92b06c9595f3c800b7aabf1c1a235bfb4b2de3f5406d5f604d8e2ddd0aecb4e -pyobjc-framework-instantmessage==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-instantmessage==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:a70b716e279135eec5666af031f536c0f32dec57cfeae55cc9ff8457f10d4f3d \ --hash=sha256:c222aa61eb009704b333f6e63df01a0e690136e7e495907e5396882779bf9525 -pyobjc-framework-intents==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695 \ - --hash=sha256:4e3ec70c02d3166088223938a7433e479659cbd8ce04be5bf515ea8d6e3c353d \ - --hash=sha256:55498040123904b685cd38555eb84d95833fcb467b497d31757d6ac648a11817 \ - --hash=sha256:9e21b3bc33de2d5f69b5c1d581e5c724a08686fe84ec324a4be365bef769e482 \ - --hash=sha256:a663e2de1b7ae7b547de013f89773963f8180023e36f2cebfe8060395dc34c33 \ - --hash=sha256:e008d542abe38fd374c9ada7c833ad6e34a2db92b4dcbfba0a59ff830b9093bc -pyobjc-framework-intentsui==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:154fd92112184e8ef29ce81e685c377422dffcff4f7900ea6e5956a0e2be2268 \ - --hash=sha256:1bd950f808efb7ba7fbbc977300d7932a1dad41fbd3c78c8002870ca602e22d5 \ - --hash=sha256:35ef9f190f480147ce797809a63cc2b5f2ea64b51255d691e5e94bd8337e01ef \ - --hash=sha256:6d7d5402c05840a45047cf905fa550c2898cf5580cdee00a36bd35dd624c7542 \ - --hash=sha256:99a3ae40eb2a6ef1125955dd513c8acc88ce7d8d90130a8cdeaec8336e6fbec5 \ +pyobjc-framework-intents==11.1 ; platform_machine == 'x86_64' and platform_release >= '16.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695 +pyobjc-framework-intentsui==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:c8182155af4dce369c18d6e6ed9c25bbd8110c161ed5f1b4fb77cf5cdb99d135 -pyobjc-framework-iobluetooth==11.1 ; platform_system == 'darwin' \ - --hash=sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1 \ - --hash=sha256:2c99ade82a79263ea71c51d430696a2ad155beb01a67df59d52be63e181e0482 \ - --hash=sha256:2ef72cef1e03468e91a2f01af2390143bd6e4fcad1c6d0494dd857c99fa0d1a7 \ - --hash=sha256:4a8b1caba9ac51435f64a6cf9c1a2be867603161af8bebdd1676072ebed2fed9 \ - --hash=sha256:883781e7223cb0c63fab029d640721ded747f2e2b067645bc8b695ef02a4a4dd \ - --hash=sha256:a9a7e11a4bbb4a364b0412ca8632a1e853270c98c24d28421133f69c0c0ecaff -pyobjc-framework-iobluetoothui==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-iobluetooth==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1 +pyobjc-framework-iobluetoothui==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:060c721f1cd8af4452493e8153b72b572edcd2a7e3b635d79d844f885afee860 \ --hash=sha256:3c5a382d81f319a1ab9ab11b7ead04e53b758fdfeb604755d39c3039485eaac6 -pyobjc-framework-iosurface==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-iosurface==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:0c36ad56f8ec675dd07616418a2bc29126412b54627655abd21de31bcafe2a79 \ --hash=sha256:a468b3a31e8cd70a2675a3ddc7176ab13aa521c035f11188b7a3af8fff8b148b -pyobjc-framework-ituneslibrary==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-ituneslibrary==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:4e87d41f82acb6d98cf70ac3c932a568ceb3c2035383cbf177f54e63de6b815f \ --hash=sha256:e2212a9340e4328056ade3c2f9d4305c71f3f6af050204a135f9fa9aa3ba9c5e -pyobjc-framework-kernelmanagement==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-kernelmanagement==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:e934d1638cd89e38d6c6c5d4d9901b4295acee2d39cbfe0bd91aae9832961b44 \ --hash=sha256:ec74690bd3383a7945c4a038cc4e1553ec5c1d2408b60e2b0003a3564bff7c47 -pyobjc-framework-latentsemanticmapping==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-latentsemanticmapping==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:57f3b183021759a100d2847a4d8aa314f4033be3d2845038b62e5e823d96e871 \ --hash=sha256:c6c3142301e4d375c24a47dfaeebc2f3d0fc33128a1c0a755794865b9a371145 -pyobjc-framework-launchservices==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-launchservices==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:80b55368b1e208d6c2c58395cc7bc12a630a2a402e00e4930493e9bace22b7bb \ --hash=sha256:8b58f1156651058b2905c87ce48468f4799db86a7edf760e1897fedd057a3908 -pyobjc-framework-libdispatch==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87 \ - --hash=sha256:5d9985b0e050cae72bf2c6a1cc8180ff4fa3a812cd63b2dc59e09c6f7f6263a1 \ - --hash=sha256:a9357736cb47b4a789f59f8fab9b0d10b0a9c84f9876367c398718d3de085888 \ - --hash=sha256:c4e219849f5426745eb429f3aee58342a59f81e3144b37aa20e81dacc6177de1 \ - --hash=sha256:cd08f32ea7724906ef504a0fd40a32e2a0be4d64b9239530a31767ca9ccfc921 \ - --hash=sha256:dc9a7b8c2e8a63789b7cf69563bb7247bde15353208ef1353fff0af61b281684 -pyobjc-framework-libxpc==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2df539d11b65e229f8436a3660d0d1dce2cc7ba571054c5b91350b836db22576 \ - --hash=sha256:4f3083fde3c366cc58bcdb2c183fae9c531fb556d35a495818019f1a5d85c24d \ - --hash=sha256:654db8e822e60a1246d4d55c7127a140e10d6faa0da5a7366a16cc10def44deb \ - --hash=sha256:6862e63f565823d4eeb56f18f90a3ee8682c52a8d4bcd486d3535c9959464eda \ - --hash=sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a \ - --hash=sha256:c29b2df8d74ff6f489afa7c39f7c848c5f3d0531a6bbe704571782ee6c820084 -pyobjc-framework-linkpresentation==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-libdispatch==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87 +pyobjc-framework-libxpc==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a +pyobjc-framework-linkpresentation==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:018093469d780a45d98f4e159f1ea90771caec456b1599abcc6f3bf3c6873094 \ --hash=sha256:a785f393b01fdaada6d7d6d8de46b7173babba205b13b44f1dc884b3695c2fc9 -pyobjc-framework-localauthentication==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1 \ - --hash=sha256:9c9446c017b13c8dcadf485b76ab1d7bc12099b504bf5c2df1aae33b5dc4ab2c \ - --hash=sha256:aa3815f936612d78e51b53beed9115c57ae2fd49500bb52c4030a35856e6569e \ - --hash=sha256:d5a2e1ea2fe8233dc244f6029d5d0c878102b2e0615cb4b81b2f30d9ee101fca \ - --hash=sha256:e41be8e2132d1517e597401c7858b22531db2e7760d898993acc03ea13edb834 \ - --hash=sha256:f49c9dbbecfa0b0a7a633c60bda8179575e3685b6a696658a835c63afee90f9a -pyobjc-framework-localauthenticationembeddedui==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-localauthentication==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1 +pyobjc-framework-localauthenticationembeddedui==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:22baf3aae606e5204e194f02bb205f244e27841ea7b4a4431303955475b4fa56 \ --hash=sha256:3539a947b102b41ea6e40e7c145f27280d2f36a2a9a1211de32fa675d91585eb -pyobjc-framework-mailkit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-mailkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:8e6026462567baba194468e710e83787f29d9e8c98ea0583f7b401ea9515966e \ --hash=sha256:bf97dc44cb09b9eb9d591660dc0a41f077699976144b954caa4b9f0479211fd7 -pyobjc-framework-mapkit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0b6fa1c4fffc3ae91adb965731a0cc943b3b6e82c8f21919a53a68b43a67b534 \ - --hash=sha256:1dc27d315849ac96647d13c82eeefce5d1d2db8c64767ce10bd3e77cbaad2291 \ - --hash=sha256:91976c6dbc8cbb020e059a0ccdeab8933184712f77164dbad5a5526c1a49599d \ - --hash=sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1 \ - --hash=sha256:fb9b1d8cd5c0e8a097438369771d296de808621bc6013aa0065bc83716f5bdb0 \ - --hash=sha256:fe4581f5370dc7a209c1135e9c664a5a78950d3f5c39613bfb15c1e02a6258f3 -pyobjc-framework-mediaaccessibility==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-mapkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1 +pyobjc-framework-mediaaccessibility==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:52479a998fec3d079d2d4590a945fc78c41fe7ac8c76f1964c9d8156880565a4 \ --hash=sha256:cd07e7fc375ff1e8d225e0aa2bd9c2c1497a4d3aa5a80bfb13b08800fcd7f034 -pyobjc-framework-mediaextension==11.1 ; (platform_release >= '24.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '24' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:06cb19004413a4b08dd75cf1e5dadea7f2df8d15feeeb7adb529d0cf947fa789 \ - --hash=sha256:29edab42d9ecd394ac26f2ae2dfd7e2118452fc60a5623843919c1e9659c9dbc \ - --hash=sha256:40f1440ccc8da6deb80810866f8c807c17567db67b53e1576ea3a3b1330c85f9 \ - --hash=sha256:5efd284932ed0e7cfbca90a142b84a3966c73e51308688f8c230af41f9fb8c39 \ - --hash=sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709 \ - --hash=sha256:ca3a3ef1f3a759b53f297ccd701d29091eec66cc629a2b48c9acbe6c297bf256 -pyobjc-framework-medialibrary==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-mediaextension==11.1 ; platform_machine == 'x86_64' and platform_release >= '24.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709 +pyobjc-framework-medialibrary==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:102f4326f789734b7b2dfe689abd3840ca75a76fb8058bd3e4f85398ae2ce29d \ --hash=sha256:779be84bd280f63837ce02028ca46b41b090902aa4205887ffd5777f49377669 -pyobjc-framework-mediaplayer==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-mediaplayer==11.1 ; platform_machine == 'x86_64' and platform_release >= '16.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:b655cf537ea52d73209eb12935a047301c30239b318a366600f0f44335d51c9a \ --hash=sha256:d07a634b98e1b9eedd82d76f35e616525da096bd341051ea74f0971e0f2f2ddd -pyobjc-framework-mediatoolbox==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2867c91645a335ee29b47e9c0e9fd3ea8c9daad0c0719c50b8bf244d76998056 \ - --hash=sha256:975de470af8e52104bd1548eb9b4b0ef98524f35a6263c0bb4182797b9c5975b \ - --hash=sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a \ - --hash=sha256:bf26348d20caef38efb9cfc02d28af83c930b2f2c9581407f8ec04b3d8321a7a \ - --hash=sha256:d781e45fb1a7e532bcbae38c0f491629eaa641cdc226019544123b51794baf34 \ - --hash=sha256:e30fd2ffdea1b2c7c314d07266bce7614197c2b3ffd5b09f7012e7df7aa5c7a6 -pyobjc-framework-metal==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1f3aae0f9a4192a7f4f158dbee126ab5ef63a81bf9165ec63bc50c353c8d0e6f \ - --hash=sha256:432fefd3b27ab58c703b2f07afbc4690af815a9a8b4f8a997c4aefa8652e71d7 \ - --hash=sha256:b4c4dcab1db5750575a49a0a903528ea64b5bb93a9f3aaac5c810117a9c07e9c \ - --hash=sha256:d9b24d0ddb98b34a9a19755e5ca507c62fcef40ee5eae017e39be29650137f8c \ - --hash=sha256:de71b46062cb533be2c025cd6018fd4db9d7fd6a65bd67131d8e484c3616321a \ +pyobjc-framework-mediatoolbox==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a +pyobjc-framework-metal==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:f9fd3b7574a824632ee9b7602973da30f172d2b575dd0c0f5ef76b44cfe9f6f9 -pyobjc-framework-metalfx==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8 \ - --hash=sha256:60e1dcdf133d2504d810c3a9ba5a02781c9d54c2112a9238de8e3ca4e8debf31 \ - --hash=sha256:934cbc969182c57f5094389fe4afe6695595757d0d61f1ab663257475fdcc593 \ - --hash=sha256:aedfee1218b5784b010d618332a2cc088ba2ff9414eaa06e5db465eb5ef0aa43 \ - --hash=sha256:e1b2819bd6a66ba55fb7019b45d38a803ea21b8258fa41c8e9ad7c28cfe74092 \ - --hash=sha256:fdced91f6b2012c556db954de0e17f6d7985d52b4af83262f4d083bcd87aa01c -pyobjc-framework-metalkit==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:3e0776886fcd79fe7f0c55c718ebcdf073ac3e05d03040ab284ee09902fe1c70 \ - --hash=sha256:4854cf74fccf6ce516b49bf7cf8fc7c22da9a3743914a2f4b00f336206ad47ec \ - --hash=sha256:62e261b7798b276fee1fee065030a5d19d173863e9c697a80d1fc9a22258ec2c \ - --hash=sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e \ - --hash=sha256:b8a378135566e3c48838c19044e17ed2598a4050516ee1c23eee7d42439ef3c8 \ - --hash=sha256:ce886f3966144774d9222148eaf29fb08097d7dab5658186ded597b7c088f927 -pyobjc-framework-metalperformanceshaders==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:06b2a4e446fe859e30f7efc7ccfbaefd443225a6ec53d949a113a6a4acc16c4c \ - --hash=sha256:21ca31e4246e491df788f00978744d37db975266065f7ccbf393f027b4c6e248 \ - --hash=sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071 \ - --hash=sha256:97be4bd0ded06c663205bd1cf821e148352346f147da48dba44cf7680f0ea23b \ - --hash=sha256:c651e62ce58e75a88cfd287357fdd8d9a7f729c87248c8f43ce16025986afe6a \ - --hash=sha256:c905a3f5a34a95c1fd26bf07da505ed84b9b0a0c88a8f004914d9173f5037142 -pyobjc-framework-metalperformanceshadersgraph==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-metalfx==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8 +pyobjc-framework-metalkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e +pyobjc-framework-metalperformanceshaders==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071 +pyobjc-framework-metalperformanceshadersgraph==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:9b8b014e8301c2ae608a25f73bbf23c8f3f73a6f5fdbafddad509a21b84df681 \ --hash=sha256:d25225aab4edc6f786b29fe3d9badc4f3e2d0caeab1054cd4f224258c1b6dbe2 -pyobjc-framework-metrickit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:14de8dcaa107fe15546df91b1f7d51dc398169c3d1b06e02291fdb8722c6bf41 \ - --hash=sha256:75c5a62abc535387eea6a1e1612cfa5b1d59512ebfa8a3352596d481b18cc714 \ - --hash=sha256:92483af233a2c31ef73dd0f7a32988a323f9560699f2f1c6c10a8a282a7b9cfd \ - --hash=sha256:95e98e96b8f122b0141e84f13ae9e0f91d09d0803b1c093fdc7d19123f000f9e \ - --hash=sha256:a034e6b982e915da881edef87d71b063e596511d52aef7a32c683571f364156e \ +pyobjc-framework-metrickit==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:a79d37575489916c35840e6a07edd958be578d3be7a3d621684d028d721f0b85 -pyobjc-framework-mlcompute==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-mlcompute==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:975150725e919f8d3d33f830898f3cd2fd19a440999faab320609487f4eae19d \ --hash=sha256:f6c4c3ea6a62e4e3927abf9783c40495aa8bb9a8c89def744b0822da58c2354b -pyobjc-framework-modelio==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1b1393ddb315c0e8bed3f6ce4e4b355869a30c81ff79bda3ca3a201c0fd06dad \ - --hash=sha256:214a4078950bc7b86a1ea70504ecf292cccebe6515c70023efdddaaa6423f455 \ - --hash=sha256:327e1f3020001fd15bfbf4d4228581a8f64bd85872fd697b7c306343c11e25a6 \ - --hash=sha256:34fabde55d28aa8a12dd4476ad40182513cf87ee2fa928043aa6702961de302b \ - --hash=sha256:5d5e11389bde0852490b2a37896aaf9eb674b2a3586f2c572f9101cecb7bc576 \ +pyobjc-framework-modelio==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:fad0fa2c09d468ac7e49848e144f7bbce6826f2178b3120add8960a83e5bfcb7 -pyobjc-framework-multipeerconnectivity==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:296e10d289887cc4141c660f884cced1ec4ce64a19b3e406f13f6ce453a9425f \ - --hash=sha256:35c1a4a4b16df68b658b8531f97799995816a5bf49efd66805e3057b9bb9e474 \ - --hash=sha256:970031deb3dbf8da1fcb04e785d4bd2eeedae8f6677db92881df6d92b05c31d6 \ - --hash=sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b \ - --hash=sha256:c28ad5c0c6d28cbc897aaebcc5f14798762aa9fec7f9110171570fef4d8d8a36 \ - --hash=sha256:c92c95ea611d5272ab37fd73bc8e68c3d8fde515a75b97d8b22dafa8acbc7daf -pyobjc-framework-naturallanguage==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-multipeerconnectivity==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b +pyobjc-framework-naturallanguage==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:65a780273d2cdd12a3fa304e9c9ad822cb71facd9281f1b35a71640c53826f7c \ --hash=sha256:ab1fc711713aa29c32719774fc623bf2d32168aed21883970d4896e901ff4b41 -pyobjc-framework-netfs==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-netfs==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:9c49f050c8171dc37e54d05dd12a63979c8b6b565c10f05092923a2250446f50 \ --hash=sha256:f202e8e0c2e73516d3eac7a43b1c66f9911cdbb37ea32750ed197d82162c994a -pyobjc-framework-network==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:04582fef567392c2a10dcee9519356b79b17ab73ded050d14592da938d95b01a \ - --hash=sha256:2e45d8fdc0ad553cc35839cae5eab221fe5f7ce28758d693b8159e619ea06eac \ - --hash=sha256:8cdc9be8ec3b0ae95e5c649e4bbcdf502cffd357dacc566223be707bdd5ac271 \ - --hash=sha256:acf16738ab447a31a9f6167171b2a00d65a9370a8e84482d435b2b31c58eed94 \ - --hash=sha256:cafdf953aa80934d30726baa681c1af61daf2cc9fe9e3ca582f4e3796bd0d053 \ +pyobjc-framework-network==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:f6df7a58a1279bbc976fd7e2efe813afbbb18427df40463e6e2ee28fba07d2df -pyobjc-framework-networkextension==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b \ - --hash=sha256:4c9d6c08b8f1cf374351bcecf8bbc91e6a8999b84d52f30964f4f1e6a323943c \ - --hash=sha256:6d730540d97662867f3cfd90c9a1e69a6adae0f5eb554c1b94a1b067e7ebc728 \ - --hash=sha256:7dea914e7b26e28c6e4f8ffd03dd8fce612d38876043944fb0cf191774634566 \ - --hash=sha256:853458aae8b43634461f6c44759750e2dc784c9aba561f9468ab14529b5a7fbe \ - --hash=sha256:d3d6e9810cb01c3a8f99aed5ee2d75f6f785204338b99b32e5f64370a18cc9dd -pyobjc-framework-notificationcenter==11.1 ; (platform_release >= '14.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '14' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717 \ - --hash=sha256:15e49491d7f091eaa643f2fd89787becbf767dd6c609aa3d01e53132cb1d9fa1 \ - --hash=sha256:65fc67374a471890245c7a1d60cf67dcf160075a9c048a5d89608a8290f33b03 \ - --hash=sha256:c3e79e9c57f130099b47bde48f26fcd90ab3b52e01d989ea15b7cdb7fa5a34d8 \ - --hash=sha256:e46285290d04e84c167606ccfcb9a20c2567f5a2a6a9c6e96760fc9d561c2740 \ - --hash=sha256:f5ce98882e301adef07651ba495ddd57b661d4c0398afd39f4591c1b44673cca -pyobjc-framework-opendirectory==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-networkextension==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b +pyobjc-framework-notificationcenter==11.1 ; platform_machine == 'x86_64' and platform_release >= '14.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717 +pyobjc-framework-opendirectory==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:319ac3424ed0350be458b78148914468a8fc13a069d62e7869e3079108e4f118 \ --hash=sha256:bb4219b0d98dff4a952c50a79b1855ce74e1defd0d241f3013def5b09256fd7b -pyobjc-framework-osakit==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-osakit==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:1b0c0cc537ffb8a8365ef9a8b46f717a7cc2906414b6a3983777a6c0e4d53d5a \ --hash=sha256:920987da78b67578367c315d208f87e8fab01dd35825d72242909f29fb43c820 -pyobjc-framework-oslog==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:7174ca2cdc073e555d5f5aea3baa7410c61a83a3741eaec23e8581340037680e \ - --hash=sha256:7ae29c31ce51c476d3a37ca303465dd8bdfa98df2f6f951cf14c497e984a1ba9 \ - --hash=sha256:a302272aa40d1655be635e0f0dd0ca71b5fce562dfcb88a87165a170a648b2fd \ - --hash=sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979 \ - --hash=sha256:cade8869e185a29fb88fc48e2e5c984548433f669c1a40ec7f5640994fa36603 \ - --hash=sha256:f03789f8d5638e1075652b331b8ebf98c03dfa809c57545f0313583a7688bb86 -pyobjc-framework-passkit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:779eaea4e1931cfda4c8701e1111307b14bf9067b359a319fc992b6848a86932 \ - --hash=sha256:a6306dda724ca812dca70154d40f32ec9bbdaff765a12f3cc45391723efe147e \ - --hash=sha256:bfff2a63850afe702ba25f661360393389ffb58e127d47488c414caa9e676aa7 \ - --hash=sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f \ - --hash=sha256:d7948d5b3369b60808a85dcadffdebb0a44e8d2c4716edc10b78cb76fa762070 \ - --hash=sha256:f6b7f3cd7c6855af1b6fc4036ae2f10779a312182107c94d36ef63c2dd4a6f87 -pyobjc-framework-pencilkit==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-oslog==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979 +pyobjc-framework-passkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f +pyobjc-framework-pencilkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:9c173e0fe70179feadc3558de113a8baad61b584fe70789b263af202bfa4c6be \ --hash=sha256:b7824907bbcf28812f588dda730e78f662313baf40befd485c6f2fcb49018019 -pyobjc-framework-phase==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-phase==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:a940d81ac5c393ae3da94144cf40af33932e0a9731244e2cfd5c9c8eb851e3fc \ --hash=sha256:cfa61f9c6c004161913946501538258aed48c448b886adbf9ed035957d93fa15 -pyobjc-framework-photos==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:3a759ebcf46493cd09e5c89c0a09096ad83ae837d9236e437571bb22ca6eab3f \ - --hash=sha256:541d8fafdb2f111f2f298e1aa0542f2d5871ce1dd481c3e9be4ed33916b38c3a \ - --hash=sha256:72e0ed9bc5f1890f882df55333797da95c0ed1c1d7a0fe7d869a8d4ee4e1bdfd \ - --hash=sha256:7cded282eaebd77645a4262f6fb63379c7a226d20f8f1763910b19927709aea2 \ - --hash=sha256:8dbfffd29cfa63a8396ede0030785c15a5bc36065d3dd98fc6176a59e7abb3d3 \ +pyobjc-framework-photos==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:c8c3b25b14a2305047f72c7c081ff3655b3d051f7ed531476c03246798f8156d -pyobjc-framework-photosui==11.1 ; (platform_release >= '15.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '15' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df \ - --hash=sha256:6f0fa9c9e363c0db54957dfe4e26214379f2698caaba1e4ff4c9e3eba5e690d9 \ - --hash=sha256:91aff7caae16a7a7f25e35692aa92b796155510b8a0575668e75f351fbf63a68 \ - --hash=sha256:b2f278f569dfd596a32468351411518a651d12cb91e60620094e852c525a5f10 \ - --hash=sha256:e607242e09fb7d4bcad2f3eb2e88529d8f2ff7cf7341cd2c6c5b3f4d6744218e \ - --hash=sha256:f11f6043c83b2c65ecad69c48844fff6368127af3956ec8df9726bbd1e5da17e -pyobjc-framework-preferencepanes==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-photosui==11.1 ; platform_machine == 'x86_64' and platform_release >= '15.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df +pyobjc-framework-preferencepanes==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:6e4a55195ec9fc921e0eaad6b3038d0ab91f0bb2f39206aa6fccd24b14a0f1d8 \ --hash=sha256:6ee5f5a7eb294e03ea3bac522ac4b69e6dc83ceceff627a0a2d289afe1e01ad9 -pyobjc-framework-pubsub==11.1 ; platform_release >= '9' and platform_release < '18' and platform_system == 'darwin' and sys_platform == 'darwin' \ - --hash=sha256:47221f63466c523516ab5d2297dd3c644d915a77ca1f1867b0055d735486f1f8 \ - --hash=sha256:cea6bd9e0af46f9ea1c8d002a92e462576dd5a772a7e0688d40c7903755af11f -pyobjc-framework-pushkit==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:21993b7e9127b05575a954faa68e85301c6a4c04e34e38aff9050f67a05c562a \ - --hash=sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4 \ - --hash=sha256:68c4f44354eab84cb54d43310fa65ca3a5ba68299c868378764cc50803cf2adc \ - --hash=sha256:80d5d8240b71631d81cfa96f398fae1d137be98f224739e50edaf9e5afc21a9d \ - --hash=sha256:bac3ee77dfbe936998f207c1579e346993485bab8849db537ed250261cf12ab3 \ - --hash=sha256:cfec36cdca24654be0465282eb31b7ff3674ea4b7f3ce696b07edbe33b000aa5 -pyobjc-framework-quartz==11.1 ; platform_system == 'darwin' \ - --hash=sha256:07cbda78b4a8fcf3a2d96e047a2ff01f44e3e1820f46f0f4b3b6d77ff6ece07c \ - --hash=sha256:39d02a3df4b5e3eee1e0da0fb150259476910d2a9aa638ab94153c24317a9561 \ - --hash=sha256:43a1138280571bbf44df27a7eef519184b5c4183a588598ebaaeb887b9e73e76 \ - --hash=sha256:9ac806067541917d6119b98d90390a6944e7d9bd737f5c0a79884202327c9204 \ - --hash=sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75 \ - --hash=sha256:b23d81c30c564adf6336e00b357f355b35aad10075dd7e837cfd52a9912863e5 -pyobjc-framework-quicklookthumbnailing==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-pushkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4 +pyobjc-framework-quartz==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75 +pyobjc-framework-quicklookthumbnailing==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:1614dc108c1d45bbf899ea84b8691288a5b1d25f2d6f0c57dfffa962b7a478c3 \ --hash=sha256:4d1863c6c83c2a199c1dbe704b4f8b71287168f4090ed218d37dc59277f0d9c9 -pyobjc-framework-replaykit==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:22c6d09be9a6e758426d723a6c3658ad6bbb66f97ba9a1909bfcf29a91d99921 \ - --hash=sha256:2bf2180feae500fdd6f14360200fda0b6650a4ec39fe5d84a5dde9e8cdd307b6 \ - --hash=sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b \ - --hash=sha256:7742ee18c8c9b61f5668698a05b88d25d34461fcdd95a8f669ecdfd8db8c4d42 \ - --hash=sha256:b503fabc33ee02117fd82c78db18cba3f0be90dea652f5553101a45185100402 \ - --hash=sha256:da84e48ba5d529ae72b975f0d81c5bd5427983c2b05d3d2c7fd54a6cbdf0d0f9 -pyobjc-framework-safariservices==11.1 ; (platform_release >= '16.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '16' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0 \ - --hash=sha256:8a4371d64052a3ffe9993a89c45f9731f86e7b6c21fd1d968815fd7930ff501a \ - --hash=sha256:8a6ec417d35a0600629eba97c0ab2f2d09fae171e8bca3d3d6aa1c7ff272c4d7 \ - --hash=sha256:8b4d4169dd21e69246d90a42f872b7148064b63de6bbbf6bc6ddabe33f143843 \ - --hash=sha256:abdbe0d8a79caa994a1d2be8ea4e5a1e4c80f7d8e1f0750f9c365129d1f1a968 \ - --hash=sha256:c92eb9e35f98368ea1bfaa8cdd41138ca8b004ea5a85833390a44e5626ca5061 -pyobjc-framework-safetykit==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:24d5ce9dfb80abb634a95ceda3da0f0cdb52c765db0f47de953a4f66b918c957 \ - --hash=sha256:761304365978d650015fe05fb624ba13ea4af6c6a76ef8e344673f5b0fed2e92 \ - --hash=sha256:8130de57f701dbccb1d84c76ec007fe04992da58cbf0eb906324393eeac3d08d \ - --hash=sha256:b76fccdb970d3d751a540c47712e9110afac9abea952cb9b7bc0d5867db896e3 \ - --hash=sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4 \ - --hash=sha256:cd8091c902037eac4a403d8462424afd711f43206af3548a34bebe1f59d2c340 -pyobjc-framework-scenekit==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2be143172b43c2cf4a2b3fad9e15ffb5d29df677d3678160cd125b94a30caaca \ - --hash=sha256:2f347d5ae42af8acddb86a45f965046bb91f8d83d33851390954439961e2a7b7 \ - --hash=sha256:3f62f2b8f26375ecfec71f7fdb23f2739cf93d213968c6ffac6a8525516ffc6e \ - --hash=sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306 \ - --hash=sha256:c803d95b30c4ce49f46ff7174806f5eb84e4c3a152f8f580c5da0313c5c67041 \ - --hash=sha256:ea2f02eea982872994d7c366f6a51060a90cc17b994c017f85c094e2bc346847 -pyobjc-framework-screencapturekit==11.1 ; platform_release >= '21.4' and platform_system == 'darwin' \ - --hash=sha256:1119d6258d6c668564ab39154cfc745fd2bb8b3beeaa4f9b2a8a4c93926678c0 \ - --hash=sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0 \ - --hash=sha256:641fa7834f54558859209e174c83551d5fa239ca6943ace52665f7d45e562ff2 \ - --hash=sha256:9972db69064b69e78fbc6a00f1de2d8eaa225b990b23687970328b061e60e26d \ - --hash=sha256:9e135b414d3829fcf7fd8a66c94e8b51135fb9f630c10488fb9d78f27f622906 \ - --hash=sha256:f93f8198741bd904d423a7b1ef941445246bdf6cb119597d981e61a13cc479a4 -pyobjc-framework-screensaver==11.1 ; platform_system == 'darwin' \ - --hash=sha256:2c01a9646bc118445cbb117e7016bd1df9fe93a65db991ab5496d59b1a7bc66d \ - --hash=sha256:46d65c1e14d35f287e7be351e2f98daf9489e31e7ca0d306e6102904ce6c40fb \ - --hash=sha256:7852c2281148cb99c87c4c25b83dca7fdd11e6eed04deadcf2201ed5a2079e5f \ - --hash=sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2 \ - --hash=sha256:e32c83e1d9e5044d482916ac42257a87d1f1068f3f6bccaa04edda40fb9f9ad1 \ - --hash=sha256:f2d22293cf9d715e4692267a1678096afd6793c0519d9417cf77c8a6c706a543 -pyobjc-framework-screentime==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-replaykit==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b +pyobjc-framework-safariservices==11.1 ; platform_machine == 'x86_64' and platform_release >= '16.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0 +pyobjc-framework-safetykit==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4 +pyobjc-framework-scenekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '11.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306 +pyobjc-framework-screencapturekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.4' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0 +pyobjc-framework-screensaver==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2 +pyobjc-framework-screentime==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:50a4e4ab33d6643a52616e990aa1c697d5e3e8f9f9bdab8d631e6d42d8287b4f \ --hash=sha256:9bb8269456bbb674e1421182efe49f9168ceefd4e7c497047c7bf63e2f510a34 -pyobjc-framework-scriptingbridge==11.1 ; (platform_release >= '9.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '9' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:226ba12d9cbd504411b702323b0507dd1690e81b4ce657c5f0d8b998c46cf374 \ - --hash=sha256:5381e9be1299e1134489e4d46662c649613214265b3b691264cfba0b083929f5 \ - --hash=sha256:57f5401826e3a008d9cfb7c164187859cadc1b1f96194dc0a7c596f502548c26 \ - --hash=sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699 \ - --hash=sha256:a84d0a8ff4fa1f0016f5d797ad93e22e437212a2fc8e6417a3b8d68f89229680 \ - --hash=sha256:c2ba0ad3d3e4e3c6a43fe3e84ab02c5c4e74000bb6f130ae47bf82a3dcd4af98 -pyobjc-framework-searchkit==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-scriptingbridge==11.1 ; platform_machine == 'x86_64' and platform_release >= '9.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699 +pyobjc-framework-searchkit==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:13a194eefcf1359ce9972cd92f2aadddf103f3efb1b18fd578ba5367dff3c10c \ --hash=sha256:9c9d6ca71cef637ccc3627225fb924a460b3d0618ed79bb0b3c12fcbe9270323 -pyobjc-framework-security==11.1 ; platform_system == 'darwin' \ - --hash=sha256:141cc3ee08627ae0698264efc3dbbaf28d2255e0fe690e336eb8f0f387c4af01 \ - --hash=sha256:158da3b2474e2567fd269531c4ee9f35b8ba4f1eccbd1fb4a37c85a18bf1243c \ - --hash=sha256:2eb4ba6d8b221b9ad5d010e026247e8aa26ee43dcaf327e848340ed227d22d7e \ - --hash=sha256:4db1ebf6395cd370139cb35ff172505fc449c7fdf5d3a28f2ada8a30ef132cd0 \ - --hash=sha256:858a18303711eb69d18d1a64cf8bb2202f64a3bd1c82203c511990dbd8326514 \ +pyobjc-framework-security==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da -pyobjc-framework-securityfoundation==11.1 ; platform_system == 'darwin' \ +pyobjc-framework-securityfoundation==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:25f2cf10f80c122f462e9d4d43efe9fd697299c194e0c357e76650e234e6d286 \ --hash=sha256:b3c4cf70735a93e9df40f3a14478143959c415778f27be8c0dc9ae0c5b696b92 -pyobjc-framework-securityinterface==11.1 ; platform_system == 'darwin' \ - --hash=sha256:0232f947b4f906097a5d758305097a8688835a52e0721b75ae3f1180eac30f50 \ - --hash=sha256:26056441b325029da06a7c7b8dd1a0c9a4ad7d980596c1b04d132a502b4cacc0 \ - --hash=sha256:2c20bedead75de7bf1f2ceda562755f64c70ee86180ed45480dc9dbc55609a0b \ - --hash=sha256:708dd1d65309f3d4043ecaf152591c240601a5d3da7ae7a500f511c54317537b \ - --hash=sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9 \ - --hash=sha256:e9ebfb32177eb06f5c894be97c6af3802f09b9890fce8e0956cc0e680af4eafd -pyobjc-framework-securityui==11.1 ; platform_release >= '24.4' and platform_system == 'darwin' \ +pyobjc-framework-securityinterface==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9 +pyobjc-framework-securityui==11.1 ; platform_machine == 'x86_64' and platform_release >= '24.4' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:3cdb101b03459fcf8e4064b90021d06761003f669181e02f43ff585e6ba2403d \ --hash=sha256:e80c93e8a56bf89e4c0333047b9f8219752dd6de290681e9e2e2b2e26d69e92d -pyobjc-framework-sensitivecontentanalysis==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-sensitivecontentanalysis==11.1 ; platform_machine == 'x86_64' and platform_release >= '23.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:5b310515c7386f7afaf13e4632d7d9590688182bb7b563f8026c304bdf317308 \ --hash=sha256:dbb78f5917f986a63878bb91263bceba28bd86fc381bad9461cf391646db369f -pyobjc-framework-servicemanagement==11.1 ; (platform_release >= '10.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '10' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-servicemanagement==11.1 ; platform_machine == 'x86_64' and platform_release >= '10.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:104f56557342a05ad68cd0c9daf63b7f4678957fe1f919f03a872f1607a50710 \ --hash=sha256:90a07164da49338480e0e135b445acc6ae7c08549a2037d1e512d2605fedd80a -pyobjc-framework-sharedwithyou==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:481362f0bde6def86634fc687abe6f4dee650c09c22b48bfe5af5322f9947cef \ - --hash=sha256:63b1cb673b844ebfeddc032d0539f913bbd6b67ab2a310a1fcff7842dba9c714 \ - --hash=sha256:70421a8fd326afd99eeae273b693a7b4d2d200c38e883d8219a84123a4ba0861 \ - --hash=sha256:99e1749187ae370be7b9c55dd076d1b8143f0d8db3e83f52540586f32e7abb33 \ - --hash=sha256:c1a1770aa2c417f17010623414fb12943570baa726d8780dd7446ba5bcee8c3d \ +pyobjc-framework-sharedwithyou==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:ece3a28a3083d0bcad0ac95b01f0eb699b9d2d0c02c61305bfd402678753ff6e -pyobjc-framework-sharedwithyoucore==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:4e19bfc74f392546ca4b7ea5271d4802617445ad493428370eafd3cddd4d977e \ - --hash=sha256:5a45c562c99017f8e057d4080012b63a9bb660c696334707c54d7b4018ca1017 \ - --hash=sha256:6aee3df8bed97a74e1f79609f9884edcaab2d305db20bdcae39e47b3e513c559 \ - --hash=sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73 \ - --hash=sha256:a3fb0e745fd022fed48cc9a5e0dcbf8d1abcb5bfc192150e3a2584f4351791fc \ - --hash=sha256:dd18c588b29de322c25821934d6aa6d2bbbdbb89b6a4efacdb248b4115fc488d -pyobjc-framework-shazamkit==11.1 ; (platform_release >= '21.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '21' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:8bac17f285742e0f13a54c7085ef3035d8034ffc43d18d3d68fb41283c5064ff \ - --hash=sha256:b323f5409b01711aa2b6e2113306084fab2cc83fa57a0c3d55bd5876358b68d8 \ - --hash=sha256:b3304c3a67e3722b895d874f215dd4277b49cedddb72fa780a791ef79e5c3d45 \ - --hash=sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6 \ - --hash=sha256:ef51f461672234076b3791ad4be05adad20a2e24b9d7d93acd7bf18d7f9b1714 \ - --hash=sha256:f7d191fb187dbb05e3f88f546d5207618d65e270d7a4316b51b1171cc491e268 -pyobjc-framework-social==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-sharedwithyoucore==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73 +pyobjc-framework-shazamkit==11.1 ; platform_machine == 'x86_64' and platform_release >= '21.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6 +pyobjc-framework-social==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:ab5878c47d7a0639704c191cee43eeb259e09688808f0905c42551b9f79e1d57 \ --hash=sha256:fbc09d7b00dad45b547f9b2329f4dcee3f5a50e2348de1870de0bd7be853a5b7 -pyobjc-framework-soundanalysis==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-soundanalysis==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:42cd25b7e0f343d8b59367f72b5dae96cf65696bdb8eeead8d7424ed37aa1434 \ --hash=sha256:6cf983c24fb2ad2aa5e7499ab2d30ff134d887fe91fd2641acf7472e546ab4e5 -pyobjc-framework-speech==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:3c80670dbad921bf1d4954a9de29525acb53ee84e064a95fbbdfddff1db2f14f \ - --hash=sha256:7726eff52cfa9cc7178ddcd1285cbc23b5f89ee55b4b850b0d2e90bb4f8e044b \ - --hash=sha256:80e577e3dfc1c10a1280deae172cdb64e105f99f47343099e3968b720a3f68da \ - --hash=sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c \ - --hash=sha256:f19778a4ace37c538a34a10ac1f595c80b83489210e6fa60c703399aee264c7e \ - --hash=sha256:f36ca8a3cfc12b7a5cdf00712eec3ad0fac34e3da36b5737c5302e224525aa70 -pyobjc-framework-spritekit==11.1 ; (platform_release >= '13.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '13' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2277e74d7be426181ae5ca7dd9d6c776426e8e825ad83b6046a7cb999015f27d \ - --hash=sha256:4e3673196b7cbc007e4aa7f14d711f3cda00e32e120bc4f6e896d54edd517c61 \ - --hash=sha256:8b470a890db69e70ef428dfff88da499500fca9b2d44da7120dc588d13a2dbdb \ - --hash=sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108 \ - --hash=sha256:d6ea27fc202b40945729db50fdc6f75a0a11a07149febf4b99e14caf96ef33b0 \ - --hash=sha256:e04d0825109a0158e551e9e2a61c56e83eadfdc5a44a47b64cb410b0498d33be -pyobjc-framework-storekit==11.1 ; (platform_release >= '11.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '11' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:2e2607116b0d53d7fda2fc48e37b1deb1d26a60e7b723a6b7c391a3f48b2ac3b \ - --hash=sha256:4944bd1fd01f486623453b68accf4445d3c5686714820c8329a0c4e4672d6fff \ - --hash=sha256:5ca3373272b6989917c88571ca170ce6d771180fe1a2b44c7643fe084569b93e \ - --hash=sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164 \ - --hash=sha256:be6c894a9f9c2b40e300005c3a3cf46f352e1711f65c0b7a8dd5035d1f6333aa \ - --hash=sha256:d312c392962e15fc842d11b0f7d937e3bd9f3ed3a80f7a6be77518475564f04d -pyobjc-framework-symbols==11.1 ; (platform_release >= '23.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '23' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-speech==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c +pyobjc-framework-spritekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '13.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108 +pyobjc-framework-storekit==11.1 ; platform_machine == 'x86_64' and platform_release >= '11.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164 +pyobjc-framework-symbols==11.1 ; platform_machine == 'x86_64' and platform_release >= '23.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:0e09b7813ef2ebdca7567d3179807444dd60f3f393202b35b755d4e1baf99982 \ --hash=sha256:1de6fc3af15fc8d5fd4869663a3250311844ec33e99ec8a1991a352ab61d641d -pyobjc-framework-syncservices==11.1 ; platform_system == 'darwin' \ - --hash=sha256:03124c8c7c7ce837f51e1c9bdcf84c6f1d5201f92c8a1c172ec34908d5e57415 \ - --hash=sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe \ - --hash=sha256:24c2b62e94d9e0e5e64abbf6d1f9994212b2a5cb8cad5a8d0394d694b20731b5 \ - --hash=sha256:711d493c7967682bee605c5909a49d268d9b3dd3cb7a71d8ab5dbe01a069eb44 \ - --hash=sha256:a0ff222472b2cb5c345c92ae4bde245f4181843379f4fd9462cd5c096ed7b2f1 \ - --hash=sha256:e5b29d6e8fe5b0015dcac5485e4fe6ede35bae7beeb647fb81d86120365029ea -pyobjc-framework-systemconfiguration==11.1 ; platform_system == 'darwin' \ - --hash=sha256:6881929b828a566bf1349f09db4943e96a2b33f42556e1f7f6f28b192420f6fc \ - --hash=sha256:ab2ff52e4228f42182b7ef398d0da504f9f8f4a889963422af9aa1f495668db2 \ - --hash=sha256:b994c613b5bea9f1c9a64f57f373563c7f424ffae5e4cb20e76c8448a35543f7 \ - --hash=sha256:c236f19cadc9fff56c0afb3e4ad6f8c8e11c5679e31ed413fe6876bf2ea73353 \ - --hash=sha256:ef266e9f83c2fc9a999709626138b427ff052a0acf4851d797c3a7654878c046 \ +pyobjc-framework-syncservices==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe +pyobjc-framework-systemconfiguration==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:f30ed0e9a8233fecb06522e67795918ab230ddcc4a18e15494eff7532f4c3ae1 -pyobjc-framework-systemextensions==11.1 ; (platform_release >= '19.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '19' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1801413066d1cbf2a0319e228060820c51ea0fb27aec339716d8c82f2e1b3125 \ - --hash=sha256:2ed65857244f18b88107e5d3ea8ea21c9da662490895b430e376423ee7c0b963 \ - --hash=sha256:3a2b1e84e4a118bfe13efb9f2888b065dc937e2a7e60afd4d0a82b51b8301a10 \ - --hash=sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6 \ - --hash=sha256:97c1b5f415f3981d0426516e014e94392f054f3898252bf6c88c3f50700c1d70 \ - --hash=sha256:9aa7595de4f8f6a252c50419c0343f7326c6a4de47da5b933a17880d1cadfa36 -pyobjc-framework-threadnetwork==11.1 ; (platform_release >= '22.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '22' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-systemextensions==11.1 ; platform_machine == 'x86_64' and platform_release >= '19.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6 +pyobjc-framework-threadnetwork==11.1 ; platform_machine == 'x86_64' and platform_release >= '22.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:55021455215a0d3ad4e40152f94154e29062e73655558c5f6e71ab097d90083e \ --hash=sha256:73a32782f44b61ca0f8a4a9811c36b1ca1cdcf96c8a3ba4de35d8e8e58a86ad5 -pyobjc-framework-uniformtypeidentifiers==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-uniformtypeidentifiers==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:6e2e8ea89eb8ca03bc2bc8e506fff901e71d916276475c8d81fbf0280059cb4c \ --hash=sha256:86c499bec8953aeb0c95af39b63f2592832384f09f12523405650b5d5f1ed5e9 -pyobjc-framework-usernotifications==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996 \ - --hash=sha256:62a4bd242b761a6f00a4374a369391346d225d68be07691e042ec7db452084c8 \ - --hash=sha256:9efa3004059a8fe3f3c52f638f0401dbcdbc7b2f539587c8868da2486a64d674 \ - --hash=sha256:bad5e650c014757159523466e5b2c127e066045e2a5579a5cac9aeca46bda017 \ - --hash=sha256:ce6006989fd4a59ec355f6797ccdc9946014ea5241ff7875854799934dbba901 \ - --hash=sha256:dcdcb657d2fa47108e4ef93ec3320025576857e8f69a15f082f5eda930b35e86 -pyobjc-framework-usernotificationsui==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-usernotifications==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996 +pyobjc-framework-usernotificationsui==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:18e0182bddd10381884530d6a28634ebb3280912592f8f2ad5bac2a9308c6a65 \ --hash=sha256:b84d73d90ab319acf8fad5c59b7a5e2b6023fbb2efd68c58b532e3b3b52f647a -pyobjc-framework-videosubscriberaccount==11.1 ; (platform_release >= '18.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '18' and platform_system == 'darwin' and sys_platform == 'darwin') \ +pyobjc-framework-videosubscriberaccount==11.1 ; platform_machine == 'x86_64' and platform_release >= '18.0' and platform_system == 'darwin' and sys_platform == 'linux' \ --hash=sha256:2dd78586260fcee51044e129197e8bf2e157176e02babeec2f873afa4235d8c6 \ --hash=sha256:d5a95ae9f2a6f0180a5bbb10e76c064f0fd327aae00a2fe90aa7b65ed4dad7ef -pyobjc-framework-videotoolbox==11.1 ; (platform_release >= '12.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '12' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:0e54bd6cfcbdda4add24e8e873baab11dfb436633100cc6664f3c068e615a6ff \ - --hash=sha256:31acfb12cea4f0624ecb92e74404f15e2755fbf0a3f4133dc93add44cf4a6a9f \ - --hash=sha256:65a96385e80cb9ad3eab7d1f3156452ff805a925c9ca287ff1491a97cca191ba \ - --hash=sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9 \ - --hash=sha256:c55285c3c78183fd2a092d582e30b562777a82985cccca9e7e99a0aff2601591 \ - --hash=sha256:e282cb07f6a51647ac19a3b5d31e26f1619285bac24171e403921d671e4756d9 -pyobjc-framework-virtualization==11.1 ; (platform_release >= '20.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '20' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:12a5ef32d2b7a56b675ea34fcb68bb9dddb7cf2c0a5ac5131f35551767bdacf1 \ - --hash=sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022 \ - --hash=sha256:59df6702b3e63200752be7d9c0dc590cb4c3b699c886f9a8634dd224c74b3c3c \ - --hash=sha256:5f35d823003a613bde27c2c699a8a7de45dc2bdd2e1121e0c4a337b877dfc64e \ - --hash=sha256:790bd2e42e8c5890319f8c576d5e171f87f95655e6fc55cf19a5f85f9e23558a \ - --hash=sha256:b2e7ab5204fe80249dd8d031b761cf9c0106d0d5e61d88930e0f334f5060d820 -pyobjc-framework-vision==11.1 ; (platform_release >= '17.0' and platform_system == 'darwin' and sys_platform != 'darwin') or (platform_release >= '17' and platform_system == 'darwin' and sys_platform == 'darwin') \ - --hash=sha256:1e5617e37dd2a7cff5e69e9aab039ea74b39ccdc528f6c828f2b60c1254e61e5 \ - --hash=sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b \ - --hash=sha256:d00830c71a30fc893b3c5ee65119c7e5e5a95a16af53b8e56a0e58cff57e3b56 \ - --hash=sha256:d1f8fdccc6135fdbfd66d8f21240d6c84465cb8e116a8e5b43601aed020051e5 \ - --hash=sha256:df076c3e3e672887182953efc934c1f9683304737e792ec09a29bfee90d2e26a \ - --hash=sha256:dfd148a6df30ac70a9c41dd90a6c8f8c7f339bd9ca6829629a902f272e02b6b4 -pyobjc-framework-webkit==11.1 ; platform_system == 'darwin' \ - --hash=sha256:13b774d4244734cb77bf3c3648149c163f62acaa105243d7c48bb3fd856b5628 \ - --hash=sha256:1a6e6f64ca53c4953f17e808ecac11da288d9a6ade738156ba161732a5e0c96a \ - --hash=sha256:1d01008756c3912b02b7c02f62432467fbee90a93e3b8e31fa351b4ca97c9c98 \ - --hash=sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae \ - --hash=sha256:864f9867a2caaeaeb83e5c0fa3dcf78169622233cf93a9a5eeb7012ced3b8076 \ - --hash=sha256:c1c00d549ab1d50e3d7e8f5f71352b999d2c32dc2365c299f317525eb9bff916 -pyotp==2.9.0 \ +pyobjc-framework-videotoolbox==11.1 ; platform_machine == 'x86_64' and platform_release >= '12.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9 +pyobjc-framework-virtualization==11.1 ; platform_machine == 'x86_64' and platform_release >= '20.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022 +pyobjc-framework-vision==11.1 ; platform_machine == 'x86_64' and platform_release >= '17.0' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b +pyobjc-framework-webkit==11.1 ; platform_machine == 'x86_64' and platform_system == 'darwin' and sys_platform == 'linux' \ + --hash=sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae +pyotp==2.9.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63 \ --hash=sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612 -pyparsing==3.2.5 \ +pyparsing==3.2.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6 \ --hash=sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e -pypdf==6.14.2 \ - --hash=sha256:3f07891af76dc002657e04993ab9b4de81de29f9013b9761d0b7968bff12e946 \ - --hash=sha256:7873f502fe4385e79539b21d872392dc0c4e3714327c15881cbc7fbfd1f95b25 -pyperclip==1.11.0 \ +pypdf==6.19.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14 \ + --hash=sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155 +pyperclip==1.11.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6 \ --hash=sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273 -python-dateutil==2.9.0.post0 \ +python-dateutil==2.9.0.post0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 -python-docx==1.2.0 \ +python-docx==1.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7 \ --hash=sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce -python-dotenv==1.2.2 \ +python-dotenv==1.2.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 -python-frontmatter==1.1.0 \ +python-frontmatter==1.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1 \ --hash=sha256:7118d2bd56af9149625745c58c9b51fb67e8d1294a0c76796dafdc72c36e5f6d -python-json-logger==4.0.0 \ +python-json-logger==4.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2 \ --hash=sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f -python-multipart==0.0.31 \ +python-multipart==0.0.31 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:8408153d68a9773291fc1da39a8b85a50044bddbabd2dd72e9229776b7b15e28 \ --hash=sha256:fc631183bb13e56db3158a4909908dfb2e23565286744e798241e63750e5d680 -pywin32==311 ; sys_platform == 'win32' \ - --hash=sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87 \ - --hash=sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d \ - --hash=sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31 \ - --hash=sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a \ - --hash=sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42 \ - --hash=sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee \ - --hash=sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067 \ - --hash=sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852 \ - --hash=sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d -pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ - --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ - --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 -pyyaml==6.0.2 \ - --hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \ - --hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \ - --hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \ - --hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \ - --hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \ - --hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \ +pyyaml==6.0.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \ --hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \ - --hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \ --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \ - --hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \ --hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \ - --hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \ - --hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \ - --hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \ - --hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \ - --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \ - --hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \ - --hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba -redis==7.2.1 \ + --hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e +redis==7.2.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:49e231fbc8df2001436ae5252b3f0f3dc930430239bfeb6da4c7ee92b16e5d33 \ --hash=sha256:6163c1a47ee2d9d01221d8456bc1c75ab953cbda18cfbc15e7140e9ba16ca3a5 -referencing==0.36.2 \ +referencing==0.36.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa \ --hash=sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0 -regex==2025.9.18 \ - --hash=sha256:032720248cbeeae6444c269b78cb15664458b7bb9ed02401d3da59fe4d68c3a5 \ +regex==2025.9.18 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:039f11b618ce8d71a1c364fdee37da1012f5a3e79b1b2819a9f389cd82fd6282 \ - --hash=sha256:05440bc172bc4b4b37fb9667e796597419404dbba62e171e1f826d7d2a9ebcef \ --hash=sha256:06104cd203cdef3ade989a1c45b6215bf42f8b9dd705ecc220c173233f7cba41 \ - --hash=sha256:0ac936537ad87cef9e0e66c5144484206c1354224ee811ab1519a32373e411f3 \ - --hash=sha256:0c3506682ea19beefe627a38872d8da65cc01ffa25ed3f2e422dffa1474f0788 \ - --hash=sha256:0dc6893b1f502d73037cf807a321cdc9be29ef3d6219f7970f842475873712ac \ - --hash=sha256:0f0d676522d68c207828dcd01fb6f214f63f238c283d9f01d85fc664c7c85b56 \ - --hash=sha256:0ffd9e230b826b15b369391bec167baed57c7ce39efc35835448618860995946 \ - --hash=sha256:1137cabc0f38807de79e28d3f6e3e3f2cc8cfb26bead754d02e6d1de5f679203 \ - --hash=sha256:13202e4c4ac0ef9a317fff817674b293c8f7e8c68d3190377d8d8b749f566e12 \ - --hash=sha256:168be0d2f9b9d13076940b1ed774f98595b4e3c7fc54584bba81b3cc4181742e \ - --hash=sha256:16eaf74b3c4180ede88f620f299e474913ab6924d5c4b89b3833bc2345d83b3d \ - --hash=sha256:1a351aff9e07a2dabb5022ead6380cff17a4f10e4feb15f9100ee56c4d6d06af \ - --hash=sha256:1b9d9a2d6cda6621551ca8cf7a06f103adf72831153f3c0d982386110870c4d3 \ - --hash=sha256:1ef86a9ebc53f379d921fb9a7e42b92059ad3ee800fcd9e0fe6181090e9f6c23 \ - --hash=sha256:274687e62ea3cf54846a9b25fc48a04459de50af30a7bd0b61a9e38015983494 \ - --hash=sha256:29cd86aa7cb13a37d0f0d7c21d8d949fe402ffa0ea697e635afedd97ab4b69f1 \ - --hash=sha256:2a40f929cd907c7e8ac7566ac76225a77701a6221bca937bdb70d56cb61f57b2 \ - --hash=sha256:2e1eddc06eeaffd249c0adb6fafc19e2118e6308c60df9db27919e96b5656096 \ - --hash=sha256:300e25dbbf8299d87205e821a201057f2ef9aa3deb29caa01cd2cac669e508d5 \ - --hash=sha256:3810a65675845c3bdfa58c3c7d88624356dd6ee2fc186628295e0969005f928d \ - --hash=sha256:3d86b5247bf25fa3715e385aa9ff272c307e0636ce0c9595f64568b41f0a9c77 \ - --hash=sha256:40532bff8a1a0621e7903ae57fce88feb2e8a9a9116d341701302c9302aef06e \ - --hash=sha256:436e1b31d7efd4dcd52091d076482031c611dde58bf9c46ca6d0a26e33053a7e \ - --hash=sha256:48317233294648bf7cd068857f248e3a57222259a5304d32c7552e2284a1b2ad \ - --hash=sha256:4b8cdbddf2db1c5e80338ba2daa3cfa3dec73a46fff2a7dda087c8efbf12d62f \ - --hash=sha256:4dc98ba7dd66bd1261927a9f49bd5ee2bcb3660f7962f1ec02617280fc00f5eb \ --hash=sha256:4f130c3a7845ba42de42f380fff3c8aebe89a810747d91bcf56d40a069f15352 \ --hash=sha256:5514b8e4031fdfaa3d27e92c75719cbe7f379e28cacd939807289bce76d0e35a \ - --hash=sha256:57929d0f92bebb2d1a83af372cd0ffba2263f13f376e19b1e4fa32aec4efddc3 \ - --hash=sha256:57a161bd3acaa4b513220b49949b07e252165e6b6dc910ee7617a37ff4f5b425 \ - --hash=sha256:5adf266f730431e3be9021d3e5b8d5ee65e563fec2883ea8093944d21863b379 \ - --hash=sha256:5f96fa342b6f54dcba928dd452e8d8cb9f0d63e711d1721cd765bb9f73bb048d \ - --hash=sha256:65d3c38c39efce73e0d9dc019697b39903ba25b1ad45ebbd730d2cf32741f40d \ - --hash=sha256:6a4b44df31d34fa51aa5c995d3aa3c999cec4d69b9bd414a8be51984d859f06d \ - --hash=sha256:6a52219a93dd3d92c675383efff6ae18c982e2d7651c792b1e6d121055808743 \ - --hash=sha256:7b47fcf9f5316c0bdaf449e879407e1b9937a23c3b369135ca94ebc8d74b1742 \ - --hash=sha256:7c9f285a071ee55cd9583ba24dde006e53e17780bb309baa8e4289cd472bcc47 \ --hash=sha256:7cc9e5525cada99699ca9223cce2d52e88c52a3d2a0e842bd53de5497c604164 \ - --hash=sha256:8620d247fb8c0683ade51217b459cb4a1081c0405a3072235ba43a40d355c09a \ - --hash=sha256:874ff523b0fecffb090f80ae53dc93538f8db954c8bb5505f05b7787ab3402a0 \ - --hash=sha256:8f9698b6f6895d6db810e0bda5364f9ceb9e5b11328700a90cae573574f61eea \ --hash=sha256:92a8e375ccdc1256401c90e9dc02b8642894443d549ff5e25e36d7cf8a80c783 \ --hash=sha256:9feb29817df349c976da9a0debf775c5c33fc1c8ad7b9f025825da99374770b7 \ - --hash=sha256:a276937d9d75085b2c91fb48244349c6954f05ee97bba0963ce24a9d915b8b68 \ - --hash=sha256:a61e85bfc63d232ac14b015af1261f826260c8deb19401c0597dbb87a864361e \ - --hash=sha256:a78722c86a3e7e6aadf9579e3b0ad78d955f2d1f1a8ca4f67d7ca258e8719d4b \ - --hash=sha256:ae77e447ebc144d5a26d50055c6ddba1d6ad4a865a560ec7200b8b06bc529368 \ - --hash=sha256:ae9b3840c5bd456780e3ddf2f737ab55a79b790f6409182012718a35c6d43282 \ - --hash=sha256:b176326bcd544b5e9b17d6943f807697c0cb7351f6cfb45bf5637c95ff7e6306 \ - --hash=sha256:b7531a8ef61de2c647cdf68b3229b071e46ec326b3138b2180acb4275f470b01 \ - --hash=sha256:b80fa342ed1ea095168a3f116637bd1030d39c9ff38dc04e54ef7c521e01fc95 \ - --hash=sha256:bbb9246568f72dce29bcd433517c2be22c7791784b223a810225af3b50d1aafb \ - --hash=sha256:bc4b8e9d16e20ddfe16430c23468a8707ccad3365b06d4536142e71823f3ca29 \ - --hash=sha256:c190af81e5576b9c5fdc708f781a52ff20f8b96386c6e2e0557a78402b029f4a \ --hash=sha256:c5ba23274c61c6fef447ba6a39333297d0c247f53059dba0bca415cac511edc4 \ - --hash=sha256:c6db75b51acf277997f3adcd0ad89045d856190d13359f15ab5dda21581d9129 \ - --hash=sha256:c90471671c2cdf914e58b6af62420ea9ecd06d1554d7474d50133ff26ae88feb \ - --hash=sha256:d13ab0490128f2bb45d596f754148cd750411afc97e813e4b3a61cf278a23bb6 \ --hash=sha256:d3bc882119764ba3a119fbf2bd4f1b47bc56c1da5d42df4ed54ae1e8e66fdf8f \ --hash=sha256:d488c236ac497c46a5ac2005a952c1a0e22a07be9f10c3e735bc7d1209a34773 \ - --hash=sha256:d59ecf3bb549e491c8104fea7313f3563c7b048e01287db0a90485734a70a730 \ - --hash=sha256:dbef80defe9fb21310948a2595420b36c6d641d9bea4c991175829b2cc4bc06a \ - --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 \ - --hash=sha256:e1dd06f981eb226edf87c55d523131ade7285137fbde837c34dc9d1bf309f459 \ - --hash=sha256:e3ef8cf53dc8df49d7e28a356cf824e3623764e9833348b655cfed4524ab8a90 \ - --hash=sha256:e4121f1ce2b2b5eec4b397cc1b277686e577e658d8f5870b7eb2d726bd2300ab \ - --hash=sha256:ec46332c41add73f2b57e2f5b642f991f6b15e50e9f86285e08ffe3a512ac39f \ - --hash=sha256:f4d97071c0ba40f0cf2a93ed76e660654c399a0a04ab7d85472239460f3da84b \ - --hash=sha256:fe5d50572bc885a0a799410a717c42b1a6b50e2f45872e2b40f4f288f9bce8a2 -reportlab==4.4.4 \ + --hash=sha256:dec57f96d4def58c422d212d414efe28218d58537b5445cf0c33afb1b4768571 +reportlab==4.4.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:299b3b0534e7202bb94ed2ddcd7179b818dcda7de9d8518a57c85a58a1ebaadb \ --hash=sha256:cb2f658b7f4a15be2cc68f7203aa67faef67213edd4f2d4bdd3eb20dab75a80d -requests==2.33.0 \ +requests==2.33.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b \ --hash=sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652 -requests-oauthlib==2.0.0 \ +requests-oauthlib==2.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36 \ --hash=sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9 -rich==14.3.3 \ +rich==14.3.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b -rich-rst==1.3.2 \ +rich-rst==1.3.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4 \ --hash=sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a -rpds-py==0.27.1 \ - --hash=sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f \ - --hash=sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60 \ +rpds-py==0.27.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2 \ --hash=sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff \ - --hash=sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef \ - --hash=sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd \ - --hash=sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf \ - --hash=sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d \ - --hash=sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e \ --hash=sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8 \ - --hash=sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5 \ --hash=sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8 \ --hash=sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418 \ - --hash=sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688 \ - --hash=sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502 \ --hash=sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675 \ - --hash=sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a \ - --hash=sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734 \ --hash=sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5 \ - --hash=sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92 \ - --hash=sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274 \ - --hash=sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3 \ - --hash=sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83 \ - --hash=sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817 \ - --hash=sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48 \ - --hash=sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772 \ - --hash=sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2 \ - --hash=sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802 \ - --hash=sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec \ - --hash=sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1 \ - --hash=sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a \ - --hash=sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39 \ - --hash=sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4 \ - --hash=sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797 \ - --hash=sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228 \ - --hash=sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881 \ --hash=sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002 \ --hash=sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998 \ - --hash=sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456 \ - --hash=sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd \ - --hash=sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e \ - --hash=sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334 \ - --hash=sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90 \ - --hash=sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2 \ - --hash=sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b \ - --hash=sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33 \ - --hash=sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2 \ - --hash=sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136 \ - --hash=sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212 \ - --hash=sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0 \ - --hash=sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb \ - --hash=sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a \ - --hash=sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21 \ - --hash=sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf \ - --hash=sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594 \ - --hash=sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e \ --hash=sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7 \ - --hash=sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3 \ - --hash=sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723 \ - --hash=sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b \ - --hash=sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb \ - --hash=sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081 \ - --hash=sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7 \ - --hash=sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d \ - --hash=sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9 \ - --hash=sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444 \ - --hash=sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a \ - --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 \ - --hash=sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83 \ - --hash=sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3 \ - --hash=sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2 \ - --hash=sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a \ - --hash=sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb \ - --hash=sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec -screeninfo==0.8.1 ; platform_system != 'darwin' \ + --hash=sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0 +screeninfo==0.8.1 ; platform_machine == 'x86_64' and platform_system != 'darwin' and sys_platform == 'linux' \ --hash=sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1 \ --hash=sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c -secretstorage==3.5.0 ; sys_platform == 'linux' \ +secretstorage==3.5.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \ --hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be -six==1.17.0 \ +six==1.17.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 -sniffio==1.3.1 \ +sniffio==1.3.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc -socksio==1.0.0 \ +socksio==1.0.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3 \ --hash=sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac -sortedcontainers==2.4.0 \ +sortedcontainers==2.4.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 -soupsieve==2.8.4 \ - --hash=sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e \ - --hash=sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65 -sse-starlette==3.0.2 \ +soupsieve==2.9.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74 \ + --hash=sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823 +sse-starlette==3.0.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a \ --hash=sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a -starlette==1.3.1 \ +starlette==1.3.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0 \ --hash=sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6 -tenacity==9.1.2 \ +tenacity==9.1.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb \ --hash=sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138 -tiktoken==0.12.0 \ - --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ - --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ +tiktoken==0.12.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ - --hash=sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff \ - --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ - --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ - --hash=sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3 \ --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ - --hash=sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a \ --hash=sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0 \ --hash=sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0 \ - --hash=sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b \ - --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ - --hash=sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3 \ --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ --hash=sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b \ - --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ - --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ - --hash=sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646 \ --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ - --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ - --hash=sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697 \ - --hash=sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8 \ - --hash=sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365 \ - --hash=sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e \ --hash=sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830 \ - --hash=sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16 \ - --hash=sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88 \ - --hash=sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f \ - --hash=sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63 \ - --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ - --hash=sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71 \ - --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 \ - --hash=sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd -tokenizers==0.23.1 \ - --hash=sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4 \ - --hash=sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288 \ - --hash=sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3 \ + --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad +tokenizers==0.23.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa \ --hash=sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4 \ - --hash=sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51 \ - --hash=sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a \ - --hash=sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948 \ - --hash=sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e \ - --hash=sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9 \ - --hash=sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959 \ - --hash=sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224 \ - --hash=sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e \ - --hash=sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96 \ - --hash=sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7 \ - --hash=sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a \ - --hash=sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6 -tom-swe==1.0.3 \ + --hash=sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96 +tom-swe==1.0.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:57c97d0104e563f15bd39edaf2aa6ac4c3e9444afd437fb92458700d22c6c0f5 \ --hash=sha256:7b1172b29eb5c8fb7f1975016e7b6a238511b9ac2a7a980bd400dcb4e29773f2 -tqdm==4.67.1 \ +tqdm==4.67.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 -tree-sitter==0.25.2 \ - --hash=sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd \ - --hash=sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5 \ +tree-sitter==0.25.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358 \ - --hash=sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b \ - --hash=sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721 \ - --hash=sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0 \ - --hash=sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897 \ --hash=sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0 \ - --hash=sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8 \ --hash=sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9 \ --hash=sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614 \ - --hash=sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f \ - --hash=sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053 \ - --hash=sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87 \ - --hash=sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c \ --hash=sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae \ - --hash=sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99 \ - --hash=sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960 \ - --hash=sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab \ - --hash=sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601 \ --hash=sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac \ --hash=sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20 -tree-sitter-bash==0.25.1 \ - --hash=sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf \ +tree-sitter-bash==0.25.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:3f484c4bb8796cde7a87ca351e6116f09653edac0eb3c6d238566359dd28b117 \ - --hash=sha256:52a6802d9218f86278aa3e8b459c3abdad67eed0fde1f9f13aca5b6c634217a6 \ - --hash=sha256:59115057ec2bae319e8082ff29559861045002964c3431ccb0fc92aa4bc9bccb \ - --hash=sha256:5e76af6df46d958c7f5b6d5884c9743218e3902a00ccb493ec92728b1084430b \ --hash=sha256:a3332d71c7b7d5f78259b19d02d0ea111fcb82b72712ee4a93aaa5b226d3f0a8 \ - --hash=sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14 \ - --hash=sha256:e76c4cfb20b076552406782b7f8c2a3946835993df0a44df006de54b7030c7dc \ - --hash=sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512 -typing-extensions==4.15.0 \ + --hash=sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14 +typing-extensions==4.15.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 -typing-inspection==0.4.2 \ +typing-inspection==0.4.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 -uncalled-for==0.2.0 \ +uncalled-for==0.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f \ --hash=sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f -uritemplate==4.2.0 \ +uritemplate==4.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \ --hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 -urllib3==2.7.0 \ +urllib3==2.7.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 -uuid7==0.1.0 \ +uuid7==0.1.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61 \ --hash=sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c -uvicorn==0.52.4 \ +uvicorn==0.52.4 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86 \ --hash=sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1 -watchfiles==1.1.1 \ - --hash=sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c \ - --hash=sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510 \ - --hash=sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0 \ - --hash=sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18 \ - --hash=sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219 \ +watchfiles==1.1.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803 \ - --hash=sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94 \ - --hash=sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6 \ - --hash=sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce \ - --hash=sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099 \ - --hash=sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae \ --hash=sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43 \ - --hash=sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd \ --hash=sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374 \ - --hash=sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051 \ - --hash=sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7 \ --hash=sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77 \ - --hash=sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b \ - --hash=sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33 \ - --hash=sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42 \ - --hash=sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5 \ - --hash=sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da \ --hash=sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05 \ - --hash=sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a \ - --hash=sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101 \ - --hash=sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb \ - --hash=sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620 \ - --hash=sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6 \ - --hash=sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef \ - --hash=sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261 \ - --hash=sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02 \ - --hash=sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af \ - --hash=sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9 \ - --hash=sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21 \ - --hash=sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d \ --hash=sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c \ --hash=sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81 \ - --hash=sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9 \ --hash=sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2 \ - --hash=sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc \ - --hash=sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404 \ --hash=sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01 \ - --hash=sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18 \ - --hash=sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04 \ - --hash=sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3 \ - --hash=sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610 \ - --hash=sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0 \ - --hash=sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150 \ - --hash=sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5 \ - --hash=sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a \ - --hash=sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b \ --hash=sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d \ - --hash=sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70 \ - --hash=sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e \ - --hash=sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5 \ - --hash=sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb \ - --hash=sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44 \ - --hash=sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428 \ - --hash=sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa \ --hash=sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf -wcwidth==0.2.14 \ +wcwidth==0.2.14 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605 \ --hash=sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1 -websockets==15.0.1 \ - --hash=sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2 \ - --hash=sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5 \ +websockets==15.0.1 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8 \ - --hash=sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375 \ --hash=sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597 \ - --hash=sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f \ - --hash=sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3 \ --hash=sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4 \ - --hash=sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665 \ - --hash=sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22 \ - --hash=sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675 \ - --hash=sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4 \ --hash=sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65 \ - --hash=sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151 \ - --hash=sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d \ --hash=sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee \ - --hash=sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa \ - --hash=sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9 \ - --hash=sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe \ - --hash=sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561 \ - --hash=sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215 \ - --hash=sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931 \ - --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f \ - --hash=sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7 -wrapt==1.17.3 \ + --hash=sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f +wrapt==1.17.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828 \ - --hash=sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396 \ - --hash=sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d \ - --hash=sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7 \ - --hash=sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb \ - --hash=sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f \ - --hash=sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067 \ --hash=sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b \ - --hash=sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7 \ - --hash=sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9 \ - --hash=sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977 \ - --hash=sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa \ - --hash=sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe \ - --hash=sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58 \ - --hash=sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8 \ - --hash=sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77 \ - --hash=sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454 \ --hash=sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e \ - --hash=sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c \ - --hash=sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6 \ - --hash=sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd \ --hash=sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277 \ --hash=sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22 \ - --hash=sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116 \ - --hash=sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16 \ --hash=sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc \ --hash=sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804 \ - --hash=sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba \ - --hash=sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0 \ - --hash=sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0 \ - --hash=sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39 \ --hash=sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4 \ - --hash=sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa \ --hash=sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050 \ - --hash=sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6 \ - --hash=sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235 \ - --hash=sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056 \ - --hash=sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c \ - --hash=sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a \ - --hash=sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6 \ - --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 \ - --hash=sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10 -yarl==1.22.0 \ - --hash=sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a \ - --hash=sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da \ - --hash=sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093 \ - --hash=sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79 \ - --hash=sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683 \ - --hash=sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2 \ + --hash=sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0 +yarl==1.22.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff \ - --hash=sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02 \ - --hash=sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03 \ - --hash=sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c \ - --hash=sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124 \ - --hash=sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c \ --hash=sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da \ - --hash=sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2 \ - --hash=sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0 \ - --hash=sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d \ - --hash=sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53 \ - --hash=sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138 \ - --hash=sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4 \ - --hash=sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d \ - --hash=sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2 \ - --hash=sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa \ - --hash=sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f \ - --hash=sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1 \ - --hash=sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d \ - --hash=sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694 \ - --hash=sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3 \ - --hash=sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a \ --hash=sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a \ - --hash=sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b \ - --hash=sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5 \ - --hash=sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f \ - --hash=sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df \ - --hash=sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b \ - --hash=sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b \ - --hash=sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2 \ - --hash=sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708 \ - --hash=sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8 \ - --hash=sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10 \ - --hash=sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b \ - --hash=sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e \ - --hash=sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33 \ - --hash=sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590 \ - --hash=sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c \ - --hash=sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53 \ - --hash=sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74 \ --hash=sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f \ - --hash=sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1 \ - --hash=sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27 \ --hash=sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520 \ - --hash=sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273 \ - --hash=sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601 \ - --hash=sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7 \ - --hash=sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb \ --hash=sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784 \ --hash=sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71 \ --hash=sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b \ - --hash=sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a \ --hash=sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c \ - --hash=sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face \ - --hash=sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d \ - --hash=sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e \ - --hash=sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9 \ - --hash=sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95 \ - --hash=sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf \ - --hash=sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca \ --hash=sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62 \ - --hash=sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df \ --hash=sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67 \ - --hash=sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f \ - --hash=sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529 \ - --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 \ - --hash=sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a \ - --hash=sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74 \ - --hash=sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d \ - --hash=sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b \ - --hash=sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2 \ - --hash=sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e \ - --hash=sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8 \ - --hash=sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82 \ - --hash=sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd \ - --hash=sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249 -zipp==3.23.0 \ + --hash=sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486 +zipp==3.23.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ --hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \ --hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166 openhands-sdk @ https://files.pythonhosted.org/packages/e9/ac/d27c5becc3b59b8cc46a1614ac478cc94eb73e626ab15831906fcdaafb29/openhands_sdk-1.49.6-py3-none-any.whl \ diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index 4051c95bc..84407828b 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -56,6 +56,9 @@ and the SDK event/LLM/MCP implementations. README.md contains immutable links. SDK wheels were downloaded as source artifacts and their bytes verified against PyPI's SHA256; they were not installed or imported. The dependency requirements were exported with the installed uv 0.12.17 from the unchanged upstream uv.lock. +Correction, 2026-09-27: the runtime lock security relock at the end of this file +restricts the lock to linux x86_64 and upgrades four packages, so the lock is no +longer that unchanged export. FastMCP 3.2.0 at 665514e19a78543709be85b4261153bbe98e882f confirms the `server_name_tool_name` namespace. QMD v2.8.3 source confirms QMD_CONFIG_DIR, @@ -219,3 +222,130 @@ The evidence remains offline adapter/synthetic evidence. Full upstream acceptanc independent trace qualification, rootless filtering, installed imports and model behavior are not inferred from these tests. General anti-pattern promotion and evidence hash registration remain coordinator-owned. + +## Runtime lock security relock (2026-09-27) + +requirements.lock is now a **local integration lock for linux/amd64 only**: +upstream v1.49.6's uv.lock, resolved for linux x86_64 with uv's `environments` +setting, plus four security upgrades. It is no longer the unchanged upstream +export. OSV-Scanner 2.6.0 reported 14 advisories in five packages, both in CI run +36351247754 and locally with the same pinned binary on the previous lock. No +upstream fix exists yet. v1.49.6 (published 2026-09-25 at fcc102a) is still the +latest release, and main at 3311ba9eec5044f40ab5d0b3d7eddc9f7e1e2d14 pins the same +five flagged versions and keeps the cryptography constraint described below. +cryptography needs no upgrade: the restriction leaves only upstream's linux +50.0.0, which fixes all three of its advisories. Returned outputs are retained in +[evidence/relock-2026-09-27.txt](evidence/relock-2026-09-27.txt); its section F +holds the final lock. + +The installed uv 0.12.17 reproduced the previous lock byte for byte in the +unchanged upstream workspace, whose uv.lock matches the pinned SHA256: + + uv export --frozen --format requirements.txt --no-header --no-annotate \ + --package openhands-tools --no-emit-workspace + +That output equals the lock's first 2,856 lines; adding `--no-dev` gives the same +bytes. The last four lines are appended from pins.json `wheels`, in file order, +as ` @ \` followed by ` --hash=sha256:`. Together they give +the previous SHA256, c4ca55604ea0bc85e74ccaa8b2c5bf9fa7b17cec35b830eda653025cb90ebd56. + +In a scratch copy of that workspace, one line was added to the existing +`[tool.uv]` table of the root pyproject.toml; nothing else in the workspace +changed: + + environments = ["sys_platform == 'linux' and platform_machine == 'x86_64'"] + +The pinned linux/amd64 image (pins.json `image.platform`) is the recipe's only +install target. The setting and lockfile preferences follow +[astral-sh/uv@0.12.17 docs/concepts/resolution.md:142-173 and 303-309](https://github.com/astral-sh/uv/blob/0.12.17/docs/concepts/resolution.md#L142-L173). +The relock uses the existing uv.lock as preferences. The workspace's relative +`exclude-newer = "7 days"` would make an unpinned selection depend on the run +time, so the upgrades are version-pinned: + + uv lock --upgrade-package anyio==4.14.2 --upgrade-package click==8.5.0 \ + --upgrade-package pypdf==6.19.0 --upgrade-package soupsieve==2.9.2 + +The coordinator reproduced the relock builder's uv.lock from a fresh copy of the +unchanged workspace with that line added: SHA256 +30e608b1fdb091db1609e8261a787020df12e0350911079cc2177c772a9ccffd, byte for byte, +and `uv lock --check` exits 0. uv printed the four updates and +`Updated cryptography v48.0.1, v50.0.0 -> v50.0.0`, which is the darwin x86_64 +fork leaving the resolved environment. It removed cython, macholib, pefile, +pyobjc-framework-pubsub, pywin32 and pywin32-ctypes from uv.lock. The export and +wheel append above then give SHA256 +02d0a7f058d08d28d0fb3f7344ed9b042bf5a317607454faf4bbb210af7fa394: + +| Package | Previous | New | Advisories (fixed in) | +| --- | --- | --- | --- | +| anyio | 4.11.0 | 4.14.2 | GHSA-82r6-8w77-94w6, GHSA-5p39-cfhj-2xmp (4.14.2) | +| click | 8.1.8 | 8.5.0 | PYSEC-2026-2132 (8.3.3) | +| cryptography | 48.0.1 for darwin x86_64, 50.0.0 elsewhere | 50.0.0 (the darwin line leaves the lock) | PYSEC-2026-3552 (50.0.0); PYSEC-2026-3553, PYSEC-2026-3554 (49.0.0) | +| pypdf | 6.14.2 | 6.19.0 | PYSEC-2026-3655, PYSEC-2026-3656, PYSEC-2026-3912 (6.15.0); PYSEC-2026-3913 (6.16.0); PYSEC-2026-3910, PYSEC-2026-3911 (6.16.1) | +| soupsieve | 2.8.4 | 2.9.2 | GHSA-gjv8-xp57-g29c, GHSA-j934-xhv5-fg8f (2.9.0) | + +A scratch classification checked every other change. It mapped each hash to its +file through the upstream uv.lock and evaluated markers with packaging 26.3 for +linux x86_64 under CPython and PyPy 3.12 to 3.15: + +- No other package changed version, and none was added. +- Six entries were removed, and none applies on linux x86_64: colorama 0.4.6, + pywin32 311 and pywin32-ctypes 0.2.3 (win32), cryptography 48.0.1 (darwin + x86_64), pyobjc-framework-pubsub 11.1 (darwin) and cython 3.1.4 + (`platform_system != 'darwin' and sys_platform == 'darwin'`). +- The export adds `platform_machine == 'x86_64' and sys_platform == 'linux'` to + every exported marker, including those of the 330 same-version entries; none + of them changes linux x86_64 applicability. The 159 pyobjc entries remain: uv + keeps their upstream `platform_system == 'darwin'` term beside the Linux + marker, and the combined markers never hold there. +- 1,694 hash lines were removed from 129 same-version entries. All are wheels for + other platforms: macOS, Windows, iOS, Android, and Linux on aarch64, armv7l, + i686, ppc64le, riscv64 and s390x. No sdist or linux x86_64 wheel hash was + removed, and none was added. + +anyio stops at 4.14.2 because 4.15.x requires typing_extensions>=4.16.0 below +Python 3.15, and typing-extensions 4.15.0 stays locked. A dry run that also +released typing-extensions selected anyio 4.15.1. soupsieve 2.10 was uploaded on +2026-09-24, inside the seven-day window. + +The restriction replaces a constraint override. cryptography 48.0.1 existed only +on the darwin x86_64 fork, which the image never installs, yet OSV-Scanner's +`--no-resolve` requirements.txt scan still reported that marker-gated line. In +the universal resolution, `uv lock --upgrade-package cryptography` printed +`Updated cryptography v48.0.1, v50.0.0 -> v48.0.1` under pyproject.toml:18, +`"cryptography<49; sys_platform == 'darwin' and platform_machine == 'x86_64'"`. +The fork collapsed, which would downgrade Linux. That line stays unchanged and +now falls outside the resolved environment. Deleting it was tried only in +scratch and declined. An intermediate universal relock of the other four +packages (SHA256 da0d550f1e03d42ef2d1f4fe31a608eb8abdb796ecb9354d30457875f2172d71) +left the three cryptography advisories open; it is superseded, and its outputs +stay in the evidence file. So is a five-package variant that also moved +cryptography to 50.0.1 (uv.lock b3e8a6f4..., requirements.lock f215ac9f...; +evidence sections C to E): the linux 50.0.0 already carries every fix. + +The consistency check used a Python 3.13.15 scratch venv and the +install-container.sh sequence, with UV_NO_CONFIG=1: + +- build-requirements.lock with `--require-hashes --no-deps --only-binary :all:` + exited 0; +- the runtime lock with `--require-hashes --no-deps --no-build-isolation` exited + 0 (`Installed 175 packages`: anyio 4.14.2, click 8.5.0, cryptography 50.0.0, + pypdf 6.19.0, soupsieve 2.9.2); func-timeout 4.3.5 is its only sdist; +- `uv pip check` exited 0 (`All installed packages are compatible`). + +A runtime-lock install with `--only-binary=:all:` exits 1, because PyPI has only +sdists for func-timeout 4.3.x, as recorded above. Without UV_NO_CONFIG=1, step 1 +run inside the workspace exits 2: the workspace's `constraint-dependencies` +(`starlette>=0.49.1`) is not a hashed `==` pin. The script's import check, run +offline under bwrap (`--unshare-net`), printed `SDK/tools 1.49.6 imports passed; +no model request`. These are host scratch checks. The image installation, model +behavior and runtime paths through the upgraded packages remain unmeasured. + +Overturn conditions: return to the unchanged upstream export once an SDK release +pins fixed versions; add linux aarch64 to `environments` if the recipe adds an +arm64 image. + +| Proven issue | Correction | +| --- | --- | +| `UV_NO_CONFIG=1` also stops `uv lock` from reading the workspace pyproject.toml. Installed help: "Avoid discovering configuration files (`pyproject.toml`, `uv.toml`)". The first relock printed `Resolving despite existing lockfile due to removal of global exclude newer` and chose soupsieve 2.10 from inside the seven-day window. | Relock without it so `[tool.uv]` applies; `uv lock --check` on the unchanged workspace exits 0. Keep UV_NO_CONFIG=1 only for the `uv pip install` steps, as install-container.sh does. | +| In the universal resolution, `--upgrade-package cryptography` collapsed the marker-split fork to the constrained 48.0.1. | Read every `Updated` and `Removed` line before accepting a relock. Resolve only the recipe's install environment with `environments` instead of deleting an upstream constraint, and classify every removed or re-marked line. | +| The coordinator's decision to keep cryptography at 50.0.0 reached the relock builder after it had relocked five packages from an earlier instruction (evidence section C). | A decision that changes a brief goes out as one message naming what it replaces. The final lock was reproduced from the unchanged workspace before commit (evidence section F). | From 708d2d1cd3c6bda3df37a16dc973b3b714d24bca Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 12:33:03 -0400 Subject: [PATCH 06/22] OpenHands worker: phase-1 takeover repairs for #425 (F16-F20, session key, port) - F16: runs that end stuck or at MaxIterationsReached now export and officially grade the partial patch and exit 1, never 0; other native errors stay exit 3. The termination comes from the native status plus the newest ConversationErrorEvent (SDK@fcc102a state.py:48-79, local_conversation.py:727-733,753-755,2021-2043,2339-2360, event_router.py:68-139, event_service.py:456-460). - F17: receipts no longer read /run-output or the server state directory; trace, skill, MCP and version fields are not_collected with the reason (receipt schema 4). - F18: README scope corrected (the relock covers only the recipe venv) and a grype 0.119.0 digest-scan receipt added for the pinned image. - F19: recorded as overturned for the reference host; documentation only. - F20: removed the unreferenced round-1 files. - Preflight requires the OH_SESSION_API_KEYS_0 name in OPENHANDS_SERVER_ENV, checking the name only (docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124). - --port is validated to 3730..3799 as in PR #428 host.py:44-45; default 3730, resolver 3740; stored in status.json and revalidated by dispatch. manifests/evidence.json is intentionally not updated; the coordinator re-registers hashes after rebasing. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 132 ++++-- .../openhands/config/skills.lock.json | 173 ------- .../runtime-workers/openhands/dispatch.py | 88 ++-- .../openhands/e2e/fixture-repo/README.md | 14 - .../openhands/e2e/fixture-repo/range_utils.py | 5 - .../e2e/fixture-repo/tests/test_ranges.py | 18 - .../runtime-workers/openhands/e2e/frozen.json | 14 - .../runtime-workers/openhands/e2e/task.txt | 10 - .../agent-server-image-grype-20260928.json | 436 ++++++++++++++++++ .../openhands/evidence/phase1-commands.json | 164 +++++++ blueprints/runtime-workers/openhands/host.py | 82 +++- .../runtime-workers/openhands/receipt.py | 66 ++- .../runtime-workers/openhands/research.md | 37 ++ tests/test_runtime_worker_openhands.py | 300 +++++++++++- 14 files changed, 1203 insertions(+), 336 deletions(-) delete mode 100644 blueprints/runtime-workers/openhands/config/skills.lock.json delete mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md delete mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py delete mode 100644 blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py delete mode 100644 blueprints/runtime-workers/openhands/e2e/frozen.json delete mode 100644 blueprints/runtime-workers/openhands/e2e/task.txt create mode 100644 blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json create mode 100644 blueprints/runtime-workers/openhands/evidence/phase1-commands.json diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index bc5460614..326ff5795 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -5,11 +5,19 @@ frozen SWE-bench task with two explicit gateway arms. SDK/server **1.49.6** owns the agent loop. **SWE-bench 4.1.0** alone supplies the task verdict through the pinned OpenHands/benchmarks converter and official Docker harness. This repair ran offline contract tests only. No installation, container, model, gateway -request or official grading run was performed. +request or official grading run was performed. The 2026-09-28 phase-1 repair +added offline fixes and pulled the pinned image only to scan its digest. It +started no container and made no model or gateway request +([research.md](research.md#takeover-phase-1-corrections-2026-09-28)). **Host acceptance is still open.** A verified firewall is required before model -execution. SDK trace, skill and version files remain worker-reported because the -terminal shares the server's UID. They cannot establish independent acceptance; +execution. The model's terminal runs under the server's UID, so it can write +both `/run-output` and the server's persisted event store. Receipts therefore +record trace, skill, MCP and version fields as `not_collected`, with the reason, +instead of reading them from either place (SDK@fcc102a +`openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:157-170`, +`openhands-agent-server/openhands/agent_server/event_service.py:420-431`, +`openhands-sdk/openhands/sdk/conversation/event_store.py:144-169,320-362`). `evidence_complete` deliberately stays false until an independent observer is qualified. A resolved official task therefore currently exits 2, not 0. This is an explicit acceptance gap, not a failed model task. @@ -37,10 +45,33 @@ The source target at lines 570-576 has a different ENTRYPOINT. Do not assume the image contains the source-target venv. Request generation uses the separately installed SDK/tools wheels in the same image, with network disabled. [pins.json](pins.json) preserves the image, wheel -and runtime lock hashes. No framework version changed in round 3. The runtime -lock is a linux/amd64 security relock of the upstream export with four -dependency upgrades, reproduced from the unchanged upstream workspace -([research.md](research.md#runtime-lock-security-relock-2026-09-27)). +and runtime lock hashes. No framework version changed in round 3. + +**The runtime-lock relock covers only the recipe venv.** requirements.lock is a +linux/amd64 security relock of the upstream export with four dependency +upgrades, reproduced from the unchanged upstream workspace +([research.md](research.md#runtime-lock-security-relock-2026-09-27)). It builds +the venv under the install prefix. That venv serializes the request, runs the +`mcp_guard.py` hook and comes first on the server container's PATH. The server +process itself is the image's PyInstaller binary +`/usr/local/bin/openhands-agent-server`. Upstream builds it from its unchanged +uv.lock with `uv sync --frozen` (SDK@fcc102a +`openhands-agent-server/openhands/agent_server/docker/Dockerfile:129,139,146-158,583-590`). +That lock, whose SHA256 is pins.json `uv_lock`, still pins anyio 4.11.0, click 8.1.8, +pypdf 6.14.2 and soupsieve 2.8.4, the four versions the relock replaces. + +[agent-server-image-grype-20260928.json](evidence/agent-server-image-grype-20260928.json) +records a grype 0.119.0 scan of the pinned linux/amd64 digest, with a database +built 2026-09-28T06:42:30Z. It found 1,464 unique matches in 164 package +versions: Critical 32, High 168, Medium 208, Low 57, Negligible 780 and +Unknown 219. The flagged packages are Debian packages, openvscode-server npm +modules and Node, Docker/containerd Go binaries and the base Python interpreter. +The cataloger does not unpack the PyInstaller archive, so the scan neither +confirms nor excludes the four lock versions inside the binary. The receipt +lists grype's advisories for those exact versions as lock evidence. On this +host's containerd image store, `docker image inspect` reports the index digest +as the image ID, so the receipt verifies the platform manifest and config +digests instead. [install.sh](install.sh) installs the worker through the existing hash-required wheel/runtime locks and preinstalled hashed build tools. The install container @@ -129,6 +160,14 @@ Set these private inputs outside all worktrees: - `OPENHANDS_GRADER_IMAGE_FILE`, `OPENHANDS_GRADER_IMAGE_SHA256`: frozen image pin. - `OPENHANDS_SERVER_ENV`: mode-0600 file containing only this attempt's `OH_SESSION_API_KEYS_0` setting. No shared service/provider secrets. + Preflight refuses the attempt unless a `OH_SESSION_API_KEYS_0=` line names + that variable. It parses Docker's env-file syntax for the name only and never + extracts, prints or logs the value (docker/cli@v29.8.1 + `pkg/kvfile/kvfile.go:92-124`). A bare name, which Docker would fill from the + caller's environment, is refused. The server listens on every container + interface only when a session key is set (SDK@fcc102a + `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, + `config.py:24`). - `OPENHANDS_HEADERS`: mode-0600 file containing the corresponding `X-Session-API-Key` header. Commands use curl's `-H @file`; no inline key. @@ -141,6 +180,13 @@ rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" prepare \ --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" ``` +`--port` selects the published loopback port. It must be an integer in +3730..3799, the range PR #428's crawl4ai recipe enforces (`host.py:44-45` at +3ad8ba2), and defaults to 3730 for this SWE-bench worker. The resolver uses +3740 (`--port 3740`). Preparation records the port in the host-owned +`status.json`; `dispatch.py` has no port flag, reads it from there and checks +the range again before any request. + The workflow child runs exactly these three Bash commands after preparation: ```sh @@ -161,9 +207,12 @@ $HOME/.local/state/native-agent-stack/runtime-workers/openhands/runs//`: POST `/api/conversations`, GET `/api/conversations/`, then GET -`/api/conversations//agent_final_response`. Start is asynchronous. The +`/api/conversations//agent_final_response`. After an `error` status it +also calls GET `/api/conversations//events/search` with +`kind=openhands.sdk.event.conversation_error.ConversationErrorEvent`, +`sort_order=TIMESTAMP_DESC` and `limit=1`. Start is asynchronous. The native SDK constructs `StartConversationRequest` and calls `model_dump(exclude_defaults=True, mode="json", context={"expose_secrets": True})`; only the fixed keyless gateway placeholder is exposed. It includes workspace, @@ -181,12 +230,27 @@ removal, then exports the candidate patch and invokes official grading. A native final message alone is never a task verdict. Sources: SDK@fcc102a `conversation_router.py:304-321`; SWE-bench@v4.1.0 `reporting.py:127-157`. +An agent-limit end still grades its partial patch. The SDK ends a detected loop +with status `stuck`. An exhausted iteration budget ends with status `error` and +a `ConversationErrorEvent` whose source is `environment` and code is +`MaxIterationsReached` (SDK@fcc102a `openhands-sdk/openhands/sdk/conversation/state.py:48-79`, +`conversation/impl/local_conversation.py:727-733,753-755,2021-2043,2339-2360`, +`event/conversation_error.py:11-42`). For `error`, result reads the newest error +event through the native search route while the server still runs +(`openhands-agent-server/openhands/agent_server/event_router.py:68-139`, +`event_service.py:456-460`, `models.py:95-99`). Only exactly that kind, source +and code counts as `max_iterations_reached`; any other error stays ungraded and +exits 3. The receipt records `agent_termination`, and an agent-limit end exits 1 +even when its partial patch resolves the task. The event store is writable by +the model's UID, so this server-reported value only chooses between exit 1 and 3 +and can never produce a pass. + | Final exit code | Meaning | | --- | --- | | 0 | Official task resolved and all required evidence complete | -| 1 | Official unresolved or empty-patch verdict | -| 2 | Incomplete evidence, including a resolved task with untrusted trace evidence | -| 3 | Setup/infrastructure failure, deadline, grading error/incomplete bucket, or transport failure | +| 1 | Official unresolved or empty-patch verdict, or an officially graded partial patch after `stuck` or `max_iterations_reached` | +| 2 | Incomplete evidence, including a resolved task whose trace evidence is `not_collected` | +| 3 | Setup/infrastructure failure, other native error, deadline, grading error/incomplete bucket, or transport failure | Preparation/start/wait exit 0 means that operation succeeded, not that the task passed. Duplicate starts do not resubmit a native POST. Retrying a collected @@ -195,8 +259,9 @@ serializes dispatches from this recipe. It does not exclude unrelated gateway clients. Interrupted attempts retain their files; cleanup removes named owned containers only, never uses prune, and records whether removal was confirmed. The coordinator must reclaim a stale reservation only after confirming its -recorded container is gone. Port 3730 must be available; another recipe using -that port must be stopped before this one is prepared. +recorded container is gone. The selected port must be available. PR #428's +crawl4ai recipe defaults to 3730, 3731, 3732 and 3734 (its README at 3ad8ba2); +stop it or choose another port in the range before preparing this recipe. Invocation counts use existing pipelines: on Claude, child usage and OTel Bash events whose command contains `dispatch.py`, the `start` subcommand and @@ -306,19 +371,32 @@ For engines-on only the endpoint changes to port 20129. This schema is an operator evidence gate, not a firewall implementation or proof by itself. Keep the actual rule dump and probe outputs privately; reject access to the other arm, ai-memory, Qdrant, embedder and other worker listeners before any model run. -Docker was not found on this repair sandbox's PATH, so rootless rule placement, -packet behavior and post-restart policy persistence remain unverified. This is -a host setup gate, not an instruction to accept loopback exposure. Official -grader containers retain upstream options; the coordinator must separately -qualify their host-service isolation without misrepresenting altered test -conditions as an unchanged environment. - -Every authoritative status/window/verdict file is outside model mounts. Files -read from worker output use bounded regular-file reads with O_NOFOLLOW on every -path component, and are labelled worker-reported. The remaining shared-UID -trace limitation is explicit at the top of this document; counterfeit events -cannot cause `evidence_complete=true`. This round does not invent an isolation -mechanism unsupported by the pin. + +Host loopback is already reachable from containers on the reference host, so +nothing needs to be switched on for the gateway. Its rootless `docker.service` +sets `DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false`, and the running +rootlesskit has no `--disable-host-loopback` flag (read-only checks, +2026-09-28). The earlier finding F19, that loopback access still had to be +enabled, is **overturned** for that host. The upstream default does disable it +(moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`, identical to +the installed 29.8.1 script), so a new host must set the variable to `false` +and repeat both checks. With that setting, a model container can address +every host loopback listener through 10.0.2.2 unless the egress policy above +blocks it; the missing piece is that limit, not loopback access. No packet probe +was run in this repair. Rule placement in the rootless network namespace, packet +behavior and post-restart policy persistence remain unverified. Official grader +containers retain upstream +options; the coordinator must separately qualify their host-service isolation +without misrepresenting altered test conditions as an unchanged environment. + +Every authoritative status/window/verdict file is outside model mounts. +Receipt creation reads nothing from the worker output or the server state +directory; fields that would need them are `not_collected`. The only file the +host reads back from worker output is the serialized request, copied with a +bounded regular-file read (O_NOFOLLOW on every path component) before any model +runs. The server-reported agent-limit classification can never produce a pass, +and nothing can cause `evidence_complete=true`. This round does not invent an +isolation mechanism unsupported by the pin. ## Official grading and remaining host gates diff --git a/blueprints/runtime-workers/openhands/config/skills.lock.json b/blueprints/runtime-workers/openhands/config/skills.lock.json deleted file mode 100644 index b25d4efdd..000000000 --- a/blueprints/runtime-workers/openhands/config/skills.lock.json +++ /dev/null @@ -1,173 +0,0 @@ -{ - "source": "adoption/skills/manifest.json", - "skills": [ - { - "name": "typesafe-ai", - "ref": "65a39f393687675ce170e6094757de20370365b9", - "url": "https://github.com/typesafe-ai/skills/tree/65a39f393687675ce170e6094757de20370365b9/skills/typesafe-ai", - "skill_md_sha256": "71ea90d7906c6554c4f4c460ef7361b2d26f59116ccdae986dc6d997b9389f52" - }, - { - "name": "gh-fix-ci", - "ref": "49f948faa9258a0c61caceaf225e179651397431", - "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/gh-fix-ci", - "skill_md_sha256": "7b326b4a2f0f5f85122144628ec02077e48841e0e0e82efce88b3415bcfb7c26" - }, - { - "name": "security-best-practices", - "ref": "49f948faa9258a0c61caceaf225e179651397431", - "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/security-best-practices", - "skill_md_sha256": "7b3dae1ffc5434d890f3c65c8f552af52d0307fab3b35dec13013c9ca3844c4f" - }, - { - "name": "iterative-retrieval", - "ref": "2b6e839771e53096d8451a213d40dc64ec8acac0", - "url": "https://github.com/affaan-m/ECC/tree/2b6e839771e53096d8451a213d40dc64ec8acac0/skills/iterative-retrieval", - "skill_md_sha256": "b453b16d3e36174b8487c05ab807d0321a66c2d058812b2ae129d82824183725" - }, - { - "name": "search-first", - "ref": "2b6e839771e53096d8451a213d40dc64ec8acac0", - "url": "https://github.com/affaan-m/ECC/tree/2b6e839771e53096d8451a213d40dc64ec8acac0/skills/search-first", - "skill_md_sha256": "d66d744228b9cf860acf35f8a90cbbe8195e61367860da9d3632f9fc65706733" - }, - { - "name": "diagnosing-bugs", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/diagnosing-bugs", - "skill_md_sha256": "77f3cf31bc99b2f49af943222526531fcc9fc41d047626d3640e875e85af3e84" - }, - { - "name": "tdd", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/tdd", - "skill_md_sha256": "cb01f66bebfaa25fa1f88e6b7e769cd9fd9f35b1120b8563749820738814c927" - }, - { - "name": "codebase-design", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/codebase-design", - "skill_md_sha256": "2c20617f87ec8af6a434859f381b2f061a69b530444e74eb39e78bb016a6d1e2" - }, - { - "name": "resolving-merge-conflicts", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/resolving-merge-conflicts", - "skill_md_sha256": "9d8114f8ef0b31f535a265fc05c364bd8cf2e2895a830040e06c22acb11f54b0" - }, - { - "name": "grill-me", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/productivity/grill-me", - "skill_md_sha256": "caaf8b8de1684f96e26b28f3c29189db5c89cce4b73e1c93d86164f66ef88637" - }, - { - "name": "improve-codebase-architecture", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/engineering/improve-codebase-architecture", - "skill_md_sha256": "d1ac25511a936ff4250a48dbcefda363837d6bb9321b3cba73df99fa37270a75" - }, - { - "name": "verification-before-completion", - "ref": "8ca22dba9a94f28898bbce59f2537ff4d87c747d", - "url": "https://github.com/obra/superpowers/tree/8ca22dba9a94f28898bbce59f2537ff4d87c747d/skills/verification-before-completion", - "skill_md_sha256": "2befe7fc55bcadaa3d97dd9e8efeb633d2561c0ebe74c5a8b17c4d9e7e4520b3" - }, - { - "name": "security-threat-model", - "ref": "49f948faa9258a0c61caceaf225e179651397431", - "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/security-threat-model", - "skill_md_sha256": "1283c0dd62a8104d9edda4583569b5d8510b4ddaa45120687c999250fd96bad2" - }, - { - "name": "gh-address-comments", - "ref": "49f948faa9258a0c61caceaf225e179651397431", - "url": "https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated/gh-address-comments", - "skill_md_sha256": "77389eefd3fb6584210668ca8e43f4b8de87e7722ddd953474bc9e24cdfaaedd" - }, - { - "name": "semgrep", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/semgrep", - "skill_md_sha256": "04df36a7685632ee39271aa432a4d32e1203b326e4a8091e6f5c0eb191ef90f1" - }, - { - "name": "codeql", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/codeql", - "skill_md_sha256": "1a6d919ff80f83b262daf650552af6eb6d2c32b19d0c529b70efe0c116b675cf" - }, - { - "name": "supply-chain-risk-auditor", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/supply-chain-risk-auditor/skills/supply-chain-risk-auditor", - "skill_md_sha256": "a1254b7f8d3a1731a9a7e1f9cd6c67b59cf84ed6ef7c7fb455db9a14c528ceb4" - }, - { - "name": "agentic-actions-auditor", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/agentic-actions-auditor/skills/agentic-actions-auditor", - "skill_md_sha256": "b2bdb88d559bc37e6c2c8314e28e7a2eb47e7cdd9da51e77da347d3517326dc0" - }, - { - "name": "property-based-testing", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/property-based-testing/skills/property-based-testing", - "skill_md_sha256": "9448cacb65c6237a7c22dc59602667177ecae405524079ae9ff92240590b995a" - }, - { - "name": "modern-python", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/modern-python/skills/modern-python", - "skill_md_sha256": "ef774b925eb1d1fc12e4f5ddc3f5ad925ecd795315827447228c70cc44c6170c" - }, - { - "name": "sarif-parsing", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/static-analysis/skills/sarif-parsing", - "skill_md_sha256": "cc5f008f40ffb8a1ae03ebd32d7234de43e828293955a0bc251b76ba249554fa" - }, - { - "name": "fp-check", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/fp-check/skills/fp-check", - "skill_md_sha256": "129223b79b8cb1e7c289c90cbe4ba288d9b210e318a0d1464f319e30329481b3" - }, - { - "name": "mcp-builder", - "ref": "33375500bcea98d610eb30ce10ac4e59b89c390d", - "url": "https://github.com/anthropics/skills/tree/33375500bcea98d610eb30ce10ac4e59b89c390d/skills/mcp-builder", - "skill_md_sha256": "0f4592dcb53cf2b5d6b7febee6b4152018b565551a1c29e3c612f57b218ab295" - }, - { - "name": "frontend-design", - "ref": "33375500bcea98d610eb30ce10ac4e59b89c390d", - "url": "https://github.com/anthropics/skills/tree/33375500bcea98d610eb30ce10ac4e59b89c390d/skills/frontend-design", - "skill_md_sha256": "d91970639e9f5c37682ac7ab60094d35f1c7c1f38d731bd56396563aee10c1d3" - }, - { - "name": "agent-browser", - "ref": "d01253d9db28d75080e36da3c1c31ef89454731e", - "url": "https://github.com/vercel-labs/agent-browser/tree/d01253d9db28d75080e36da3c1c31ef89454731e/skills/agent-browser", - "skill_md_sha256": "3a7520e64de2dfaa516b6721c001e84e06e3c06a187b82af029ae721540ebfac" - }, - { - "name": "find-skills", - "ref": "7407f3893ad4dceab546ac002c3ef806e4000c73", - "url": "https://github.com/vercel-labs/skills/tree/7407f3893ad4dceab546ac002c3ef806e4000c73/skills/find-skills", - "skill_md_sha256": "c00eeea0e13e74fe4a9d84ba0a8542205a1b736d65f13134fe1a6647eb14976f" - }, - { - "name": "variant-analysis", - "ref": "0cc1c73a5e96749ab32d7ea5e14892fafa6972ae", - "url": "https://github.com/trailofbits/skills/tree/0cc1c73a5e96749ab32d7ea5e14892fafa6972ae/plugins/variant-analysis/skills/variant-analysis", - "skill_md_sha256": "7b6b78e499bcb88d05c68254ffc91ebc28edabf4e9808adcc40e2628ebb9407e" - }, - { - "name": "writing-for-agents", - "ref": "c55ee46073ed923f86ce59a5eb3b6d895095d1b7", - "url": "https://github.com/mattpocock/skills/tree/c55ee46073ed923f86ce59a5eb3b6d895095d1b7/skills/productivity/writing-for-agents", - "skill_md_sha256": "551adca942227b44192edba88acd4e8db911f0121ce58ad16944ccf6a896a74a" - } - ] -} diff --git a/blueprints/runtime-workers/openhands/dispatch.py b/blueprints/runtime-workers/openhands/dispatch.py index 2816b561f..a8af1ef5c 100644 --- a/blueprints/runtime-workers/openhands/dispatch.py +++ b/blueprints/runtime-workers/openhands/dispatch.py @@ -11,28 +11,27 @@ from pathlib import Path import re import signal -import stat import subprocess import tempfile import time import uuid -from recipe import HERE, arm_config, read_json -from host import DOCKER, cleanup_container, docker_args, grade, logged_command, result_exit, utc_now, write_json +from recipe import arm_config, read_json +from host import (AGENT_LIMITS, DEFAULT_PORT, DOCKER, cleanup_container, docker_args, grade, logged_command, + owned_port, private_file, result_exit, utc_now, write_json) from receipt import create_receipt, read_bounded -SERVER_URL = "http://127.0.0.1:3730" TERMINAL = {"finished", "error", "stuck"} +# Latest ConversationErrorEvent only. SDK@fcc102a event_router.py:68-139 serves +# the page; event_service.py:456-460 matches kind on the module-qualified class +# name; models.py:95-99 defines TIMESTAMP_DESC. +ERROR_EVENT_SEARCH = ("/events/search?kind=openhands.sdk.event.conversation_error.ConversationErrorEvent" + "&sort_order=TIMESTAMP_DESC&limit=1") -def private_file(path): - path = Path(path).absolute() - info = path.stat() - if (path.resolve() != path or not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() - or stat.S_IMODE(info.st_mode) != 0o600 or path.is_relative_to(HERE.parents[2])): - raise ValueError("owned_mode_0600_file_outside_checkout_required") - return path +def server_url(port): + return f"http://127.0.0.1:{owned_port(port)}" def curl_json(url, *, method="GET", headers=None, body=None): @@ -52,10 +51,36 @@ def curl_json(url, *, method="GET", headers=None, body=None): return json.loads(raw) -def api_request(method, path, *, body=None): - if not re.fullmatch(r"/api/conversations(?:/[a-f0-9-]{36}(?:/(?:agent_final_response|interrupt))?)?", path): +def api_request(method, path, *, body=None, port=DEFAULT_PORT): + if not (re.fullmatch(r"/api/conversations(?:/[a-f0-9-]{36}(?:/(?:agent_final_response|interrupt))?)?", path) + or re.fullmatch(r"/api/conversations/[a-f0-9-]{36}" + re.escape(ERROR_EVENT_SEARCH), path)): raise ValueError("unexpected_native_route") - return curl_json(SERVER_URL + path, method=method, headers=os.environ["OPENHANDS_HEADERS"], body=body) + return curl_json(server_url(port) + path, method=method, headers=os.environ["OPENHANDS_HEADERS"], body=body) + + +def agent_termination(conversation_id, execution, port): + """Classify a terminal native status; agent limits keep their partial patch. + + SDK@fcc102a conversation/state.py:48-79 makes finished, error and stuck + terminal. local_conversation.py:753-755 sets STUCK; :2021-2043 and + :2339-2360 set ERROR with ConversationErrorEvent(source="environment", + code="MaxIterationsReached"). Every other ERROR stays an infrastructure + failure. The event page is agent-server reported, and the server shares the + model terminal's UID and store, so it selects only exit 1 or exit 3; it can + never produce a pass. The module-qualified kind is source-reviewed, not yet + observed from the image binary; a mismatch fails closed to "error". + """ + if execution in {"finished", "stuck"}: + return execution + if execution != "error": + return "error" + page = api_request("GET", f"/api/conversations/{conversation_id}{ERROR_EVENT_SEARCH}", port=port) + items = page.get("items") if isinstance(page, dict) else None + if (isinstance(items, list) and len(items) == 1 and isinstance(items[0], dict) + and {key: items[0].get(key) for key in ("kind", "source", "code")} + == {"kind": "ConversationErrorEvent", "source": "environment", "code": "MaxIterationsReached"}): + return "max_iterations_reached" + return "error" def compression_snapshot(): @@ -82,32 +107,33 @@ def compression_delta(before, after): "basis": "entry gateway global analytics delta; separate from call_logs usage; concurrent callers may contribute"} -def server_command(pins, name, mounts, network, env_file): +def server_command(pins, name, mounts, network, env_file, *, port=DEFAULT_PORT): # Keep the image ENTRYPOINT. Its binary target is Dockerfile:580-590, not # the source target at :571. Supported preload: __main__.py:74-135,240-273. return [*docker_args(pins, name, network=network), *mounts, "--detach", - "--publish", "127.0.0.1:3730:8000", "--env-file", str(env_file), + "--publish", f"127.0.0.1:{owned_port(port)}:8000", "--env-file", str(env_file), "--env", "OH_ENABLE_VSCODE=0", "--env", "OH_CONVERSATIONS_PATH=/state/server/conversations", "--env", "OH_WORKSPACE_PATH=/workspace", "--env", "OH_BASH_EVENTS_DIR=/state/server/bash_events", "--env", "OPENHANDS_OWNED_CONTAINER=1", "--workdir", "/workspace", pins["image"]["ref"], "--extra-python-path", "/recipe", "--import-modules", "server_transport"] -def check_server(body): +def check_server(body, *, port=DEFAULT_PORT): """Native health/docs gate, then cross-check SDK output against live OpenAPI.""" + base = server_url(port) deadline = time.monotonic() + 60 while True: try: for path in ("/health", "/docs"): subprocess.run(["curl", "-fsS", "--noproxy", "*", "--max-time", "5", "-o", os.devnull, - SERVER_URL + path], check=True, stdout=subprocess.DEVNULL, + base + path], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=6) break except (OSError, subprocess.SubprocessError): if time.monotonic() >= deadline: raise RuntimeError("native_server_health_failed") time.sleep(1) - schema = curl_json(SERVER_URL + "/openapi.json") + schema = curl_json(base + "/openapi.json") request = schema["paths"]["/api/conversations"]["post"]["requestBody"]["content"]["application/json"]["schema"] if "$ref" in request: request = schema["components"]["schemas"][request["$ref"].split("/")[-1]] @@ -129,15 +155,22 @@ def stop_server(result, status): def finish_result(result, status, window): removed = stop_server(result, status) - window["worker_exit_code"] = 0 if status.get("execution_status") == "finished" else 1 - if window["worker_exit_code"]: + termination = status.get("agent_termination") or status.get("execution_status") + if termination not in {"finished", "error", *AGENT_LIMITS}: + termination = "error" + window["agent_termination"] = termination + window["worker_exit_code"] = 0 if termination == "finished" else 1 + # A partial patch after an agent limit is still officially graded; only + # other native errors skip export (exit 3). + graded = termination == "finished" or termination in AGENT_LIMITS + if not graded: window["failure_stage"] = "agent" checked = {"upstream_resolved": None, "grader_exit_code": None} stage = "export" try: if not removed: raise RuntimeError("worker_removal_not_confirmed") - if not window["worker_exit_code"]: + if graded: task = read_json(result / "task-identity.json") workspace = result / "workspace" # git/git@v2.43.0 Documentation/git.txt (GIT_CONFIG_GLOBAL/NOSYSTEM) @@ -184,6 +217,8 @@ def execute(action, state, run_id, arm): window = json.loads(read_bounded(result / "window.json")) if window.get("run_id") != run_id or window.get("arm") != arm: raise ValueError("attempt_identity_mismatch") + # Prepared by host.py into host-owned status.json; re-validated on read. + port = owned_port(status.get("port", DEFAULT_PORT)) if action == "start": if status.get("status") != "prepared": print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), @@ -204,7 +239,7 @@ def execute(action, state, run_id, arm): window["started_at"] = utc_now() window["finished_at"] = None write_json(result / "window.json", window) - response = api_request("POST", "/api/conversations", body=result / "start.json") + response = api_request("POST", "/api/conversations", body=result / "start.json", port=port) conversation_id = str(uuid.UUID(response["id"])) status.update(status="running", conversation_id=conversation_id) elif action == "wait": @@ -215,10 +250,10 @@ def execute(action, state, run_id, arm): if time.time() >= status["deadline"]: stage = "deadline" try: - api_request("POST", f"/api/conversations/{conversation_id}/interrupt") + api_request("POST", f"/api/conversations/{conversation_id}/interrupt", port=port) finally: raise TimeoutError("native_conversation_deadline") - response = api_request("GET", f"/api/conversations/{conversation_id}") + response = api_request("GET", f"/api/conversations/{conversation_id}", port=port) execution = response["execution_status"] if execution in TERMINAL: status.update(status="terminal", execution_status=execution) @@ -238,10 +273,11 @@ def execute(action, state, run_id, arm): if status.get("status") != "terminal": raise ValueError("terminal_attempt_required") conversation_id = str(uuid.UUID(status["conversation_id"])) - response = api_request("GET", f"/api/conversations/{conversation_id}/agent_final_response") + response = api_request("GET", f"/api/conversations/{conversation_id}/agent_final_response", port=port) if not isinstance(response.get("response"), str): raise ValueError("native_final_response_contract") write_json(result / "final-response.json", response) + status["agent_termination"] = agent_termination(conversation_id, status.get("execution_status"), port) receipt = finish_result(result, status, window) write_json(receipt_path, receipt) code = result_exit(receipt) diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md b/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md deleted file mode 100644 index db55f63b7..000000000 --- a/blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Compact integer ranges - -Implement `compress_ranges(values)` in `range_utils.py`. Accept a finite iterable -of integers. Return a list of inclusive `(start, end)` tuples in ascending order, -merging consecutive integers, ignoring duplicates, and preserving gaps. An empty -input produces `[]`. Do not mutate the input. For example, -`[8, 2, 1, 3, 5, 5, 7]` produces `[(1, 3), (5, 5), (7, 8)]`. - -This is a self-contained synthetic coding task. Only `range_utils.py` may change. -The test and this README are frozen. First run the existing failing test, then -repair the implementation, then run the same test again. Do not delete, skip or -edit tests; do not add files, install dependencies or use Git. - -Run: `python -B -m unittest discover -s tests -v` diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py b/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py deleted file mode 100644 index 50d57a662..000000000 --- a/blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Synthetic fixture: intentionally fails the frozen range-coalescing test.""" - - -def compress_ranges(values): - return [(value, value) for value in sorted(set(values))] diff --git a/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py b/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py deleted file mode 100644 index e48cee654..000000000 --- a/blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest - -from range_utils import compress_ranges - - -class RangeTests(unittest.TestCase): - def test_ranges_follow_the_spec(self): - inputs = [[], [4], [8, 2, 1, 3, 5, 5, 7], [-2, -1, 0, 4, 4, 5]] - before = [list(values) for values in inputs] - self.assertEqual( - [compress_ranges(values) for values in inputs], - [[], [(4, 4)], [(1, 3), (5, 5), (7, 8)], [(-2, 0), (4, 5)]], - ) - self.assertEqual(inputs, before) - - -if __name__ == "__main__": - unittest.main() diff --git a/blueprints/runtime-workers/openhands/e2e/frozen.json b/blueprints/runtime-workers/openhands/e2e/frozen.json deleted file mode 100644 index 365489760..000000000 --- a/blueprints/runtime-workers/openhands/e2e/frozen.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "schema_version": 1, - "evidence_class": "synthetic fixture", - "allowed_files": [ - "range_utils.py" - ], - "expected_test_count": 1, - "test_command": "python -B -m unittest discover -s tests -v", - "files": { - "README.md": "3af3bcbdb2c08a268481be21a5b3a136d35cafaa8107a83f14db0a0358497f06", - "range_utils.py": "219dfcb3d11544b62e9fd496fc2e544f4689c286253118c48248c88d0802c66d", - "tests/test_ranges.py": "bfa9a8f4fb795f6201b45f7efce2a9477fbbd94041e56382be0756e6b1bd1b73" - } -} diff --git a/blueprints/runtime-workers/openhands/e2e/task.txt b/blueprints/runtime-workers/openhands/e2e/task.txt deleted file mode 100644 index 9d8c4f22c..000000000 --- a/blueprints/runtime-workers/openhands/e2e/task.txt +++ /dev/null @@ -1,10 +0,0 @@ -Fix the tiny repository at /workspace according to its README. -The allowed edit is range_utils.py only. The existing test seam and test-first -procedure are already authorized. Invoke the pinned tdd skill with invoke_skill. -Before any edit, run exactly: python -B -m unittest discover -s tests -v -Observe its failing assertion, repair the implementation, and rerun that command. -Use context-mode for a bounded inspection when useful, and use the installed -coding/retrieval tools only when they improve this task. Do not manufacture MCP -calls to inflate coverage. Do not edit tests, README, runner state or recipe files. -Do not install software, start services, contact broker APIs or use Git. Finish -with the change and the actual test result. diff --git a/blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json b/blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json new file mode 100644 index 000000000..fcdeb4212 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json @@ -0,0 +1,436 @@ +{ + "schema_version": 1, + "evidence_class": "Native grype 0.119.0 and syft 1.52.0 executions against the pinned agent-server digest pulled into this host's rootless Docker daemon; no container was created or started. A package-to-advisory match inventory, not a reachability, exploitability or runtime assessment, and not upstream acceptance.", + "sanitization": "Scratch output paths are shown as , the rootless daemon socket as $XDG_RUNTIME_DIR/docker.sock and the docker CLI without its install prefix. Paths inside the image are kept. Raw grype (18,253,938 bytes) and syft (15,550,859 bytes) JSON stayed in private scratch and are not published.", + "scanned_at": "2026-09-28T16:03:11Z", + "image": { + "ref": "ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37", + "platform": "linux/amd64", + "manifest_sha256": "ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12", + "config_sha256": "39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f", + "identity": { + "daemon_repo_digests": [ + "ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37" + ], + "daemon_platform_descriptor_digest": "sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12", + "cataloged_image_id": "sha256:39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f", + "cataloged_config_sha256": "39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f", + "daemon_image_id": "sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37", + "daemon_image_id_note": "This daemon uses the containerd image store (io.containerd.snapshotter.v1), whose image .Id is the pulled index digest, not the config digest. The platform descriptor and the cataloged config bytes carry the identity check instead." + }, + "config_build_env": [ + "OPENHANDS_BUILD_GIT_SHA=fcc102a697874d54a357e36004e02c95040dbdc0", + "OPENHANDS_BUILD_GIT_REF=refs/tags/v1.49.6" + ], + "entrypoint": [ + "tini", + "--", + "/usr/local/bin/openhands-agent-server" + ], + "user": "openhands", + "distro": "debian 13.7", + "layers": 21, + "daemon_size_bytes": 4562632840, + "containers_for_image_before_and_after": [ + 0, + 0 + ] + }, + "scanner": { + "name": "grype", + "version": "0.119.0", + "commit": "b6f5194537747ee7f705f4113069ac9eb269919f", + "repository": "https://github.com/anchore/grype", + "embedded_syft": "v1.52.0", + "stack_pin": "not pinned in manifests/stack.json; version and commit from `grype version` on this host" + }, + "cataloger": { + "name": "syft", + "version": "1.52.0", + "commit": "02ba369d13b4248395b20a504eca94b0cab564d8", + "repository": "https://github.com/anchore/syft", + "stack_pin": "manifests/stack.json component syft 1.52.0", + "artifacts": 3136, + "python_artifacts": [ + "pip 26.2.1" + ] + }, + "database": { + "built": "2026-09-28T06:42:30Z", + "schema": "v6.1.9", + "source": "https://grype.anchore.io/databases/v6/vulnerability-db_v6.1.9_2026-09-28T00:37:02Z_1790577750.tar.zst", + "checksum": "sha256:df23160af6e2d67cf3a5b6ccbc7e1de0c2c4ecd15cbd7dc6127ef4f9010ea714", + "providers": 26, + "auto_update_during_scan": false + }, + "commands": [ + { + "id": "pull", + "command": "docker --context rootless pull --platform linux/amd64 ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37", + "started": "2026-09-28T16:00:35Z", + "finished": "2026-09-28T16:01:14Z", + "exit_code": 0 + }, + { + "id": "db-update", + "command": "GRYPE_DB_CACHE_DIR=/grype-db GRYPE_CHECK_FOR_APP_UPDATE=false grype db update", + "started": "2026-09-28T16:01:14Z", + "finished": "2026-09-28T16:02:37Z", + "exit_code": 0 + }, + { + "id": "grype-image", + "command": "DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock GRYPE_DB_CACHE_DIR=/grype-db GRYPE_DB_AUTO_UPDATE=false GRYPE_CHECK_FOR_APP_UPDATE=false grype docker:ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 --platform linux/amd64 -o json --file /grype.json", + "started": "2026-09-28T16:03:11Z", + "finished": "2026-09-28T16:04:06Z", + "exit_code": 0 + }, + { + "id": "syft-image", + "command": "DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock SYFT_CHECK_FOR_APP_UPDATE=false syft scan docker:ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 --platform linux/amd64 -o syft-json=/syft.json", + "started": "2026-09-28T16:04:12Z", + "finished": "2026-09-28T16:04:53Z", + "exit_code": 0 + }, + { + "id": "grype-upstream-lock-purls", + "command": "GRYPE_DB_CACHE_DIR=/grype-db GRYPE_DB_AUTO_UPDATE=false GRYPE_CHECK_FOR_APP_UPDATE=false grype purl:/upstream-lock.purls -o json --file /grype-purls.json", + "input": [ + "pkg:pypi/anyio@4.11.0", + "pkg:pypi/click@8.1.8", + "pkg:pypi/pypdf@6.14.2", + "pkg:pypi/soupsieve@2.8.4" + ], + "exit_code": 0 + }, + { + "id": "identity-read-back", + "command": "DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock docker image inspect ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 --format 'Id={{.Id}} RepoDigests={{.RepoDigests}} Arch={{.Architecture}} Os={{.Os}}'; docker image inspect --platform linux/amd64 ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 --format '{{json .Descriptor}}'; docker ps -a --filter ancestor=ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37 --format '{{.ID}}' | wc -l", + "exit_code": 0 + }, + { + "id": "upstream-lock-read", + "command": "curl -fsSL -o /uv.lock https://raw.githubusercontent.com/OpenHands/software-agent-sdk/fcc102a697874d54a357e36004e02c95040dbdc0/uv.lock; sha256sum /uv.lock; python3 tomllib read of the package table", + "exit_code": 0 + } + ], + "counts": { + "unit": "Unique (vulnerability id, package name, package version, package type) matches. One advisory matched at several paths of the same package version counts once.", + "raw_matches": 1478, + "total": 1464, + "by_severity": { + "Critical": 32, + "High": 168, + "Medium": 208, + "Low": 57, + "Negligible": 780, + "Unknown": 219 + }, + "by_fix_state": { + "fixed": 319, + "not-fixed": 807, + "wont-fix": 333, + "unknown": 5 + }, + "unique_vulnerability_ids": 534, + "flagged_packages": 164, + "flagged_packages_by_type": { + "deb": 129, + "go-module": 17, + "npm": 16, + "binary": 2 + }, + "python_package_matches": 0 + }, + "flagged_packages_note": "One row per flagged package version. fixed_available counts that row's findings with a fixed version in the DB. Advisory IDs are listed for Critical findings only; locations are image paths for non-deb packages.", + "flagged_packages": [ + {"name": "curl", "version": "8.14.1-2+deb13u5", "type": "deb", "by_severity": {"Critical": 8, "High": 10, "Medium": 1, "Negligible": 6}, "fixed_available": 0, "critical_ids": ["CVE-2026-10536", "CVE-2026-11856", "CVE-2026-18924", "CVE-2026-19931", "CVE-2026-8924", "CVE-2026-8926", "CVE-2026-8927", "CVE-2026-9079"]}, + {"name": "libcurl3t64-gnutls", "version": "8.14.1-2+deb13u5", "type": "deb", "by_severity": {"Critical": 8, "High": 10, "Medium": 1, "Negligible": 6}, "fixed_available": 0, "critical_ids": ["CVE-2026-10536", "CVE-2026-11856", "CVE-2026-18924", "CVE-2026-19931", "CVE-2026-8924", "CVE-2026-8926", "CVE-2026-8927", "CVE-2026-9079"]}, + {"name": "libcurl4t64", "version": "8.14.1-2+deb13u5", "type": "deb", "by_severity": {"Critical": 8, "High": 10, "Medium": 1, "Negligible": 6}, "fixed_available": 0, "critical_ids": ["CVE-2026-10536", "CVE-2026-11856", "CVE-2026-18924", "CVE-2026-19931", "CVE-2026-8924", "CVE-2026-8926", "CVE-2026-8927", "CVE-2026-9079"]}, + {"name": "handlebars", "version": "1.0.0", "type": "npm", "by_severity": {"Critical": 3, "High": 5, "Medium": 1}, "fixed_available": 9, "critical_ids": ["GHSA-765h-qjxv-5f44", "GHSA-f2jv-r9rf-7988", "GHSA-w457-6q6x-cgp9"], "locations": ["/openhands/.openvscode-server/extensions/handlebars/package.json"]}, + {"name": "node", "version": "22.21.1", "type": "binary", "by_severity": {"Critical": 1, "High": 10, "Medium": 10, "Low": 6}, "fixed_available": 26, "critical_ids": ["CVE-2025-55130"], "locations": ["/openhands/.openvscode-server/node"]}, + {"name": "libxml2", "version": "2.12.7+dfsg+really2.9.14-2.1+deb13u3", "type": "deb", "by_severity": {"Critical": 1, "High": 7, "Medium": 2, "Low": 1, "Negligible": 1}, "fixed_available": 0, "critical_ids": ["CVE-2026-6653"]}, + {"name": "shell-quote", "version": "1.8.3", "type": "npm", "by_severity": {"Critical": 1, "High": 1}, "fixed_available": 2, "critical_ids": ["GHSA-w7jw-789q-3m8p"], "locations": ["/openhands/.openvscode-server/node_modules/shell-quote/package.json"]}, + {"name": "gh", "version": "2.101.0", "type": "deb", "by_severity": {"Critical": 1, "Medium": 3, "Low": 4}, "fixed_available": 0, "critical_ids": ["CVE-2026-48501"]}, + {"name": "libtiff6", "version": "4.7.0-3+deb13u3", "type": "deb", "by_severity": {"Critical": 1, "Medium": 1, "Negligible": 11, "Unknown": 1}, "fixed_available": 0, "critical_ids": ["CVE-2026-52490"]}, + {"name": "undici", "version": "7.19.0", "type": "npm", "by_severity": {"High": 5, "Medium": 9, "Low": 2}, "fixed_available": 16, "locations": ["/openhands/.openvscode-server/node_modules/undici/package.json"]}, + {"name": "npm", "version": "1.0.1", "type": "npm", "by_severity": {"High": 5, "Medium": 1, "Low": 1}, "fixed_available": 7, "locations": ["/openhands/.openvscode-server/extensions/npm/package.json"]}, + {"name": "bsdutils", "version": "1:2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libblkid1", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "liblastlog2-2", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libmount1", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libsmartcols1", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libuuid1", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "login", "version": "1:4.16.0-2+really2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "mount", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "util-linux", "version": "2.41.5-0+deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libexpat1", "version": "2.8.3-1~deb13u1", "type": "deb", "by_severity": {"High": 4, "Medium": 1}, "fixed_available": 0}, + {"name": "wget", "version": "1.25.0-2", "type": "deb", "by_severity": {"High": 3, "Medium": 3, "Negligible": 1}, "fixed_available": 0}, + {"name": "google.golang.org/grpc", "version": "v1.80.0", "type": "go-module", "by_severity": {"High": 3}, "fixed_available": 3, "locations": ["/usr/bin/containerd", "/usr/bin/ctr"]}, + {"name": "libc-bin", "version": "2.41-12+deb13u4", "type": "deb", "by_severity": {"High": 2, "Medium": 9, "Low": 2, "Negligible": 7}, "fixed_available": 0}, + {"name": "libc-dev-bin", "version": "2.41-12+deb13u4", "type": "deb", "by_severity": {"High": 2, "Medium": 9, "Low": 2, "Negligible": 7}, "fixed_available": 0}, + {"name": "libc6", "version": "2.41-12+deb13u4", "type": "deb", "by_severity": {"High": 2, "Medium": 9, "Low": 2, "Negligible": 7}, "fixed_available": 0}, + {"name": "libc6-dev", "version": "2.41-12+deb13u4", "type": "deb", "by_severity": {"High": 2, "Medium": 9, "Low": 2, "Negligible": 7}, "fixed_available": 0}, + {"name": "libcups2t64", "version": "2.4.10-3+deb13u2", "type": "deb", "by_severity": {"High": 2, "Medium": 9, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "libperl5.40", "version": "5.40.1-6+deb13u1", "type": "deb", "by_severity": {"High": 2, "Medium": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "perl", "version": "5.40.1-6+deb13u1", "type": "deb", "by_severity": {"High": 2, "Medium": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "perl-base", "version": "5.40.1-6+deb13u1", "type": "deb", "by_severity": {"High": 2, "Medium": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "perl-modules-5.40", "version": "5.40.1-6+deb13u1", "type": "deb", "by_severity": {"High": 2, "Medium": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "libsndfile1", "version": "1.2.2-2+deb13u1", "type": "deb", "by_severity": {"High": 2, "Medium": 2}, "fixed_available": 0}, + {"name": "grunt", "version": "1.0.0", "type": "npm", "by_severity": {"High": 2, "Medium": 1}, "fixed_available": 3, "locations": ["/openhands/.openvscode-server/extensions/grunt/package.json"]}, + {"name": "golang.org/x/crypto", "version": "v0.52.0", "type": "go-module", "by_severity": {"High": 2}, "fixed_available": 2, "locations": ["/usr/bin/rootlesskit"]}, + {"name": "golang.org/x/crypto", "version": "v0.55.0", "type": "go-module", "by_severity": {"High": 2}, "fixed_available": 2, "locations": ["/usr/libexec/docker/cli-plugins/docker-buildx"]}, + {"name": "libacl1", "version": "2.3.2-2+b1", "type": "deb", "by_severity": {"High": 2}, "fixed_available": 0}, + {"name": "python", "version": "3.13.15", "type": "binary", "by_severity": {"High": 1, "Medium": 5, "Low": 1}, "fixed_available": 5, "locations": ["/usr/local/bin/python3.13", "/usr/local/lib/libpython3.13.so.1.0"]}, + {"name": "libxi6", "version": "2:1.8.2-1", "type": "deb", "by_severity": {"High": 1, "Medium": 5, "Unknown": 1}, "fixed_available": 0}, + {"name": "dirmngr", "version": "2.4.7-21+deb13u1+b5", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gnupg", "version": "2.4.7-21+deb13u1", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gnupg-l10n", "version": "2.4.7-21+deb13u1", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gpg", "version": "2.4.7-21+deb13u1+b5", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gpg-agent", "version": "2.4.7-21+deb13u1+b5", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gpgconf", "version": "2.4.7-21+deb13u1+b5", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "gpgsm", "version": "2.4.7-21+deb13u1+b5", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "xdg-utils", "version": "1.2.1-2", "type": "deb", "by_severity": {"High": 1, "Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "github.com/docker/docker", "version": "v28.5.2+incompatible", "type": "go-module", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0, "locations": ["/usr/libexec/docker/cli-plugins/docker-buildx"]}, + {"name": "ip-address", "version": "9.0.5", "type": "npm", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 2, "locations": ["/openhands/.openvscode-server/node_modules/ip-address/package.json"]}, + {"name": "libminizip1t64", "version": "1:1.3.dfsg+really1.3.1-1+b1", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "libncursesw6", "version": "6.5+20250216-2", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "libtinfo6", "version": "6.5+20250216-2", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "lodash-es", "version": "4.17.23", "type": "npm", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 2, "locations": ["/openhands/.openvscode-server/node_modules/lodash-es/package.json"]}, + {"name": "ncurses-base", "version": "6.5+20250216-2", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "ncurses-bin", "version": "6.5+20250216-2", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "picomatch", "version": "4.0.3", "type": "npm", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 2, "locations": ["/openhands/.openvscode-server/node_modules/picomatch/package.json"]}, + {"name": "zlib1g", "version": "1:1.3.dfsg+really1.3.1-1+b1", "type": "deb", "by_severity": {"High": 1, "Medium": 1}, "fixed_available": 0}, + {"name": "diff", "version": "1.0.0", "type": "npm", "by_severity": {"High": 1, "Low": 1}, "fixed_available": 2, "locations": ["/openhands/.openvscode-server/extensions/diff/package.json"]}, + {"name": "sudo", "version": "1.9.16p2-3+deb13u2", "type": "deb", "by_severity": {"High": 1, "Negligible": 2}, "fixed_available": 0}, + {"name": "github.com/moby/go-archive", "version": "v0.2.0", "type": "go-module", "by_severity": {"High": 1}, "fixed_available": 1, "locations": ["/usr/libexec/docker/cli-plugins/docker-buildx"]}, + {"name": "golang.org/x/net", "version": "v0.55.0", "type": "go-module", "by_severity": {"High": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd", "/usr/bin/ctr", "/usr/bin/rootlesskit"]}, + {"name": "golang.org/x/text", "version": "v0.38.0", "type": "go-module", "by_severity": {"High": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd", "/usr/bin/ctr"]}, + {"name": "ini", "version": "1.0.0", "type": "npm", "by_severity": {"High": 1}, "fixed_available": 1, "locations": ["/openhands/.openvscode-server/extensions/ini/package.json"]}, + {"name": "json", "version": "1.0.0", "type": "npm", "by_severity": {"High": 1}, "fixed_available": 1, "locations": ["/openhands/.openvscode-server/extensions/json/package.json"]}, + {"name": "libx11-6", "version": "2:1.8.12-1", "type": "deb", "by_severity": {"High": 1}, "fixed_available": 0}, + {"name": "libx11-data", "version": "2:1.8.12-1", "type": "deb", "by_severity": {"High": 1}, "fixed_available": 0}, + {"name": "libx11-xcb1", "version": "2:1.8.12-1", "type": "deb", "by_severity": {"High": 1}, "fixed_available": 0}, + {"name": "libxrender1", "version": "1:0.9.12-1", "type": "deb", "by_severity": {"High": 1}, "fixed_available": 0}, + {"name": "libavahi-client3", "version": "0.8-16", "type": "deb", "by_severity": {"Medium": 8}, "fixed_available": 0}, + {"name": "libavahi-common-data", "version": "0.8-16", "type": "deb", "by_severity": {"Medium": 8}, "fixed_available": 0}, + {"name": "libavahi-common3", "version": "0.8-16", "type": "deb", "by_severity": {"Medium": 8}, "fixed_available": 0}, + {"name": "libopenjp2-7", "version": "2.5.3-2.1~deb13u2", "type": "deb", "by_severity": {"Medium": 3, "Low": 1, "Negligible": 9}, "fixed_available": 0}, + {"name": "libasound2-data", "version": "1.2.14-1+deb13u1", "type": "deb", "by_severity": {"Medium": 3, "Negligible": 1}, "fixed_available": 0}, + {"name": "libasound2t64", "version": "1.2.14-1+deb13u1", "type": "deb", "by_severity": {"Medium": 3, "Negligible": 1}, "fixed_available": 0}, + {"name": "tar", "version": "1.35+dfsg-3.1", "type": "deb", "by_severity": {"Medium": 3, "Negligible": 1}, "fixed_available": 0}, + {"name": "libgdk-pixbuf-2.0-0", "version": "2.42.12+dfsg-4+deb13u1", "type": "deb", "by_severity": {"Medium": 3}, "fixed_available": 0}, + {"name": "libgdk-pixbuf2.0-common", "version": "2.42.12+dfsg-4+deb13u1", "type": "deb", "by_severity": {"Medium": 3}, "fixed_available": 0}, + {"name": "libnss-systemd", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "libpam-systemd", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "libsystemd-shared", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "libsystemd0", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "libudev1", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "systemd", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "systemd-cryptsetup", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "systemd-sysv", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "systemd-timesyncd", "version": "257.13-1~deb13u1", "type": "deb", "by_severity": {"Medium": 2, "Low": 1, "Negligible": 4}, "fixed_available": 0}, + {"name": "libsqlite3-0", "version": "3.46.1-7+deb13u2", "type": "deb", "by_severity": {"Medium": 2, "Negligible": 2}, "fixed_available": 0}, + {"name": "github.com/containerd/containerd/v2", "version": "v2.3.4", "type": "go-module", "by_severity": {"Medium": 2}, "fixed_available": 2, "locations": ["/usr/libexec/docker/cli-plugins/docker-buildx", "/usr/libexec/docker/cli-plugins/docker-compose"]}, + {"name": "libp11-kit0", "version": "0.25.5-3", "type": "deb", "by_severity": {"Medium": 2}, "fixed_available": 0}, + {"name": "pug", "version": "1.0.0", "type": "npm", "by_severity": {"Medium": 2}, "fixed_available": 2, "locations": ["/openhands/.openvscode-server/extensions/pug/package.json"]}, + {"name": "libglib2.0-0t64", "version": "2.84.4-3~deb13u5", "type": "deb", "by_severity": {"Medium": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "bzip2", "version": "1.0.8-6", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "github.com/containerd/containerd/v2", "version": "v2.3.5", "type": "go-module", "by_severity": {"Medium": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd", "/usr/bin/containerd-shim-runc-v2", "/usr/bin/ctr", "/usr/bin/dockerd"]}, + {"name": "go.opentelemetry.io/otel", "version": "v1.43.0", "type": "go-module", "by_severity": {"Medium": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd"]}, + {"name": "libattr1", "version": "1:2.5.2-3", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libbz2-1.0", "version": "1.0.8-6", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libnghttp2-14", "version": "1.64.0-1.1+deb13u1", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libpam-modules", "version": "1.7.0-5", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libpam-modules-bin", "version": "1.7.0-5", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libpam-runtime", "version": "1.7.0-5", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "libpam0g", "version": "1.7.0-5", "type": "deb", "by_severity": {"Medium": 1}, "fixed_available": 0}, + {"name": "uuid", "version": "9.0.1", "type": "npm", "by_severity": {"Medium": 1}, "fixed_available": 1, "locations": ["/openhands/.openvscode-server/node_modules/uuid/package.json"]}, + {"name": "yaml", "version": "1.0.0", "type": "npm", "by_severity": {"Medium": 1}, "fixed_available": 1, "locations": ["/openhands/.openvscode-server/extensions/yaml/package.json"]}, + {"name": "libcairo-gobject2", "version": "1.18.4-1+b1", "type": "deb", "by_severity": {"Low": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "libcairo2", "version": "1.18.4-1+b1", "type": "deb", "by_severity": {"Low": 2, "Negligible": 1}, "fixed_available": 0}, + {"name": "libllvm19", "version": "1:19.1.7-3+b1", "type": "deb", "by_severity": {"Low": 1, "Negligible": 2}, "fixed_available": 0}, + {"name": "login.defs", "version": "1:4.17.4-2", "type": "deb", "by_severity": {"Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "passwd", "version": "1:4.17.4-2", "type": "deb", "by_severity": {"Low": 1, "Negligible": 1}, "fixed_available": 0}, + {"name": "@tootallnate/once", "version": "3.0.0", "type": "npm", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/openhands/.openvscode-server/node_modules/@tootallnate/once/package.json"]}, + {"name": "github.com/cilium/ebpf", "version": "v0.16.0", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd", "/usr/bin/containerd-shim-runc-v2", "/usr/bin/ctr"]}, + {"name": "github.com/cilium/ebpf", "version": "v0.17.3", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/dockerd", "/usr/bin/runc"]}, + {"name": "go.opentelemetry.io/otel/exporters/otlp/otlptrace", "version": "v1.43.0", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd"]}, + {"name": "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc", "version": "v1.43.0", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd"]}, + {"name": "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp", "version": "v1.43.0", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd"]}, + {"name": "go.opentelemetry.io/otel/sdk", "version": "v1.43.0", "type": "go-module", "by_severity": {"Low": 1}, "fixed_available": 1, "locations": ["/usr/bin/containerd"]}, + {"name": "markdown", "version": "1.0.0", "type": "npm", "by_severity": {"Low": 1}, "fixed_available": 0, "locations": ["/openhands/.openvscode-server/extensions/markdown-basics/package.json"]}, + {"name": "binutils", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "binutils-common", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "binutils-x86-64-linux-gnu", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libbinutils", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libctf-nobfd0", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libctf0", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libgprofng0", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libsframe1", "version": "2.44-3", "type": "deb", "by_severity": {"Negligible": 69}, "fixed_available": 0}, + {"name": "libelf1t64", "version": "0.192-4", "type": "deb", "by_severity": {"Negligible": 7}, "fixed_available": 0}, + {"name": "patch", "version": "2.8-2", "type": "deb", "by_severity": {"Negligible": 6}, "fixed_available": 0}, + {"name": "libldap2", "version": "2.6.10+dfsg-1", "type": "deb", "by_severity": {"Negligible": 5}, "fixed_available": 0}, + {"name": "coreutils", "version": "9.7-3", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libgbm1", "version": "25.0.7-2+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libgl1-mesa-dri", "version": "25.0.7-2+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libglx-mesa0", "version": "25.0.7-2+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libgssapi-krb5-2", "version": "1.21.3-5+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libk5crypto3", "version": "1.21.3-5+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libkrb5-3", "version": "1.21.3-5+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libkrb5support0", "version": "1.21.3-5+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "libnss3", "version": "2:3.110-1+deb13u4", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "mesa-libgallium", "version": "25.0.7-2+deb13u1", "type": "deb", "by_severity": {"Negligible": 4}, "fixed_available": 0}, + {"name": "git", "version": "1:2.47.3-0+deb13u1", "type": "deb", "by_severity": {"Negligible": 3}, "fixed_available": 0}, + {"name": "git-man", "version": "1:2.47.3-0+deb13u1", "type": "deb", "by_severity": {"Negligible": 3}, "fixed_available": 0}, + {"name": "libgcrypt20", "version": "1.11.0-7+deb13u1", "type": "deb", "by_severity": {"Negligible": 2}, "fixed_available": 0}, + {"name": "libpng16-16t64", "version": "1.6.48-1+deb13u5", "type": "deb", "by_severity": {"Negligible": 2}, "fixed_available": 0}, + {"name": "apt", "version": "3.0.3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "apt-transport-https", "version": "3.0.3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "apt-utils", "version": "3.0.3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "diffutils", "version": "1:3.10-4", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "dmsetup", "version": "2:1.02.205-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "iptables", "version": "1.8.11-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "jq", "version": "1.7.1-6+deb13u3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libapt-pkg7.0", "version": "3.0.3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libdevmapper1.02.1", "version": "2:1.02.205-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libgl1", "version": "1.7.0-1+b2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libglvnd0", "version": "1.7.0-1+b2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libglx0", "version": "1.7.0-1+b2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libgnutls30t64", "version": "3.8.9-3+deb13u4", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libip4tc2", "version": "1.8.11-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libip6tc2", "version": "1.8.11-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libjbig0", "version": "2.1-6.1+b2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libjq1", "version": "1.7.1-6+deb13u3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "liblcms2-2", "version": "2.16-2+deb13u2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libpixman-1-0", "version": "0.44.0-3", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "libxtables12", "version": "1.8.11-2", "type": "deb", "by_severity": {"Negligible": 1}, "fixed_available": 0}, + {"name": "chromium", "version": "153.0.8010.52-1~deb13u1", "type": "deb", "by_severity": {"Unknown": 108}, "fixed_available": 108}, + {"name": "chromium-common", "version": "153.0.8010.52-1~deb13u1", "type": "deb", "by_severity": {"Unknown": 108}, "fixed_available": 108}, + {"name": "golang.org/x/crypto", "version": "v0.53.0", "type": "go-module", "by_severity": {"Unknown": 1}, "fixed_available": 0, "locations": ["/usr/bin/containerd"]} + ], + "upstream_lock": { + "source": "pins.json uv_lock: OpenHands/software-agent-sdk@fcc102a697874d54a357e36004e02c95040dbdc0 uv.lock", + "sha256": "d13a41b86c48e66e1719b19d8e2d3a315abfab87c257fb18881ce4e31075c910", + "sha256_matches_pins": true, + "locked_versions": { + "anyio": "4.11.0", + "click": "8.1.8", + "pypdf": "6.14.2", + "soupsieve": "2.8.4", + "pyinstaller": "6.22.2" + } + }, + "upstream_lock_flagged_note": "The server binary /usr/local/bin/openhands-agent-server is a PyInstaller build from upstream's unchanged uv.lock (pins.json uv_lock; SDK@fcc102a openhands-agent-server/openhands/agent_server/docker/Dockerfile:129,139,146-158,583-590). These four locked versions carry advisories; the recipe relock changes only the recipe venv. The rows below are grype DB matches for the exact versions given as package URLs, which is lock evidence, not an observation inside the image.", + "upstream_lock_flagged": [ + { + "name": "anyio", + "version": "4.11.0", + "recipe_relock_version": "4.14.2", + "observed_by_cataloger": false, + "grype_db_advisories": [ + { + "id": "GHSA-5p39-cfhj-2xmp", + "severity": "Medium", + "fixed_in": [ + "4.14.2" + ] + }, + { + "id": "GHSA-82r6-8w77-94w6", + "severity": "Critical", + "fixed_in": [ + "4.14.2" + ] + } + ] + }, + { + "name": "click", + "version": "8.1.8", + "recipe_relock_version": "8.5.0", + "observed_by_cataloger": false, + "grype_db_advisories": [], + "note": "This grype DB returned no match; research.md records OSV-Scanner's PYSEC-2026-2132 (fixed in 8.3.3) for this version." + }, + { + "name": "pypdf", + "version": "6.14.2", + "recipe_relock_version": "6.19.0", + "observed_by_cataloger": false, + "grype_db_advisories": [ + { + "id": "GHSA-23w6-3w8w-8484", + "severity": "Medium", + "fixed_in": [ + "6.16.1" + ] + }, + { + "id": "GHSA-763m-79hh-57f2", + "severity": "Medium", + "fixed_in": [ + "6.16.1" + ] + }, + { + "id": "GHSA-fc8x-2rww-xw9m", + "severity": "Medium", + "fixed_in": [ + "6.15.0" + ] + }, + { + "id": "GHSA-fp3f-mc75-235c", + "severity": "Medium", + "fixed_in": [ + "6.15.0" + ] + }, + { + "id": "GHSA-fwg2-594c-jp42", + "severity": "Medium", + "fixed_in": [ + "6.15.0" + ] + }, + { + "id": "GHSA-jp53-mhqp-8xcg", + "severity": "Medium", + "fixed_in": [ + "6.16.0" + ] + } + ] + }, + { + "name": "soupsieve", + "version": "2.8.4", + "recipe_relock_version": "2.9.2", + "observed_by_cataloger": false, + "grype_db_advisories": [ + { + "id": "GHSA-gjv8-xp57-g29c", + "severity": "Medium", + "fixed_in": [ + "2.9.0" + ] + }, + { + "id": "GHSA-j934-xhv5-fg8f", + "severity": "Medium", + "fixed_in": [ + "2.9.0" + ] + } + ] + } + ], + "scope_limits": [ + "Syft cataloged 3,136 artifacts but only one Python package (pip 26.2.1 in the base interpreter). The PyInstaller single-file server archive is not unpacked by the catalogers, so this scan neither confirms nor excludes the four upstream-lock versions inside it.", + "The recipe venv built from requirements.lock lives in the host install prefix mounted into the container. It is not part of this image and is not covered by this scan.", + "Negligible and Unknown are distribution or database classifications, not zero risk. Counts measure matched advisories, not reachability or exploitability in this recipe.", + "Only the linux/amd64 manifest of the index was scanned.", + "Counts are specific to the database built 2026-09-28T06:42:30Z. A later database can change them." + ] +} diff --git a/blueprints/runtime-workers/openhands/evidence/phase1-commands.json b/blueprints/runtime-workers/openhands/evidence/phase1-commands.json new file mode 100644 index 000000000..d29547a8d --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/phase1-commands.json @@ -0,0 +1,164 @@ +{ + "evidence_class": "Offline synthetic and source-contract unit checks of the recipe, not native runtime, grader or model acceptance.", + "sanitization": "The worktree prefix is shown as and the scratch directory as . In every unit output the repeated module prefix `tests.test_runtime_worker_openhands.` inside parentheses is shortened. Red and mid outputs are excerpts: the summary lines, every non-ok test line and each failure's first exception line, with tracebacks omitted; rerunning the command on the named tree reproduces them. Other outputs are complete.", + "base": "45d40f6cd3a148b4cb12de159f52f089a36c16c4", + "item_tests": { + "F16": [ + "OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success", + "OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1", + "OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures" + ], + "F17": [ + "OpenHandsReceiptTests.test_model_writable_files_never_populate_receipt_evidence", + "OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected", + "OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt" + ], + "F18": [ + "OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest" + ], + "F20": [ + "OpenHandsRecipeTests.test_round1_orphans_are_not_shipped" + ], + "session_key_preflight": [ + "OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only", + "OpenHandsUpstreamAdapterTests.test_missing_session_key_name_stops_preflight_without_exposing_values" + ], + "port": [ + "OpenHandsUpstreamAdapterTests.test_port_outside_owned_range_fails_in_preflight", + "OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch" + ], + "F19": [] + }, + "item_notes": { + "F19": "Documentation only: recorded as overturned in README.md and research.md; no test, because no code changed.", + "F18": "The red and mid runs used an earlier draft of this test that expected local_image_id_matches_config and per-package vulnerability lists. After the containerd image store was found to report the index digest as the image ID, the test was revised to the written receipt's identity and by_severity fields before the green run." + }, + "commands": [ + { + "id": "01-red-unit", + "tree": "New and changed tests on the unfixed recipe at the base: orphans present, no scan receipt, README and research.md unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 69 tests in 0.683s", + "FAILED (failures=16, errors=19)" + ], + "non_ok_tests": [ + " test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='stuck', bucket='unresolved_ids') ... FAIL", + " test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='error', bucket='empty_patch_ids') ... FAIL", + " test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='error', bucket='resolved_ids') ... FAIL", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'LLMAuthenticationError', 'detail': 'Agent reached maximum iterations limit (40).'}], 'next_page_id': None}) ... ERROR", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [], 'next_page_id': None}) ... ERROR", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'agent', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) ... ERROR", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'MessageEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) ... ERROR", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}, {'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) ... ERROR", + " test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page=[{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]) ... ERROR", + "test_selected_port_flows_from_prepare_to_dispatch (OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch) ... ERROR", + "test_model_writable_files_never_populate_receipt_evidence (OpenHandsReceiptTests.test_model_writable_files_never_populate_receipt_evidence) ... FAIL", + "test_truncated_native_summary_retains_failed_attempt_receipt (OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... FAIL", + "test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... FAIL", + "test_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) ... ERROR", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='config/skills.lock.json') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/frozen.json') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/task.txt') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/README.md') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/range_utils.py') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/tests/test_ranges.py') ... FAIL", + " test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo') ... FAIL", + " test_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) (termination='stuck', verdict={'upstream_resolved': True, 'upstream_bucket': 'resolved_ids', 'grader_exit_code': 0, 'conversion_exit_code': 0}) ... FAIL", + " test_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) (termination='max_iterations_reached', verdict={'upstream_resolved': True, 'upstream_bucket': 'resolved_ids', 'grader_exit_code': 0, 'conversion_exit_code': 0}) ... FAIL", + "test_missing_session_key_name_stops_preflight_without_exposing_values (OpenHandsUpstreamAdapterTests.test_missing_session_key_name_stops_preflight_without_exposing_values) ... FAIL", + "test_port_outside_owned_range_fails_in_preflight (OpenHandsUpstreamAdapterTests.test_port_outside_owned_range_fails_in_preflight) ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0=\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='\\ufeff \\tOH_SESSION_API_KEYS_0=\\r\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OTHER_NAME=1\\nOH_SESSION_API_KEYS_0=') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='# OH_SESSION_API_KEYS_0=\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0_1=\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text=' OH_SESSION_API_KEYS_0 =\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OTHER_NAME=\\x0cOH_SESSION_API_KEYS_0=\\n') ... ERROR", + " test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='') ... ERROR", + "test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) ... ERROR" + ], + "first_exception_lines": [ + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'LLMAuthenticationError', 'detail': 'Agent reached maximum iterations limit (40).'}], 'next_page_id': None}) => KeyError: 'agent_termination'", + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [], 'next_page_id': None}) => KeyError: 'agent_termination'", + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'agent', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) => KeyError: 'agent_termination'", + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'MessageEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) => KeyError: 'agent_termination'", + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page={'items': [{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}, {'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]}) => KeyError: 'agent_termination'", + "ERROR: test_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) (page=[{'kind': 'ConversationErrorEvent', 'source': 'environment', 'code': 'MaxIterationsReached', 'detail': 'Agent reached maximum iterations limit (40).'}]) => KeyError: 'agent_termination'", + "ERROR: test_selected_port_flows_from_prepare_to_dispatch (OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch) => TypeError: prepare_native_dispatch() got an unexpected keyword argument 'port'", + "ERROR: test_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json'", + "ERROR: test_port_outside_owned_range_fails_in_preflight (OpenHandsUpstreamAdapterTests.test_port_outside_owned_range_fails_in_preflight) => AttributeError: module 'openhands_recipe_test_host' has no attribute 'DEFAULT_PORT'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0=\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='\\ufeff \\tOH_SESSION_API_KEYS_0=\\r\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OTHER_NAME=1\\nOH_SESSION_API_KEYS_0=') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='# OH_SESSION_API_KEYS_0=\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OH_SESSION_API_KEYS_0_1=\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text=' OH_SESSION_API_KEYS_0 =\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='OTHER_NAME=\\x0cOH_SESSION_API_KEYS_0=\\n') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) (text='') => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "ERROR: test_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_server_env'", + "FAIL: test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='stuck', bucket='unresolved_ids') => AssertionError: 3 != 1", + "FAIL: test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='error', bucket='empty_patch_ids') => AssertionError: 3 != 1", + "FAIL: test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) (execution='error', bucket='resolved_ids') => AssertionError: 3 != 1", + "FAIL: test_model_writable_files_never_populate_receipt_evidence (OpenHandsReceiptTests.test_model_writable_files_never_populate_receipt_evidence) => AssertionError: 'observed' != 'not_collected'", + "FAIL: test_truncated_native_summary_retains_failed_attempt_receipt (OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) => AssertionError: None != 'not_collected'", + "FAIL: test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) => AssertionError: 'missing' != 'not_collected'", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='config/skills.lock.json') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/frozen.json') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/task.txt') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/README.md') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/range_utils.py') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo/tests/test_ranges.py') => AssertionError: True is not false", + "FAIL: test_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) (path='e2e/fixture-repo') => AssertionError: True is not false", + "FAIL: test_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) (termination='stuck', verdict={'upstream_resolved': True, 'upstream_bucket': 'resolved_ids', 'grader_exit_code': 0, 'conversion_exit_code': 0}) => AssertionError: 2 != 1", + "FAIL: test_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) (termination='max_iterations_reached', verdict={'upstream_resolved': True, 'upstream_bucket': 'resolved_ids', 'grader_exit_code': 0, 'conversion_exit_code': 0}) => AssertionError: 2 != 1", + "FAIL: test_missing_session_key_name_stops_preflight_without_exposing_values (OpenHandsUpstreamAdapterTests.test_missing_session_key_name_stops_preflight_without_exposing_values) => AssertionError: Expected 'read_json' to not have been called. Called 1 times." + ] + }, + { + "id": "02-mid-unit", + "tree": "Code fixes and orphan removal; no scan receipt yet, README and research.md not yet edited.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 69 tests in 0.727s", + "FAILED (errors=1)" + ], + "non_ok_tests": [ + "test_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) ... ERROR" + ], + "first_exception_lines": [ + "ERROR: test_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json'" + ] + }, + { + "id": "03-green-unit", + "tree": "Final tree, including the scan receipt, documentation and this record.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "output": "test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) ... ok\ntest_compression_delta_is_separate_and_missing_or_reset_is_unknown (OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_result_retry_preserves_collected_official_receipt (OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt) ... ok\ntest_selected_port_flows_from_prepare_to_dispatch (OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_cleanup_timeout_does_not_erase_primary_exit (OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_model_writable_files_never_populate_receipt_evidence (OpenHandsReceiptTests.test_model_writable_files_never_populate_receipt_evidence) ... ok\ntest_skills_and_mcp_require_observations (OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\ntest_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) ... ok\ntest_dispatch_fixtures_pass_publication_identifier_policy (OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... ok\ntest_every_container_is_owned_and_has_no_published_port (OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_grader_sync_keeps_the_hash_seeded_interpreter (OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_host_template_names_the_two_required_network_policy_receipts (OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\ntest_immutable_artifacts (OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_model_container_is_nonroot_and_network_is_explicit (OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) ... ok\ntest_runtime_model_override_and_native_tool_filter (OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\ntest_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) ... ok\ntest_clone_uses_pinned_swebench_branch_mapping (OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_missing_session_key_name_stops_preflight_without_exposing_values (OpenHandsUpstreamAdapterTests.test_missing_session_key_name_stops_preflight_without_exposing_values) ... ok\ntest_mount_roots_match_selected_adoption_layout (OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_port_outside_owned_range_fails_in_preflight (OpenHandsUpstreamAdapterTests.test_port_outside_owned_range_fails_in_preflight) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) ... ok\ntest_shared_skills_installer_is_the_only_install_path (OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 69 tests in 0.715s\n\nOK\n" + }, + { + "id": "04-diff-check", + "command": "git diff --check", + "exit_code": 0, + "output": "" + }, + { + "id": "05-static", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'\nimport ast\nimport subprocess\nfrom pathlib import Path\nfrom scripts.validate import PRIVATE_CONTENT\nroot=Path('blueprints/runtime-workers/openhands')\npaths=list(root.rglob('*.py'))+[Path('tests/test_runtime_worker_openhands.py')]\nfor path in paths: ast.parse(path.read_text(), filename=str(path))\nscripts=list(root.rglob('*.sh'))\nfor path in scripts: subprocess.run(['bash','-n',str(path)], check=True)\npaths=[root/'evidence/agent-server-image-grype-20260928.json',root/'research.md',root/'README.md']\nhits=[f'{p}: {name}' for p in paths for name,pattern in PRIVATE_CONTENT if pattern.search(p.read_text())]\nprint(f'Parsed Python sources: {len(list(root.rglob(\"*.py\")))+1}; shell syntax checks: {len(scripts)}')\nprint('Publication text scan: '+ ('; '.join(hits) if hits else 'no private-content matches'))\nraise SystemExit(bool(hits))\nPY", + "exit_code": 0, + "output": "Parsed Python sources: 12; shell syntax checks: 4\nPublication text scan: no private-content matches\n" + }, + { + "id": "06-validate", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py", + "exit_code": 1, + "output": "Publication validation failed:\nblueprints/runtime-workers/openhands/README.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/README.md: byte count mismatch\nfiles[1024]: file missing\nblueprints/runtime-workers/openhands/dispatch.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/dispatch.py: byte count mismatch\nfiles[1029]: file missing\nfiles[1030]: file missing\nfiles[1031]: file missing\nfiles[1032]: file missing\nfiles[1034]: file missing\nblueprints/runtime-workers/openhands/host.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/host.py: byte count mismatch\nblueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/receipt.py: byte count mismatch\nblueprints/runtime-workers/openhands/research.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/research.md: byte count mismatch\ntests/test_runtime_worker_openhands.py: SHA-256 mismatch\ntests/test_runtime_worker_openhands.py: byte count mismatch\nblueprints/runtime-workers/openhands/config/skills.lock.json: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/README.md: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py: file missing\nblueprints/runtime-workers/openhands/e2e/frozen.json: file missing\nblueprints/runtime-workers/openhands/e2e/task.txt: file missing\n", + "note": "Expected until the coordinator re-registers evidence: SHA-256 and byte-count mismatches for the six changed registered files, and file-missing entries (by index and by path) for the six deleted orphans. manifests/evidence.json is not edited in this phase." + } + ] +} diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index ae208a55e..0f93366b2 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -31,6 +31,66 @@ DOCKER = ["docker", "--context", "rootless"] +# SWE-bench worker default; the resolver mode uses 3740 (README "Workflow dispatch"). +DEFAULT_PORT = 3730 +# Agent limits whose partial patch is still graded: SDK@fcc102a +# conversation/impl/local_conversation.py:753-755 (STUCK) and :2021-2043, +# :2339-2360 (ERROR with ConversationErrorEvent code "MaxIterationsReached"). +AGENT_LIMITS = frozenset({"stuck", "max_iterations_reached"}) +# SDK@fcc102a openhands-agent-server/openhands/agent_server/config.py:24. +SESSION_KEY_NAME = "OH_SESSION_API_KEYS_0" + + +def owned_port(value): + """Mirror PR #428 crawl4ai host.py:44-45: an exact int (bool refused) in 3730..3799.""" + if type(value) is not int or not 3730 <= value <= 3799: + raise ValueError("owned_port_3730_3799_required") + return value + + +def cli_port(text): + # Non-decimal text reaches run() unchanged, so preflight refuses it with a receipt. + return int(text) if re.fullmatch(r"[0-9]{1,5}", text) else text + + +def private_file(path): + path = Path(path).absolute() + info = path.stat() + if (path.resolve() != path or not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() + or stat.S_IMODE(info.st_mode) != 0o600 or path.is_relative_to(HERE.parents[2])): + raise ValueError("owned_mode_0600_file_outside_checkout_required") + return path + + +def check_server_env(): + """Require the session-key variable NAME in the server env file; values stay unread. + + SDK@fcc102a agent_server/__main__.py:282-285 binds all interfaces only with a + session API key (config.py:24 names OH_SESSION_API_KEYS_0). Without one the + server listens on container loopback, which the published port cannot reach. + docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124 is the --env-file format: + newline-delimited lines, a BOM dropped on line one, leading whitespace + trimmed, "#" comments, and the name ends at the first "=". A bare name copies + the Docker CLI's own environment, so only the NAME= form is accepted. Only + the text before "=" is compared; no value is kept, printed or logged. + """ + name = os.environ.get("OPENHANDS_SERVER_ENV") + if not name: + raise ValueError("OPENHANDS_SERVER_ENV_required") + path = private_file(name) + try: + text = read_bounded(path, limit=64 * 1024) + except UnicodeDecodeError: + raise ValueError("server_env_must_be_utf8") from None + for number, line in enumerate(text.split("\n")): + if number == 0: + line = line.removeprefix("\N{BYTE ORDER MARK}") + # Go unicode.IsSpace: str.isspace() without U+001C..U+001F. + line = re.sub(r"^[^\S\x1c-\x1f]+", "", line) + separator = line.find("=") + if not line.startswith("#") and separator > 0 and line[:separator] == SESSION_KEY_NAME: + return path + raise ValueError("server_env_session_key_name_required") def utc_now(): @@ -449,6 +509,8 @@ def result_exit(receipt): return 3 if verdict.get("upstream_resolved") is False: return 1 if verdict.get("upstream_bucket") in {"unresolved_ids", "empty_patch_ids"} else 3 + if verdict.get("upstream_resolved") is True and receipt.get("agent_termination") in AGENT_LIMITS: + return 1 # An officially graded partial patch after an agent limit is never a pass. if receipt.get("task_passed") and receipt.get("evidence_complete"): return 0 return 2 @@ -488,7 +550,7 @@ def model_visible(root, *, writable): raise ValueError("special_file_in_model_mount") -def prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id): +def prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id, port=DEFAULT_PORT): from dispatch import check_server, private_file, server_command stem = run_id + "-" + selection["arm"] env_file = private_file(os.environ["OPENHANDS_SERVER_ENV"]) @@ -510,18 +572,19 @@ def prepare_native_dispatch(result, prefix, pins, base, network, host, selection mounts = base + environment + mount(result / "server", "/state/server", False) + mount(result / "worker", "/run-output", False) name = stem + "-server" try: - if logged_command(server_command(pins, name, mounts, network, env_file), + if logged_command(server_command(pins, name, mounts, network, env_file, port=port), result / "server-start.log", cwd=result, timeout=60): raise RuntimeError("native_server_launch_failed") - check_server(body) + check_server(body, port=port) except (Exception, KeyboardInterrupt): cleanup_container(name, result / "server.log") raise + # Host-owned status is the dispatch commands' only port source. write_json(result / "status.json", {"run_id": run_id, "arm": selection["arm"], "status": "prepared", - "server_name": name, "receipt": str(result / "receipt.json")}) + "server_name": name, "port": port, "receipt": str(result / "receipt.json")}) -def run(prefix, state, *, run_id=None, arm=None, prepare_only=False): +def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAULT_PORT): run_id = run_id or os.environ.get("OPENHANDS_RUN_ID", "rw-openhands-e2e-" + uuid.uuid4().hex[:12]) arm = arm or os.environ.get("OPENHANDS_ARM", "control") try: @@ -537,7 +600,9 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False): prepared = False native_receipt = None try: + port = owned_port(port) pins, host, mcp = preflight(prefix, state) + check_server_env() installed = read_json(state / "installation.json") if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: raise ValueError("matching_successful_installation_required") @@ -621,7 +686,7 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False): if execute_container(args, stem + "-qmd", result / "qmd-setup.log", 900): raise RuntimeError("qmd_setup_failed") stage = "start" - prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id) + prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id, port=port) prepared = True write_json(result / "window.json", window) if not prepare_only: @@ -660,6 +725,8 @@ def main(): parser.add_argument("--state", required=True, type=Path) parser.add_argument("--run-id") parser.add_argument("--arm", choices=("control", "engines-on")) + parser.add_argument("--port", type=cli_port, default=DEFAULT_PORT, + help="published loopback port in 3730..3799 (default 3730; resolver mode 3740)") args = parser.parse_args() os.umask(0o077) # SIGTERM goes through container cleanup and the failed-attempt receipt path. @@ -667,7 +734,8 @@ def main(): if args.action == "install": install(args.prefix, args.state) return 0 - return run(args.prefix, args.state, run_id=args.run_id, arm=args.arm, prepare_only=args.action == "prepare") + return run(args.prefix, args.state, run_id=args.run_id, arm=args.arm, + prepare_only=args.action == "prepare", port=args.port) if __name__ == "__main__": diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index e69fa1c33..48219aa97 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -1,15 +1,15 @@ -"""Sanitized evidence from native SDK events, independent check and gateway rows. +"""Sanitized evidence from the official grader report, host files and gateway rows. Uses Python's sqlite3 URI mode=ro + query_only. The only SQL SELECT reads the authorized usage/effort columns from call_logs. Correlation IDs may be used -in memory for matching, but are never returned in receipts. +in memory for matching, but are never returned in receipts. SDK trace, skill, +MCP and version fields are not_collected (NOT_COLLECTED_REASON). """ from __future__ import annotations from collections import Counter from datetime import datetime, timezone -import json import os from pathlib import Path import re @@ -25,6 +25,15 @@ "reasoning_effort_upstream", "tokens_in", "tokens_cache_read", "tokens_reasoning", "correlation_id", ) EFFORTS = {"none", "minimal", "low", "medium", "high", "xhigh", "max", "auto"} +AGENT_TERMINATIONS = {"finished", "error", "stuck", "max_iterations_reached"} +NOT_COLLECTED = "not_collected" +NOT_COLLECTED_REASON = ( + "The model terminal runs under the agent-server's UID, which can write both /run-output " + "(host.py model_visible on the worker directory) and the server persistence mounted at " + "/state/server. The server's event API reads that same store and rebuilds its index from " + "disk (SDK@fcc102a event_service.py:420-431, event_store.py:144-169,320-362). No event " + "source outside the model's write reach exists yet, so trace, skill, MCP and version " + "fields are not read.") def read_bounded(path, limit=4 * 1024 * 1024): @@ -164,15 +173,7 @@ def create_receipt(result, database=None): result = Path(result) window = read_json(result / "window.json") checked = read_json(result / "check.json") - events_path = result / "worker/events.jsonl" - events = [] - trace_status = "missing" - if events_path.is_file(): - try: - events = [json.loads(line) for line in read_bounded(events_path).splitlines() if line] - trace_status = "observed" if events else "empty" - except (ValueError, OSError): - trace_status = "unreadable" + # F17: nothing under result/worker or result/server is read (NOT_COLLECTED_REASON). selection = arm_config(window.get("arm", "control"), window.get("requested_model", window.get("model")), window.get("base_url")) db = database or gateway_database(selection["arm"]) @@ -183,16 +184,6 @@ def create_receipt(result, database=None): except (OSError, sqlite3.Error, ValueError, TypeError): pass # Unknown usage is not zero usage. No raw exception text is exported. pins = read_json(HERE / "pins.json") - versions = None - summary = result / "worker/native-summary.json" - if summary.is_file(): - try: - native = json.loads(read_bounded(summary)) - candidate = native.get("versions", {}) if isinstance(native, dict) else {} - if isinstance(candidate, dict) and all(candidate.get(name) == pins["version"] for name in ("openhands-sdk", "openhands-tools")): - versions = {name: candidate[name] for name in ("openhands-sdk", "openhands-tools")} - except (OSError, ValueError, TypeError): - pass # Re-read the actual upstream report; a wrapper's own passed field is never # a task oracle. The official grader determines all resolved/unresolved IDs. verdict = {"upstream_resolved": None, "upstream_bucket": None, "report_sha256": None} @@ -204,17 +195,7 @@ def create_receipt(result, database=None): pass task_passed = (verdict["upstream_resolved"] is True and checked.get("grader_exit_code") == 0 and checked.get("conversion_exit_code") == 0 and window.get("worker_exit_code") == 0) - listed_skills, skills_match = [], False - try: - expected = read_json(result / "input/skills.json")["names"] - listed = json.loads(read_bounded(result / "worker/skills-startup.json"))["listed_skills"] - if (isinstance(expected, list) and isinstance(listed, list) and expected - and all(isinstance(name, str) and re.fullmatch(r"[a-z0-9-]+", name) for name in expected) - and sorted(expected) == sorted(listed)): - listed_skills, skills_match = sorted(listed), True - except (OSError, ValueError, TypeError, KeyError): - pass - observed = observations(events, listed_skills) + termination = window.get("agent_termination") usage = summarize_gateway(rows, selection) cleanup = [] for path in sorted(result.glob("*.log.cleanup.json")): @@ -223,22 +204,26 @@ def create_receipt(result, database=None): except (OSError, ValueError, AttributeError): cleanup.append(False) return { - "schema_version": 3, "evidence_class": "SDK inference adapter with official SWE-bench grading", + "schema_version": 4, "evidence_class": "SDK inference adapter with official SWE-bench grading", **{k: selection[k] for k in ("arm", "base_url", "requested_model", "gateway_model", "gateway_path")}, "header_names": window.get("header_names", sorted(selection["headers"])), - "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], "observed_versions": versions, + "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], + "observed_versions": NOT_COLLECTED, "image": pins["image"]["ref"], "source_commit": pins["commit"], "window": {key: window[key] for key in ("started_at", "finished_at")}, "worker_exit_code": window.get("worker_exit_code"), + "agent_termination": termination if termination in AGENT_TERMINATIONS else None, + "agent_termination_basis": ("agent-server REST status and latest ConversationErrorEvent; the server shares " + "the model terminal's UID and store, so this selects exit 1 or 3 only"), "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"preflight", "prepare", "skills", "qmd_setup", "agent", "export", "grader", "start", "wait", "result", "deadline"} else None, "task_passed": task_passed, # The terminal shares the SDK's UID and writable persistence. Source: - # SDK@fcc102a tools/terminal/terminal/subprocess_terminal.py. Without a - # separate observer these files cannot prove skill use or SDK identity. + # SDK@fcc102a tools/terminal/terminal/subprocess_terminal.py:157-170. + # Without a separate observer nothing can prove skill use or SDK identity. "evidence_complete": False, - "worker_evidence_trust": "worker-reported; not independent acceptance", "independent_trace_required": True, - "skills_listed_at_start": listed_skills, "skill_listing_matches_manifest": skills_match, + "not_collected_reason": NOT_COLLECTED_REASON, + "skills_listed_at_start": NOT_COLLECTED, "skill_listing_matches_manifest": NOT_COLLECTED, "container_cleanup": {"attempts": len(cleanup), "confirmed_removed": sum(cleanup), "complete": bool(cleanup) and all(cleanup)}, "gateway": { "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, **usage, @@ -247,7 +232,8 @@ def create_receipt(result, database=None): "usage_note": "Sum each entry row once, including failed calls; never add 20128 to 20129. Cache-read is an input subset. Missing counters keep totals null.", }, "compression": window.get("compression", {"delta": None, "status": "unavailable"}), - "trace_status": trace_status, **observed, + "trace_status": NOT_COLLECTED, "mcp_calls_observed": NOT_COLLECTED, + "mcp_errors_observed": NOT_COLLECTED, "skills_observed": NOT_COLLECTED, "upstream_grader": { **{key: verdict.get(key) for key in ("upstream_resolved", "upstream_bucket", "report_sha256")}, "benchmark_commit": pins["grader"]["commit"], diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index 84407828b..792d530fa 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -238,6 +238,14 @@ cryptography needs no upgrade: the restriction leaves only upstream's linux [evidence/relock-2026-09-27.txt](evidence/relock-2026-09-27.txt); its section F holds the final lock. +Scope, corrected 2026-09-28 (F18): this relock changes only the recipe venv +under the install prefix. The image's server binary is a PyInstaller build from +upstream's unchanged uv.lock, which still pins anyio 4.11.0, click 8.1.8, pypdf +6.14.2 and soupsieve 2.8.4 (SDK@fcc102a +`openhands-agent-server/openhands/agent_server/docker/Dockerfile:129,139,146-158,583-590`). +[evidence/agent-server-image-grype-20260928.json](evidence/agent-server-image-grype-20260928.json) +records the digest scan and its limits. + The installed uv 0.12.17 reproduced the previous lock byte for byte in the unchanged upstream workspace, whose uv.lock matches the pinned SHA256: @@ -349,3 +357,32 @@ arm64 image. | `UV_NO_CONFIG=1` also stops `uv lock` from reading the workspace pyproject.toml. Installed help: "Avoid discovering configuration files (`pyproject.toml`, `uv.toml`)". The first relock printed `Resolving despite existing lockfile due to removal of global exclude newer` and chose soupsieve 2.10 from inside the seven-day window. | Relock without it so `[tool.uv]` applies; `uv lock --check` on the unchanged workspace exits 0. Keep UV_NO_CONFIG=1 only for the `uv pip install` steps, as install-container.sh does. | | In the universal resolution, `--upgrade-package cryptography` collapsed the marker-split fork to the constrained 48.0.1. | Read every `Updated` and `Removed` line before accepting a relock. Resolve only the recipe's install environment with `environments` instead of deleting an upstream constraint, and classify every removed or re-marked line. | | The coordinator's decision to keep cryptography at 50.0.0 reached the relock builder after it had relocked five packages from an earlier instruction (evidence section C). | A decision that changes a brief goes out as one message naming what it replaces. The final lock was reproduced from the unchanged workspace before commit (evidence section F). | + +## Takeover phase 1 corrections (2026-09-28) + +These are offline repairs on the round-3 head 45d40f6c. The network and +isolation design (egress policy, arm endpoints, MCP endpoints) is phase 2 and is +unchanged here. Red and green unit runs are in +[evidence/phase1-commands.json](evidence/phase1-commands.json). SDK sources are +at fcc102a; paths are abbreviated below and given in full in README.md. + +| Finding | Correction and source | +| --- | --- | +| F16: a run that ended `stuck` or at the iteration limit skipped grading and exited 3. | Status `stuck`, or status `error` whose newest `ConversationErrorEvent` has source `environment` and code `MaxIterationsReached`, now exports and officially grades the partial patch and exits 1, never 0. Sources: `state.py:48-79`, `local_conversation.py:727-733,753-755,2021-2043,2339-2360`, `conversation_error.py:11-42`, `event_router.py:68-139`, `event_service.py:456-460`, `models.py:95-99`. Any other error stays ungraded with exit 3. | +| F17: receipt trace, skill, MCP and version fields were read from `/run-output`, which the model can write. | The REST event store is not an independent source either: the terminal runs under the server's UID (`subprocess_terminal.py:157-170`), and the server reads persisted events from files it re-indexes from disk (`event_service.py:420-431`, `event_store.py:144-169,320-362`). Those fields are now `not_collected` with the reason (receipt schema 4). The server-reported agent-limit value only selects exit 1 or 3. | +| F18: the README implied the relock covered the image. | The relock covers only the recipe venv; see the scope note in the relock section. [The digest scan receipt](evidence/agent-server-image-grype-20260928.json) records grype 0.119.0 counts and the cataloger's PyInstaller boundary. | +| F19: host loopback access still had to be enabled for the gateway. | **Overturned** for the reference host by read-only checks on 2026-09-28: its rootless `docker.service` sets `DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false`, and the running rootlesskit has no `--disable-host-loopback` flag. The upstream default is still to disable it (moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`, identical to the installed 29.8.1 script), so a new host repeats both checks. The open item is the egress limit (phase 2). | +| F20: round-1 files were no longer used. | `git grep` at 45d40f6c found no reference outside evidence logs and manifests/evidence.json. Removed config/skills.lock.json, e2e/frozen.json, e2e/task.txt and e2e/fixture-repo/. | +| The server env file was not checked for the session key. Without one the server binds only its container loopback (`__main__.py:282-285`, `config.py:24`). | Preflight parses the file's variable names with Docker's env-file rules (docker/cli@v29.8.1 `pkg/kvfile/kvfile.go:92-124`) and requires `OH_SESSION_API_KEYS_0`. Values are never extracted, printed or logged. | +| The published port was fixed at 3730, which PR #428's crawl4ai recipe also uses by default. | `--port` accepts integers in 3730..3799, as PR #428's `host.py:44-45` does at 3ad8ba2. The default stays 3730 and the resolver uses 3740. The port is stored in status.json and checked again by dispatch. | + +Open findings from this phase, not changed here: + +- `install()` compares `docker image inspect` `.Id` with the config digest + (host.py:419-421). The reference host's containerd image store reports the + index digest as `.Id` (see the scan receipt), so `install()` there would raise + `image_configuration_hash_mismatch`. This is inferred from the code and the + observed `.Id`; installation was not run. +- The module-qualified event kind string is source-reviewed only. If the live + server used a different kind, the agent-limit check would fail closed to + exit 3. diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 858fbfca8..ada50a871 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -355,6 +355,50 @@ def test_serena_activates_plain_fixture_and_keeps_metadata_outside_it(self): self.assertIn("/state/mcp/serena/projects/$projectFolderName", native) self.assertNotIn("$projectDir/.serena", native) + def test_round1_orphans_are_not_shipped(self): + # F20: `git grep -F` at 45d40f6c found no reference to these round-1 + # files outside evidence logs and manifests/evidence.json. + for relative in ("config/skills.lock.json", "e2e/frozen.json", "e2e/task.txt", + "e2e/fixture-repo/README.md", "e2e/fixture-repo/range_utils.py", + "e2e/fixture-repo/tests/test_ranges.py", "e2e/fixture-repo"): + with self.subTest(path=relative): + self.assertFalse(os.path.lexists(RECIPE / relative)) + + def test_agent_server_image_scan_receipt_matches_the_pinned_digest(self): + # F18: the relock covers only the recipe venv. The image's server binary + # is built from upstream's unchanged uv.lock, so the digest is scanned. + from scripts.validate import PRIVATE_CONTENT + name = "evidence/agent-server-image-grype-20260928.json" + all_pins = self.read_json("pins.json") + pins = all_pins["image"] + receipt = self.read_json(name) + keys = ("ref", "platform", "manifest_sha256", "config_sha256") + self.assertEqual({k: receipt["image"][k] for k in keys}, {k: pins[k] for k in keys}) + # A containerd image store reports the index digest as .Id, so identity + # rests on the platform descriptor and the cataloged config bytes. + identity = receipt["image"]["identity"] + self.assertIn(pins["ref"], identity["daemon_repo_digests"]) + self.assertEqual(identity["daemon_platform_descriptor_digest"], "sha256:" + pins["manifest_sha256"]) + self.assertEqual(identity["cataloged_image_id"], "sha256:" + pins["config_sha256"]) + self.assertEqual(identity["cataloged_config_sha256"], pins["config_sha256"]) + self.assertEqual({k: receipt["scanner"][k] for k in ("name", "version", "repository")}, + {"name": "grype", "version": "0.119.0", "repository": "https://github.com/anchore/grype"}) + self.assertRegex(receipt["database"]["built"], r"^\d{4}-\d{2}-\d{2}T") + counts = receipt["counts"] + self.assertEqual(set(counts["by_severity"]), {"Critical", "High", "Medium", "Low", "Negligible", "Unknown"}) + self.assertEqual(sum(counts["by_severity"].values()), counts["total"]) + self.assertEqual(sum(sum(p["by_severity"].values()) for p in receipt["flagged_packages"]), counts["total"]) + self.assertEqual(len(receipt["flagged_packages"]), counts["flagged_packages"]) + self.assertEqual(receipt["upstream_lock"]["sha256"], all_pins["uv_lock"]["sha256"]) + self.assertEqual({p["name"]: p["version"] for p in receipt["upstream_lock_flagged"]}, + {"anyio": "4.11.0", "click": "8.1.8", "pypdf": "6.14.2", "soupsieve": "2.8.4"}) + # The PyInstaller archive is opaque to the cataloger: lock evidence, not an image observation. + self.assertTrue(all(p["observed_by_cataloger"] is False for p in receipt["upstream_lock_flagged"])) + text = json.dumps(receipt) + for description, pattern in PRIVATE_CONTENT: + self.assertIsNone(pattern.search(text), description) + self.assertIn(name, (RECIPE / "README.md").read_text()) + class OpenHandsUpstreamAdapterTests(unittest.TestCase): """Synthetic transport controls from SWE-bench 4.1.0 reporting.py. @@ -508,11 +552,17 @@ def test_task_adapter_hides_oracle_and_requires_frozen_original_bytes(self): with self.assertRaises(ValueError): module.load_task(path, hashlib.sha256(path.read_bytes()).hexdigest()) + def absent_gateway_receipt(self): + # Never open a live gateway database from a unit test. + receipts = load_recipe_module("receipt.py") + return lambda result: receipts.create_receipt(result, database=result / "absent.sqlite") + def test_preflight_failure_still_has_status_receipt_and_setup_exit(self): host = load_recipe_module("host.py") output = io.StringIO() run_id = "rw-openhands-fixture" with patch.object(host, "preflight", side_effect=ValueError("synthetic_setup_failure")), \ + patch.object(host, "create_receipt", side_effect=self.absent_gateway_receipt()), \ contextlib.redirect_stdout(output): code = host.run(self.result / "prefix", self.result, run_id=run_id, arm="engines-on") self.assertEqual(code, 3) @@ -532,6 +582,97 @@ def test_preflight_failure_still_has_status_receipt_and_setup_exit(self): ({"failure_stage": "preflight"}, 3)): self.assertEqual(host.result_exit(result), expected) + def test_agent_limit_terminations_are_graded_but_never_success(self): + # F16: an officially graded partial patch after an agent limit exits 1; + # grading infrastructure failures keep exit 3. + host = load_recipe_module("host.py") + graded = {"grader_exit_code": 0, "conversion_exit_code": 0} + for termination in ("stuck", "max_iterations_reached"): + for verdict, expected in (({"upstream_resolved": True, "upstream_bucket": "resolved_ids", **graded}, 1), + ({"upstream_resolved": False, "upstream_bucket": "unresolved_ids", **graded}, 1), + ({"upstream_resolved": False, "upstream_bucket": "empty_patch_ids", **graded}, 1), + ({"upstream_resolved": False, "upstream_bucket": "error_ids", **graded}, 3), + ({"upstream_resolved": None, "grader_exit_code": 124}, 3)): + with self.subTest(termination=termination, verdict=verdict): + self.assertEqual(host.result_exit({ + "agent_termination": termination, "failure_stage": None, "task_passed": False, + "evidence_complete": False, "upstream_grader": verdict}), expected) + + def test_session_key_preflight_checks_the_variable_name_only(self): + # SDK@fcc102a agent_server/__main__.py:282-285 binds all interfaces only + # with a session key; config.py:24 names OH_SESSION_API_KEYS_0. File + # format: docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124. + host = load_recipe_module("host.py") + name = "OH_SESSION_API_KEYS_0" + marker = "-".join(("synthetic", "fixture", "marker")) + env = self.result / "server.env" + for text, accepted in ((f"{name}={marker}\n", True), + (f"\N{BYTE ORDER MARK} \t{name}={marker}\r\n", True), + (f"OTHER_NAME=1\n{name}={marker}", True), + (f"# {name}={marker}\n", False), + (f"{name}\n", False), + (f"{name}_1={marker}\n", False), + (f" {name} ={marker}\n", False), + (f"OTHER_NAME={marker}\x0c{name}={marker}\n", False), + ("", False)): + with self.subTest(text=text.replace(marker, "")): + env.write_text(text) + env.chmod(0o600) + with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}): + if accepted: + self.assertEqual(host.check_server_env(), env) + continue + with self.assertRaises(ValueError) as caught: + host.check_server_env() + self.assertNotIn(marker, str(caught.exception)) + env.write_text(f"{name}={marker}\n") + env.chmod(0o644) + with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}), self.assertRaises(ValueError): + host.check_server_env() + with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": ""}), self.assertRaises(ValueError): + host.check_server_env() + + def test_missing_session_key_name_stops_preflight_without_exposing_values(self): + host = load_recipe_module("host.py") + marker = "-".join(("synthetic", "fixture", "marker")) + env = self.result / "server.env" + env.write_text(f"OTHER_NAME={marker}\n") + env.chmod(0o600) + output = io.StringIO() + with patch.object(host, "preflight", return_value=({}, {}, {})), \ + patch.object(host, "read_json") as read_json, \ + patch.object(host, "create_receipt", side_effect=self.absent_gateway_receipt()), \ + patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}), contextlib.redirect_stdout(output): + code = host.run(self.result / "prefix", self.result, run_id="rw-openhands-fixture", arm="control") + self.assertEqual(code, 3) + read_json.assert_not_called() # Stopped before the installation record. + path = self.result / "runs/rw-openhands-fixture/control" + self.assertEqual(json.loads(output.getvalue())["failure_stage"], "preflight") + self.assertEqual(json.loads((path / "window.json").read_text())["failure_type"], "ValueError") + for text in (output.getvalue(), *(p.read_text() for p in path.glob("*.json"))): + self.assertNotIn(marker, text) + + def test_port_outside_owned_range_fails_in_preflight(self): + # PR #428 crawl4ai host.py:44-45: exact int (bool refused) in 3730..3799. + host = load_recipe_module("host.py") + self.assertEqual(host.DEFAULT_PORT, 3730) + for port in (3730, 3740, 3799): + self.assertEqual(host.owned_port(port), port) + self.assertEqual(host.cli_port("3740"), 3740) + for index, port in enumerate((3729, 3800, True, 3740.0, "3740", None, host.cli_port("37x0"))): + with self.subTest(port=port): + with self.assertRaises(ValueError): + host.owned_port(port) + output = io.StringIO() + with patch.object(host, "preflight") as preflight, \ + patch.object(host, "create_receipt", side_effect=self.absent_gateway_receipt()), \ + contextlib.redirect_stdout(output): + code = host.run(self.result / "prefix", self.result, run_id=f"rw-openhands-port-{index}", + arm="control", port=port) + self.assertEqual(code, 3) + self.assertEqual(json.loads(output.getvalue())["failure_stage"], "preflight") + preflight.assert_not_called() + def test_clone_uses_pinned_swebench_branch_mapping(self): host = load_recipe_module("host.py") task = {"repo": "pytest-dev/pytest", "base_commit": "a" * 40} @@ -665,7 +806,8 @@ def test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected(self) (root / "window.json").write_text(json.dumps({"started_at": "2026-09-27T18:00:00Z", "finished_at": "2026-09-27T18:01:00Z"})) (root / "check.json").write_text("{}") report = module.create_receipt(root, database=root / "missing.sqlite") - self.assertEqual(report["worker_evidence_trust"], "worker-reported; not independent acceptance") + self.assertEqual(report["trace_status"], "not_collected") + self.assertIn("not_collected_reason", report) self.assertFalse(report["evidence_complete"]) def test_local_pass_field_is_never_a_worker_verdict(self): @@ -713,9 +855,45 @@ def test_truncated_native_summary_retains_failed_attempt_receipt(self): report = module.create_receipt(result, database=result / "absent.sqlite") self.assertFalse(report["task_passed"]) self.assertFalse(report["evidence_complete"]) - self.assertIsNone(report["observed_versions"]) + self.assertEqual(report["observed_versions"], "not_collected") self.assertEqual(report["gateway"]["status"], "unavailable") + def test_model_writable_files_never_populate_receipt_evidence(self): + # F17: /run-output and /state/server are writable by the model terminal's + # UID (host.py model_visible), and the agent-server event API reads that + # store (SDK@fcc102a event_store.py:144-169,320-362). Forged files stay unread. + module = load_recipe_module("receipt.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp).resolve() + for directory in ("worker", "input", "server/conversations"): + (result / directory).mkdir(parents=True) + (result / "window.json").write_text(json.dumps({ + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z", "worker_exit_code": 0})) + (result / "check.json").write_text("{}") + skills = ["tdd", "verification-before-completion"] + (result / "input/skills.json").write_text(json.dumps({"names": skills})) + forged = "".join(json.dumps(event) + "\n" for event in ( + {"kind": "ObservationEvent", "tool_name": "invoke_skill", + "observation": {"skill_name": "tdd", "is_error": False}}, + {"kind": "ObservationEvent", "tool_name": "ai-memory_memory_query", "observation": {"is_error": False}})) + (result / "worker/events.jsonl").write_text(forged) + (result / "server/conversations/events.jsonl").write_text(forged) + (result / "worker/skills-startup.json").write_text(json.dumps({"listed_skills": skills})) + (result / "worker/native-summary.json").write_text(json.dumps( + {"versions": {"openhands-sdk": pins["version"], "openhands-tools": pins["version"]}})) + opened = [] + bounded, parsed = module.read_bounded, module.read_json + with patch.object(module, "read_bounded", side_effect=lambda p, *a, **k: opened.append(Path(p)) or bounded(p, *a, **k)), \ + patch.object(module, "read_json", side_effect=lambda p: opened.append(Path(p)) or parsed(p)): + receipt = module.create_receipt(result, database=result / "absent.sqlite") + for field in ("trace_status", "observed_versions", "skills_listed_at_start", "skill_listing_matches_manifest", + "mcp_calls_observed", "mcp_errors_observed", "skills_observed"): + self.assertEqual(receipt[field], "not_collected", field) + self.assertIn("not_collected_reason", receipt) + self.assertFalse(receipt["evidence_complete"]) + self.assertEqual([p for p in opened if p.is_relative_to(result / "worker") or p.is_relative_to(result / "server")], []) + def test_cleanup_timeout_does_not_erase_primary_exit(self): host = load_recipe_module("host.py") with tempfile.TemporaryDirectory() as tmp: @@ -954,6 +1132,124 @@ def test_patch_export_cannot_use_host_git_filters_or_external_diff(self): self.assertIn("--no-ext-diff", diff.call_args.args[0]) self.assertIn("--no-textconv", diff.call_args.args[0]) + # SDK@fcc102a local_conversation.py:2021-2043,2339-2360 emit this event; + # utils/models.py:202-205 serializes kind as the class name. + LIMIT_EVENT = {"kind": "ConversationErrorEvent", "source": "environment", "code": "MaxIterationsReached", + "detail": "Agent reached maximum iterations limit (40)."} + ERROR_SEARCH = ("/events/search?kind=openhands.sdk.event.conversation_error.ConversationErrorEvent" + "&sort_order=TIMESTAMP_DESC&limit=1") + + def terminal_attempt(self, execution): + (self.result / "status.json").write_text(json.dumps({ + "status": "terminal", "execution_status": execution, "conversation_id": FIXTURE_CONVERSATION_ID, + "server_name": "rw-openhands-fixture-engines-on-server"})) + (self.result / "window.json").write_text(json.dumps({ + "arm": self.arm, "run_id": self.run_id, "instance_id": "django__django-11333", + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z"})) + (self.result / "task-identity.json").write_text(json.dumps({"instance_id": "django__django-11333", "base_commit": "a" * 40})) + (self.result / "dataset.json").write_text("[]") + + @staticmethod + def synthetic_report(bucket): + """Writes a SWE-bench 4.1.0 schema-2 report fixture; no grader runs.""" + def grade(prefix, result, dataset, instance_id, run_id): + report = {key: [] for key in ("resolved_ids", "unresolved_ids", "error_ids", "empty_patch_ids", "incomplete_ids")} + report.update({"schema_version": 2, "submitted_ids": [instance_id], bucket: [instance_id]}) + (result / ("OpenHands." + run_id + ".json")).write_text(json.dumps(report)) + return {"upstream_resolved": bucket == "resolved_ids", "grader_exit_code": 0, "conversion_exit_code": 0} + return grade + + def collect(self, dispatch, replies, grader=None): + receipts = load_recipe_module("receipt.py") + with patch.object(dispatch, "api_request", side_effect=replies) as api, \ + patch.object(dispatch, "stop_server", return_value=True), \ + patch.object(dispatch, "logged_command", return_value=0), \ + patch.object(dispatch.subprocess, "check_output", return_value=""), \ + patch.object(dispatch, "grade", side_effect=grader) as grade, \ + patch.object(dispatch, "create_receipt", + side_effect=lambda result: receipts.create_receipt(result, database=result / "absent.sqlite")), \ + contextlib.redirect_stdout(io.StringIO()): + code = dispatch.execute("result", self.state, self.run_id, self.arm) + return code, api, grade, json.loads((self.result / "receipt.json").read_text()) + + def test_agent_limit_terminations_grade_the_partial_patch_and_exit_1(self): + # F16. SDK@fcc102a conversation/state.py:48-79 terminal statuses; + # local_conversation.py:753-755 sets STUCK; :2021-2043 and :2339-2360 + # set ERROR plus ConversationErrorEvent code "MaxIterationsReached". + dispatch = load_recipe_module("dispatch.py") + final, limit = {"response": ""}, {"items": [self.LIMIT_EVENT], "next_page_id": None} + for execution, replies, bucket, termination in ( + ("stuck", [final], "unresolved_ids", "stuck"), + ("error", [final, limit], "empty_patch_ids", "max_iterations_reached"), + ("error", [final, limit], "resolved_ids", "max_iterations_reached")): + with self.subTest(execution=execution, bucket=bucket): + self.terminal_attempt(execution) + code, api, grade, receipt = self.collect(dispatch, replies, self.synthetic_report(bucket)) + self.assertEqual(code, 1) + grade.assert_called_once() + self.assertEqual(receipt["agent_termination"], termination) + self.assertIsNone(receipt["failure_stage"]) + self.assertFalse(receipt["task_passed"]) + self.assertEqual(receipt["upstream_grader"]["upstream_bucket"], bucket) + self.assertEqual(api.call_count, len(replies)) + if execution == "error": + self.assertEqual(api.call_args.args[:2], + ("GET", f"/api/conversations/{FIXTURE_CONVERSATION_ID}{self.ERROR_SEARCH}")) + + def test_other_native_errors_stay_ungraded_infrastructure_failures(self): + # SDK@fcc102a local_conversation.py:2044-2057 emits LLMAuthenticationError. + dispatch = load_recipe_module("dispatch.py") + other = dict(self.LIMIT_EVENT, code="LLMAuthenticationError") + for page in ({"items": [other], "next_page_id": None}, {"items": [], "next_page_id": None}, + {"items": [dict(self.LIMIT_EVENT, source="agent")]}, + {"items": [dict(self.LIMIT_EVENT, kind="MessageEvent")]}, + {"items": [self.LIMIT_EVENT, self.LIMIT_EVENT]}, [self.LIMIT_EVENT]): + with self.subTest(page=page): + self.terminal_attempt("error") + code, _, grade, receipt = self.collect(dispatch, [{"response": ""}, page]) + self.assertEqual(code, 3) + grade.assert_not_called() + self.assertEqual(receipt["failure_stage"], "agent") + self.assertEqual(receipt["agent_termination"], "error") + + def test_selected_port_flows_from_prepare_to_dispatch(self): + host = load_recipe_module("host.py") + dispatch = load_recipe_module("dispatch.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + (self.result / "worker").mkdir() + + def rendered(args, name, log, timeout): + (self.result / "worker/start.json").write_text(json.dumps({"agent": {"llm": {}}, "workspace": {}, "initial_message": {}})) + return 0 + + with patch.dict(sys.modules, {"dispatch": dispatch}), \ + patch.object(host, "execute_container", side_effect=rendered), \ + patch.object(host, "logged_command", return_value=0) as launch, \ + patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ + patch.object(dispatch, "check_server", return_value=None) as check, \ + patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}): + host.prepare_native_dispatch(self.result, self.state / "prefix", pins, [], "rw-openhands-egress-engines-on", + {"variables": {"HOST_PATH": "/usr/bin"}}, + load_recipe_module("recipe.py").arm_config(self.arm), self.run_id, port=3740) + self.assertIn("127.0.0.1:3740:8000", launch.call_args.args[0]) + self.assertNotIn("127.0.0.1:3730:8000", launch.call_args.args[0]) + self.assertEqual(check.call_args.kwargs["port"], 3740) + self.assertEqual(json.loads((self.result / "status.json").read_text())["port"], 3740) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + with patch.object(dispatch, "api_request", return_value={"id": FIXTURE_CONVERSATION_ID}) as api, \ + patch.object(dispatch, "compression_snapshot", return_value=None), contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 0) + self.assertEqual(api.call_args.kwargs["port"], 3740) + status = json.loads((self.result / "status.json").read_text()) + (self.result / "status.json").write_text(json.dumps({**status, "port": 8000})) + with patch.object(dispatch, "api_request") as api, patch.object(dispatch, "stop_server"), \ + contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(dispatch.execute("wait", self.state, self.run_id, self.arm), 3) + api.assert_not_called() + args = dispatch.server_command(pins, "rw-openhands-fixture-server", [], "rw-openhands-egress-control", + "/private/server.env") + self.assertIn("127.0.0.1:3730:8000", args) + if __name__ == "__main__": unittest.main() From 12a70dbdf6ecaf6bd531a1edcf76dfe632ecffbe Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 13:06:22 -0400 Subject: [PATCH 07/22] OpenHands takeover phase 1 follow-up: bound F16, fix install image identity Applies the coordinator decisions of 2026-09-28 on top of bac0b278. - F16: finish_result keeps a termination label only as a refinement of the REST execution_status (PERMITTED_TERMINATIONS). Exit 0 requires REST "finished"; a planted event can move exit 3 to 1 at most. The README states the residual (SDK@fcc102a event_store.py:144-169,320-362, event_service.py:420-431, subprocess_terminal.py:157-170) and the analysis rule: exit 1 and 3 are both non-success, and reruns re-queue only exit 3. - install(): pinned_image_identity() requires the exact pinned ref in RepoDigests. It accepts a plain .Id equal to the index digest (containerd store) or the config digest (classic store), and on containerd it checks the linux/amd64 platform manifest. Sources: moby docker-v29.8.1@464cd50c daemon/containerd/image_inspect.go:28,71-73, 95,97; daemon/images/image_inspect.go:59; daemon/internal/image/store.go:152,160; fs.go:120; daemon/internal/distribution/pull_v2.go:431-434. - The empty OH_SESSION_API_KEYS_0 value is documented as a residual. - run_worker, record_event and observations() are kept because they are still referenced. Red: 73 tests, failures=29, errors=1. Green: 73 tests OK. Record: blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 73 ++++++++- .../runtime-workers/openhands/dispatch.py | 11 +- .../evidence/phase1-followup-commands.json | 148 ++++++++++++++++++ blueprints/runtime-workers/openhands/host.py | 50 +++++- .../runtime-workers/openhands/research.md | 18 ++- tests/test_runtime_worker_openhands.py | 146 ++++++++++++++++- 6 files changed, 426 insertions(+), 20 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 326ff5795..1d5a852da 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -73,6 +73,42 @@ host's containerd image store, `docker image inspect` reports the index digest as the image ID, so the receipt verifies the platform manifest and config digests instead. +`install()` checks the pulled image on either Docker image store before any +container starts. It runs two inspects, one plain and one with +`--platform linux/amd64`. The primary check is that both list the exact pinned +reference in `.RepoDigests`; otherwise it raises `image_repo_digest_mismatch`. +The plain `.Id` must then be the index digest (containerd store) or the config +digest (classic store); any other value is refused with +`image_configuration_hash_mismatch`. A wrong platform manifest raises +`image_platform_manifest_mismatch`. `installation.json` records the store. + +| Check | containerd image store | Classic store | +| --- | --- | --- | +| Plain `.Id` | index `sha256:02ef66fd...` | config `sha256:39426d8c...` | +| Platform `.Id` | manifest `sha256:ec7ed86f...` | config `sha256:39426d8c...` | +| Platform `.Descriptor.digest` | manifest `sha256:ec7ed86f...` | absent | +| Config digest | not reported; bound by the content-addressed manifest, as in the scan receipt | checked as `.Id` | +| Platform manifest | checked | not retained; the pinned index names it | + +Sources: moby docker-v29.8.1 (464cd50c, the engine commit this host reports) +`daemon/containerd/image_inspect.go:28,71-73,95,97` for the containerd store. +For the classic store: `daemon/images/image_inspect.go:59`, +`daemon/images/image.go:160-197`, `daemon/internal/image/store.go:152,160` +and `daemon/internal/image/fs.go:120`. A classic pull by index digest verifies +the index and the selected platform manifest, then records the pinned index +reference in RepoDigests +(`daemon/internal/distribution/pull_v2.go:431-434,705-747,845-867`). Inspect +with `--platform` needs Engine API v1.49 or later +(`api/docs/CHANGELOG.md:175-180`). The moby client at the same tag refuses the +option below that version (`client/image_inspect.go:33-36`), so an older daemon +fails the install before any container starts. The Engine API reference still +describes `Id` as the config digest (`api/swagger.yaml:1826-1850`). The earlier +config-only check followed that description, so it would have refused the +pinned pull on this host. Docker's containerd image-store page says that store +is the default for fresh Engine 29 installations (docker/docs@3c117d8e +`content/manuals/engine/storage/containerd.md:11-14`). It does not describe +image IDs, so the moby source is the citation here. + [install.sh](install.sh) installs the worker through the existing hash-required wheel/runtime locks and preinstalled hashed build tools. The install container can write only the worker venv and owned cache; the recipe snapshot is mounted @@ -164,7 +200,12 @@ Set these private inputs outside all worktrees: that variable. It parses Docker's env-file syntax for the name only and never extracts, prints or logs the value (docker/cli@v29.8.1 `pkg/kvfile/kvfile.go:92-124`). A bare name, which Docker would fill from the - caller's environment, is refused. The server listens on every container + caller's environment, is refused. An empty value (`OH_SESSION_API_KEYS_0=`) + still passes. This residual follows from never reading values. SDK@fcc102a + `openhands-agent-server/openhands/agent_server/env_parser.py:183-197` then + yields a key list holding one empty string, so the server treats + authentication as enabled (`__main__.py:30-48`). The coordinator must write a + non-empty key for each attempt. The server listens on every container interface only when a session key is set (SDK@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, `config.py:24`). @@ -241,9 +282,33 @@ event through the native search route while the server still runs `event_service.py:456-460`, `models.py:95-99`). Only exactly that kind, source and code counts as `max_iterations_reached`; any other error stays ungraded and exits 3. The receipt records `agent_termination`, and an agent-limit end exits 1 -even when its partial patch resolves the task. The event store is writable by -the model's UID, so this server-reported value only chooses between exit 1 and 3 -and can never produce a pass. +even when its partial patch resolves the task. + +`finish_result` keeps a termination label only if it refines the REST +`execution_status`. `finished` stays `finished`, `stuck` stays `stuck`, and +`error` may become `max_iterations_reached`. Any other pairing becomes an +ungraded `error`. Only REST `finished` can set `worker_exit_code` 0, so exit 0 +never depends on the event store. + +**Residual (F16).** The error-event page is not independent evidence. The +model's terminal runs as the server's user (SDK@fcc102a +`openhands-tools/openhands/tools/terminal/terminal/subprocess_terminal.py:157-170`). +The server serves events from files in the conversation store, reads them back +and rebuilds a stale index from disk +(`openhands-sdk/openhands/sdk/conversation/event_store.py:144-169,320-362`, +`openhands-agent-server/openhands/agent_server/event_service.py:420-431`). A +planted `MaxIterationsReached` event can therefore turn an ungraded native +error (exit 3) into a graded agent-limit end (exit 1). No planted event can +reach exit 0. Tests cover both the 3-to-1 move and planted success-looking +events. Treating every `error` as exit 3 would remove this move. It would also +label every honest iteration-limit run an infrastructure failure, a systematic +bias larger than this bounded forgery. + +**Analysis rule.** Every A/B or acceptance tally counts exit 1 and exit 3 alike +as non-success, and a rerun policy may re-queue only exit 3. A forged 3-to-1 +move therefore cannot improve an arm's success rate; at most it suppresses a +rerun. +A receipt with `upstream_resolved: true` that exited 1 is not a success. | Final exit code | Meaning | | --- | --- | diff --git a/blueprints/runtime-workers/openhands/dispatch.py b/blueprints/runtime-workers/openhands/dispatch.py index a8af1ef5c..dcc3f63f2 100644 --- a/blueprints/runtime-workers/openhands/dispatch.py +++ b/blueprints/runtime-workers/openhands/dispatch.py @@ -23,6 +23,11 @@ TERMINAL = {"finished", "error", "stuck"} +# F16 bound: a termination label is kept only as a refinement of the REST +# execution_status. The event-store label can turn "error" into an agent limit +# (exit 3 -> 1) but never stands in for "finished", the only route to exit 0. +PERMITTED_TERMINATIONS = {"finished": {"finished"}, "stuck": {"stuck"}, + "error": {"error", "max_iterations_reached"}} # Latest ConversationErrorEvent only. SDK@fcc102a event_router.py:68-139 serves # the page; event_service.py:456-460 matches kind on the module-qualified class # name; models.py:95-99 defines TIMESTAMP_DESC. @@ -155,8 +160,10 @@ def stop_server(result, status): def finish_result(result, status, window): removed = stop_server(result, status) - termination = status.get("agent_termination") or status.get("execution_status") - if termination not in {"finished", "error", *AGENT_LIMITS}: + execution = status.get("execution_status") + termination = status.get("agent_termination") or execution + permitted = PERMITTED_TERMINATIONS.get(execution, set()) if isinstance(execution, str) else set() + if not isinstance(termination, str) or termination not in permitted: termination = "error" window["agent_termination"] = termination window["worker_exit_code"] = 0 if termination == "finished" else 1 diff --git a/blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json b/blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json new file mode 100644 index 000000000..cb33fc762 --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json @@ -0,0 +1,148 @@ +{ + "evidence_class": "Offline synthetic and source-contract unit checks of the recipe, plus one read-only image inspect of the pinned image on this host; not native runtime, grader or model acceptance.", + "sanitization": "The worktree prefix is shown as and the scratch directory as . In every unit output the repeated module prefix `tests.test_runtime_worker_openhands.` inside parentheses is shortened. The red output is an excerpt: the summary lines, every non-ok test line and each failure's first exception line, with tracebacks omitted; rerunning the command on the named tree reproduces it. Other outputs are complete.", + "base": "bac0b278cfe7834f85bef71a199c0757a29e1825", + "decisions": "Coordinator decisions of 2026-09-28: keep the newest-ConversationErrorEvent design and bound it so exit 0 never depends on the event store; make install() image identity work on the containerd and classic stores; keep the empty OH_SESSION_API_KEYS_0 value as a documented residual; delete run_worker, record_event and observations() only if nothing references them.", + "item_tests": { + "F16_bound": [ + "OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass", + "OpenHandsDispatchTests.test_planted_success_looking_events_move_exit_3_to_1_at_most" + ], + "install_image_identity": [ + "OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores", + "OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container", + "OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock" + ], + "empty_session_key_value": [], + "unused_symbols": [] + }, + "item_notes": { + "F16_bound": "test_planted_success_looking_events_move_exit_3_to_1_at_most also passes on the base tree, so it is a regression lock rather than a red-to-green item. test_only_the_rest_status_can_admit_a_pass is the red item.", + "install_image_identity": "Seven refusal subtests also pass on the base tree, because the base code refused every inspect output with image_configuration_hash_mismatch. They are plain .Id equal to the manifest, an unrelated digest or null on both stores, and a classic platform .Id equal to the manifest. Each differs in one field from an accepting shape that fails on the base tree. Command 03 shows that the refusal cases detect four targeted weakenings.", + "live_daemon": "Command 04 ran pinned_image_identity() against this host's rootless Docker 29.8.1 containerd store. It used read-only image inspects and started no container; the container count was 1 before and after, a container this work did not create. The classic-store path is covered by mocks only, because no classic-store daemon was available.", + "empty_session_key_value": "Documentation only (README.md, research.md). The residual stays by coordinator decision; values are never read.", + "unused_symbols": "No deletion. git grep at the base shows run_worker (worker.py:232), record_event (worker.py:202) and observations (tests/test_runtime_worker_openhands.py:936-937) are still referenced." + }, + "commands": [ + { + "id": "01-red-unit", + "tree": "New and changed tests on the unfixed recipe at the base: host.py, dispatch.py, README.md and research.md unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 73 tests in 0.871s", + "FAILED (failures=29, errors=1)" + ], + "non_ok_tests": [ + " test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='error', label='finished') ... FAIL", + " test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='stuck', label='finished') ... FAIL", + " test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution=None, label='finished') ... FAIL", + " test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='error', label='stuck') ... FAIL", + " test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='finished', label='max_iterations_reached') ... FAIL", + " test_install_accepts_the_pinned_image_on_containerd_and_classic_stores (OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores) (store='containerd') ... FAIL", + " test_install_accepts_the_pinned_image_on_containerd_and_classic_stores (OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores) (store='classic') ... FAIL", + "test_install_actually_uses_narrow_mounts_and_grader_lock (OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ERROR", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': []}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': None}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['docker.io/ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/other@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server:1.49.6-python']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:0000000000000000000000000000000000000000000000000000000000000000']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Architecture': 'arm64'}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': []}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': None}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['docker.io/ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/other@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server:1.49.6-python']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:0000000000000000000000000000000000000000000000000000000000000000']}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'Architecture': 'arm64'}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Id': 'sha256:0000000000000000000000000000000000000000000000000000000000000000'}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Descriptor': {'mediaType': 'application/vnd.oci.image.manifest.v1+json', 'digest': 'sha256:0000000000000000000000000000000000000000000000000000000000000000', 'size': 4313, 'platform': {'architecture': 'amd64', 'os': 'linux'}}}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Descriptor': None}) ... FAIL", + " test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'Descriptor': {'mediaType': 'application/vnd.oci.image.manifest.v1+json', 'digest': 'sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12', 'size': 4313, 'platform': {'architecture': 'amd64', 'os': 'linux'}}}) ... FAIL" + ], + "first_exception_lines": [ + "ERROR: test_install_actually_uses_narrow_mounts_and_grader_lock (OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) => AttributeError: 'NoneType' object has no attribute 'args'", + "FAIL: test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='error', label='finished') => AssertionError: Expected 'grade' to not have been called. Called 1 times.", + "FAIL: test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='stuck', label='finished') => AssertionError: Expected 'grade' to not have been called. Called 1 times.", + "FAIL: test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution=None, label='finished') => AssertionError: Expected 'grade' to not have been called. Called 1 times.", + "FAIL: test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='error', label='stuck') => AssertionError: Expected 'grade' to not have been called. Called 1 times.", + "FAIL: test_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) (execution='finished', label='max_iterations_reached') => AssertionError: Expected 'grade' to not have been called. Called 1 times.", + "FAIL: test_install_accepts_the_pinned_image_on_containerd_and_classic_stores (OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores) (store='containerd') => AssertionError: 'image_configuration_hash_mismatch' is not None", + "FAIL: test_install_accepts_the_pinned_image_on_containerd_and_classic_stores (OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores) (store='classic') => AssertionError: 'image_configuration_hash_mismatch' is not None", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': []}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': None}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['docker.io/ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/other@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server:1.49.6-python']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:0000000000000000000000000000000000000000000000000000000000000000']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Architecture': 'arm64'}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': []}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': None}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['docker.io/ghcr.io/openhands/agent-server@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/other@sha256:02ef66fdf0b22a40b0b55c5cca8d1790cfd260c5069f7c0b99edc1b505d5ab37']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='plain', change={'RepoDigests': ['ghcr.io/openhands/agent-server:1.49.6-python']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'RepoDigests': ['ghcr.io/openhands/agent-server@sha256:0000000000000000000000000000000000000000000000000000000000000000']}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_repo_digest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'Architecture': 'arm64'}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Id': 'sha256:0000000000000000000000000000000000000000000000000000000000000000'}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Descriptor': {'mediaType': 'application/vnd.oci.image.manifest.v1+json', 'digest': 'sha256:0000000000000000000000000000000000000000000000000000000000000000', 'size': 4313, 'platform': {'architecture': 'amd64', 'os': 'linux'}}}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='containerd', view='platform', change={'Descriptor': None}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'", + "FAIL: test_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) (store='classic', view='platform', change={'Descriptor': {'mediaType': 'application/vnd.oci.image.manifest.v1+json', 'digest': 'sha256:ec7ed86f4021a21e161815853156521b44929d913f2e6ee06a173ef2f660aa12', 'size': 4313, 'platform': {'architecture': 'amd64', 'os': 'linux'}}}) => AssertionError: 'image_configuration_hash_mismatch' != 'image_platform_manifest_mismatch'" + ] + }, + { + "id": "02-green-unit", + "tree": "Final tree, including the documentation and this record.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "output": "test_agent_limit_terminations_grade_the_partial_patch_and_exit_1 (OpenHandsDispatchTests.test_agent_limit_terminations_grade_the_partial_patch_and_exit_1) ... ok\ntest_compression_delta_is_separate_and_missing_or_reset_is_unknown (OpenHandsDispatchTests.test_compression_delta_is_separate_and_missing_or_reset_is_unknown) ... ok\ntest_duplicate_start_keeps_running_status_and_serial_lock (OpenHandsDispatchTests.test_duplicate_start_keeps_running_status_and_serial_lock) ... ok\ntest_model_mount_permissions_do_not_follow_symlinks (OpenHandsDispatchTests.test_model_mount_permissions_do_not_follow_symlinks) ... ok\ntest_only_the_rest_status_can_admit_a_pass (OpenHandsDispatchTests.test_only_the_rest_status_can_admit_a_pass) ... ok\ntest_other_native_errors_stay_ungraded_infrastructure_failures (OpenHandsDispatchTests.test_other_native_errors_stay_ungraded_infrastructure_failures) ... ok\ntest_patch_export_cannot_use_host_git_filters_or_external_diff (OpenHandsDispatchTests.test_patch_export_cannot_use_host_git_filters_or_external_diff) ... ok\ntest_planted_success_looking_events_move_exit_3_to_1_at_most (OpenHandsDispatchTests.test_planted_success_looking_events_move_exit_3_to_1_at_most) ... ok\ntest_preload_installs_dynamic_call_adapter_through_upstream_hook (OpenHandsDispatchTests.test_preload_installs_dynamic_call_adapter_through_upstream_hook) ... ok\ntest_prepare_serializes_body_offline_then_launches_native_server (OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server) ... ok\ntest_request_serialization_failure_cannot_leave_prepared_receipt (OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt) ... ok\ntest_result_refuses_export_until_container_removal_is_confirmed (OpenHandsDispatchTests.test_result_refuses_export_until_container_removal_is_confirmed) ... ok\ntest_result_retry_preserves_collected_official_receipt (OpenHandsDispatchTests.test_result_retry_preserves_collected_official_receipt) ... ok\ntest_selected_port_flows_from_prepare_to_dispatch (OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch) ... ok\ntest_server_preserves_native_entrypoint_and_uses_supported_preload (OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload) ... ok\ntest_start_wait_result_use_native_routes_and_deterministic_host_files (OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files) ... ok\ntest_wait_deadline_interrupts_and_emits_setup_failure (OpenHandsDispatchTests.test_wait_deadline_interrupts_and_emits_setup_failure) ... ok\ntest_cleanup_timeout_does_not_erase_primary_exit (OpenHandsReceiptTests.test_cleanup_timeout_does_not_erase_primary_exit) ... ok\ntest_entry_gateway_usage_and_effort_are_counted_once_without_ids (OpenHandsReceiptTests.test_entry_gateway_usage_and_effort_are_counted_once_without_ids) ... ok\ntest_gateway_reads_only_authorized_columns_and_preserves_unknown_usage (OpenHandsReceiptTests.test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage) ... ok\ntest_local_pass_field_is_never_a_worker_verdict (OpenHandsReceiptTests.test_local_pass_field_is_never_a_worker_verdict) ... ok\ntest_model_writable_files_never_populate_receipt_evidence (OpenHandsReceiptTests.test_model_writable_files_never_populate_receipt_evidence) ... ok\ntest_skills_and_mcp_require_observations (OpenHandsReceiptTests.test_skills_and_mcp_require_observations) ... ok\ntest_truncated_native_summary_retains_failed_attempt_receipt (OpenHandsReceiptTests.test_truncated_native_summary_retains_failed_attempt_receipt) ... ok\ntest_verdict_is_read_from_official_report_even_when_wrapper_claims_pass (OpenHandsReceiptTests.test_verdict_is_read_from_official_report_even_when_wrapper_claims_pass) ... ok\ntest_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected (OpenHandsReceiptTests.test_worker_evidence_cannot_complete_receipt_and_symlinks_are_rejected) ... ok\ntest_agent_server_image_scan_receipt_matches_the_pinned_digest (OpenHandsRecipeTests.test_agent_server_image_scan_receipt_matches_the_pinned_digest) ... ok\ntest_dispatch_fixtures_pass_publication_identifier_policy (OpenHandsRecipeTests.test_dispatch_fixtures_pass_publication_identifier_policy) ... ok\ntest_every_container_is_owned_and_has_no_published_port (OpenHandsRecipeTests.test_every_container_is_owned_and_has_no_published_port) ... ok\ntest_explicit_arms_keep_one_slash_routes_and_compression_separate (OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate) ... ok\ntest_fresh_call_headers_keep_session_affinity (OpenHandsRecipeTests.test_fresh_call_headers_keep_session_affinity) ... ok\ntest_gateway_model_and_container_topology (OpenHandsRecipeTests.test_gateway_model_and_container_topology) ... ok\ntest_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes (OpenHandsRecipeTests.test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes) ... ok\ntest_grader_sync_keeps_the_hash_seeded_interpreter (OpenHandsRecipeTests.test_grader_sync_keeps_the_hash_seeded_interpreter) ... ok\ntest_header_transport_preserves_exact_native_llm_type_and_restores_methods (OpenHandsRecipeTests.test_header_transport_preserves_exact_native_llm_type_and_restores_methods) ... ok\ntest_host_template_names_the_two_required_network_policy_receipts (OpenHandsRecipeTests.test_host_template_names_the_two_required_network_policy_receipts) ... ok\ntest_immutable_artifacts (OpenHandsRecipeTests.test_immutable_artifacts) ... ok\ntest_installer_and_runtime_are_scoped (OpenHandsRecipeTests.test_installer_and_runtime_are_scoped) ... ok\ntest_mcp_limits (OpenHandsRecipeTests.test_mcp_limits) ... ok\ntest_model_container_is_nonroot_and_network_is_explicit (OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_explicit) ... ok\ntest_mount_allowlist_checks_resolved_targets_and_secret_ancestors (OpenHandsRecipeTests.test_mount_allowlist_checks_resolved_targets_and_secret_ancestors) ... ok\ntest_native_start_request_uses_sdk_serialization_and_shared_agent (OpenHandsRecipeTests.test_native_start_request_uses_sdk_serialization_and_shared_agent) ... ok\ntest_no_public_binds_or_secret_material (OpenHandsRecipeTests.test_no_public_binds_or_secret_material) ... ok\ntest_qmd_guard_requires_explicit_scope_and_lexical_query (OpenHandsRecipeTests.test_qmd_guard_requires_explicit_scope_and_lexical_query) ... ok\ntest_remote_mcp_services_are_not_registered_in_model_network (OpenHandsRecipeTests.test_remote_mcp_services_are_not_registered_in_model_network) ... ok\ntest_round1_orphans_are_not_shipped (OpenHandsRecipeTests.test_round1_orphans_are_not_shipped) ... ok\ntest_runtime_model_override_and_native_tool_filter (OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter) ... ok\ntest_serena_activates_plain_fixture_and_keeps_metadata_outside_it (OpenHandsRecipeTests.test_serena_activates_plain_fixture_and_keeps_metadata_outside_it) ... ok\ntest_worker_arm_headers_and_response_correlation_capture (OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture) ... ok\ntest_agent_limit_terminations_are_graded_but_never_success (OpenHandsUpstreamAdapterTests.test_agent_limit_terminations_are_graded_but_never_success) ... ok\ntest_clone_uses_pinned_swebench_branch_mapping (OpenHandsUpstreamAdapterTests.test_clone_uses_pinned_swebench_branch_mapping) ... ok\ntest_egress_policy_missing_or_wrong_arm_fails_before_model_start (OpenHandsUpstreamAdapterTests.test_egress_policy_missing_or_wrong_arm_fails_before_model_start) ... ok\ntest_frozen_grader_digest_is_checked_before_retagging (OpenHandsUpstreamAdapterTests.test_frozen_grader_digest_is_checked_before_retagging) ... ok\ntest_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags (OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags) ... ok\ntest_input_sanity_does_not_grade_the_patch (OpenHandsUpstreamAdapterTests.test_input_sanity_does_not_grade_the_patch) ... ok\ntest_install_accepts_the_pinned_image_on_containerd_and_classic_stores (OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores) ... ok\ntest_install_actually_uses_narrow_mounts_and_grader_lock (OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock) ... ok\ntest_install_refuses_each_image_identity_mismatch_before_any_container (OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container) ... ok\ntest_installer_mounts_exclude_snapshot_from_writable_paths (OpenHandsUpstreamAdapterTests.test_installer_mounts_exclude_snapshot_from_writable_paths) ... ok\ntest_known_fail_is_not_a_successful_process_verdict (OpenHandsUpstreamAdapterTests.test_known_fail_is_not_a_successful_process_verdict) ... ok\ntest_known_pass_relays_upstream_resolution (OpenHandsUpstreamAdapterTests.test_known_pass_relays_upstream_resolution) ... ok\ntest_malformed_missing_duplicate_and_conflicting_outputs_fail_closed (OpenHandsUpstreamAdapterTests.test_malformed_missing_duplicate_and_conflicting_outputs_fail_closed) ... ok\ntest_missing_session_key_name_stops_preflight_without_exposing_values (OpenHandsUpstreamAdapterTests.test_missing_session_key_name_stops_preflight_without_exposing_values) ... ok\ntest_mount_roots_match_selected_adoption_layout (OpenHandsUpstreamAdapterTests.test_mount_roots_match_selected_adoption_layout) ... ok\ntest_official_grader_container_transport_preserves_upstream_settings (OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings) ... ok\ntest_port_outside_owned_range_fails_in_preflight (OpenHandsUpstreamAdapterTests.test_port_outside_owned_range_fails_in_preflight) ... ok\ntest_preflight_failure_still_has_status_receipt_and_setup_exit (OpenHandsUpstreamAdapterTests.test_preflight_failure_still_has_status_receipt_and_setup_exit) ... ok\ntest_project_skills_bookkeeping_is_excluded_from_worker_patch (OpenHandsUpstreamAdapterTests.test_project_skills_bookkeeping_is_excluded_from_worker_patch) ... ok\ntest_project_skills_install_refuses_task_path_conflicts (OpenHandsUpstreamAdapterTests.test_project_skills_install_refuses_task_path_conflicts) ... ok\ntest_session_key_preflight_checks_the_variable_name_only (OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only) ... ok\ntest_shared_skills_installer_is_the_only_install_path (OpenHandsUpstreamAdapterTests.test_shared_skills_installer_is_the_only_install_path) ... ok\ntest_shell_entrypoints_forward_stable_dispatch_flags (OpenHandsUpstreamAdapterTests.test_shell_entrypoints_forward_stable_dispatch_flags) ... ok\ntest_task_adapter_hides_oracle_and_requires_frozen_original_bytes (OpenHandsUpstreamAdapterTests.test_task_adapter_hides_oracle_and_requires_frozen_original_bytes) ... ok\n\n----------------------------------------------------------------------\nRan 73 tests in 0.848s\n\nOK\n" + }, + { + "id": "03-mutation", + "tree": "Copies of the final test file and recipe; one mutation at a time.", + "command": "M=/mut # copy of tests/, scripts/validate.py and the recipe from the fixed tree\nH=$M/blueprints/runtime-workers/openhands/host.py\nT=tests.test_runtime_worker_openhands.OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container\n# each mutation is applied to a fresh copy of host.py, then: (cd $M && TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest $T)\nM1: sed 's/store = \"containerd\" if image_id == index else \"classic\" if image_id == config else None/store = \"classic\" if image_id == config else \"containerd\"/'\nM2: sed 's/if any(ref not in (view.get(\"RepoDigests\") or \\[\\]) for view in views):/if False:/'\nM3: sed 's/ elif selected.get(\"Id\") != config:/ elif False:/'\nM4: sed 's/if any(ref not in (view.get(\"RepoDigests\") or \\[\\]) for view in views):/if any(not any(ref in str(v) for v in (view.get(\"RepoDigests\") or [])) for view in views):/'", + "results": { + "baseline": "OK", + "M1": "FAILED (failures=6)", + "M2": "FAILED (failures=16)", + "M3": "FAILED (failures=1)", + "M4": "FAILED (failures=2)" + } + }, + { + "id": "04-live-identity", + "tree": "Final host.py.", + "command": "cd /blueprints/runtime-workers/openhands\nPATH=\"$HOME/.local/share/codex-ecosystem/tools/docker-rootless-29.8.1/bin:$PATH\" TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'\nimport json, sys\nsys.path.insert(0, \".\")\nimport host\npins = json.load(open(\"pins.json\"))\nprint(\"pinned_image_identity ->\", host.pinned_image_identity(pins[\"image\"]))\nPY", + "exit_code": 0, + "output": "pinned_image_identity -> containerd\n" + }, + { + "id": "05-diff-check", + "command": "git diff --check", + "exit_code": 0, + "output": "", + "note": "Run on the tracked changes. Before the commit, git diff --cached --check also covered this new record." + }, + { + "id": "06-static", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 - <<'PY'\nimport ast\nimport subprocess\nfrom pathlib import Path\nfrom scripts.validate import PRIVATE_CONTENT\nroot=Path('blueprints/runtime-workers/openhands')\npaths=list(root.rglob('*.py'))+[Path('tests/test_runtime_worker_openhands.py')]\nfor path in paths: ast.parse(path.read_text(), filename=str(path))\nscripts=list(root.rglob('*.sh'))\nfor path in scripts: subprocess.run(['bash','-n',str(path)], check=True)\npaths=[root/'evidence/phase1-followup-commands.json',root/'research.md',root/'README.md',Path('tests/test_runtime_worker_openhands.py'),root/'host.py',root/'dispatch.py']\nhits=[f'{p}: {name}' for p in paths for name,pattern in PRIVATE_CONTENT if pattern.search(p.read_text())]\nprint(f'Parsed Python sources: {len(list(root.rglob(\"*.py\")))+1}; shell syntax checks: {len(scripts)}')\nprint('Publication text scan: '+ ('; '.join(hits) if hits else 'no private-content matches'))\nraise SystemExit(bool(hits))\nPY", + "exit_code": 0, + "output": "Parsed Python sources: 12; shell syntax checks: 4\nPublication text scan: no private-content matches\n" + }, + { + "id": "07-validate", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py", + "exit_code": 1, + "output": "Publication validation failed:\nblueprints/runtime-workers/openhands/README.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/README.md: byte count mismatch\nfiles[1024]: file missing\nblueprints/runtime-workers/openhands/dispatch.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/dispatch.py: byte count mismatch\nfiles[1029]: file missing\nfiles[1030]: file missing\nfiles[1031]: file missing\nfiles[1032]: file missing\nfiles[1034]: file missing\nblueprints/runtime-workers/openhands/host.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/host.py: byte count mismatch\nblueprints/runtime-workers/openhands/receipt.py: SHA-256 mismatch\nblueprints/runtime-workers/openhands/receipt.py: byte count mismatch\nblueprints/runtime-workers/openhands/research.md: SHA-256 mismatch\nblueprints/runtime-workers/openhands/research.md: byte count mismatch\ntests/test_runtime_worker_openhands.py: SHA-256 mismatch\ntests/test_runtime_worker_openhands.py: byte count mismatch\nblueprints/runtime-workers/openhands/config/skills.lock.json: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/README.md: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py: file missing\nblueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py: file missing\nblueprints/runtime-workers/openhands/e2e/frozen.json: file missing\nblueprints/runtime-workers/openhands/e2e/task.txt: file missing\n", + "note": "Expected until the coordinator re-registers evidence. The failures match phase 1: SHA-256 and byte-count mismatches for the six changed registered files, and file-missing entries (by index and by path) for the six deleted orphans. This commit changes only files already on that list and adds this unregistered record. manifests/evidence.json is not edited." + } + ] +} diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index 0f93366b2..8296d1ed0 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -408,6 +408,51 @@ def download_verified(artifact, target): temporary.replace(target) +def pinned_image_identity(image): + """Check the pulled image on the containerd or the classic image store. + + moby docker-v29.8.1 (464cd50c). containerd store: daemon/containerd/ + image_inspect.go:28,71-73,95,97 report the pulled index as Id and + Descriptor, or the platform manifest when --platform is given. Classic + store: daemon/images/image_inspect.go:59 reports the config digest + (daemon/internal/image/store.go:152,160; fs.go:120) and no Descriptor + (api/swagger.yaml:1839-1850); its pull by index digest verifies the + platform manifest and records the index reference in RepoDigests + (daemon/internal/distribution/pull_v2.go:431-434,705-747,845-867). The + containerd store reports no config digest; there the content-addressed + platform manifest binds it. + """ + ref, platform = image["ref"], image["platform"] + index = "sha256:" + ref.rsplit("@sha256:", 1)[1] + manifest, config = "sha256:" + image["manifest_sha256"], "sha256:" + image["config_sha256"] + views = [] + for extra in ([], ["--platform", platform]): + found = json.loads(subprocess.check_output(DOCKER + ["image", "inspect", *extra, ref], text=True, timeout=30)) + if not isinstance(found, list) or len(found) != 1 or not isinstance(found[0], dict): + raise ValueError("image_configuration_hash_mismatch") + views.append(found[0]) + plain, selected = views + # Primary check: the daemon recorded the exact pinned repository digest. + if any(ref not in (view.get("RepoDigests") or []) for view in views): + raise ValueError("image_repo_digest_mismatch") + image_id = plain.get("Id") + store = "containerd" if image_id == index else "classic" if image_id == config else None + if store is None: + raise ValueError("image_configuration_hash_mismatch") + if f"{selected.get('Os')}/{selected.get('Architecture')}" != platform: + raise ValueError("image_platform_manifest_mismatch") + descriptor = selected.get("Descriptor") + if store == "containerd": + if (selected.get("Id") != manifest or not isinstance(descriptor, dict) + or descriptor.get("digest") != manifest): + raise ValueError("image_platform_manifest_mismatch") + elif selected.get("Id") != config: + raise ValueError("image_configuration_hash_mismatch") + elif descriptor is not None: + raise ValueError("image_platform_manifest_mismatch") + return store + + def install(prefix, state): pins, _, _ = preflight(prefix, state) for directory in (prefix, prefix / "venv", state, state / "cache"): @@ -416,9 +461,7 @@ def install(prefix, state): download_verified(pins["source_archive"], state / "cache/upstream.tar.gz") download_verified(pins["uv_lock"], state / "cache/upstream.uv.lock") subprocess.run(DOCKER + ["pull", "--platform", pins["image"]["platform"], pins["image"]["ref"]], check=True) - image_id = subprocess.check_output(DOCKER + ["image", "inspect", "--format", "{{.Id}}", pins["image"]["ref"]], text=True).strip() - if image_id != "sha256:" + pins["image"]["config_sha256"]: - raise ValueError("image_configuration_hash_mismatch") + image_store = pinned_image_identity(pins["image"]) # A reviewable snapshot, with no repository metadata or host configuration. if HERE.resolve() != (prefix / "recipe").resolve(): shutil.copytree(HERE, prefix / "recipe", dirs_exist_ok=True, @@ -431,6 +474,7 @@ def install(prefix, state): attempt = Path(tempfile.mkdtemp(prefix="attempt-", dir=state / "install-attempts")) code = execute_container(args, name, attempt / "install.log", 900) write_json(state / "installation.json", {"version": pins["version"], "image": pins["image"]["ref"], + "image_store": image_store, "requirements_sha256": pins["requirements_sha256"], "exit_code": code}) if code: raise RuntimeError("container_install_failed_see_private_install_log") diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index 792d530fa..f65b40161 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -376,13 +376,19 @@ at fcc102a; paths are abbreviated below and given in full in README.md. | The server env file was not checked for the session key. Without one the server binds only its container loopback (`__main__.py:282-285`, `config.py:24`). | Preflight parses the file's variable names with Docker's env-file rules (docker/cli@v29.8.1 `pkg/kvfile/kvfile.go:92-124`) and requires `OH_SESSION_API_KEYS_0`. Values are never extracted, printed or logged. | | The published port was fixed at 3730, which PR #428's crawl4ai recipe also uses by default. | `--port` accepts integers in 3730..3799, as PR #428's `host.py:44-45` does at 3ad8ba2. The default stays 3730 and the resolver uses 3740. The port is stored in status.json and checked again by dispatch. | -Open findings from this phase, not changed here: +A follow-up commit applied the coordinator's decisions of 2026-09-28. Red and +green runs are in +[evidence/phase1-followup-commands.json](evidence/phase1-followup-commands.json). + +| Item | Decision and source | +| --- | --- | +| F16 forgery bound | The newest-`ConversationErrorEvent` design stays. The strict alternative, exit 3 for every `error`, would label every honest iteration-limit run an infrastructure failure. `dispatch.py` `PERMITTED_TERMINATIONS` keeps a label only as a refinement of the REST status, so exit 0 requires REST `finished`. A planted event can move exit 3 to 1 and no further. The README states the residual and the analysis rule: exit 1 and exit 3 are both non-success, and reruns re-queue only exit 3. | +| `install()` image identity, open after phase 1 | Fixed. Both a plain and a `--platform linux/amd64` inspect must list the exact pinned ref in `.RepoDigests`. The plain `.Id` must be the index digest (containerd store) or the config digest (classic store). On containerd the platform `.Id` and `.Descriptor.digest` must be manifest `ec7ed86f...`; on classic the platform `.Id` must be the config and no descriptor may appear. Sources: moby docker-v29.8.1@464cd50c `daemon/containerd/image_inspect.go:28,71-73,95,97`; `daemon/images/image_inspect.go:59`, `daemon/images/image.go:160-197`, `daemon/internal/image/store.go:152,160`, `daemon/internal/image/fs.go:120`, `daemon/internal/distribution/pull_v2.go:431-434,705-747,845-867`; `api/swagger.yaml:1826-1850`. Inspect with `--platform` needs Engine API v1.49 (`api/docs/CHANGELOG.md:175-180`). On the containerd store no field reports the config digest. That covers inspect, history (`daemon/containerd/image_history.go:113-115`) and the manifest summary (`api/swagger.yaml:8216-8335`). The content-addressed platform manifest binds the config there. The Docker containerd image-store page (docker/docs@3c117d8e `content/manuals/engine/storage/containerd.md`) does not describe image IDs. | +| Empty `OH_SESSION_API_KEYS_0=` value | Documented residual, because preflight never reads values. SDK@fcc102a `env_parser.py:183-197` turns it into a one-item list holding an empty string, and `__main__.py:30-48` then treats authentication as enabled. | +| Unused `run_worker`, `record_event`, `observations()` | Kept, because references remain: `worker.py:202,232`, and `observations` is used in `tests/test_runtime_worker_openhands.py`. | + +Open finding from this phase, not changed here: -- `install()` compares `docker image inspect` `.Id` with the config digest - (host.py:419-421). The reference host's containerd image store reports the - index digest as `.Id` (see the scan receipt), so `install()` there would raise - `image_configuration_hash_mismatch`. This is inferred from the code and the - observed `.Id`; installation was not run. - The module-qualified event kind string is source-reviewed only. If the live server used a different kind, the agent-limit check would fail closed to exit 3. diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index ada50a871..d84a16f33 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -690,15 +690,99 @@ def test_installer_mounts_exclude_snapshot_from_writable_paths(self): self.assertTrue(any(f"src={prefix}/venv," in v for v in writable)) self.assertFalse(any(f"src={prefix}," in v for v in writable)) + # Docker image inspect shapes for the pinned agent-server image. moby + # docker-v29.8.1 (464cd50c) containerd store: daemon/containerd/ + # image_inspect.go:28,71-73,95,97 report the pulled index as Id/Descriptor, + # or the platform manifest when --platform is given (this host's probe, + # 2026-09-28). Classic store: daemon/images/image_inspect.go:59 reports the + # config digest (daemon/internal/image/store.go:152,160; fs.go:120) and no + # Descriptor (api/swagger.yaml:1839-1850); a pull by index digest records + # the index reference in RepoDigests (distribution/pull_v2.go:431-434). + @staticmethod + def image_inspect(pins, store): + image = pins["image"] + index = "sha256:" + image["ref"].rsplit("@sha256:", 1)[1] + manifest, config = "sha256:" + image["manifest_sha256"], "sha256:" + image["config_sha256"] + common = {"RepoDigests": [image["ref"]], "Os": "linux", "Architecture": "amd64"} + if store == "classic": + return {**common, "Id": config}, {**common, "Id": config} + return ({**common, "Id": index, "Descriptor": { + "mediaType": "application/vnd.oci.image.index.v1+json", "digest": index, "size": 1609}}, + {**common, "Id": manifest, "Descriptor": { + "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": manifest, "size": 4313, + "platform": {"architecture": "amd64", "os": "linux"}}}) + + def install_with(self, host, pins, plain, selected, root): + """Run install() on mocked inspect output; no Docker command runs.""" + def inspect(argv, **kwargs): + self.assertEqual(argv[:len(host.DOCKER) + 2], [*host.DOCKER, "image", "inspect"]) + self.assertEqual(argv[-1], pins["image"]["ref"]) + return json.dumps([selected if "--platform" in argv else plain]) + + with patch.object(host, "preflight", return_value=(pins, {}, {})), \ + patch.object(host, "download_verified"), patch.object(host.subprocess, "run"), \ + patch.object(host.subprocess, "check_output", side_effect=inspect) as calls, \ + patch.object(host, "execute_container", return_value=0) as execute, \ + contextlib.redirect_stdout(io.StringIO()): + try: + host.install(root / "prefix", root / "state") + except ValueError as exc: + return str(exc), execute, calls + return None, execute, calls + + def test_install_accepts_the_pinned_image_on_containerd_and_classic_stores(self): + host = load_recipe_module("host.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + ref = pins["image"]["ref"] + for store in ("containerd", "classic"): + with self.subTest(store=store): + error, execute, calls = self.install_with(host, pins, *self.image_inspect(pins, store), + self.result / store) + self.assertIsNone(error) + execute.assert_called_once() + argv = [call.args[0] for call in calls.call_args_list] + self.assertEqual(argv, [[*host.DOCKER, "image", "inspect", ref], + [*host.DOCKER, "image", "inspect", "--platform", "linux/amd64", ref]]) + installed = json.loads((self.result / store / "state/installation.json").read_text()) + self.assertEqual(installed["image_store"], store) + + def test_install_refuses_each_image_identity_mismatch_before_any_container(self): + host = load_recipe_module("host.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + image = pins["image"] + repo, index = image["ref"].split("@", 1) + manifest, config = "sha256:" + image["manifest_sha256"], "sha256:" + image["config_sha256"] + other = "sha256:" + "0" * 64 + cases = [] + for store in ("containerd", "classic"): + # Each case changes one field of a passing shape. + for digests in ([], None, ["docker.io/" + image["ref"]], ["ghcr.io/openhands/other@" + index], + [repo + "@" + manifest], [repo + "@" + config], [repo + ":1.49.6-python"]): + cases.append((store, "plain", {"RepoDigests": digests}, "image_repo_digest_mismatch")) + cases.append((store, "platform", {"RepoDigests": [repo + "@" + other]}, "image_repo_digest_mismatch")) + for value in (manifest, other, None): + cases.append((store, "plain", {"Id": value}, "image_configuration_hash_mismatch")) + cases.append((store, "platform", {"Architecture": "arm64"}, "image_platform_manifest_mismatch")) + wrong = {"mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": other, "size": 4313, + "platform": {"architecture": "amd64", "os": "linux"}} + cases += [("containerd", "platform", {"Id": other}, "image_platform_manifest_mismatch"), + ("containerd", "platform", {"Descriptor": wrong}, "image_platform_manifest_mismatch"), + ("containerd", "platform", {"Descriptor": None}, "image_platform_manifest_mismatch"), + ("classic", "platform", {"Id": manifest}, "image_configuration_hash_mismatch"), + ("classic", "platform", {"Descriptor": dict(wrong, digest=manifest)}, "image_platform_manifest_mismatch")] + for number, (store, view, change, expected) in enumerate(cases): + with self.subTest(store=store, view=view, change=change): + plain, selected = self.image_inspect(pins, store) + (plain if view == "plain" else selected).update(change) + error, execute, _ = self.install_with(host, pins, plain, selected, self.result / f"refused-{number}") + self.assertEqual(error, expected) + execute.assert_not_called() + def test_install_actually_uses_narrow_mounts_and_grader_lock(self): host = load_recipe_module("host.py") pins = json.loads((RECIPE / "pins.json").read_text()) prefix, state = self.result / "prefix", self.result / "state" - with patch.object(host, "preflight", return_value=(pins, {}, {})), \ - patch.object(host, "download_verified"), patch.object(host.subprocess, "run"), \ - patch.object(host.subprocess, "check_output", return_value="sha256:" + pins["image"]["config_sha256"]), \ - patch.object(host, "execute_container", return_value=0) as execute, contextlib.redirect_stdout(io.StringIO()): - host.install(prefix, state) + _, execute, _ = self.install_with(host, pins, *self.image_inspect(pins, "containerd"), self.result) args = execute.call_args.args[0] self.assertNotIn(f"type=bind,src={prefix},dst={prefix}", args) self.assertIn(f"type=bind,src={prefix}/venv,dst={prefix}/venv", args) @@ -1212,6 +1296,58 @@ def test_other_native_errors_stay_ungraded_infrastructure_failures(self): self.assertEqual(receipt["failure_stage"], "agent") self.assertEqual(receipt["agent_termination"], "error") + def test_only_the_rest_status_can_admit_a_pass(self): + # F16 bound. A label derived from the model-writable event store may + # refine REST "error" into an agent limit (exit 3 -> 1) but can never + # stand in for REST "finished", even with an officially resolved patch. + dispatch = load_recipe_module("dispatch.py") + receipts = load_recipe_module("receipt.py") + self.terminal_attempt("error") + window = json.loads((self.result / "window.json").read_text()) + for execution, label in (("error", "finished"), ("stuck", "finished"), (None, "finished"), + ("error", "stuck"), ("finished", "max_iterations_reached")): + with self.subTest(execution=execution, label=label): + for report in self.result.glob("OpenHands.*.json"): + report.unlink() + status = {"execution_status": execution, "agent_termination": label, + "server_name": "rw-openhands-fixture-engines-on-server"} + with patch.object(dispatch, "stop_server", return_value=True), \ + patch.object(dispatch, "logged_command", return_value=0), \ + patch.object(dispatch.subprocess, "check_output", return_value=""), \ + patch.object(dispatch, "grade", side_effect=self.synthetic_report("resolved_ids")) as grade, \ + patch.object(dispatch, "create_receipt", side_effect=lambda result: receipts.create_receipt( + result, database=result / "absent.sqlite")): + receipt = dispatch.finish_result(self.result, status, dict(window)) + grade.assert_not_called() + self.assertEqual(receipt["agent_termination"], "error") + self.assertEqual(receipt["failure_stage"], "agent") + self.assertEqual(receipt["worker_exit_code"], 1) + self.assertFalse(receipt["task_passed"]) + self.assertEqual(dispatch.result_exit(receipt), 3) + self.assertEqual(dispatch.result_exit({**receipt, "evidence_complete": True}), 3) + + def test_planted_success_looking_events_move_exit_3_to_1_at_most(self): + # Regression lock through the native result path: REST reports + # "error" and the official grader resolves the patch, yet no planted + # event page yields a pass, even if evidence were complete. + dispatch = load_recipe_module("dispatch.py") + planted = ([self.LIMIT_EVENT], + [dict(self.LIMIT_EVENT, execution_status="finished", task_passed=True, resolved=True)], + [{"kind": "ConversationStateUpdateEvent", "key": "execution_status", "value": "finished"}], + [dict(self.LIMIT_EVENT, code="Finished")], + [{"kind": "ActionEvent", "source": "agent", "tool_name": "finish"}]) + for items in planted: + with self.subTest(items=items): + for report in self.result.glob("OpenHands.*.json"): + report.unlink() + self.terminal_attempt("error") + code, _, _, receipt = self.collect(dispatch, [{"response": "Done"}, {"items": items, "next_page_id": None}], + self.synthetic_report("resolved_ids")) + self.assertIn(code, {1, 3}) + self.assertEqual(receipt["worker_exit_code"], 1) + self.assertFalse(receipt["task_passed"]) + self.assertIn(dispatch.result_exit({**receipt, "evidence_complete": True}), {1, 3}) + def test_selected_port_flows_from_prepare_to_dispatch(self): host = load_recipe_module("host.py") dispatch = load_recipe_module("dispatch.py") From f951134c976a3fc22b3471806598faa25ec71f55 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 14:26:34 -0400 Subject: [PATCH 08/22] OpenHands phase 2 (1/4): proxy base URL, recorded compression combos, unreachable host MCP Both arms now call the per-attempt proxy alias http://gw:8081/v1; the arm's gateway (10.0.2.2:20128 control, :20129 engines-on) becomes the proxy's fixed upstream (gateway_upstream). engines-on selects one of the seven header combos recorded in the 20129 apply record (default allow-lossy); control sends none. One helper, environment_selection, reads OPENHANDS_* for host.run and the container worker, with an empty OPENHANDS_COMPRESSION meaning unset. The receipt (schema 5) records the proxy base URL, the upstream and the combo. ai-memory and socraticode are disabled in the tool policy as unreachable by design under O1; no proxy route is added for them. Red: 75 tests, failures=5 errors=7 (all in the changed contracts). Green: 75 tests OK. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../openhands/config/mcp-policy.json | 8 +- .../openhands/config/worker.json | 7 +- blueprints/runtime-workers/openhands/host.py | 11 +- .../runtime-workers/openhands/receipt.py | 7 +- .../runtime-workers/openhands/recipe.py | 49 ++++++-- .../runtime-workers/openhands/worker.py | 10 +- tests/test_runtime_worker_openhands.py | 106 ++++++++++++++++-- 7 files changed, 162 insertions(+), 36 deletions(-) diff --git a/blueprints/runtime-workers/openhands/config/mcp-policy.json b/blueprints/runtime-workers/openhands/config/mcp-policy.json index 992d6fc49..80c99a36c 100644 --- a/blueprints/runtime-workers/openhands/config/mcp-policy.json +++ b/blueprints/runtime-workers/openhands/config/mcp-policy.json @@ -10,11 +10,15 @@ "collections": ["foundation-docs", "foundation-adoption", "us-equities-foundation", "us-equities-catalog"] }, "ai-memory": { - "enabled_tools": ["memory_query", "memory_read_page", "memory_recent", "memory_status", "memory_briefing"] + "enabled": false, + "enabled_tools": ["memory_query", "memory_read_page", "memory_recent", "memory_status", "memory_briefing"], + "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services, and the proxy forwards only three /v1 gateway routes (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." }, "socraticode": { + "enabled": false, "enabled_tools": ["codebase_search", "codebase_status", "codebase_list_projects", "codebase_health"], - "env": {"SOCRATICODE_WATCHER": "manual"} + "env": {"SOCRATICODE_WATCHER": "manual"}, + "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services (Qdrant, the embedder), and the proxy forwards only three /v1 gateway routes (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." }, "headroom": { "enabled": false, diff --git a/blueprints/runtime-workers/openhands/config/worker.json b/blueprints/runtime-workers/openhands/config/worker.json index eaa3f5e16..3fb8c67fc 100644 --- a/blueprints/runtime-workers/openhands/config/worker.json +++ b/blueprints/runtime-workers/openhands/config/worker.json @@ -3,10 +3,11 @@ "model_env": "OPENHANDS_MODEL", "arm_env": "OPENHANDS_ARM", "base_url_env": "OPENHANDS_BASE_URL", + "compression_env": "OPENHANDS_COMPRESSION", "llm": { "model": "cx/gpt-6-astra-max", "model_canonical_name": "openai/gpt-6", - "base_url": "http://127.0.0.1:20128/v1", + "base_url": "http://gw:8081/v1", "api_key": "local-loopback", "api_mode": "responses", "reasoning_effort": "max", @@ -25,8 +26,8 @@ "agent_loop": "container", "llm_calls": "container", "stdio_mcp": "container", - "gateway_base_url": "http://10.0.2.2:20128/v1", - "published_ports": ["127.0.0.1:3730:8000"], + "gateway_base_url": "http://gw:8081/v1", + "published_ports": ["127.0.0.1:3730:8080"], "max_iteration_per_run": 40, "timeout_seconds": 1200, "cpus": "4", diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index 8296d1ed0..c26135350 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -25,7 +25,7 @@ import urllib.request import uuid -from recipe import HERE, arm_config, digest, llm_config, read_json, render_mcp +from recipe import HERE, arm_config, digest, environment_selection, llm_config, read_json, render_mcp from e2e.task import load_task, worker_instruction from receipt import create_receipt, read_bounded, time_value @@ -178,6 +178,9 @@ def preflight(prefix, state): if "<" in json.dumps(host): raise ValueError("host_file_has_unfilled_placeholders") variables = host["variables"] + # These endpoints belong to the disabled ai-memory and socraticode entries + # (config/mcp-policy.json). Under O1 they are unreachable by design from + # the internal run network; the values only keep the template renderable. if variables["AI_MEMORY_URL"] != "10.0.2.2:49474": raise ValueError("ai_memory_container_endpoint_required") if variables["EMBED_URL"] != "10.0.2.2:18232": @@ -603,6 +606,7 @@ def prepare_native_dispatch(result, prefix, pins, base, network, host, selection "--env", "OPENHANDS_RUN_ID=" + run_id, "--env", "OPENHANDS_ARM=" + selection["arm"], "--env", "OPENHANDS_MODEL=" + selection["requested_model"], "--env", "OPENHANDS_BASE_URL=" + selection["base_url"], + "--env", "OPENHANDS_COMPRESSION=" + (selection["compression_combo"] or ""), "--env", f"PATH={prefix}/venv/bin:" + host["variables"]["HOST_PATH"]] render = docker_args(pins, stem + "-request") + base + mount(result / "worker", "/run-output", False) render += environment + ["--entrypoint", str(prefix / "venv/bin/python"), pins["image"]["ref"], "/recipe/worker.py", "--request"] @@ -653,8 +657,9 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL if not os.path.lexists(prefix / "venv/bin/python"): raise ValueError("owned_sdk_venv_missing") cfg = read_json(HERE / "config/worker.json") - selection = arm_config(arm, os.environ.get("OPENHANDS_MODEL"), os.environ.get("OPENHANDS_BASE_URL")) - llm_config(cfg, selection["requested_model"], arm=arm, base_url=selection["base_url"]) + selection = environment_selection(os.environ, arm) + llm_config(cfg, selection["requested_model"], arm=arm, base_url=selection["base_url"], + compression=selection["compression_combo"]) window.update({k: v for k, v in selection.items() if k != "headers"}) window["header_names"] = sorted([*selection["headers"], "x-omniroute-session", "X-Correlation-Id", "Idempotency-Key"]) network = model_network(host, arm) diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 48219aa97..34dce4355 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -175,7 +175,7 @@ def create_receipt(result, database=None): checked = read_json(result / "check.json") # F17: nothing under result/worker or result/server is read (NOT_COLLECTED_REASON). selection = arm_config(window.get("arm", "control"), window.get("requested_model", window.get("model")), - window.get("base_url")) + window.get("base_url"), window.get("compression_combo")) db = database or gateway_database(selection["arm"]) rows, db_status = [], "unavailable" try: @@ -204,8 +204,9 @@ def create_receipt(result, database=None): except (OSError, ValueError, AttributeError): cleanup.append(False) return { - "schema_version": 4, "evidence_class": "SDK inference adapter with official SWE-bench grading", - **{k: selection[k] for k in ("arm", "base_url", "requested_model", "gateway_model", "gateway_path")}, + "schema_version": 5, "evidence_class": "SDK inference adapter with official SWE-bench grading", + **{k: selection[k] for k in ("arm", "base_url", "gateway_upstream", "requested_model", "gateway_model", + "gateway_path", "compression_combo")}, "header_names": window.get("header_names", sorted(selection["headers"])), "framework": "OpenHands software-agent-sdk", "expected_version": pins["version"], "observed_versions": NOT_COLLECTED, diff --git a/blueprints/runtime-workers/openhands/recipe.py b/blueprints/runtime-workers/openhands/recipe.py index 34ae7e989..28b21efa4 100644 --- a/blueprints/runtime-workers/openhands/recipe.py +++ b/blueprints/runtime-workers/openhands/recipe.py @@ -14,6 +14,17 @@ HERE = Path(__file__).resolve().parent +# Phase 2 (O1): the agent reaches its arm's gateway only through the per-attempt +# proxy alias on the internal run network (config/proxy-nginx.conf); the +# proxy's upstream port selects the arm. +PROXY_BASE_URL = "http://gw:8081/v1" +# Header-selected combos in the 20129 apply record read back at +# 2026-09-28T03:50:11Z (all twelve engines globally off there). The engines-on +# default stays the round-3 "allow-lossy"; the control arm sends none. +COMPRESSION_COMBOS = frozenset({ + "allow-lossy", "fw-ccr", "fw-codex-responses", "fw-headroom", "fw-lite", "fw-rtk", "fw-session-dedup", +}) +DEFAULT_COMPRESSION = "allow-lossy" def read_json(path): @@ -69,11 +80,13 @@ def expand(value): return config -def arm_config(arm="control", model=None, base_url=None): +def arm_config(arm="control", model=None, base_url=None, compression=None): """Round-3 common contract; provider prefix belongs to LiteLLM only. SDK@fcc102a llm/llm.py:442-446; coordinator's 2026-09-27 one-slash probe. Entry-gateway model matching is explicit: OmniRoute logs the routed model. + Both arms call PROXY_BASE_URL; gateway_upstream is the proxy's fixed + upstream, so the rendered proxy config and this selection cannot disagree. """ if arm not in {"control", "engines-on"}: raise ValueError("unknown_arm") @@ -83,18 +96,36 @@ def arm_config(arm="control", model=None, base_url=None): if arm == "control" else selected == "sharedgw/gpt-6-astra-max") if not valid: raise ValueError("gateway_requires_gpt6_route_for_selected_arm") - expected_url = f"http://10.0.2.2:{port}/v1" - if base_url is not None and base_url != expected_url: + if base_url is not None and base_url != PROXY_BASE_URL: raise ValueError("base_url_must_match_arm") - return {"arm": arm, "requested_model": selected, "base_url": expected_url, - "gateway_port": port, "gateway_model": selected.split("/", 1)[1], - "gateway_path": "/v1/responses", - "headers": {"x-omniroute-compression": "allow-lossy"} if arm == "engines-on" else {}} + if arm == "control": + if compression is not None: + raise ValueError("control_arm_sends_no_compression_header") + combo = None + else: + combo = DEFAULT_COMPRESSION if compression is None else compression + if combo not in COMPRESSION_COMBOS: + raise ValueError("unrecorded_compression_combo") + return {"arm": arm, "requested_model": selected, "base_url": PROXY_BASE_URL, + "gateway_port": port, "gateway_upstream": f"http://10.0.2.2:{port}/v1", + "gateway_model": selected.split("/", 1)[1], "gateway_path": "/v1/responses", + "compression_combo": combo, + "headers": {"x-omniroute-compression": combo} if combo else {}} + + +def environment_selection(environment, arm=None): + """The one reader of OPENHANDS_* selection variables for host and worker. + + An explicit arm (the host CLI flag) wins over OPENHANDS_ARM. An empty + OPENHANDS_COMPRESSION means unset: the control arm's container gets "". + """ + return arm_config(arm or environment.get("OPENHANDS_ARM", "control"), environment.get("OPENHANDS_MODEL"), + environment.get("OPENHANDS_BASE_URL"), environment.get("OPENHANDS_COMPRESSION") or None) -def llm_config(config, model=None, *, arm="control", base_url=None): +def llm_config(config, model=None, *, arm="control", base_url=None, compression=None): result = dict(config["llm"]) - selected = arm_config(arm, model, base_url) + selected = arm_config(arm, model, base_url, compression) if result.get("reasoning_effort") != "max": raise ValueError("max_reasoning_effort_required") if result.get("temperature") is not None and result["temperature"] <= 0.1: diff --git a/blueprints/runtime-workers/openhands/worker.py b/blueprints/runtime-workers/openhands/worker.py index 5a20542e0..c6d2c020c 100644 --- a/blueprints/runtime-workers/openhands/worker.py +++ b/blueprints/runtime-workers/openhands/worker.py @@ -17,7 +17,7 @@ import sys import uuid -from recipe import HERE, llm_config, read_json, tool_filter +from recipe import HERE, environment_selection, llm_config, read_json, tool_filter def headers_for_call(static_headers): @@ -25,10 +25,12 @@ def headers_for_call(static_headers): def worker_llm_config(environment, dispatch_id): + # The phase-2 proxy replaces these three headers with its own fixed values + # (config/proxy-nginx.conf); the SDK copies stay equal to them by design. cfg = read_json(HERE / "config/worker.json") - fields = llm_config(cfg, environment.get("OPENHANDS_MODEL"), - arm=environment.get("OPENHANDS_ARM", "control"), - base_url=environment.get("OPENHANDS_BASE_URL")) + selection = environment_selection(environment) + fields = llm_config(cfg, selection["requested_model"], arm=selection["arm"], + base_url=selection["base_url"], compression=selection["compression_combo"]) fields["extra_headers"].update({"x-omniroute-session": dispatch_id, "X-Correlation-Id": dispatch_id}) fields["usage_id"] = "agent" return fields diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index d84a16f33..450673a23 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -61,8 +61,14 @@ def test_grader_sync_keeps_the_hash_seeded_interpreter(self): self.assertIn('--python "$grader_dir/.venv/bin/python"', sync) def test_explicit_arms_keep_one_slash_routes_and_compression_separate(self): + # Phase 2 (F21 refresh): both arms call the proxy alias, whose upstream + # port selects the arm. The 20129 apply record read back at + # 2026-09-28T03:50:11Z has seven header-selected combos and all twelve + # engines globally off; default-caveman is outside that set. helpers = load_recipe_module("recipe.py") cfg = self.read_json("config/worker.json") + self.assertEqual(helpers.COMPRESSION_COMBOS, frozenset({ + "allow-lossy", "fw-ccr", "fw-codex-responses", "fw-headroom", "fw-lite", "fw-rtk", "fw-session-dedup"})) for arm, model, port, headers in ( ("control", "cx/gpt-6-astra-max", 20128, {}), ("engines-on", "sharedgw/gpt-6-astra-max", 20129, @@ -71,18 +77,33 @@ def test_explicit_arms_keep_one_slash_routes_and_compression_separate(self): with self.subTest(arm=arm): selected = helpers.arm_config(arm) self.assertEqual(selected["requested_model"], model) - self.assertEqual(selected["base_url"], f"http://10.0.2.2:{port}/v1") + self.assertEqual(selected["base_url"], "http://gw:8081/v1") + self.assertEqual(selected["gateway_port"], port) + self.assertEqual(selected["gateway_upstream"], f"http://10.0.2.2:{port}/v1") self.assertEqual(selected["headers"], headers) llm = helpers.llm_config(cfg, arm=arm) self.assertEqual(llm["model"], "openai/" + model) + self.assertEqual(llm["base_url"], "http://gw:8081/v1") + self.assertEqual(llm["api_key"], "local-loopback") self.assertEqual(llm["reasoning_effort"], "max") self.assertEqual(llm["extra_headers"], headers) + for combo in sorted(helpers.COMPRESSION_COMBOS): + with self.subTest(combo=combo): + selected = helpers.arm_config("engines-on", compression=combo) + self.assertEqual(selected["compression_combo"], combo) + self.assertEqual(selected["headers"], {"x-omniroute-compression": combo}) + self.assertIsNone(helpers.arm_config("control")["compression_combo"]) for kwargs in ( {"arm": "engines-on", "model": "sharedgw/cx/gpt-6-astra-max"}, {"arm": "engines-on", "model": "sharedgw/claude-opus-5-5"}, {"arm": "control", "model": "sharedgw/gpt-6-astra-max"}, - {"arm": "engines-on", "base_url": "http://10.0.2.2:20128/v1"}, + {"arm": "engines-on", "base_url": "http://10.0.2.2:20129/v1"}, + {"arm": "control", "base_url": "http://10.0.2.2:20128/v1"}, {"arm": "control", "base_url": "https://example.invalid/v1"}, + {"arm": "control", "compression": "allow-lossy"}, + {"arm": "engines-on", "compression": "default-caveman"}, + {"arm": "engines-on", "compression": ""}, + {"arm": "engines-on", "compression": "ALLOW-LOSSY"}, {"arm": "other"}, ): with self.subTest(kwargs=kwargs), self.assertRaises(ValueError): @@ -93,17 +114,19 @@ def test_explicit_arms_keep_one_slash_routes_and_compression_separate(self): def test_gateway_model_and_container_topology(self): config = self.read_json("config/worker.json") - self.assertEqual(config["llm"]["base_url"], "http://127.0.0.1:20128/v1") + self.assertEqual(config["llm"]["base_url"], "http://gw:8081/v1") self.assertEqual(config["llm"]["model"], "cx/gpt-6-astra-max") self.assertEqual(config["model_env"], "OPENHANDS_MODEL") self.assertEqual(config["llm"]["api_key"], "local-loopback") self.assertEqual(config["llm"]["api_mode"], "responses") self.assertIsNone(config["llm"].get("temperature")) self.assertNotEqual(config["llm"].get("reasoning_effort"), "auto") - self.assertEqual(config["runtime"]["gateway_base_url"], "http://10.0.2.2:20128/v1") + self.assertEqual(config["runtime"]["gateway_base_url"], "http://gw:8081/v1") self.assertEqual(config["runtime"]["agent_loop"], "container") - self.assertEqual(config["runtime"]["published_ports"], ["127.0.0.1:3730:8000"]) + # Only the proxy publishes, on loopback, to its host-driver server. + self.assertEqual(config["runtime"]["published_ports"], ["127.0.0.1:3730:8080"]) self.assertEqual(config["arm_env"], "OPENHANDS_ARM") + self.assertEqual(config["compression_env"], "OPENHANDS_COMPRESSION") self.assertEqual(config["base_url_env"], "OPENHANDS_BASE_URL") self.assertEqual(config["condenser"]["kind"], "LLMSummarizingCondenser") self.assertGreater(config["condenser"]["max_tokens"], 0) @@ -127,6 +150,30 @@ def test_mcp_limits(self): "headroom_compress", "headroom_retrieve", "headroom_stats", }) self.assertIn("serena", policy) + # Phase 2 (O1): host services have no route from the internal run + # network, so both remote servers are documented as unreachable by + # design; no proxy route is added for them. + for name in ("ai-memory", "socraticode"): + with self.subTest(server=name): + self.assertIs(policy[name]["enabled"], False) + self.assertIn("unreachable by design", policy[name]["reason"]) + self.assertIn("internal run network", policy[name]["reason"]) + + def test_environment_selection_maps_empty_compression_to_unset(self): + # One helper serves host.run and the container worker, so both sides + # read OPENHANDS_COMPRESSION the same way ("" means unset). + helpers = load_recipe_module("recipe.py") + self.assertIsNone(helpers.environment_selection({"OPENHANDS_COMPRESSION": ""})["compression_combo"]) + self.assertEqual(helpers.environment_selection({}, "engines-on")["compression_combo"], "allow-lossy") + self.assertEqual(helpers.environment_selection({"OPENHANDS_ARM": "control"}, "engines-on")["arm"], "engines-on") + selected = helpers.environment_selection({"OPENHANDS_ARM": "engines-on", "OPENHANDS_COMPRESSION": "fw-lite"}) + self.assertEqual((selected["arm"], selected["compression_combo"]), ("engines-on", "fw-lite")) + self.assertEqual(selected["headers"], {"x-omniroute-compression": "fw-lite"}) + for environment in ({"OPENHANDS_COMPRESSION": "fw-lite"}, + {"OPENHANDS_ARM": "engines-on", "OPENHANDS_COMPRESSION": "fw-unknown"}, + {"OPENHANDS_ARM": "engines-on", "OPENHANDS_BASE_URL": "http://10.0.2.2:20129/v1"}): + with self.subTest(environment=environment), self.assertRaises(ValueError): + helpers.environment_selection(environment) def test_immutable_artifacts(self): pins = self.read_json("pins.json") @@ -179,6 +226,13 @@ def test_worker_arm_headers_and_response_correlation_capture(self): cfg = worker.worker_llm_config({"OPENHANDS_ARM": arm}, "dispatch-fixture") self.assertEqual(cfg["extra_headers"]["x-omniroute-session"], "dispatch-fixture") self.assertEqual("x-omniroute-compression" in cfg["extra_headers"], arm == "engines-on") + self.assertEqual(cfg["base_url"], "http://gw:8081/v1") + selected = worker.worker_llm_config({"OPENHANDS_ARM": "engines-on", "OPENHANDS_COMPRESSION": "fw-rtk"}, "d") + self.assertEqual(selected["extra_headers"]["x-omniroute-compression"], "fw-rtk") + unset = worker.worker_llm_config({"OPENHANDS_ARM": "control", "OPENHANDS_COMPRESSION": ""}, "d") + self.assertNotIn("x-omniroute-compression", unset["extra_headers"]) + with self.assertRaises(ValueError): + worker.worker_llm_config({"OPENHANDS_ARM": "control", "OPENHANDS_COMPRESSION": "fw-rtk"}, "d") response = SimpleNamespace(raw_response=SimpleNamespace( _hidden_params={"headers": {"X-Correlation-Id": "opaque-response-id"}})) @@ -263,11 +317,15 @@ def test_runtime_model_override_and_native_tool_filter(self): helpers = load_recipe_module("recipe.py") result = helpers.llm_config(self.read_json("config/worker.json"), "cx/gpt-6-sol-max") self.assertEqual(result["model"], "openai/cx/gpt-6-sol-max") - self.assertEqual(result["base_url"], "http://10.0.2.2:20128/v1") + self.assertEqual(result["base_url"], "http://gw:8081/v1") pattern = re.compile(helpers.tool_filter(self.read_json("config/mcp-policy.json"))) - for name in ("terminal", "file_editor", "context-mode_ctx_execute", "serena_find_symbol", "ai-memory_memory_query", "qmd_query", "socraticode_codebase_health", "jcodemunch_order"): + for name in ("terminal", "file_editor", "context-mode_ctx_execute", "serena_find_symbol", "qmd_query", "jcodemunch_order"): self.assertIsNotNone(pattern.fullmatch(name), name) - for name in ("context-mode_ctx_upgrade", "context-mode_ctx_purge", "ai-memory_memory_write", "socraticode_codebase_index", "jcodemunch_delete_index", "headroom_headroom_compress", "memory_query", "qmd_query_evil"): + # Phase 2 contract change: tools of the unreachable host services are + # no longer admitted by the native filter either. + for name in ("context-mode_ctx_upgrade", "context-mode_ctx_purge", "ai-memory_memory_query", "ai-memory_memory_write", + "socraticode_codebase_health", "socraticode_codebase_index", "jcodemunch_delete_index", + "headroom_headroom_compress", "memory_query", "qmd_query_evil"): self.assertIsNone(pattern.fullmatch(name), name) def test_gateway_rejects_non_gpt6_models_and_unsafe_structured_modes(self): @@ -1013,15 +1071,37 @@ def test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage(self) self.assertIn("?mode=ro", source) def test_skills_and_mcp_require_observations(self): + # Phase 2 disables the unreachable host services in the policy, so the + # fixture uses a container-local server the native filter still admits. module = load_recipe_module("receipt.py") - action = {"kind": "ActionEvent", "tool_name": "ai-memory_memory_query"} - observed = {"kind": "ObservationEvent", "tool_name": "ai-memory_memory_query", "observation": {"is_error": False}} + action = {"kind": "ActionEvent", "tool_name": "jcodemunch_order"} + observed = {"kind": "ObservationEvent", "tool_name": "jcodemunch_order", "observation": {"is_error": False}} + disabled = {"kind": "ObservationEvent", "tool_name": "ai-memory_memory_query", "observation": {"is_error": False}} skill = {"kind": "ObservationEvent", "tool_name": "invoke_skill", "observation": {"skill_name": "tdd", "is_error": False}} self.assertEqual(module.observations([action])["mcp_calls_observed"], {}) - report = module.observations([action, observed, skill], ["tdd"]) - self.assertEqual(report["mcp_calls_observed"], {"ai-memory_memory_query": 1}) + report = module.observations([action, observed, disabled, skill], ["tdd"]) + self.assertEqual(report["mcp_calls_observed"], {"jcodemunch_order": 1}) self.assertEqual(report["skills_observed"], {"tdd": 1}) + def test_receipt_records_proxy_base_url_and_selected_compression_combo(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp).resolve() + (result / "check.json").write_text("{}") + for window, combo, port in (({"arm": "engines-on", "compression_combo": "fw-rtk"}, "fw-rtk", 20129), + ({"arm": "engines-on"}, "allow-lossy", 20129), + ({"arm": "control"}, None, 20128)): + with self.subTest(window=window): + (result / "window.json").write_text(json.dumps({ + **window, "base_url": "http://gw:8081/v1", + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z"})) + receipt = module.create_receipt(result, database=result / "absent.sqlite") + self.assertEqual(receipt["schema_version"], 5) + self.assertEqual(receipt["base_url"], "http://gw:8081/v1") + self.assertEqual(receipt["gateway_upstream"], f"http://10.0.2.2:{port}/v1") + self.assertEqual(receipt["compression_combo"], combo) + self.assertEqual(receipt["gateway"]["entry_port"], port) + class OpenHandsDispatchTests(unittest.TestCase): """Synthetic native REST transport, not an agent-server/model execution.""" @@ -1110,6 +1190,8 @@ def rendered(args, name, log, timeout): selection, self.run_id) self.assertIn("--network=none", calls[0]) self.assertIn("OPENHANDS_ARM=engines-on", calls[0]) + self.assertIn("OPENHANDS_BASE_URL=http://gw:8081/v1", calls[0]) + self.assertIn("OPENHANDS_COMPRESSION=allow-lossy", calls[0]) self.assertEqual(calls[0][-1], "--request") launched = launch.call_args.args[0] self.assertNotIn("--entrypoint", launched) From c7eda5be6643e19aebcd48f17ac7edc14c74a14c Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 14:41:16 -0400 Subject: [PATCH 09/22] OpenHands phase 2 (2/4): O1 topology, pinned nginx allowlist proxy, per-attempt session key Replaces the DOCKER-USER receipt contract (model_network, the egress host-file block) with the per-attempt topology from the phase-2 plan (section 1, E1). For stem S = -: - S-int is created --internal --ipv6=false with gateway_mode_ipv4=isolated and S-gw --ipv6=false, both owner-labelled and read back through inspect --format; a rejected option fails closed with no fallback. - The agent-server joins S-int only and publishes nothing. - A pinned nginx-unprivileged proxy (index ed04ec1f, linux/amd64 manifest f4522a5f) is created on S-gw with 127.0.0.1::8080, connected to S-int with alias gw, then started. config/proxy-nginx.conf allows three exact /v1 routes with fixed methods, refuses query strings, and passes only an allowlisted header set with fixed correlation, session and compression values. host.py renders it per attempt from arm_config and refuses any value outside its pattern; the rendered file is 0644 and outside every model mount. - docker_args allows S-int only for the server and the P1/P2 probe, and S-gw only for the P0 probe. - Teardown removes proxy, server, then both networks by exact name with confirmed-removal records; never prune. E2: the session key is generated per attempt with secrets, written with O_CREAT|O_EXCL|O_NOFOLLOW at 0600 under the state root (docker env-file syntax plus the curl header file), never in argv, and deleted after the containers are confirmed removed. The OPENHANDS_SERVER_ENV and OPENHANDS_HEADERS pointer variables are retired; dispatch derives the header path from the validated attempt identity. Inventory row openhands-session and its docs/secret-storage.md row are added. install() pulls and identity-checks the proxy image; run() re-checks it. Red: 85 tests, failures=2 errors=90 (implementation stashed). Green: 85 tests OK; secret guard, credential, host-request and codex-lane suites: 153 tests OK (9 skipped). Co-Authored-By: Claude Opus 5.5 (1M context) --- adoption/credential-inventory.json | 21 + .../openhands/config/host.example.json | 10 - .../openhands/config/proxy-nginx.conf | 72 +++ .../runtime-workers/openhands/dispatch.py | 52 +- blueprints/runtime-workers/openhands/host.py | 337 +++++++++-- .../runtime-workers/openhands/pins.json | 9 + .../runtime-workers/openhands/receipt.py | 18 +- docs/secret-storage.md | 1 + tests/test_runtime_worker_openhands.py | 571 ++++++++++++++---- 9 files changed, 872 insertions(+), 219 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/config/proxy-nginx.conf diff --git a/adoption/credential-inventory.json b/adoption/credential-inventory.json index 6abc45313..88ad7c968 100644 --- a/adoption/credential-inventory.json +++ b/adoption/credential-inventory.json @@ -177,6 +177,27 @@ "rotation": "Regenerate through the host's own nativestack-generation service.", "notes": "Host-local; no code in this repository reads it. Listed so the checker and the agent deny rules cover it." }, + { + "id": "openhands-session", + "label": "OpenHands agent-server session key (self-generated, one per attempt)", + "class": "local_service_secret", + "status": "generated_local", + "lane": "foundation-evaluation", + "store": { + "kind": "private_file", + "path_template": "$HOME/.local/state/native-agent-stack/runtime-workers/openhands/secrets/-.server.env" + }, + "variables": [], + "optional_variables": [], + "pointer_variables": [], + "loaders": [ + "blueprints/runtime-workers/openhands/host.py#generate_session_files", + "blueprints/runtime-workers/openhands/dispatch.py#execute" + ], + "environment_only_consumers": [], + "rotation": "Never reused: host.py generates a new value for every attempt and deletes both files after the attempt's containers are confirmed removed.", + "notes": "Generated with Python's secrets module and written 0600 with O_CREAT|O_EXCL|O_NOFOLLOW in a 0700 directory. -.server.env holds one OH_SESSION_API_KEYS_0= line in docker env-file syntax and is passed only through docker --env-file, so the variable exists only inside the agent-server container; the sibling -.headers file carries the X-Session-API-Key header for curl -H @file. Never in argv, never printed; the server is never read with a full docker inspect. No operator-supplied value and no pointer variable. OH_SESSION_API_KEYS_0 is not a host variable, so it is not in variables." + }, { "id": "claude-native", "label": "Claude Code native sign-in", diff --git a/blueprints/runtime-workers/openhands/config/host.example.json b/blueprints/runtime-workers/openhands/config/host.example.json index 76f5f3553..dd5781861 100644 --- a/blueprints/runtime-workers/openhands/config/host.example.json +++ b/blueprints/runtime-workers/openhands/config/host.example.json @@ -10,16 +10,6 @@ "mcp_readonly_mounts": [ "" ], - "egress": { - "control": { - "network": "rw-openhands-egress-control", - "policy_receipt": "" - }, - "engines-on": { - "network": "rw-openhands-egress-engines-on", - "policy_receipt": "" - } - }, "qmd_collections": { "foundation-docs": "", "foundation-adoption": "", diff --git a/blueprints/runtime-workers/openhands/config/proxy-nginx.conf b/blueprints/runtime-workers/openhands/config/proxy-nginx.conf new file mode 100644 index 000000000..f27fe98b5 --- /dev/null +++ b/blueprints/runtime-workers/openhands/config/proxy-nginx.conf @@ -0,0 +1,72 @@ +# Per-attempt gateway proxy for the O1 topology (README "Security posture"; +# docs/decisions/2026-09-28-openhands-resolver-isolation.md). +# +# Composed locally from documented nginx directives; no upstream ships this +# file. Directive sources, fetched 2026-09-28: nginx.org ngx_http_core_module +# (location, limit_except, merge_slashes, client_max_body_size), +# ngx_http_proxy_module (proxy_pass, proxy_pass_request_headers, +# proxy_set_header, proxy_http_version, proxy_buffering, +# proxy_request_buffering) and ngx_http_rewrite_module (if, return). Source +# lines cite nginx/nginx release-1.30.5 (4556c714). +# +# host.py render_proxy_config fills the four placeholders (gateway port, +# server name, run id, compression combo) from recipe.arm_config and the +# attempt identity, and refuses any value outside its own pattern. +pid /tmp/nginx.pid; +error_log /dev/stderr warn; +events { worker_connections 256; } +http { + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + server_tokens off; + access_log /dev/stdout; + + # Agent side: gw:8081, reached from the internal run network. + server { + listen 8081; + client_max_body_size 50m; + proxy_http_version 1.1; + proxy_buffering off; + proxy_request_buffering off; + proxy_read_timeout 900s; + proxy_send_timeout 900s; + # A proxy_pass that carries a URI still appends the request arguments + # (ngx_http_proxy_module.c:1240-1251,1365-1384). $is_args is "?" + # exactly when arguments exist (ngx_http_variables.c:1609-1624) and is + # never "0", so no agent-chosen query string reaches the gateway. + if ($is_args) { return 403; } + # Header allowlist: no client header reaches the gateway unless it is + # set here. A header whose value is empty is not sent + # (ngx_http_proxy_module.c:1301-1303,1417-1418), so the control arm's + # empty compression value drops that header. + proxy_pass_request_headers off; + proxy_set_header Host "10.0.2.2:@PORT@"; + proxy_set_header Content-Type $content_type; + proxy_set_header Accept $http_accept; + proxy_set_header Idempotency-Key $http_idempotency_key; + proxy_set_header Authorization "Bearer local-loopback"; + proxy_set_header X-Correlation-Id "@RUN@"; + proxy_set_header x-omniroute-session "@RUN@"; + proxy_set_header x-omniroute-compression "@COMPRESSION@"; + # Exact matches on the normalized URI (percent-decoding, dot segments + # and merged slashes are resolved first); each forwards to one fixed + # upstream URI, and limit_except refuses every other method. + location = /v1/responses { limit_except POST { deny all; } proxy_pass http://10.0.2.2:@PORT@/v1/responses; } + location = /v1/chat/completions { limit_except POST { deny all; } proxy_pass http://10.0.2.2:@PORT@/v1/chat/completions; } + location = /v1/models { limit_except GET { deny all; } proxy_pass http://10.0.2.2:@PORT@/v1/models; } + location / { return 403; } + } + + # Host side: 127.0.0.1: is published to 8080 and forwarded to the + # attempt's agent-server. The dispatch driver's event search carries a + # query string, so this block has no argument refusal. + server { + listen 8080; + client_max_body_size 50m; + proxy_read_timeout 900s; + location / { proxy_pass http://@SERVER@:8000; } + } +} diff --git a/blueprints/runtime-workers/openhands/dispatch.py b/blueprints/runtime-workers/openhands/dispatch.py index dcc3f63f2..ddbe243c3 100644 --- a/blueprints/runtime-workers/openhands/dispatch.py +++ b/blueprints/runtime-workers/openhands/dispatch.py @@ -16,9 +16,9 @@ import time import uuid -from recipe import arm_config, read_json -from host import (AGENT_LIMITS, DEFAULT_PORT, DOCKER, cleanup_container, docker_args, grade, logged_command, - owned_port, private_file, result_exit, utc_now, write_json) +from recipe import read_json +from host import (AGENT_LIMITS, DEFAULT_PORT, docker_args, grade, logged_command, owned_port, private_file, + result_exit, session_files, teardown_attempt, utc_now, write_json) from receipt import create_receipt, read_bounded @@ -56,14 +56,15 @@ def curl_json(url, *, method="GET", headers=None, body=None): return json.loads(raw) -def api_request(method, path, *, body=None, port=DEFAULT_PORT): +def api_request(method, path, *, headers, body=None, port=DEFAULT_PORT): + """headers is the attempt's host.session_files header file, checked by private_file.""" if not (re.fullmatch(r"/api/conversations(?:/[a-f0-9-]{36}(?:/(?:agent_final_response|interrupt))?)?", path) or re.fullmatch(r"/api/conversations/[a-f0-9-]{36}" + re.escape(ERROR_EVENT_SEARCH), path)): raise ValueError("unexpected_native_route") - return curl_json(server_url(port) + path, method=method, headers=os.environ["OPENHANDS_HEADERS"], body=body) + return curl_json(server_url(port) + path, method=method, headers=headers, body=body) -def agent_termination(conversation_id, execution, port): +def agent_termination(conversation_id, execution, port, headers): """Classify a terminal native status; agent limits keep their partial patch. SDK@fcc102a conversation/state.py:48-79 makes finished, error and stuck @@ -79,7 +80,7 @@ def agent_termination(conversation_id, execution, port): return execution if execution != "error": return "error" - page = api_request("GET", f"/api/conversations/{conversation_id}{ERROR_EVENT_SEARCH}", port=port) + page = api_request("GET", f"/api/conversations/{conversation_id}{ERROR_EVENT_SEARCH}", port=port, headers=headers) items = page.get("items") if isinstance(page, dict) else None if (isinstance(items, list) and len(items) == 1 and isinstance(items[0], dict) and {key: items[0].get(key) for key in ("kind", "source", "code")} @@ -112,11 +113,12 @@ def compression_delta(before, after): "basis": "entry gateway global analytics delta; separate from call_logs usage; concurrent callers may contribute"} -def server_command(pins, name, mounts, network, env_file, *, port=DEFAULT_PORT): +def server_command(pins, name, mounts, network, env_file): # Keep the image ENTRYPOINT. Its binary target is Dockerfile:580-590, not # the source target at :571. Supported preload: __main__.py:74-135,240-273. - return [*docker_args(pins, name, network=network), *mounts, "--detach", - "--publish", f"127.0.0.1:{owned_port(port)}:8000", "--env-file", str(env_file), + # O1: the server joins only -int and publishes nothing; host ingress + # is the proxy's loopback port (host.proxy_commands). + return [*docker_args(pins, name, network=network), *mounts, "--detach", "--env-file", str(env_file), "--env", "OH_ENABLE_VSCODE=0", "--env", "OH_CONVERSATIONS_PATH=/state/server/conversations", "--env", "OH_WORKSPACE_PATH=/workspace", "--env", "OH_BASH_EVENTS_DIR=/state/server/bash_events", "--env", "OPENHANDS_OWNED_CONTAINER=1", "--workdir", "/workspace", @@ -147,15 +149,16 @@ def check_server(body, *, port=DEFAULT_PORT): raise ValueError("live_openapi_request_mismatch") -def stop_server(result, status): - name = status.get("server_name") - if not isinstance(name, str) or not re.fullmatch(r"rw-openhands-[a-z0-9-]+-server", name): - return False +def stop_server(result, status=None): + """Tear down the attempt at result = /runs// (host.begin_attempt). + + host.teardown_attempt removes the proxy, the server and both networks by + exact name and deletes the key files after confirmed container removal. + """ try: - logged_command(DOCKER + ["logs", name], result / "server.log", cwd=result, timeout=30) - finally: - cleanup_container(name, result / "server.log") - return read_json(result / "server.log.cleanup.json").get("confirmed_removed") is True + return teardown_attempt(result.parents[2], result.parent.name, result.name) + except ValueError: + return False def finish_result(result, status, window): @@ -226,6 +229,8 @@ def execute(action, state, run_id, arm): raise ValueError("attempt_identity_mismatch") # Prepared by host.py into host-owned status.json; re-validated on read. port = owned_port(status.get("port", DEFAULT_PORT)) + # Derived from the validated identity only; curl_json re-checks the file. + headers = session_files(state, run_id, arm)[1] if action == "start": if status.get("status") != "prepared": print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), @@ -246,7 +251,7 @@ def execute(action, state, run_id, arm): window["started_at"] = utc_now() window["finished_at"] = None write_json(result / "window.json", window) - response = api_request("POST", "/api/conversations", body=result / "start.json", port=port) + response = api_request("POST", "/api/conversations", body=result / "start.json", port=port, headers=headers) conversation_id = str(uuid.UUID(response["id"])) status.update(status="running", conversation_id=conversation_id) elif action == "wait": @@ -257,10 +262,10 @@ def execute(action, state, run_id, arm): if time.time() >= status["deadline"]: stage = "deadline" try: - api_request("POST", f"/api/conversations/{conversation_id}/interrupt", port=port) + api_request("POST", f"/api/conversations/{conversation_id}/interrupt", port=port, headers=headers) finally: raise TimeoutError("native_conversation_deadline") - response = api_request("GET", f"/api/conversations/{conversation_id}", port=port) + response = api_request("GET", f"/api/conversations/{conversation_id}", port=port, headers=headers) execution = response["execution_status"] if execution in TERMINAL: status.update(status="terminal", execution_status=execution) @@ -280,11 +285,12 @@ def execute(action, state, run_id, arm): if status.get("status") != "terminal": raise ValueError("terminal_attempt_required") conversation_id = str(uuid.UUID(status["conversation_id"])) - response = api_request("GET", f"/api/conversations/{conversation_id}/agent_final_response", port=port) + response = api_request("GET", f"/api/conversations/{conversation_id}/agent_final_response", port=port, + headers=headers) if not isinstance(response.get("response"), str): raise ValueError("native_final_response_contract") write_json(result / "final-response.json", response) - status["agent_termination"] = agent_termination(conversation_id, status.get("execution_status"), port) + status["agent_termination"] = agent_termination(conversation_id, status.get("execution_status"), port, headers) receipt = finish_result(result, status, window) write_json(receipt_path, receipt) code = result_exit(receipt) diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index c26135350..18685f5b3 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -16,6 +16,7 @@ import os from pathlib import Path import re +import secrets import shutil import signal import stat @@ -25,7 +26,8 @@ import urllib.request import uuid -from recipe import HERE, arm_config, digest, environment_selection, llm_config, read_json, render_mcp +from recipe import (COMPRESSION_COMBOS, HERE, arm_config, digest, environment_selection, llm_config, read_json, + render_mcp) from e2e.task import load_task, worker_instruction from receipt import create_receipt, read_bounded, time_value @@ -37,8 +39,29 @@ # conversation/impl/local_conversation.py:753-755 (STUCK) and :2021-2043, # :2339-2360 (ERROR with ConversationErrorEvent code "MaxIterationsReached"). AGENT_LIMITS = frozenset({"stuck", "max_iterations_reached"}) -# SDK@fcc102a openhands-agent-server/openhands/agent_server/config.py:24. +# SDK@fcc102a openhands-agent-server/openhands/agent_server/config.py:24 and +# dependencies.py:19 (the header the server checks). SESSION_KEY_NAME = "OH_SESSION_API_KEYS_0" +SESSION_HEADER = "X-Session-API-Key" +OWNER_KEY, OWNER_VALUE = "com.native-agent-stack.owner", "gpt6-omniroute-framework-integration" +OWNER_LABEL = OWNER_KEY + "=" + OWNER_VALUE +# Every Docker resource of one attempt is named from S = - +# (O1 topology, README "Security posture"). +RUN_ID = re.compile(r"rw-openhands-[a-z0-9-]{1,64}") +STEM = re.compile(RUN_ID.pattern + r"-(?:control|engines-on)") +# docker/docs@4e9a5751 content/manuals/engine/network/port-publishing.md:121-131,186-192. +ISOLATED_OPTION = "com.docker.network.bridge.gateway_mode_ipv4" +# Selected fields only. The Docker CLI inspector falls back to the raw JSON +# field names when the typed template fails; checked read-only against this +# host's default bridge network on 2026-09-28 (evidence/phase2-commands.json). +NETWORK_FORMAT = ('{"id":{{json .Id}},"name":{{json .Name}},"driver":{{json .Driver}},' + '"internal":{{json .Internal}},"ipv6":{{json .EnableIPv6}},"options":{{json .Options}},' + '"labels":{{json .Labels}},"ipam":{{json .IPAM.Config}}}') +# Never a full container inspect: the server's Config.Env holds the session key. +CONTAINER_FORMAT = ('{"id":{{json .Id}},"name":{{json .Name}},"running":{{json .State.Running}},' + '"labels":{{json .Config.Labels}},"networks":{{json .NetworkSettings.Networks}}}') +PROXY_TEMPLATE = HERE / "config/proxy-nginx.conf" +PLACEHOLDER = re.compile(r"@[A-Z]+@") def owned_port(value): @@ -62,22 +85,19 @@ def private_file(path): return path -def check_server_env(): +def check_server_env(path): """Require the session-key variable NAME in the server env file; values stay unread. SDK@fcc102a agent_server/__main__.py:282-285 binds all interfaces only with a session API key (config.py:24 names OH_SESSION_API_KEYS_0). Without one the - server listens on container loopback, which the published port cannot reach. + server listens on container loopback, which the proxy cannot reach. docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124 is the --env-file format: newline-delimited lines, a BOM dropped on line one, leading whitespace trimmed, "#" comments, and the name ends at the first "=". A bare name copies the Docker CLI's own environment, so only the NAME= form is accepted. Only the text before "=" is compared; no value is kept, printed or logged. """ - name = os.environ.get("OPENHANDS_SERVER_ENV") - if not name: - raise ValueError("OPENHANDS_SERVER_ENV_required") - path = private_file(name) + path = private_file(path) try: text = read_bounded(path, limit=64 * 1024) except UnicodeDecodeError: @@ -235,18 +255,46 @@ def workspace_skills(stack_root, workspace): return {"names": sorted(names), "manifest_sha256": digest(manifest)} +def attempt_stem(run_id, arm): + if not isinstance(run_id, str) or not RUN_ID.fullmatch(run_id) or arm not in {"control", "engines-on"}: + raise ValueError("owned_attempt_identity_required") + return run_id + "-" + arm + + +def network_names(stem): + if not STEM.fullmatch(stem): + raise ValueError("owned_attempt_identity_required") + return {"int": stem + "-int", "gw": stem + "-gw"} + + +def network_allowed(name, network, installer): + """Allow no network, the installer's bridge, or the attempt's own network per role. + + Only the agent-server and the P1/P2 probe join -int; only the P0 + probe joins -gw. The proxy is created by proxy_commands, not here. + """ + if network == "none": + return True + if installer: + return network == "bridge" and "-install-" in name + for kind, roles in (("-int", ("-server", "-probe-int")), ("-gw", ("-probe-gw",))): + stem = network.removesuffix(kind) + if network.endswith(kind) and STEM.fullmatch(stem): + return name in {stem + role for role in roles} + return False + + def docker_args(pins, name, *, network="none", installer=False): if not re.fullmatch(r"rw-openhands-[a-z0-9-]+", name): raise ValueError("owned_container_name_required") limits = read_json(HERE / "config/worker.json")["runtime"] if network is False: network = "none" - if network not in {"none", "rw-openhands-egress-control", "rw-openhands-egress-engines-on"}: - if not (installer and network == "bridge" and "-install-" in name): - raise ValueError("explicit_scoped_model_network_required") + if not isinstance(network, str) or not network_allowed(name, network, installer): + raise ValueError("attempt_scoped_network_required") args = DOCKER + [ "run", "--rm", "--pull=never", "--name", name, "--platform", pins["image"]["platform"], - "--label", "com.native-agent-stack.owner=gpt6-omniroute-framework-integration", + "--label", OWNER_LABEL, "--user", "0:0" if installer else "10001:10001", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", "--tmpfs", "/state/home:rw,nosuid,nodev,mode=0700," + ("uid=0,gid=0" if installer else "uid=10001,gid=10001"), @@ -255,7 +303,7 @@ def docker_args(pins, name, *, network="none", installer=False): "--cpus", limits["cpus"], "--memory", limits["memory"], "--pids-limit", str(limits["pids_limit"]), ] args += ["--network=" + network] - # Base launches publish nothing; dispatch adds only the loopback mapping. + # Launches built here publish nothing; only the proxy publishes loopback ingress. return args @@ -463,8 +511,13 @@ def install(prefix, state): # Source bytes are retained for reproducibility, not executed or patched. download_verified(pins["source_archive"], state / "cache/upstream.tar.gz") download_verified(pins["uv_lock"], state / "cache/upstream.uv.lock") - subprocess.run(DOCKER + ["pull", "--platform", pins["image"]["platform"], pins["image"]["ref"]], check=True) - image_store = pinned_image_identity(pins["image"]) + stores = {} + # The agent-server image, then the pinned gateway proxy image; each is + # checked before anything else is pulled or any container runs. + for key in ("image", "gateway_proxy"): + subprocess.run(DOCKER + ["pull", "--platform", pins[key]["platform"], pins[key]["ref"]], check=True) + stores[key] = pinned_image_identity(pins[key]) + image_store = stores["image"] # A reviewable snapshot, with no repository metadata or host configuration. if HERE.resolve() != (prefix / "recipe").resolve(): shutil.copytree(HERE, prefix / "recipe", dirs_exist_ok=True, @@ -478,6 +531,8 @@ def install(prefix, state): code = execute_container(args, name, attempt / "install.log", 900) write_json(state / "installation.json", {"version": pins["version"], "image": pins["image"]["ref"], "image_store": image_store, + "gateway_proxy": pins["gateway_proxy"]["ref"], + "gateway_proxy_store": stores["gateway_proxy"], "requirements_sha256": pins["requirements_sha256"], "exit_code": code}) if code: raise RuntimeError("container_install_failed_see_private_install_log") @@ -501,36 +556,181 @@ def install_mounts(prefix, state): + mount(prefix / "recipe", "/recipe")) -def model_network(host, arm): - """Require coordinator network-policy evidence before granting any egress. +def inspect_selected(kind, template, name): + """One `docker inspect --format` read of selected fields, as JSON.""" + value = json.loads(subprocess.check_output(DOCKER + [kind, "inspect", "--format", template, name], + text=True, timeout=30)) + if not isinstance(value, dict): + raise ValueError("unexpected_inspect_shape") + return value - Docker docs@4e9a575 firewall-iptables.md:22-24,48-95 defines DOCKER-USER. - Rootless namespace placement and actual rule effects require host probes. - A network name or MCP allowlist alone is never network acceptance. + +def checked_network(name, kind): + """Read one attempt network back and require its contract (plan E1).""" + info = inspect_selected("network", NETWORK_FORMAT, name) + internal = kind == "int" + if (info.get("name") != name or info.get("driver") != "bridge" or info.get("internal") is not internal + or info.get("ipv6") is not False or (info.get("labels") or {}).get(OWNER_KEY) != OWNER_VALUE + or not re.fullmatch(r"[a-f0-9]{64}", str(info.get("id"))) + or (internal and (info.get("options") or {}).get(ISOLATED_OPTION) != "isolated")): + raise ValueError(f"attempt_{kind}_network_contract_mismatch") + return info + + +def create_topology(result, stem): + """Create the attempt's internal and gateway networks; fail closed. + + docker/docs@4e9a5751 content/manuals/engine/network/port-publishing.md: + 186-192: containers on an internal network can reach host services through + the bridge address, including services listening on all host addresses; + no address is assigned to the bridge with gateway mode isolated (options + :121-131). A rejected option is never retried without it. """ - selection = arm_config(arm) - entry = host.get("egress", {}).get(arm, {}) - name = "rw-openhands-egress-" + arm - if entry.get("network") != name or not entry.get("policy_receipt"): - raise ValueError("verified_per_port_egress_policy_required") - path = Path(entry["policy_receipt"]) - if path.stat().st_uid != os.getuid() or stat.S_IMODE(path.stat().st_mode) != 0o600: - raise ValueError("private_host_policy_receipt_required") - proof = json.loads(read_bounded(path)) - age = datetime.now(timezone.utc) - time_value(proof["verified_at"]) - if not 0 <= age.total_seconds() <= 900: - raise ValueError("fresh_egress_verification_required") - if (proof.get("mechanism") != "docker-user-iptables" - or proof.get("allowed_tcp") != [f"10.0.2.2:{selection['gateway_port']}"] - or any(proof.get(k) is not True for k in ( - "default_deny", "host_input_denied", "ipv6_disabled", "gateway_reachable", "other_host_ports_denied"))): - raise ValueError("egress_policy_contract_mismatch") - info = json.loads(subprocess.check_output(DOCKER + ["network", "inspect", name], text=True, timeout=30))[0] - if (info.get("Id") != proof.get("network_id") or info.get("Driver") != "bridge" - or info.get("EnableIPv6") or info.get("Labels", {}).get("com.native-agent-stack.owner") - != "gpt6-omniroute-framework-integration"): - raise ValueError("egress_network_identity_mismatch") - return name + names = network_names(stem) + commands = { + "int": ["network", "create", "--internal", "--ipv6=false", "-o", ISOLATED_OPTION + "=isolated", + "--label", OWNER_LABEL, names["int"]], + "gw": ["network", "create", "--ipv6=false", "--label", OWNER_LABEL, names["gw"]], + } + for kind in ("int", "gw"): + if logged_command(DOCKER + commands[kind], result / f"network-{kind}-create.log", cwd=result, timeout=60): + raise RuntimeError(f"attempt_{kind}_network_create_failed") + return {kind: checked_network(names[kind], kind) for kind in ("int", "gw")} + + +def session_files(state, run_id, arm): + """The attempt's key files under the state root, derived from its identity only.""" + stem = attempt_stem(run_id, arm) + directory = Path(state) / "secrets" + return directory / (stem + ".server.env"), directory / (stem + ".headers") + + +def generate_session_files(state, run_id, arm): + """Generate this attempt's agent-server key; return the two paths, never the value. + + One value from Python's secrets module goes into .server.env in + docker env-file syntax (NAME=value, docs.docker.com container run + --env-file) and into .headers for curl -H @file. Each file is created + with O_CREAT|O_EXCL|O_NOFOLLOW at 0600, so an existing file or a planted + symlink is refused. teardown_attempt deletes both after confirmed removal. + """ + paths = session_files(state, run_id, arm) + private_directory(paths[0].parent) + value = secrets.token_urlsafe(32) + for path, line in zip(paths, (f"{SESSION_KEY_NAME}={value}\n", f"{SESSION_HEADER}: {value}\n")): + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(descriptor, "w") as stream: + os.fchmod(stream.fileno(), 0o600) + stream.write(line) + return paths + + +def render_proxy_config(selection, run_id): + """Fill config/proxy-nginx.conf for one attempt; refuse anything unexpected. + + The selection must equal what arm_config returns for its own fields, so a + cross-arm pairing or an unrecorded compression combo cannot be rendered, + and every value must match its own pattern before substitution. + """ + checked = arm_config(selection.get("arm"), selection.get("requested_model"), selection.get("base_url"), + selection.get("compression_combo")) + if checked != selection: + raise ValueError("proxy_selection_mismatch") + stem = attempt_stem(run_id, checked["arm"]) + values = {"@PORT@": str(checked["gateway_port"]), "@SERVER@": stem + "-server", "@RUN@": run_id, + "@COMPRESSION@": checked["compression_combo"] or ""} + patterns = {"@PORT@": r"2012[89]", "@SERVER@": STEM.pattern + "-server", "@RUN@": RUN_ID.pattern, + "@COMPRESSION@": "|".join(map(re.escape, sorted(COMPRESSION_COMBOS))) + "|"} + text = PROXY_TEMPLATE.read_text() + found = PLACEHOLDER.findall(text) + if set(found) != set(values) or text.count("@") != 2 * len(found): + raise ValueError("proxy_template_placeholders_changed") + for key, value in values.items(): + if not re.fullmatch(patterns[key], value): + raise ValueError("proxy_value_outside_pattern") + text = text.replace(key, value) + return text + + +def write_proxy_config(result, text): + """Host-owned 0644 file outside every model mount: /proxy/nginx.conf.""" + directory = result / "proxy" + private_directory(directory) + path = directory / "nginx.conf" + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644) + with os.fdopen(descriptor, "w") as stream: + # umask 077 would leave 0600; the image's non-root user reads the bind mount. + os.fchmod(stream.fileno(), 0o644) + stream.write(text) + return path, digest(path) + + +def proxy_commands(pins, stem, config, port): + """Create on -gw with loopback ingress, join -int as "gw", start. + + A created container joins its configured networks when it runs, and + network connect --alias adds a network-scoped name (docs.docker.com + container run and network connect, fetched 2026-09-28). Starting after the + connect lets nginx resolve -server when it loads. --entrypoint nginx + bypasses the image's entrypoint scripts, unprobed under --read-only. + """ + image, names = pins["gateway_proxy"], network_names(stem) + name = stem + "-proxy" + create = DOCKER + [ + "create", "--pull=never", "--name", name, "--platform", image["platform"], "--label", OWNER_LABEL, + "--network=" + names["gw"], "--publish", f"127.0.0.1:{owned_port(port)}:8080", + "--read-only", "--tmpfs", "/tmp:rw,nosuid,nodev,mode=1777", "--cap-drop=ALL", + "--security-opt=no-new-privileges", "--pids-limit", "64", "--memory", "256m", + *mount(config, "/etc/nginx/nginx.conf"), "--entrypoint", "nginx", image["ref"], "-g", "daemon off;", + ] + return create, DOCKER + ["network", "connect", "--alias", "gw", names["int"], name], DOCKER + ["start", name] + + +def launch_proxy(result, pins, stem, config, port): + for step, argv in zip(("create", "connect", "start"), proxy_commands(pins, stem, config, port)): + if logged_command(argv, result / f"proxy-{step}.log", cwd=result, timeout=60): + raise RuntimeError(f"proxy_{step}_failed") + + +def cleanup_network(name, record): + """Remove one network by exact name; confirm by exact membership in network ls.""" + cleanup = {"confirmed_removed": False, "error_type": None} + try: + subprocess.run(DOCKER + ["network", "rm", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + check=False, timeout=30) + listed = subprocess.run(DOCKER + ["network", "ls", "--format", "{{.Name}}"], capture_output=True, + text=True, check=False, timeout=30) + cleanup["confirmed_removed"] = listed.returncode == 0 and name not in listed.stdout.splitlines() + except (OSError, subprocess.TimeoutExpired) as exc: + cleanup["error_type"] = type(exc).__name__ + write_json(record, cleanup) + return cleanup["confirmed_removed"] + + +def teardown_attempt(state, run_id, arm): + """Remove the attempt's proxy, server and networks by exact name; never prune. + + Container logs go to private files first (the proxy access log is the + denied-path evidence). The key files are deleted only after both + containers are confirmed removed. True only if all four are confirmed. + """ + stem = attempt_stem(run_id, arm) + result = Path(state) / "runs" / run_id / arm + containers = [] + for role in ("proxy", "server"): + name, logfile = f"{stem}-{role}", result / f"{role}.log" + try: + logged_command(DOCKER + ["logs", name], logfile, cwd=result, timeout=30) + finally: + cleanup_container(name, logfile) + record = read_json(logfile.with_suffix(logfile.suffix + ".cleanup.json")) + containers.append(record.get("confirmed_removed") is True) + networks = [cleanup_network(name, result / f"network-{kind}.cleanup.json") + for kind, name in network_names(stem).items()] + if all(containers): + for path in session_files(state, run_id, arm): + path.unlink(missing_ok=True) + return all(containers) and all(networks) def clone_command(prefix, task, workspace): @@ -597,11 +797,19 @@ def model_visible(root, *, writable): raise ValueError("special_file_in_model_mount") -def prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id, port=DEFAULT_PORT): - from dispatch import check_server, private_file, server_command - stem = run_id + "-" + selection["arm"] - env_file = private_file(os.environ["OPENHANDS_SERVER_ENV"]) - private_file(os.environ["OPENHANDS_HEADERS"]) +def prepare_native_dispatch(state, run_id, selection, prefix, pins, base, host, port=DEFAULT_PORT): + """Serialize the request offline, then build the O1 topology around the server. + + Order: request render with no network; the two attempt networks; the + per-attempt key; the agent-server on -int with no published port; + the rendered proxy config; proxy create/connect/start; the native health + and OpenAPI gate through the proxy's loopback ingress. The caller tears + everything down on any failure (teardown_attempt). + """ + from dispatch import check_server, server_command + stem = attempt_stem(run_id, selection["arm"]) + result = Path(state) / "runs" / run_id / selection["arm"] + port = owned_port(port) environment = ["--env", "OPENHANDS_OWNED_CONTAINER=1", "--env", "OPENHANDS_RUN_ID=" + run_id, "--env", "OPENHANDS_ARM=" + selection["arm"], "--env", "OPENHANDS_MODEL=" + selection["requested_model"], @@ -617,19 +825,22 @@ def prepare_native_dispatch(result, prefix, pins, base, network, host, selection write_json(result / "start.json", body) private_directory(result / "server") model_visible(result / "server", writable=True) + networks = create_topology(result, stem) + env_file, _ = generate_session_files(state, run_id, selection["arm"]) + check_server_env(env_file) mounts = base + environment + mount(result / "server", "/state/server", False) + mount(result / "worker", "/run-output", False) name = stem + "-server" - try: - if logged_command(server_command(pins, name, mounts, network, env_file, port=port), - result / "server-start.log", cwd=result, timeout=60): - raise RuntimeError("native_server_launch_failed") - check_server(body, port=port) - except (Exception, KeyboardInterrupt): - cleanup_container(name, result / "server.log") - raise + if logged_command(server_command(pins, name, mounts, networks["int"]["name"], env_file), + result / "server-start.log", cwd=result, timeout=60): + raise RuntimeError("native_server_launch_failed") + config, config_sha256 = write_proxy_config(result, render_proxy_config(selection, run_id)) + launch_proxy(result, pins, stem, config, port) + check_server(body, port=port) # Host-owned status is the dispatch commands' only port source. write_json(result / "status.json", {"run_id": run_id, "arm": selection["arm"], "status": "prepared", - "server_name": name, "port": port, "receipt": str(result / "receipt.json")}) + "server_name": name, "proxy_name": stem + "-proxy", "port": port, + "proxy_config_sha256": config_sha256, + "receipt": str(result / "receipt.json")}) def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAULT_PORT): @@ -650,19 +861,19 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL try: port = owned_port(port) pins, host, mcp = preflight(prefix, state) - check_server_env() installed = read_json(state / "installation.json") if installed.get("exit_code") != 0 or installed.get("requirements_sha256") != pins["requirements_sha256"]: raise ValueError("matching_successful_installation_required") if not os.path.lexists(prefix / "venv/bin/python"): raise ValueError("owned_sdk_venv_missing") + # Present locally by digest (install pulls it); proxy launches never pull. + pinned_image_identity(pins["gateway_proxy"]) cfg = read_json(HERE / "config/worker.json") selection = environment_selection(os.environ, arm) llm_config(cfg, selection["requested_model"], arm=arm, base_url=selection["base_url"], compression=selection["compression_combo"]) window.update({k: v for k, v in selection.items() if k != "headers"}) window["header_names"] = sorted([*selection["headers"], "x-omniroute-session", "X-Correlation-Id", "Idempotency-Key"]) - network = model_network(host, arm) stage = "prepare" original = Path(os.environ["OPENHANDS_TASK_FILE"]).resolve() task_sha = os.environ["OPENHANDS_TASK_SHA256"] @@ -735,7 +946,7 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL if execute_container(args, stem + "-qmd", result / "qmd-setup.log", 900): raise RuntimeError("qmd_setup_failed") stage = "start" - prepare_native_dispatch(result, prefix, pins, base, network, host, selection, run_id, port=port) + prepare_native_dispatch(state, run_id, selection, prefix, pins, base, host, port=port) prepared = True write_json(result / "window.json", window) if not prepare_only: @@ -748,6 +959,12 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL except (Exception, KeyboardInterrupt) as exc: window["failure_stage"] = stage window["failure_type"] = type(exc).__name__ + if stage == "start": + # Resources may exist from the first network onward; remove them by name. + try: + teardown_attempt(state, run_id, arm) + except (OSError, ValueError, subprocess.SubprocessError): + pass # The cleanup records keep the uncertainty. finally: if native_receipt is not None: receipt = native_receipt diff --git a/blueprints/runtime-workers/openhands/pins.json b/blueprints/runtime-workers/openhands/pins.json index ae6898e1c..637b07c2f 100644 --- a/blueprints/runtime-workers/openhands/pins.json +++ b/blueprints/runtime-workers/openhands/pins.json @@ -20,6 +20,15 @@ "config_sha256": "39426d8ce5c3ecaf6829063ec8ccfe5c289f049ae007fb14d89317b3982d196f", "provenance": "GHCR returned manifest bytes SHA256; image config OPENHANDS_BUILD_GIT_SHA and OPENHANDS_BUILD_GIT_REF match the SDK tag." }, + "gateway_proxy": { + "tag_observed": "1.30.5-alpine", + "ref": "ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e", + "platform": "linux/amd64", + "manifest_sha256": "f4522a5f23f9caebcae9be6e3940ea0599a2b9eb05222c7118d01fc887e848d6", + "config_sha256": "61640a44f99dcfd7f1af9f144f023195e82fdefd2f5aba7f917548867d4f3a6f", + "source": "https://github.com/nginx/docker-nginx-unprivileged/tree/588b4cbc88065a313909f47a02510639e01cc508", + "provenance": "Anonymous GHCR OCI reads on 2026-09-28: the SHA256 of the returned index, linux/amd64 manifest and config bytes equal their digests. Config: NGINX_VERSION=1.30.5, org.opencontainers.image.version 1.30.5-alpine, revision 588b4cbc, user 101, exposed 8080/tcp. Pulled by index digest into the rootless store and checked with host.pinned_image_identity." + }, "wheels": { "openhands-sdk": { "url": "https://files.pythonhosted.org/packages/e9/ac/d27c5becc3b59b8cc46a1614ac478cc94eb73e626ab15831906fcdaafb29/openhands_sdk-1.49.6-py3-none-any.whl", diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 34dce4355..71ca1e512 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -197,12 +197,14 @@ def create_receipt(result, database=None): and checked.get("conversion_exit_code") == 0 and window.get("worker_exit_code") == 0) termination = window.get("agent_termination") usage = summarize_gateway(rows, selection) - cleanup = [] - for path in sorted(result.glob("*.log.cleanup.json")): - try: - cleanup.append(read_json(path).get("confirmed_removed") is True) - except (OSError, ValueError, AttributeError): - cleanup.append(False) + def removals(pattern): + found = [] + for path in sorted(result.glob(pattern)): + try: + found.append(read_json(path).get("confirmed_removed") is True) + except (OSError, ValueError, AttributeError): + found.append(False) + return {"attempts": len(found), "confirmed_removed": sum(found), "complete": bool(found) and all(found)} return { "schema_version": 5, "evidence_class": "SDK inference adapter with official SWE-bench grading", **{k: selection[k] for k in ("arm", "base_url", "gateway_upstream", "requested_model", "gateway_model", @@ -225,7 +227,9 @@ def create_receipt(result, database=None): "independent_trace_required": True, "not_collected_reason": NOT_COLLECTED_REASON, "skills_listed_at_start": NOT_COLLECTED, "skill_listing_matches_manifest": NOT_COLLECTED, - "container_cleanup": {"attempts": len(cleanup), "confirmed_removed": sum(cleanup), "complete": bool(cleanup) and all(cleanup)}, + # host.cleanup_container and host.cleanup_network records, counted separately. + "container_cleanup": removals("*.log.cleanup.json"), + "network_cleanup": removals("network-*.cleanup.json"), "gateway": { "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, **usage, "entry_port": selection["gateway_port"], diff --git a/docs/secret-storage.md b/docs/secret-storage.md index 628d78b73..f01ac7cd8 100644 --- a/docs/secret-storage.md +++ b/docs/secret-storage.md @@ -21,6 +21,7 @@ against it. | `tavily` | Tavily API key, memory only ([kernel keyring](#memory-only-option-linux-kernel-keyring-2026-09-26)) | optional | Linux kernel user keyring, key `tavily_api_key`; never a file (macOS: the login Keychain) | `TAVILY_API_KEY`, set only in the environment of the command that `exec` or `tvly-keyring` starts | | `grafana-admin` | Local Grafana admin account and secret key | generated locally | `~/.config/ecosystem-observability/ecosystem-grafana.env` | `GF_SECURITY_*` | | `nativestack-generation-key` | Host service key | generated locally | `~/.config/nativestack/generation.key` | none | +| `openhands-session` | OpenHands agent-server session key for one runtime-worker attempt ([decision](decisions/2026-09-28-openhands-resolver-isolation.md)) | generated locally, per attempt; deleted after the attempt's containers are confirmed removed | `~/.local/state/native-agent-stack/runtime-workers/openhands/secrets/-.server.env`, plus the `.headers` file beside it | none on the host; `OH_SESSION_API_KEYS_0` exists only inside the agent-server container (Docker `--env-file`) | | `claude-native`, `codex-native`, `gh-native` | Native sign-ins | stored by each tool | each tool's own store | none | | `huggingface-native`, `huggingface-native-stored` | Hugging Face sign-in: the active token and every saved token | stored by `hf auth login`, one token per host ([Hugging Face sign-in](#hugging-face-sign-in)) | `$HF_HOME/token` and `$HF_HOME/stored_tokens`; `HF_HOME` defaults to `${XDG_CACHE_HOME:-$HOME/.cache}/huggingface` | none | | `ibkr-gateway` | IB Gateway / TWS login | typed in at login, nothing stored | none | none | diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 450673a23..075af46b7 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -8,6 +8,7 @@ import asyncio import contextlib +import hashlib import io import json import os @@ -16,6 +17,7 @@ import re import shutil import sqlite3 +import stat import subprocess import sys import tempfile @@ -182,6 +184,14 @@ def test_immutable_artifacts(self): for name in ("source_archive", "uv_lock"): self.assertRegex(pins[name]["sha256"], r"^[0-9a-f]{64}$") self.assertRegex(pins["image"]["ref"], r"^ghcr\.io/openhands/agent-server@sha256:[0-9a-f]{64}$") + # Phase 2: the gateway proxy image is pinned by index digest, with its + # linux/amd64 manifest and config digests for pinned_image_identity. + proxy = pins["gateway_proxy"] + self.assertRegex(proxy["ref"], r"^ghcr\.io/nginx/nginx-unprivileged@sha256:[0-9a-f]{64}$") + self.assertEqual(proxy["platform"], "linux/amd64") + for key in ("manifest_sha256", "config_sha256"): + self.assertRegex(proxy[key], r"^[0-9a-f]{64}$") + self.assertNotEqual(proxy[key], proxy["ref"].rsplit(":", 1)[1]) def test_installer_and_runtime_are_scoped(self): for filename in ("install.sh", "run-e2e.sh"): @@ -348,19 +358,31 @@ def test_every_container_is_owned_and_has_no_published_port(self): with self.assertRaises(ValueError): host.docker_args(self.read_json("pins.json"), "unowned") - def test_model_container_is_nonroot_and_network_is_explicit(self): + def test_model_container_is_nonroot_and_network_is_per_attempt(self): + # Plan E1: "none", or one of the attempt's own two networks for exactly + # the roles that need it. No shared or cross-attempt network is allowed. host = load_recipe_module("host.py") pins = self.read_json("pins.json") args = host.docker_args(pins, "rw-openhands-fixture") self.assertEqual(args[args.index("--user") + 1], "10001:10001") self.assertIn("--network=none", args) - with self.assertRaises(ValueError): - host.docker_args(pins, "rw-openhands-agent", network="bridge") - args = host.docker_args(pins, "rw-openhands-agent", network="rw-openhands-egress-control") - self.assertIn("--network=rw-openhands-egress-control", args) self.assertIn("--cap-drop=ALL", args) self.assertIn("--security-opt=no-new-privileges", args) self.assertIn("HOME=/state/home", args) + stem = "rw-openhands-fixture-control" + for name, network in ((stem + "-server", stem + "-int"), (stem + "-probe-int", stem + "-int"), + (stem + "-probe-gw", stem + "-gw"), (stem + "-request", "none")): + with self.subTest(name=name, network=network): + self.assertIn("--network=" + network, host.docker_args(pins, name, network=network)) + for name, network in (("rw-openhands-agent", "bridge"), (stem + "-server", stem + "-gw"), + (stem + "-request", stem + "-int"), (stem + "-qmd", stem + "-gw"), + (stem + "-server", "rw-openhands-other-control-int"), + ("rw-openhands-other-control-server", stem + "-int"), + (stem + "-server", "rw-openhands-egress-control"), (stem + "-server", "host"), + (stem + "-server", stem + "-int-x"), + ("rw-openhands-fixture-other-server", "rw-openhands-fixture-other-int")): + with self.subTest(name=name, network=network), self.assertRaises(ValueError): + host.docker_args(pins, name, network=network) def test_mount_allowlist_checks_resolved_targets_and_secret_ancestors(self): host = load_recipe_module("host.py") @@ -389,12 +411,19 @@ def test_remote_mcp_services_are_not_registered_in_model_network(self): self.assertFalse(mcp["ai-memory"]["enabled"]) self.assertFalse(mcp["socraticode"]["enabled"]) - def test_host_template_names_the_two_required_network_policy_receipts(self): + def test_host_template_has_no_firewall_receipt_contract(self): + # F19 superseded (plan section 5): containment comes from the + # per-attempt topology and its probe, not a DOCKER-USER receipt. host = self.read_json("config/host.example.json") - self.assertEqual(set(host["egress"]), {"control", "engines-on"}) - for arm, entry in host["egress"].items(): - self.assertEqual(entry["network"], "rw-openhands-egress-" + arm) - self.assertIn("policy_receipt", entry) + self.assertNotIn("egress", host) + source = (RECIPE / "host.py").read_text() + for retired in ("model_network", "docker-user-iptables", "policy_receipt", "rw-openhands-egress-"): + self.assertNotIn(retired, source) + # The pointer variables are retired: the driver generates the key files. + for name in ("host.py", "dispatch.py"): + text = (RECIPE / name).read_text() + self.assertNotIn("OPENHANDS_SERVER_ENV", text) + self.assertNotIn("OPENHANDS_HEADERS", text) def test_qmd_guard_requires_explicit_scope_and_lexical_query(self): guard = load_recipe_module("mcp_guard.py") @@ -676,39 +705,50 @@ def test_session_key_preflight_checks_the_variable_name_only(self): with self.subTest(text=text.replace(marker, "")): env.write_text(text) env.chmod(0o600) - with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}): - if accepted: - self.assertEqual(host.check_server_env(), env) - continue - with self.assertRaises(ValueError) as caught: - host.check_server_env() + if accepted: + self.assertEqual(host.check_server_env(env), env) + continue + with self.assertRaises(ValueError) as caught: + host.check_server_env(env) self.assertNotIn(marker, str(caught.exception)) env.write_text(f"{name}={marker}\n") env.chmod(0o644) - with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}), self.assertRaises(ValueError): - host.check_server_env() - with patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": ""}), self.assertRaises(ValueError): - host.check_server_env() + with self.assertRaises(ValueError): + host.check_server_env(env) - def test_missing_session_key_name_stops_preflight_without_exposing_values(self): + def test_session_key_is_generated_per_attempt_and_never_returned(self): + # Plan E2: generated with Python's secrets module, written with + # O_CREAT|O_EXCL|O_NOFOLLOW at 0600 under the state root, in docker + # env-file syntax (N4), plus the curl header file. SDK@fcc102a + # agent_server/dependencies.py:19 names the X-Session-API-Key header. host = load_recipe_module("host.py") - marker = "-".join(("synthetic", "fixture", "marker")) - env = self.result / "server.env" - env.write_text(f"OTHER_NAME={marker}\n") - env.chmod(0o600) - output = io.StringIO() - with patch.object(host, "preflight", return_value=({}, {}, {})), \ - patch.object(host, "read_json") as read_json, \ - patch.object(host, "create_receipt", side_effect=self.absent_gateway_receipt()), \ - patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": str(env)}), contextlib.redirect_stdout(output): - code = host.run(self.result / "prefix", self.result, run_id="rw-openhands-fixture", arm="control") - self.assertEqual(code, 3) - read_json.assert_not_called() # Stopped before the installation record. - path = self.result / "runs/rw-openhands-fixture/control" - self.assertEqual(json.loads(output.getvalue())["failure_stage"], "preflight") - self.assertEqual(json.loads((path / "window.json").read_text())["failure_type"], "ValueError") - for text in (output.getvalue(), *(p.read_text() for p in path.glob("*.json"))): - self.assertNotIn(marker, text) + env_path, headers_path = host.generate_session_files(self.result, "rw-openhands-fixture", "control") + self.assertEqual((env_path, headers_path), host.session_files(self.result, "rw-openhands-fixture", "control")) + self.assertEqual(env_path.parent, self.result / "secrets") + self.assertEqual(stat.S_IMODE(env_path.parent.stat().st_mode), 0o700) + for path in (env_path, headers_path): + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + self.assertEqual(host.private_file(path), path) + name, value = env_path.read_text().rstrip("\n").split("=", 1) + self.assertEqual(name, "OH_SESSION_API_KEYS_0") + self.assertRegex(value, r"^[A-Za-z0-9_-]{43}$") + self.assertEqual(env_path.read_text().count("\n"), 1) + self.assertEqual(headers_path.read_text(), "X-Session-API-Key: " + value + "\n") + self.assertEqual(host.check_server_env(env_path), env_path) + with self.assertRaises(FileExistsError): + host.generate_session_files(self.result, "rw-openhands-fixture", "control") + other, _ = host.generate_session_files(self.result, "rw-openhands-fixture", "engines-on") + self.assertNotEqual(other.read_text().split("=", 1)[1], value + "\n") + # A planted symlink is neither followed nor replaced. + target = self.result / "planted-target" + target.write_text("unchanged") + host.session_files(self.result, "rw-openhands-planted", "control")[0].symlink_to(target) + with self.assertRaises(OSError): + host.generate_session_files(self.result, "rw-openhands-planted", "control") + self.assertEqual(target.read_text(), "unchanged") + for run_id, arm in (("../escape", "control"), ("rw-openhands-fixture", "other"), ("rw-openhands-A", "control")): + with self.subTest(run_id=run_id, arm=arm), self.assertRaises(ValueError): + host.session_files(self.result, run_id, arm) def test_port_outside_owned_range_fails_in_preflight(self): # PR #428 crawl4ai host.py:44-45: exact int (bool refused) in 3730..3799. @@ -757,8 +797,8 @@ def test_installer_mounts_exclude_snapshot_from_writable_paths(self): # Descriptor (api/swagger.yaml:1839-1850); a pull by index digest records # the index reference in RepoDigests (distribution/pull_v2.go:431-434). @staticmethod - def image_inspect(pins, store): - image = pins["image"] + def image_inspect(pins, store, key="image"): + image = pins[key] index = "sha256:" + image["ref"].rsplit("@sha256:", 1)[1] manifest, config = "sha256:" + image["manifest_sha256"], "sha256:" + image["config_sha256"] common = {"RepoDigests": [image["ref"]], "Os": "linux", "Architecture": "amd64"} @@ -770,18 +810,22 @@ def image_inspect(pins, store): "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": manifest, "size": 4313, "platform": {"architecture": "amd64", "os": "linux"}}}) - def install_with(self, host, pins, plain, selected, root): + def install_with(self, host, pins, plain, selected, root, proxy=None): """Run install() on mocked inspect output; no Docker command runs.""" + views = {pins["image"]["ref"]: (plain, selected), + pins["gateway_proxy"]["ref"]: proxy or self.image_inspect(pins, "containerd", "gateway_proxy")} + def inspect(argv, **kwargs): self.assertEqual(argv[:len(host.DOCKER) + 2], [*host.DOCKER, "image", "inspect"]) - self.assertEqual(argv[-1], pins["image"]["ref"]) - return json.dumps([selected if "--platform" in argv else plain]) + found_plain, found_selected = views[argv[-1]] + return json.dumps([found_selected if "--platform" in argv else found_plain]) with patch.object(host, "preflight", return_value=(pins, {}, {})), \ - patch.object(host, "download_verified"), patch.object(host.subprocess, "run"), \ + patch.object(host, "download_verified"), patch.object(host.subprocess, "run") as run, \ patch.object(host.subprocess, "check_output", side_effect=inspect) as calls, \ patch.object(host, "execute_container", return_value=0) as execute, \ contextlib.redirect_stdout(io.StringIO()): + self.install_runs = run try: host.install(root / "prefix", root / "state") except ValueError as exc: @@ -791,18 +835,25 @@ def inspect(argv, **kwargs): def test_install_accepts_the_pinned_image_on_containerd_and_classic_stores(self): host = load_recipe_module("host.py") pins = json.loads((RECIPE / "pins.json").read_text()) - ref = pins["image"]["ref"] + ref, proxy = pins["image"]["ref"], pins["gateway_proxy"]["ref"] for store in ("containerd", "classic"): with self.subTest(store=store): error, execute, calls = self.install_with(host, pins, *self.image_inspect(pins, store), - self.result / store) + self.result / store, + self.image_inspect(pins, store, "gateway_proxy")) self.assertIsNone(error) execute.assert_called_once() argv = [call.args[0] for call in calls.call_args_list] self.assertEqual(argv, [[*host.DOCKER, "image", "inspect", ref], - [*host.DOCKER, "image", "inspect", "--platform", "linux/amd64", ref]]) + [*host.DOCKER, "image", "inspect", "--platform", "linux/amd64", ref], + [*host.DOCKER, "image", "inspect", proxy], + [*host.DOCKER, "image", "inspect", "--platform", "linux/amd64", proxy]]) + pulls = [call.args[0] for call in self.install_runs.call_args_list if "pull" in call.args[0]] + self.assertEqual(pulls, [[*host.DOCKER, "pull", "--platform", "linux/amd64", ref], + [*host.DOCKER, "pull", "--platform", "linux/amd64", proxy]]) installed = json.loads((self.result / store / "state/installation.json").read_text()) self.assertEqual(installed["image_store"], store) + self.assertEqual((installed["gateway_proxy"], installed["gateway_proxy_store"]), (proxy, store)) def test_install_refuses_each_image_identity_mismatch_before_any_container(self): host = load_recipe_module("host.py") @@ -835,6 +886,20 @@ def test_install_refuses_each_image_identity_mismatch_before_any_container(self) error, execute, _ = self.install_with(host, pins, plain, selected, self.result / f"refused-{number}") self.assertEqual(error, expected) execute.assert_not_called() + # The gateway proxy pin goes through the same identity check. + proxy_repo = pins["gateway_proxy"]["ref"].split("@", 1)[0] + for number, (view, change, expected) in enumerate(( + ("plain", {"RepoDigests": [proxy_repo + ":1.30.5-alpine"]}, "image_repo_digest_mismatch"), + ("plain", {"Id": other}, "image_configuration_hash_mismatch"), + ("platform", {"Architecture": "arm64"}, "image_platform_manifest_mismatch"))): + with self.subTest(proxy=view, change=change): + proxy_plain, proxy_selected = self.image_inspect(pins, "containerd", "gateway_proxy") + (proxy_plain if view == "plain" else proxy_selected).update(change) + error, execute, _ = self.install_with(host, pins, *self.image_inspect(pins, "containerd"), + self.result / f"proxy-refused-{number}", + (proxy_plain, proxy_selected)) + self.assertEqual(error, expected) + execute.assert_not_called() def test_install_actually_uses_narrow_mounts_and_grader_lock(self): host = load_recipe_module("host.py") @@ -863,13 +928,6 @@ def test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags(self): module.checked_image(collection, "swebench/example:latest", None) self.assertIn("ImageCollection.pull = forbidden", (RECIPE / "e2e/docker_grader.py").read_text()) - def test_egress_policy_missing_or_wrong_arm_fails_before_model_start(self): - host = load_recipe_module("host.py") - with self.assertRaises(ValueError): - host.model_network({}, "control") - with self.assertRaises(ValueError): - host.model_network({"egress": {"control": {"network": "bridge"}}}, "control") - def test_frozen_grader_digest_is_checked_before_retagging(self): host = load_recipe_module("host.py") ref = "docker.io/swebench/sweb.eval.x86_64.django_1776_django-11333@sha256:" + "a" * 64 @@ -902,6 +960,240 @@ def test_shell_entrypoints_forward_stable_dispatch_flags(self): self.assertIn("locked grader", (RECIPE / "install.sh").read_text()) +class OpenHandsIsolationTests(unittest.TestCase): + """Phase-2 O1 topology with Docker mocked: our integration checks only. + + Nothing here creates a network or container; the live probe sequence in + README "Security posture" is the evidence for the topology itself. + """ + OWNER = {"com.native-agent-stack.owner": "gpt6-omniroute-framework-integration"} + ISOLATED = "com.docker.network.bridge.gateway_mode_ipv4" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.state = Path(self.tmp.name).resolve() + self.run_id, self.arm = "rw-openhands-fixture", "control" + self.stem = self.run_id + "-" + self.arm + self.result = self.state / "runs" / self.run_id / self.arm + self.result.mkdir(parents=True) + + def network_view(self, name, **changes): + internal = name.endswith("-int") + view = {"id": hashlib.sha256(name.encode()).hexdigest(), "name": name, "driver": "bridge", + "internal": internal, "ipv6": False, "options": {self.ISOLATED: "isolated"} if internal else {}, + "labels": dict(self.OWNER), "ipam": [{"Subnet": "172.30.0.0/16", "Gateway": "172.30.0.1"}]} + view.update(changes) + return view + + def test_topology_creates_an_isolated_internal_network_and_a_gateway_network(self): + # docker/docs@4e9a5751 port-publishing.md:186-192 (plan N1): no bridge + # address in gateway mode isolated; network_create.md: --internal. + host = load_recipe_module("host.py") + commands = [] + views = {self.stem + kind: self.network_view(self.stem + kind) for kind in ("-int", "-gw")} + with patch.object(host, "logged_command", side_effect=lambda argv, *a, **k: commands.append(argv) or 0), \ + patch.object(host.subprocess, "check_output", + side_effect=lambda argv, **k: json.dumps(views[argv[-1]])) as inspect: + networks = host.create_topology(self.result, self.stem) + label = "com.native-agent-stack.owner=gpt6-omniroute-framework-integration" + self.assertEqual(commands, [ + [*host.DOCKER, "network", "create", "--internal", "--ipv6=false", + "-o", "com.docker.network.bridge.gateway_mode_ipv4=isolated", "--label", label, self.stem + "-int"], + [*host.DOCKER, "network", "create", "--ipv6=false", "--label", label, self.stem + "-gw"]]) + self.assertEqual([call.args[0] for call in inspect.call_args_list], + [[*host.DOCKER, "network", "inspect", "--format", host.NETWORK_FORMAT, self.stem + kind] + for kind in ("-int", "-gw")]) + self.assertEqual({kind: view["name"] for kind, view in networks.items()}, + {"int": self.stem + "-int", "gw": self.stem + "-gw"}) + + def test_topology_fails_closed_when_the_isolated_gateway_mode_is_rejected(self): + host = load_recipe_module("host.py") + commands = [] + with patch.object(host, "logged_command", side_effect=lambda argv, *a, **k: commands.append(argv) or 1), \ + patch.object(host.subprocess, "check_output") as inspect, self.assertRaises(RuntimeError): + host.create_topology(self.result, self.stem) + # No retry without the option (never a plain --internal fallback). + self.assertEqual(len(commands), 1) + inspect.assert_not_called() + + def test_topology_refuses_networks_whose_inspect_differs(self): + host = load_recipe_module("host.py") + for kind, change in (("-int", {"internal": False}), ("-int", {"options": {}}), + ("-int", {"options": {self.ISOLATED: "nat"}}), ("-int", {"ipv6": True}), + ("-int", {"labels": {}}), ("-int", {"driver": "macvlan"}), + ("-int", {"name": "other"}), ("-int", {"id": "short"}), + ("-gw", {"internal": True}), ("-gw", {"ipv6": True}), ("-gw", {"labels": {}})): + with self.subTest(kind=kind, change=change): + views = {self.stem + k: self.network_view(self.stem + k) for k in ("-int", "-gw")} + views[self.stem + kind].update(change) + with patch.object(host, "logged_command", return_value=0), \ + patch.object(host.subprocess, "check_output", + side_effect=lambda argv, **k: json.dumps(views[argv[-1]])), \ + self.assertRaises(ValueError): + host.create_topology(self.result, self.stem) + + @staticmethod + def proxy_blocks(): + text = (RECIPE / "config/proxy-nginx.conf").read_text() + body = "\n".join(line.split("#", 1)[0] for line in text.splitlines()) + return text, body, body[body.index("listen 8081;"):body.index("listen 8080;")], body[body.index("listen 8080;"):] + + def test_proxy_template_is_the_three_route_v1_allowlist(self): + text, body, agent, host_side = self.proxy_blocks() + placeholders = re.findall(r"@[A-Z]+@", text) + self.assertEqual(sorted(set(placeholders)), ["@COMPRESSION@", "@PORT@", "@RUN@", "@SERVER@"]) + self.assertEqual(text.count("@"), 2 * len(placeholders)) + self.assertEqual(re.findall(r"listen\s+([^;]+);", body), ["8081", "8080"]) + self.assertEqual(re.findall(r"location\s+(=?)\s*(\S+)\s*\{", agent), + [("=", "/v1/responses"), ("=", "/v1/chat/completions"), ("=", "/v1/models"), ("", "/")]) + for route, method in (("/v1/responses", "POST"), ("/v1/chat/completions", "POST"), ("/v1/models", "GET")): + self.assertRegex(agent, r"location = " + re.escape(route) + r"\s*\{\s*limit_except " + method + + r"\s*\{ deny all; \}\s*proxy_pass http://10\.0\.2\.2:@PORT@" + re.escape(route) + r";\s*\}") + self.assertRegex(agent, r"location / \{ return 403; \}") + self.assertRegex(agent, r"if \(\$is_args\) \{ return 403; \}") + self.assertIn("proxy_pass_request_headers off;", agent) + self.assertEqual(dict(re.findall(r'proxy_set_header\s+(\S+)\s+("[^"]*"|\$\w+);', agent)), { + "Host": '"10.0.2.2:@PORT@"', "Content-Type": "$content_type", "Accept": "$http_accept", + "Idempotency-Key": "$http_idempotency_key", "Authorization": '"Bearer local-loopback"', + "X-Correlation-Id": '"@RUN@"', "x-omniroute-session": '"@RUN@"', + "x-omniroute-compression": '"@COMPRESSION@"'}) + self.assertEqual(agent.count("proxy_set_header"), 8) + self.assertEqual(set(re.findall(r"\$http_\w+", agent)), {"$http_accept", "$http_idempotency_key"}) + # The host side carries the event-search query string, so it refuses none. + self.assertNotIn("$is_args", host_side) + self.assertNotIn("proxy_pass_request_headers", host_side) + self.assertRegex(host_side, r"location / \{ proxy_pass http://@SERVER@:8000; \}") + self.assertNotIn("resolver", body) + self.assertIn("server_tokens off;", body) + self.assertIn("pid /tmp/nginx.pid;", body) + for path in re.findall(r"_temp_path\s+(\S+);", body): + self.assertTrue(path.startswith("/tmp/"), path) + + def test_proxy_render_fills_each_placeholder_from_the_arm_selection(self): + host = load_recipe_module("host.py") + recipe = load_recipe_module("recipe.py") + for selection, port, other, combo in ( + (recipe.arm_config("control"), 20128, 20129, ""), + (recipe.arm_config("engines-on"), 20129, 20128, "allow-lossy"), + (recipe.arm_config("engines-on", compression="fw-rtk"), 20129, 20128, "fw-rtk")): + with self.subTest(arm=selection["arm"], combo=combo): + stem = self.run_id + "-" + selection["arm"] + text = host.render_proxy_config(selection, self.run_id) + self.assertNotIn("@", text) + self.assertEqual(text.count(f"proxy_pass http://10.0.2.2:{port}/v1/"), 3) + self.assertNotIn(f"10.0.2.2:{other}", text) + self.assertIn(f"proxy_pass http://{stem}-server:8000;", text) + values = dict(re.findall(r'proxy_set_header\s+(\S+)\s+"([^"]*)";', text)) + self.assertEqual(values, {"Host": f"10.0.2.2:{port}", "Authorization": "Bearer local-loopback", + "X-Correlation-Id": self.run_id, "x-omniroute-session": self.run_id, + "x-omniroute-compression": combo}) + + def test_proxy_render_refuses_values_outside_their_patterns(self): + host = load_recipe_module("host.py") + recipe = load_recipe_module("recipe.py") + control, engines = recipe.arm_config("control"), recipe.arm_config("engines-on") + for run_id in ("rw-openhands-UPPER", "rw-openhands-x;", "rw-openhands-x\n", 'rw-openhands-x"', + "rw-openhands-x}", "other-run", "rw-openhands-" + "a" * 65): + with self.subTest(run_id=run_id), self.assertRaises(ValueError): + host.render_proxy_config(control, run_id) + for forged in (dict(control, gateway_port=20129), dict(engines, gateway_port=20128), + dict(control, compression_combo="allow-lossy"), dict(engines, compression_combo=None), + dict(engines, compression_combo="default-caveman"), dict(engines, compression_combo='x";'), + dict(control, arm="other"), dict(control, base_url="http://10.0.2.2:20128/v1"), + dict(control, gateway_upstream="http://10.0.2.2:20129/v1")): + with self.subTest(forged=forged), self.assertRaises(ValueError): + host.render_proxy_config(forged, self.run_id) + template = self.result / "proxy-template.conf" + template.write_text((RECIPE / "config/proxy-nginx.conf").read_text() + "# @EXTRA@\n") + with patch.object(host, "PROXY_TEMPLATE", template), self.assertRaises(ValueError): + host.render_proxy_config(control, self.run_id) + + def test_rendered_proxy_config_is_host_owned_0644_outside_model_mounts(self): + host = load_recipe_module("host.py") + text = "events {}\n" + path, sha = host.write_proxy_config(self.result, text) + self.assertEqual(path, self.result / "proxy/nginx.conf") + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o644) + self.assertEqual(stat.S_IMODE(path.parent.stat().st_mode), 0o700) + self.assertEqual(path.stat().st_uid, os.getuid()) + self.assertEqual(sha, hashlib.sha256(text.encode()).hexdigest()) + with self.assertRaises(FileExistsError): + host.write_proxy_config(self.result, text) + + def test_proxy_launch_is_create_connect_start_with_loopback_ingress_only(self): + host = load_recipe_module("host.py") + pins = json.loads((RECIPE / "pins.json").read_text()) + config = self.result / "proxy/nginx.conf" + create, connect, start = host.proxy_commands(pins, self.stem, config, 3740) + self.assertEqual(create[:len(host.DOCKER) + 1], [*host.DOCKER, "create"]) + for flag in ("--pull=never", "--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", + "--network=" + self.stem + "-gw"): + self.assertIn(flag, create) + options = {create[i]: create[i + 1] for i in range(len(create) - 1) if create[i].startswith("--")} + self.assertEqual(options["--name"], self.stem + "-proxy") + self.assertEqual(options["--label"], "com.native-agent-stack.owner=gpt6-omniroute-framework-integration") + self.assertEqual(options["--publish"], "127.0.0.1:3740:8080") + self.assertEqual(options["--platform"], "linux/amd64") + self.assertEqual(options["--tmpfs"], "/tmp:rw,nosuid,nodev,mode=1777") + self.assertEqual((options["--pids-limit"], options["--memory"]), ("64", "256m")) + self.assertEqual(options["--mount"], f"type=bind,src={config},dst=/etc/nginx/nginx.conf,readonly") + self.assertEqual(options["--entrypoint"], "nginx") + self.assertEqual(create[-3:], [pins["gateway_proxy"]["ref"], "-g", "daemon off;"]) + self.assertEqual(sum(arg == "--publish" or re.match(r"-p\b", arg) is not None for arg in create), 1) + self.assertEqual(connect, [*host.DOCKER, "network", "connect", "--alias", "gw", self.stem + "-int", + self.stem + "-proxy"]) + self.assertEqual(start, [*host.DOCKER, "start", self.stem + "-proxy"]) + for port in (3729, 3800, True): + with self.subTest(port=port), self.assertRaises(ValueError): + host.proxy_commands(pins, self.stem, config, port) + with self.assertRaises(ValueError): + host.proxy_commands(pins, self.run_id, config, 3740) + + def docker_run_fake(self, host, *, removed=True, listed=""): + calls = [] + + def run(argv, **kwargs): + calls.append(list(argv)) + tail = argv[len(host.DOCKER):] + if tail[:1] == ["rm"]: + return subprocess.CompletedProcess(argv, 0 if removed else 1) + if tail[:1] == ["ps"]: + return subprocess.CompletedProcess(argv, 0, stdout="" if removed else "still-there\n") + if tail[:2] == ["network", "ls"]: + return subprocess.CompletedProcess(argv, 0, stdout="bridge\nhost\nnone\n" + listed) + return subprocess.CompletedProcess(argv, 0, stdout="") + return calls, run + + def test_teardown_removes_proxy_then_server_then_networks_by_exact_name(self): + host = load_recipe_module("host.py") + keys = host.generate_session_files(self.state, self.run_id, self.arm) + calls, run = self.docker_run_fake(host) + with patch.object(host.subprocess, "run", side_effect=run): + self.assertTrue(host.teardown_attempt(self.state, self.run_id, self.arm)) + self.assertEqual([argv[len(host.DOCKER):] for argv in calls], [ + ["logs", self.stem + "-proxy"], ["rm", "-f", self.stem + "-proxy"], + ["logs", self.stem + "-server"], ["rm", "-f", self.stem + "-server"], + ["network", "rm", self.stem + "-int"], ["network", "ls", "--format", "{{.Name}}"], + ["network", "rm", self.stem + "-gw"], ["network", "ls", "--format", "{{.Name}}"]]) + self.assertNotIn("prune", json.dumps(calls)) + for record in ("proxy.log.cleanup.json", "server.log.cleanup.json", + "network-int.cleanup.json", "network-gw.cleanup.json"): + self.assertIs(json.loads((self.result / record).read_text())["confirmed_removed"], True, record) + self.assertFalse(any(path.exists() for path in keys)) + + def test_teardown_keeps_key_files_while_removal_is_unconfirmed(self): + host = load_recipe_module("host.py") + keys = host.generate_session_files(self.state, self.run_id, self.arm) + calls, run = self.docker_run_fake(host, removed=False, listed=self.stem + "-int\n") + with patch.object(host.subprocess, "run", side_effect=run): + self.assertFalse(host.teardown_attempt(self.state, self.run_id, self.arm)) + self.assertTrue(all(path.exists() for path in keys)) + self.assertIs(json.loads((self.result / "server.log.cleanup.json").read_text())["confirmed_removed"], False) + self.assertIs(json.loads((self.result / "network-int.cleanup.json").read_text())["confirmed_removed"], False) + self.assertIs(json.loads((self.result / "network-gw.cleanup.json").read_text())["confirmed_removed"], True) + + class OpenHandsReceiptTests(unittest.TestCase): def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): module = load_recipe_module("receipt.py") @@ -1102,6 +1394,20 @@ def test_receipt_records_proxy_base_url_and_selected_compression_combo(self): self.assertEqual(receipt["compression_combo"], combo) self.assertEqual(receipt["gateway"]["entry_port"], port) + def test_receipt_counts_network_removal_separately_from_containers(self): + module = load_recipe_module("receipt.py") + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp).resolve() + (result / "check.json").write_text("{}") + (result / "window.json").write_text(json.dumps({ + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z"})) + for name, removed in (("server.log.cleanup.json", True), ("proxy.log.cleanup.json", True), + ("network-int.cleanup.json", True), ("network-gw.cleanup.json", False)): + (result / name).write_text(json.dumps({"confirmed_removed": removed, "error_type": None})) + receipt = module.create_receipt(result, database=result / "absent.sqlite") + self.assertEqual(receipt["container_cleanup"], {"attempts": 2, "confirmed_removed": 2, "complete": True}) + self.assertEqual(receipt["network_cleanup"], {"attempts": 2, "confirmed_removed": 1, "complete": False}) + class OpenHandsDispatchTests(unittest.TestCase): """Synthetic native REST transport, not an agent-server/model execution.""" @@ -1151,12 +1457,16 @@ def test_wait_deadline_interrupts_and_emits_setup_failure(self): def test_server_preserves_native_entrypoint_and_uses_supported_preload(self): dispatch = load_recipe_module("dispatch.py") pins = json.loads((RECIPE / "pins.json").read_text()) - args = dispatch.server_command(pins, "rw-openhands-fixture-server", [], - "rw-openhands-egress-control", "/private/server.env") + stem = self.run_id + "-control" + args = dispatch.server_command(pins, stem + "-server", [], stem + "-int", "/private/server.env") self.assertNotIn("--entrypoint", args) - self.assertIn("127.0.0.1:3730:8000", args) + # Plan E1: the agent-server publishes nothing; only the proxy does. + self.assertFalse(any(arg == "--publish" or arg.startswith(("--publish=", "-p")) for arg in args)) + self.assertIn("--network=" + stem + "-int", args) self.assertEqual(args[-4:], ["--extra-python-path", "/recipe", "--import-modules", "server_transport"]) - self.assertIn("--env-file", args) + self.assertEqual(args[args.index("--env-file") + 1], "/private/server.env") + with self.assertRaises(ValueError): + dispatch.server_command(pins, stem + "-server", [], stem + "-gw", "/private/server.env") def test_compression_delta_is_separate_and_missing_or_reset_is_unknown(self): dispatch = load_recipe_module("dispatch.py") @@ -1166,37 +1476,77 @@ def test_compression_delta_is_separate_and_missing_or_reset_is_unknown(self): self.assertIsNone(dispatch.compression_delta(None, after)["delta"]) self.assertIsNone(dispatch.compression_delta(after, before)["delta"]) - def test_prepare_serializes_body_offline_then_launches_native_server(self): + def prepare(self, port=3730, render_exit=0, arm=None): + """prepare_native_dispatch with Docker mocked; no container or network.""" host = load_recipe_module("host.py") dispatch = load_recipe_module("dispatch.py") pins = json.loads((RECIPE / "pins.json").read_text()) - (self.result / "worker").mkdir() - selection = load_recipe_module("recipe.py").arm_config("engines-on") - calls = [] - - def rendered(args, name, log, timeout): - calls.append(args) - (self.result / "worker/start.json").write_text(json.dumps({"agent": {"llm": {}}, "workspace": {}, "initial_message": {}})) - return 0 - + arm = arm or self.arm + result = self.state / "runs" / self.run_id / arm + (result / "worker").mkdir(parents=True, exist_ok=True) + stem = self.run_id + "-" + arm + rendered, commands = [], [] + + def render(args, name, log, timeout): + rendered.append(args) + (result / "worker/start.json").write_text(json.dumps({"agent": {"llm": {}}, "workspace": {}, "initial_message": {}})) + return render_exit + + topology = {kind: {"name": f"{stem}-{kind}", "id": hashlib.sha256(kind.encode()).hexdigest()} + for kind in ("int", "gw")} with patch.dict(sys.modules, {"dispatch": dispatch}), \ - patch.object(host, "execute_container", side_effect=rendered), \ - patch.object(host, "logged_command", return_value=0) as launch, \ - patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ - patch.object(dispatch, "check_server", return_value=None), \ - patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}): - host.prepare_native_dispatch(self.result, self.state / "prefix", pins, [], - "rw-openhands-egress-engines-on", {"variables": {"HOST_PATH": "/usr/bin"}}, - selection, self.run_id) - self.assertIn("--network=none", calls[0]) - self.assertIn("OPENHANDS_ARM=engines-on", calls[0]) - self.assertIn("OPENHANDS_BASE_URL=http://gw:8081/v1", calls[0]) - self.assertIn("OPENHANDS_COMPRESSION=allow-lossy", calls[0]) - self.assertEqual(calls[0][-1], "--request") - launched = launch.call_args.args[0] - self.assertNotIn("--entrypoint", launched) - self.assertTrue((self.result / "start.json").exists()) - self.assertEqual(json.loads((self.result / "status.json").read_text())["status"], "prepared") + patch.object(host, "execute_container", side_effect=render), \ + patch.object(host, "create_topology", return_value=topology) as create, \ + patch.object(host, "logged_command", side_effect=lambda argv, *a, **k: commands.append(argv) or 0), \ + patch.object(dispatch, "check_server", return_value=None) as check: + host.prepare_native_dispatch(self.state, self.run_id, load_recipe_module("recipe.py").arm_config(arm), + self.state / "prefix", pins, [], {"variables": {"HOST_PATH": "/usr/bin"}}, + port=port) + return SimpleNamespace(host=host, dispatch=dispatch, result=result, stem=stem, rendered=rendered, + commands=commands, create=create, check=check, pins=pins) + + def test_prepare_serializes_body_offline_then_launches_native_server(self): + prepared = self.prepare() + host, result, stem, commands = prepared.host, prepared.result, prepared.stem, prepared.commands + request = prepared.rendered[0] + self.assertIn("--network=none", request) + self.assertIn("OPENHANDS_ARM=engines-on", request) + self.assertIn("OPENHANDS_BASE_URL=http://gw:8081/v1", request) + self.assertIn("OPENHANDS_COMPRESSION=allow-lossy", request) + self.assertEqual(request[-1], "--request") + prepared.create.assert_called_once_with(result, stem) + # Order: server on the internal network, then proxy create/connect/start. + server, create, connect, start = commands + self.assertNotIn("--entrypoint", server) + self.assertIn("--network=" + stem + "-int", server) + self.assertFalse(any(arg == "--publish" or arg.startswith("-p") for arg in server)) + env_file, headers = host.session_files(self.state, self.run_id, self.arm) + self.assertEqual(server[server.index("--env-file") + 1], str(env_file)) + self.assertEqual(create[len(host.DOCKER)], "create") + self.assertIn("--network=" + stem + "-gw", create) + self.assertEqual(create[create.index("--publish") + 1], "127.0.0.1:3730:8080") + self.assertIn(f"type=bind,src={result}/proxy/nginx.conf,dst=/etc/nginx/nginx.conf,readonly", create) + self.assertEqual(connect[len(host.DOCKER):], ["network", "connect", "--alias", "gw", stem + "-int", stem + "-proxy"]) + self.assertEqual(start[len(host.DOCKER):], ["start", stem + "-proxy"]) + # The key is generated per attempt and is never part of any argv. + value = env_file.read_text().split("=", 1)[1].strip() + self.assertTrue(value and headers.is_file()) + for argv in prepared.rendered + commands: + self.assertNotIn(value, " ".join(map(str, argv))) + # The rendered proxy config sits outside every model mount. + config = result / "proxy/nginx.conf" + self.assertEqual(stat.S_IMODE(config.stat().st_mode), 0o644) + self.assertIn("proxy_pass http://10.0.2.2:20129/v1/responses;", config.read_text()) + for argv in [request, server]: + self.assertFalse(any(f"src={result}/proxy" in arg for arg in argv)) + prepared.check.assert_called_once() + self.assertEqual(prepared.check.call_args.kwargs["port"], 3730) + self.assertTrue((result / "start.json").exists()) + status = json.loads((result / "status.json").read_text()) + self.assertEqual(status["status"], "prepared") + self.assertEqual((status["server_name"], status["proxy_name"], status["port"]), + (stem + "-server", stem + "-proxy", 3730)) + self.assertEqual(status["proxy_config_sha256"], hashlib.sha256(config.read_bytes()).hexdigest()) def test_model_mount_permissions_do_not_follow_symlinks(self): host = load_recipe_module("host.py") @@ -1271,17 +1621,14 @@ def test_result_refuses_export_until_container_removal_is_confirmed(self): self.assertEqual(report["failure_stage"], "export") def test_request_serialization_failure_cannot_leave_prepared_receipt(self): + with self.assertRaises(RuntimeError): + self.prepare(render_exit=7) + # Nothing was created: no network, key file, server or proxy. host = load_recipe_module("host.py") - dispatch = load_recipe_module("dispatch.py") - with patch.dict(sys.modules, {"dispatch": dispatch}), \ - patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ - patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}), \ - patch.object(host, "execute_container", return_value=7), \ - patch.object(host, "logged_command") as launch, self.assertRaises(RuntimeError): - host.prepare_native_dispatch(self.result, self.state / "prefix", json.loads((RECIPE / "pins.json").read_text()), [], - "rw-openhands-egress-engines-on", {"variables": {"HOST_PATH": "/usr/bin"}}, - load_recipe_module("recipe.py").arm_config(self.arm), self.run_id) - launch.assert_not_called() + self.assertFalse((self.state / "secrets").exists()) + self.assertFalse(any(path.exists() for path in host.session_files(self.state, self.run_id, self.arm))) + self.assertFalse((self.result / "status.json").exists()) + self.assertFalse((self.result / "proxy").exists()) def test_patch_export_cannot_use_host_git_filters_or_external_diff(self): dispatch = load_recipe_module("dispatch.py") @@ -1431,42 +1778,28 @@ def test_planted_success_looking_events_move_exit_3_to_1_at_most(self): self.assertIn(dispatch.result_exit({**receipt, "evidence_complete": True}), {1, 3}) def test_selected_port_flows_from_prepare_to_dispatch(self): - host = load_recipe_module("host.py") - dispatch = load_recipe_module("dispatch.py") - pins = json.loads((RECIPE / "pins.json").read_text()) - (self.result / "worker").mkdir() - - def rendered(args, name, log, timeout): - (self.result / "worker/start.json").write_text(json.dumps({"agent": {"llm": {}}, "workspace": {}, "initial_message": {}})) - return 0 - - with patch.dict(sys.modules, {"dispatch": dispatch}), \ - patch.object(host, "execute_container", side_effect=rendered), \ - patch.object(host, "logged_command", return_value=0) as launch, \ - patch.object(dispatch, "private_file", side_effect=lambda x: Path(x)), \ - patch.object(dispatch, "check_server", return_value=None) as check, \ - patch.dict(os.environ, {"OPENHANDS_SERVER_ENV": "/private/server.env", "OPENHANDS_HEADERS": "/private/headers"}): - host.prepare_native_dispatch(self.result, self.state / "prefix", pins, [], "rw-openhands-egress-engines-on", - {"variables": {"HOST_PATH": "/usr/bin"}}, - load_recipe_module("recipe.py").arm_config(self.arm), self.run_id, port=3740) - self.assertIn("127.0.0.1:3740:8000", launch.call_args.args[0]) - self.assertNotIn("127.0.0.1:3730:8000", launch.call_args.args[0]) - self.assertEqual(check.call_args.kwargs["port"], 3740) + prepared = self.prepare(port=3740) + dispatch, commands = prepared.dispatch, prepared.commands + published = [argv[argv.index("--publish") + 1] for argv in commands if "--publish" in argv] + self.assertEqual(published, ["127.0.0.1:3740:8080"]) + self.assertEqual(prepared.check.call_args.kwargs["port"], 3740) self.assertEqual(json.loads((self.result / "status.json").read_text())["port"], 3740) (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + headers = prepared.host.session_files(self.state, self.run_id, self.arm)[1] with patch.object(dispatch, "api_request", return_value={"id": FIXTURE_CONVERSATION_ID}) as api, \ + patch.object(dispatch, "verify_isolation", return_value=None, create=True), \ patch.object(dispatch, "compression_snapshot", return_value=None), contextlib.redirect_stdout(io.StringIO()): self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 0) self.assertEqual(api.call_args.kwargs["port"], 3740) + # The headers path is derived from the attempt identity, never read + # from status.json or the environment. + self.assertEqual(api.call_args.kwargs["headers"], headers) status = json.loads((self.result / "status.json").read_text()) (self.result / "status.json").write_text(json.dumps({**status, "port": 8000})) with patch.object(dispatch, "api_request") as api, patch.object(dispatch, "stop_server"), \ contextlib.redirect_stdout(io.StringIO()): self.assertEqual(dispatch.execute("wait", self.state, self.run_id, self.arm), 3) api.assert_not_called() - args = dispatch.server_command(pins, "rw-openhands-fixture-server", [], "rw-openhands-egress-control", - "/private/server.env") - self.assertIn("127.0.0.1:3730:8000", args) if __name__ == "__main__": From 1a5047efd06c1d35e5d5267e18e06d6be3979b62 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 15:08:09 -0400 Subject: [PATCH 10/22] OpenHands phase 2 (3/4): P0-P2 containment probe, dispatch-start gate, method pairing - e2e/netprobe.py: locally composed stdlib probe (plan section 1), run in the pinned agent image on -gw (P0) and -int (P1, P2). Raw targets go through http.client; only status, route class and errno names are kept. P3 is a skeleton and is not run. - host.py: P2 targets from ss/ip plus both attempt networks, a selected-field container contract, run_probe writing a 0600 isolation-probe.json bound to network and container IDs, verify_isolation (at most 900 s old, live IDs), `host.py probe`, and a probe stage in run() that tears down on failure. - dispatch.py: start refuses without a fresh bound receipt, before the serial lock and without mutation; E3 pairs each native route with its one method. - receipt.py: schema 6 with an ID-free isolation summary, the "probe" failure stage, and probe container removals counted. Red: 104 tests, failures=16, errors=43. Green: 104 OK. Docker is mocked; no container, network or gateway request. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/dispatch.py | 25 +- .../runtime-workers/openhands/e2e/netprobe.py | 306 +++++++ blueprints/runtime-workers/openhands/host.py | 303 ++++++- .../runtime-workers/openhands/receipt.py | 60 +- tests/test_runtime_worker_openhands.py | 753 +++++++++++++++++- 5 files changed, 1431 insertions(+), 16 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/e2e/netprobe.py diff --git a/blueprints/runtime-workers/openhands/dispatch.py b/blueprints/runtime-workers/openhands/dispatch.py index ddbe243c3..052206fa5 100644 --- a/blueprints/runtime-workers/openhands/dispatch.py +++ b/blueprints/runtime-workers/openhands/dispatch.py @@ -18,7 +18,7 @@ from recipe import read_json from host import (AGENT_LIMITS, DEFAULT_PORT, docker_args, grade, logged_command, owned_port, private_file, - result_exit, session_files, teardown_attempt, utc_now, write_json) + result_exit, session_files, teardown_attempt, utc_now, verify_isolation, write_json) from receipt import create_receipt, read_bounded @@ -33,6 +33,17 @@ # name; models.py:95-99 defines TIMESTAMP_DESC. ERROR_EVENT_SEARCH = ("/events/search?kind=openhands.sdk.event.conversation_error.ConversationErrorEvent" "&sort_order=TIMESTAMP_DESC&limit=1") +# Plan E3: each native route has exactly one method. POST starts or +# interrupts a conversation; GET only reads (conversation_router.py:163-228, +# 257-287, 304-321; event_router.py:68-139). +CONVERSATION = r"/api/conversations/[a-f0-9-]{36}" +NATIVE_ROUTES = ( + ("POST", r"/api/conversations"), + ("POST", CONVERSATION + r"/interrupt"), + ("GET", CONVERSATION), + ("GET", CONVERSATION + r"/agent_final_response"), + ("GET", CONVERSATION + re.escape(ERROR_EVENT_SEARCH)), +) def server_url(port): @@ -58,8 +69,7 @@ def curl_json(url, *, method="GET", headers=None, body=None): def api_request(method, path, *, headers, body=None, port=DEFAULT_PORT): """headers is the attempt's host.session_files header file, checked by private_file.""" - if not (re.fullmatch(r"/api/conversations(?:/[a-f0-9-]{36}(?:/(?:agent_final_response|interrupt))?)?", path) - or re.fullmatch(r"/api/conversations/[a-f0-9-]{36}" + re.escape(ERROR_EVENT_SEARCH), path)): + if not any(method == allowed and re.fullmatch(pattern, path) for allowed, pattern in NATIVE_ROUTES): raise ValueError("unexpected_native_route") return curl_json(server_url(port) + path, method=method, headers=headers, body=body) @@ -236,6 +246,15 @@ def execute(action, state, run_id, arm): print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), "failure_stage": "start", "task_passed": False, "evidence_complete": False})) return 3 # Do not mutate the running attempt or release its lock. + # Plan E1/G7: a fresh P0-P2 receipt bound to this live topology, + # checked before the serial lock. A refusal changes nothing; run + # `host.py probe` again, or tear the attempt down. + try: + verify_isolation(state, run_id, arm, port) + except Exception: + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": str(receipt_path), + "failure_stage": "probe", "task_passed": False, "evidence_complete": False})) + return 3 try: fd = os.open(lock, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) except FileExistsError: diff --git a/blueprints/runtime-workers/openhands/e2e/netprobe.py b/blueprints/runtime-workers/openhands/e2e/netprobe.py new file mode 100644 index 000000000..da5bf4ed4 --- /dev/null +++ b/blueprints/runtime-workers/openhands/e2e/netprobe.py @@ -0,0 +1,306 @@ +"""Containment probe for the O1 topology (phase-2 plan section 1, P0-P2); stdlib only. + +A locally composed integration check, not an upstream test and not a model +run. host.run_probe starts it in the pinned agent-server image with the +recipe's docker_args hardening, with only the venv, /recipe (read-only) and +a dedicated output directory mounted: + +- mode gw, on -gw: P0, the negative control. The vehicle must see the + gateway (GET /v1/models -> 200 + CLIENT_API) and its management exposure + (GET /api/settings -> MANAGEMENT). +- mode int, on -int: P1, only the proxy's three /v1 routes answer and + every other method or target gets the proxy's own 403 with no route-class + header; then P2, every TCP connect to a host-side address fails, public + names and host.docker.internal do not resolve, gw and the server do, and + no interface other than lo has an IPv6 address. + +Raw request targets go through http.client, which sends them verbatim (the +equivalent of curl --path-as-is). Only the status code, the route-class +value class and errno names are kept; response bodies and all other headers +are never read. The probe exits nonzero on the first unexpected result; +host.run_probe re-derives every verdict from the written observations with +the evaluate_* functions below. P3-P5 are live model calls, documented in +README "Isolation probe"; p3_control_call is a skeleton and never runs here. +""" +from __future__ import annotations + +import argparse +from collections import Counter +import concurrent.futures +import errno +import http.client +import ipaddress +import json +import os +from pathlib import Path +import re +import socket + + +# OmniRoute's response header separating gateway answers from proxy denials +# (plan F18); only these values are kept, anything else is "". +ROUTE_CLASS = "x-omniroute-route-class" +ROUTE_CLASSES = ("CLIENT_API", "MANAGEMENT") +GATEWAY_HOST = "10.0.2.2" # host loopback as seen from containers (rootlesskit docs/network.md:88) +PROXY_HOST, PROXY_PORT = "gw", 8081 +DENIED = ( + *(("GET", target) for target in ( + "/api/settings", "/api/providers", "/api/keys", "/dashboard", "/v1/ws", "/v1/alpha/search", + "/v1/files", "/V1/models", "/v1/models/", "/v1/responses/x", "/v1/../api/settings", + "/v1/%2e%2e/api/settings", "/v1/models/../../api/keys", "//api/settings", "/codex/responses", + "/api/v1/responses", "/v1/models?x=1", "/v1/models?0")), + *(("POST", target) for target in ( + "/responses", "/chat/completions", "/api/v1/responses", "/api/settings/require-login", + "/v1/responses?provider=x")), + ("DELETE", "/v1/models"), ("PUT", "/v1/responses"), ("GET", "/v1/responses"), +) +# (host, port, method, target, expected status or None for any, expected route class or None for absent) +P1_EXPECTED = ((PROXY_HOST, PROXY_PORT, "GET", "/v1/models", 200, "CLIENT_API"), + *((PROXY_HOST, PROXY_PORT, method, target, 403, None) for method, target in DENIED)) +UNRESOLVABLE = ("example.com", "github.com", "host.docker.internal") +# Plan P2 fixed addresses; the host adds its own IPv4 addresses and the +# attempt networks' gateways (host.probe_targets). +FIXED_ADDRESSES = ("10.0.2.2", "10.0.2.3", "172.17.0.1", "10.0.0.1", "10.255.255.254") +EXTRA_PORTS = (53,) +SERVER_NAME = re.compile(r"rw-openhands-[a-z0-9-]{1,64}-(?:control|engines-on)-server") +UPSTREAM_PORTS = (20128, 20129) +HTTP_TIMEOUT = 10.0 +CONNECT_TIMEOUT = 2.0 +WORKERS = 64 +IF_INET6 = "/proc/net/if_inet6" + + +def p0_expected(upstream_port): + if upstream_port not in UPSTREAM_PORTS: + raise ValueError("gateway_port_required") + return ((GATEWAY_HOST, upstream_port, "GET", "/v1/models", 200, "CLIENT_API"), + (GATEWAY_HOST, upstream_port, "GET", "/api/settings", None, "MANAGEMENT")) + + +def dns_expected(server): + if not isinstance(server, str) or not SERVER_NAME.fullmatch(server): + raise ValueError("attempt_server_name_required") + return (*((name, False) for name in UNRESOLVABLE), (PROXY_HOST, True), (server, True)) + + +def error_name(exc): + if isinstance(exc, socket.gaierror): + names = {getattr(socket, name): name for name in dir(socket) if name.startswith("EAI_")} + return names.get(exc.errno, "gaierror") + if isinstance(exc, TimeoutError): + return "timeout" + if isinstance(exc, OSError) and exc.errno in errno.errorcode: + return errno.errorcode[exc.errno] + return type(exc).__name__ + + +def route_class(value): + return None if value is None else value if value in ROUTE_CLASSES else "" + + +def http_probe(host, port, method, target, *, timeout=HTTP_TIMEOUT): + """One request with a raw target; the body is never read.""" + record = {"host": host, "port": port, "method": method, "target": target, + "status": None, "route_class": None, "error": None} + body = b"{}" if method == "POST" else None + connection = http.client.HTTPConnection(host, port, timeout=timeout) + try: + connection.request(method, target, body=body, + headers={"Content-Type": "application/json"} if body is not None else {}) + response = connection.getresponse() + record.update(status=response.status, route_class=route_class(response.getheader(ROUTE_CLASS))) + except (OSError, http.client.HTTPException) as exc: + record["error"] = error_name(exc) + finally: + connection.close() + return record + + +def connect_probe(address, port, *, timeout=CONNECT_TIMEOUT): + """One TCP connect to an IPv4 literal; no name resolution.""" + record = {"address": address, "port": port, "connected": False, "error": None} + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + try: + sock.connect((address, port)) + record["connected"] = True + except OSError as exc: + record["error"] = error_name(exc) + return record + + +def connect_all(pairs, *, timeout=CONNECT_TIMEOUT, workers=WORKERS): + """A bounded concurrent batch; results keep the order of pairs.""" + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool: + return list(pool.map(lambda pair: connect_probe(*pair, timeout=timeout), pairs)) + + +def resolve(name): + try: + found = socket.getaddrinfo(name, None, proto=socket.IPPROTO_TCP) + return {"name": name, "resolved": bool(found), "error": None} + except OSError as exc: + return {"name": name, "resolved": False, "error": error_name(exc)} + + +def ipv6_summary(text): + """Classify /proc/net/if_inet6 rows: address, ifindex, prefix, scope, flags, name.""" + rows = [line.split() for line in text.splitlines() if line.strip()] + if any(len(row) != 6 or not re.fullmatch(r"[0-9a-f]{32}", row[0]) for row in rows): + raise ValueError("unexpected_if_inet6_format") + other = [row for row in rows if row[5] != "lo"] + return {"available": True, "loopback": len(rows) - len(other), "non_loopback": len(other), + "link_local": sum(int(row[3], 16) == 0x20 for row in other)} + + +def read_ipv6(): + try: + with open(IF_INET6) as stream: + return ipv6_summary(stream.read()) + except FileNotFoundError: + return {"available": False, "loopback": 0, "non_loopback": 0, "link_local": 0} + + +def http_matches(record, expected): + host, port, method, target, status, route = expected + return (isinstance(record, dict) and record.get("host") == host and record.get("port") == port + and record.get("method") == method and record.get("target") == target and record.get("error") is None + and type(record.get("status")) is int and (status is None or record["status"] == status) + and record.get("route_class") == route) + + +def evaluate_http(records, expected): + records = records if isinstance(records, list) else [] + matched = sum(http_matches(record, item) for record, item in zip(records, expected)) + return {"requests": len(expected), "observed": len(records), "matched": matched, + "passed": len(records) == len(expected) == matched} + + +def count(value): + return value if type(value) is int and value >= 0 else None + + +def evaluate_p2(p2, addresses, ports, server): + p2 = p2 if isinstance(p2, dict) else {} + pairs = [(address, port) for address in addresses for port in ports] + connects = p2.get("connects") if isinstance(p2.get("connects"), list) else [] + aligned = len(connects) == len(pairs) and all( + isinstance(record, dict) and (record.get("address"), record.get("port")) == pair + for record, pair in zip(connects, pairs)) + connected = sum(not isinstance(record, dict) or record.get("connected") is not False for record in connects) + errors = Counter(record.get("error") if isinstance(record, dict) and isinstance(record.get("error"), str) + and re.fullmatch(r"[A-Za-z_]{1,32}", record["error"]) else "" + for record in connects if isinstance(record, dict) and record.get("connected") is False) + expected = dns_expected(server) + dns = p2.get("dns") if isinstance(p2.get("dns"), list) else [] + dns_matched = sum(isinstance(record, dict) and record.get("name") == name and record.get("resolved") is ok + for record, (name, ok) in zip(dns, expected)) + ipv6 = p2.get("ipv6") if isinstance(p2.get("ipv6"), dict) else {} + non_loopback = count(ipv6.get("non_loopback")) + return {"connects": len(pairs), "observed": len(connects), "connected": connected, + "errors": dict(sorted(errors.items())), "dns": len(expected), "dns_matched": dns_matched, + "ipv6_non_loopback": non_loopback, "ipv6_link_local": count(ipv6.get("link_local")), + "ipv6_loopback": count(ipv6.get("loopback")), + "passed": (aligned and connected == 0 and len(dns) == len(expected) == dns_matched + and non_loopback == 0)} + + +def evaluate(gw, internal, upstream_port, addresses, ports, server): + """The host's verdict, re-derived from both observation files.""" + p0 = evaluate_http(gw.get("p0") if isinstance(gw, dict) else None, p0_expected(upstream_port)) + p1 = evaluate_http(internal.get("p1") if isinstance(internal, dict) else None, P1_EXPECTED) + p2 = evaluate_p2(internal.get("p2") if isinstance(internal, dict) else None, addresses, ports, server) + return {"p0": p0, "p1": p1, "p2": p2, "passed": p0["passed"] and p1["passed"] and p2["passed"]} + + +def p3_control_call(*_args, **_kwargs): + """Skeleton only; P3 is a live model call and is never run by this recipe yet. + + From a throwaway container on -int with the recipe's LLM config: + one SDK call with one tool definition through gw to the control arm, also + sending a forged X-Correlation-Id, X-Forwarded-For and one x-omniroute-* + header; then a second call with a query string, which must get the + proxy's 403 and no call_logs row. The call_logs row must show + /v1/responses, 200, cx/gpt-6-astra-max, effort max requested and sent + upstream, and the proxy's fixed run id as the correlation value. The + streamed tool call must parse and its usage equal the row; record the + client peer the gateway logged (F10) and the proxy access log lines. + """ + raise NotImplementedError("p3_is_a_documented_skeleton_not_run") + + +def run_http(observations, key, expected): + records = observations.setdefault(key, []) + for item in expected: + record = http_probe(*item[:4]) + records.append(record) + if not http_matches(record, item): + return 1 + return 0 + + +def run_p2(observations, addresses, ports, server): + p2 = observations.setdefault("p2", {}) + # One bounded batch, recorded whole and evaluated in order. + p2["connects"] = connect_all([(address, port) for address in addresses for port in ports]) + if any(record.get("connected") is not False for record in p2["connects"]): + return 1 + p2["dns"] = [] + for name, resolvable in dns_expected(server): + record = resolve(name) + p2["dns"].append(record) + if record.get("resolved") is not resolvable: + return 1 + p2["ipv6"] = read_ipv6() + return 0 if p2["ipv6"].get("non_loopback") == 0 else 1 + + +def parse_addresses(text): + addresses = [part for part in text.split(",") if part] + for value in addresses: + ipaddress.IPv4Address(value) # ValueError for anything but an IPv4 literal + if not addresses: + raise ValueError("probe_addresses_required") + return addresses + + +def parse_ports(text): + ports = [int(part) if re.fullmatch(r"[0-9]{1,5}", part) else -1 for part in text.split(",") if part] + if not ports or any(not 0 < port < 65536 for port in ports): + raise ValueError("probe_ports_required") + return ports + + +def write_observations(path, observations): + """0644 whatever the umask, so the host user can read what UID 10001 wrote.""" + descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644) + with os.fdopen(descriptor, "w") as stream: + os.fchmod(stream.fileno(), 0o644) + stream.write(json.dumps(observations, indent=1) + "\n") + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument("mode", choices=("gw", "int")) + parser.add_argument("--out", required=True, type=Path) + parser.add_argument("--upstream-port", type=int, choices=UPSTREAM_PORTS) + parser.add_argument("--server") + parser.add_argument("--addresses", default="") + parser.add_argument("--ports", default="") + args = parser.parse_args(argv) + if args.mode == "gw": + expected = p0_expected(args.upstream_port) + else: + dns_expected(args.server) + addresses, ports = parse_addresses(args.addresses), parse_ports(args.ports) + observations = {"probe": "netprobe-v1", "mode": args.mode} + try: + if args.mode == "gw": + return run_http(observations, "p0", expected) + return run_http(observations, "p1", P1_EXPECTED) or run_p2(observations, addresses, ports, args.server) + finally: + write_observations(args.out, observations) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index 18685f5b3..e4af38002 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -13,6 +13,7 @@ from datetime import datetime, timezone import json import io +import ipaddress import os from pathlib import Path import re @@ -28,8 +29,9 @@ from recipe import (COMPRESSION_COMBOS, HERE, arm_config, digest, environment_selection, llm_config, read_json, render_mcp) +from e2e import netprobe from e2e.task import load_task, worker_instruction -from receipt import create_receipt, read_bounded, time_value +from receipt import PROBE_MECHANISM, create_receipt, read_bounded, time_value DOCKER = ["docker", "--context", "rootless"] @@ -62,6 +64,9 @@ '"labels":{{json .Config.Labels}},"networks":{{json .NetworkSettings.Networks}}}') PROXY_TEMPLATE = HERE / "config/proxy-nginx.conf" PLACEHOLDER = re.compile(r"@[A-Z]+@") +# Plan section 1 and E1: a P0-P2 receipt gates dispatch start for at most 900 s +# and only for the topology whose network and container IDs it records. +PROBE_MAX_AGE_SECONDS = 900 def owned_port(value): @@ -124,6 +129,21 @@ def write_json(path, value): temporary.replace(path) +def write_private_json(path, value): + """Owner-only (0600) whatever the umask: mkstemp beside the target, then an atomic replace.""" + path = Path(path) + descriptor, temporary = tempfile.mkstemp(prefix="." + path.name + ".", dir=path.parent) + try: + with os.fdopen(descriptor, "w") as stream: + os.fchmod(stream.fileno(), 0o600) + stream.write(json.dumps(value, indent=2) + "\n") + os.replace(temporary, path) + except BaseException: + with contextlib.suppress(FileNotFoundError): + os.unlink(temporary) + raise + + def private_directory(path): path.mkdir(parents=True, exist_ok=True, mode=0o700) if path.is_symlink(): @@ -733,6 +753,266 @@ def teardown_attempt(state, run_id, arm): return all(containers) and all(networks) +def listener_ports(text): + """TCP listener ports from `ss -ltnH` (iproute2); the fourth column is Local-Address:Port.""" + ports = set() + for line in text.splitlines(): + fields = line.split() + if not fields: + continue + port = fields[3].rsplit(":", 1)[-1] if len(fields) >= 5 and fields[0] == "LISTEN" else "" + if not re.fullmatch(r"[0-9]{1,5}", port) or not 0 < int(port) < 65536: + raise ValueError("unexpected_ss_output") + ports.add(int(port)) + return sorted(ports) + + +def host_ipv4_addresses(text): + """Host IPv4 addresses from `ip -4 -o addr`, loopback excluded by address. + + 127.0.0.0/8 inside the probe is the container's own loopback, not the + host's. Other addresses on lo (WSL's 10.255.255.254) stay. + """ + found = set() + for line in text.splitlines(): + fields = line.split() + if "inet" not in fields: + continue + position = fields.index("inet") + 1 + if position >= len(fields): + raise ValueError("unexpected_ip_output") + address = ipaddress.IPv4Interface(fields[position]).ip + if not address.is_loopback: + found.add(address) + return [str(address) for address in sorted(found)] + + +def probe_targets(networks, listeners, interfaces): + """P2 targets (plan section 1): addresses times ports, both sorted and deduplicated. + + Addresses: netprobe.FIXED_ADDRESSES, every non-loopback host IPv4 address, + and each attempt network's IPAM gateway and subnet .1. Ports: every host + TCP listener at probe time plus netprobe.EXTRA_PORTS (53). + """ + addresses = {ipaddress.IPv4Address(value) for value in netprobe.FIXED_ADDRESSES} + addresses |= {ipaddress.IPv4Address(value) for value in host_ipv4_addresses(interfaces)} + for kind in ("int", "gw"): + entries = [entry for entry in networks[kind].get("ipam") or [] + if isinstance(entry, dict) and ":" not in str(entry.get("Subnet", ""))] + if not entries: + raise ValueError("attempt_network_ipam_required") + for entry in entries: + addresses.add(ipaddress.IPv4Network(entry["Subnet"]).network_address + 1) + if entry.get("Gateway"): + addresses.add(ipaddress.IPv4Address(entry["Gateway"])) + ports = sorted(set(listener_ports(listeners)) | set(netprobe.EXTRA_PORTS)) + return [str(address) for address in sorted(addresses)], ports + + +def checked_container(name, networks, aliases=None): + """Read one attempt container through CONTAINER_FORMAT and require its attachments. + + moby@464cd50c api/swagger.yaml: the name "may be prefixed with a + forward-slash" (:5637-5643); EndpointSettings.Aliases holds the + network-scoped aliases (:3369-3375) and NetworkID binds each attachment + to its network (:3400). Never a full inspect: the server's Config.Env + holds the session key. + """ + info = inspect_selected("container", CONTAINER_FORMAT, name) + attached = info.get("networks") if isinstance(info.get("networks"), dict) else {} + labels = info.get("labels") if isinstance(info.get("labels"), dict) else {} + if (info.get("name") not in (name, "/" + name) or info.get("running") is not True + or labels.get(OWNER_KEY) != OWNER_VALUE or not re.fullmatch(r"[a-f0-9]{64}", str(info.get("id"))) + or set(attached) != set(networks) + or any(not isinstance(attached[network], dict) or attached[network].get("NetworkID") != identity + for network, identity in networks.items()) + or any(alias not in (attached[network].get("Aliases") or []) + for network, alias in (aliases or {}).items())): + raise ValueError("attempt_container_contract_mismatch") + return info + + +def attempt_bindings(stem): + """Live selected-field reads of the attempt's two networks and two containers.""" + names = network_names(stem) + networks = {kind: checked_network(names[kind], kind) for kind in ("int", "gw")} + server = checked_container(stem + "-server", {names["int"]: networks["int"]["id"]}) + proxy = checked_container(stem + "-proxy", {names["gw"]: networks["gw"]["id"], names["int"]: networks["int"]["id"]}, + aliases={names["int"]: "gw"}) + return networks, server, proxy + + +def probe_command(pins, prefix, stem, role, output, arguments): + """e2e/netprobe.py in the pinned agent image, with docker_args hardening and three mounts only. + + The venv, /recipe read-only and this container's own output directory: + no workspace, server state, key file or published port. + """ + if role not in ("gw", "int"): + raise ValueError("probe_role_required") + name = f"{stem}-probe-{role}" + args = docker_args(pins, name, network=network_names(stem)[role]) + args += (mount(Path(prefix) / "venv", Path(prefix) / "venv") + mount(HERE, "/recipe") + + mount(output, "/probe-output", False)) + args += ["--entrypoint", str(Path(prefix) / "venv/bin/python"), pins["image"]["ref"], + "/recipe/e2e/netprobe.py", role, "--out", "/probe-output/observations.json", *arguments] + return name, args + + +def run_probe(state, run_id, arm, prefix, pins): + """Run P0 on -gw, then P1 and P2 on -int; True only if everything passes. + + The attempt must be prepared, its rendered proxy config unchanged, and + its networks and containers must pass the live contract reads. P0 is the + negative control: if it fails, P1 and P2 do not run. The verdict is + re-derived from the observation files with netprobe.evaluate and also + needs both exit codes to be 0. /isolation-probe.json is written + at 0600 whether or not the probe passes; it holds counts and IDs, never + host addresses. + """ + stem = attempt_stem(run_id, arm) + result = Path(state) / "runs" / run_id / arm + status = json.loads(read_bounded(result / "status.json")) + if (not isinstance(status, dict) or status.get("status") != "prepared" or status.get("run_id") != run_id + or status.get("arm") != arm): + raise ValueError("prepared_attempt_required") + port = owned_port(status.get("port")) + upstream = arm_config(arm)["gateway_port"] + config = digest(result / "proxy/nginx.conf") + if status.get("proxy_config_sha256") != config: + raise ValueError("proxy_config_changed_since_prepare") + networks, server, proxy = attempt_bindings(stem) + listeners = subprocess.check_output(["ss", "-ltnH"], text=True, timeout=30) + interfaces = subprocess.check_output(["ip", "-4", "-o", "addr"], text=True, timeout=30) + addresses, ports = probe_targets(networks, listeners, interfaces) + # A target holding one of the attempt's own containers would connect and + # read as a containment failure; refuse the probe instead. + internal = networks["int"]["name"] + if {view["networks"][internal].get("IPAddress") for view in (server, proxy)} & set(addresses): + raise ValueError("probe_target_is_attempt_container") + private_directory(result / "probes") + directory = Path(tempfile.mkdtemp(prefix="probe-", dir=result / "probes")) + arguments = {"gw": ["--upstream-port", str(upstream)], + "int": ["--server", stem + "-server", "--addresses", ",".join(addresses), + "--ports", ",".join(map(str, ports))]} + observed, codes = {"gw": None, "int": None}, {"gw": None, "int": None} + for role in ("gw", "int"): + output = directory / role + output.mkdir(mode=0o700) + model_visible(output, writable=True) + name, argv = probe_command(pins, prefix, stem, role, output, arguments[role]) + codes[role] = execute_container(argv, name, directory / (role + ".log"), 300) + with contextlib.suppress(OSError, ValueError): + observed[role] = json.loads(read_bounded(output / "observations.json")) + if role == "gw" and (codes["gw"] != 0 or not netprobe.evaluate_http( + observed["gw"].get("p0") if isinstance(observed["gw"], dict) else None, + netprobe.p0_expected(upstream))["passed"]): + break + verdict = netprobe.evaluate(observed["gw"], observed["int"], upstream, addresses, ports, stem + "-server") + passed = verdict["passed"] and codes == {"gw": 0, "int": 0} + write_private_json(result / "isolation-probe.json", { + "mechanism": PROBE_MECHANISM, "run_id": run_id, "arm": arm, + "run_network_id": networks["int"]["id"], "gw_network_id": networks["gw"]["id"], + "server_container_id": server["id"], "proxy_container_id": proxy["id"], + "proxy_image": pins["gateway_proxy"]["ref"], "proxy_config_sha256": config, + "probe_script_sha256": digest(HERE / "e2e/netprobe.py"), + "upstream": f"{netprobe.GATEWAY_HOST}:{upstream}", "host_ingress": f"127.0.0.1:{port}", + "verified_at": utc_now(), "exit_codes": codes, + "targets": {"addresses": len(addresses), "ports": ports, "pairs": len(addresses) * len(ports)}, + "p0": verdict["p0"], "p1": verdict["p1"], "p2": verdict["p2"], "passed": passed, + }) + return passed + + +def verify_isolation(state, run_id, arm, port, *, now=None): + """The dispatch-start gate (plan E1, G7): a fresh passed probe bound to the live attempt. + + Refuses unless /isolation-probe.json is an owner-only regular file + written at most PROBE_MAX_AGE_SECONDS ago, for this arm's upstream, this + ingress port, the pinned proxy image, the rendered config and this probe + script, with every section passed, and unless the live network and + container IDs still match. File and time checks come before any Docker + read. A pure check: it changes nothing. + """ + stem = attempt_stem(run_id, arm) + result = Path(state) / "runs" / run_id / arm + receipt = json.loads(read_bounded(private_file(result / "isolation-probe.json"), limit=1024 * 1024)) + if not isinstance(receipt, dict): + raise ValueError("isolation_probe_receipt_shape") + age = ((now or datetime.now(timezone.utc)) - time_value(receipt.get("verified_at"))).total_seconds() + if not 0 <= age <= PROBE_MAX_AGE_SECONDS: + raise ValueError("isolation_probe_not_fresh") + status = json.loads(read_bounded(result / "status.json")) + config = digest(result / "proxy/nginx.conf") + expected = {"mechanism": PROBE_MECHANISM, "run_id": run_id, "arm": arm, + "upstream": f"{netprobe.GATEWAY_HOST}:{arm_config(arm)['gateway_port']}", + "host_ingress": f"127.0.0.1:{owned_port(port)}", + "proxy_image": read_json(HERE / "pins.json")["gateway_proxy"]["ref"], + "proxy_config_sha256": config, "probe_script_sha256": digest(HERE / "e2e/netprobe.py"), + "passed": True} + if (any(receipt.get(key) != value for key, value in expected.items()) + or not isinstance(status, dict) or status.get("proxy_config_sha256") != config + or any(not isinstance(receipt.get(section), dict) or receipt[section].get("passed") is not True + for section in ("p0", "p1", "p2"))): + raise ValueError("isolation_probe_mismatch") + networks, server, proxy = attempt_bindings(stem) + live = {"run_network_id": networks["int"]["id"], "gw_network_id": networks["gw"]["id"], + "server_container_id": server["id"], "proxy_container_id": proxy["id"]} + if any(receipt.get(key) != value for key, value in live.items()): + raise ValueError("isolation_probe_bound_to_other_resources") + return receipt + + +def probe_action(prefix, state, run_id, arm): + """`host.py probe`: re-run P0-P2 on a prepared attempt, e.g. to refresh its receipt. + + Needs an explicit run id and never mints one. A refusal before the probe + runs changes nothing. A failed or crashed probe tears the attempt down + and records failure_stage "probe" (exit 3), as run() does. + """ + def refuse(stage, receipt=None): + print(json.dumps({"run_id": run_id, "arm": arm, "receipt": receipt, "failure_stage": stage, + "task_passed": False, "evidence_complete": False})) + return 3 + try: + attempt_stem(run_id, arm) + except ValueError: + return refuse("preflight") + result = Path(state) / "runs" / run_id / arm + # Nothing has run before the probe starts; these refusals leave the attempt unchanged. + try: + status = json.loads(read_bounded(result / "status.json")) + if not isinstance(status, dict) or status.get("status") != "prepared": + raise ValueError("prepared_attempt_required") + except (OSError, ValueError): + return refuse("probe") + try: + pins, _, _ = preflight(prefix, state) + except Exception: + return refuse("preflight") + failure_type = "RuntimeError" + try: + if run_probe(state, run_id, arm, prefix, pins): + print(json.dumps({"run_id": run_id, "arm": arm, "isolation_probe": str(result / "isolation-probe.json"), + "passed": True})) + return 0 + except (Exception, KeyboardInterrupt) as exc: + failure_type = type(exc).__name__ + with contextlib.suppress(OSError, ValueError, subprocess.SubprocessError): + teardown_attempt(state, run_id, arm) # The cleanup records keep any uncertainty. + try: + window = json.loads(read_bounded(result / "window.json")) + except (OSError, ValueError): + window = {"run_id": run_id, "arm": arm, "started_at": utc_now()} + window.update(failure_stage="probe", failure_type=failure_type, finished_at=window.get("finished_at") or utc_now()) + write_json(result / "window.json", window) + if not (result / "check.json").is_file(): + write_json(result / "check.json", {"upstream_resolved": None, "grader_exit_code": None}) + write_json(result / "receipt.json", create_receipt(result)) + write_json(result / "status.json", {**status, "status": "failed", "failure_stage": "probe"}) + return refuse("probe", str(result / "receipt.json")) + + def clone_command(prefix, task, workspace): # SWE-bench@v4.1.0 test_spec/python.py:271-277, including branch exceptions. code = ("import json,sys; from importlib.metadata import version; " @@ -947,6 +1227,11 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL raise RuntimeError("qmd_setup_failed") stage = "start" prepare_native_dispatch(state, run_id, selection, prefix, pins, base, host, port=port) + # Plan E1/G7: P0-P2 before any conversation; dispatch start re-checks the receipt. + stage = "probe" + if not run_probe(state, run_id, arm, prefix, pins): + raise RuntimeError("isolation_probe_failed") + stage = "start" prepared = True write_json(result / "window.json", window) if not prepare_only: @@ -959,7 +1244,7 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL except (Exception, KeyboardInterrupt) as exc: window["failure_stage"] = stage window["failure_type"] = type(exc).__name__ - if stage == "start": + if stage in {"start", "probe"}: # Resources may exist from the first network onward; remove them by name. try: teardown_attempt(state, run_id, arm) @@ -984,22 +1269,30 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL return result_exit(receipt) -def main(): +def build_parser(): parser = argparse.ArgumentParser() - parser.add_argument("action", choices=("install", "run", "prepare")) + parser.add_argument("action", choices=("install", "run", "prepare", "probe")) parser.add_argument("--prefix", required=True, type=Path) parser.add_argument("--state", required=True, type=Path) parser.add_argument("--run-id") parser.add_argument("--arm", choices=("control", "engines-on")) parser.add_argument("--port", type=cli_port, default=DEFAULT_PORT, help="published loopback port in 3730..3799 (default 3730; resolver mode 3740)") - args = parser.parse_args() + return parser + + +def main(): + args = build_parser().parse_args() os.umask(0o077) # SIGTERM goes through container cleanup and the failed-attempt receipt path. signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt())) if args.action == "install": install(args.prefix, args.state) return 0 + if args.action == "probe": + # An existing attempt only: no run id is ever generated for a probe. + return probe_action(args.prefix, args.state, args.run_id or os.environ.get("OPENHANDS_RUN_ID"), + args.arm or os.environ.get("OPENHANDS_ARM", "control")) return run(args.prefix, args.state, run_id=args.run_id, arm=args.arm, prepare_only=args.action == "prepare", port=args.port) diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 71ca1e512..2710090b6 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -10,6 +10,7 @@ from collections import Counter from datetime import datetime, timezone +import json import os from pathlib import Path import re @@ -20,6 +21,15 @@ from e2e.check import read_report +# host.run_probe's receipt (plan E1); the summary copies counts and booleans only. +PROBE_MECHANISM = "internal-isolated+nginx-v1-allowlist" +PROBE_FIELDS = { + "p0": ("requests", "observed", "matched", "passed"), + "p1": ("requests", "observed", "matched", "passed"), + "p2": ("connects", "observed", "connected", "errors", "dns", "dns_matched", "ipv6_non_loopback", + "ipv6_link_local", "ipv6_loopback", "passed"), + "targets": ("addresses", "ports", "pairs"), +} COLUMNS = ( "timestamp", "path", "status", "model", "reasoning_effort_requested", "reasoning_effort_upstream", "tokens_in", "tokens_cache_read", "tokens_reasoning", "correlation_id", @@ -169,6 +179,42 @@ def observations(events, known_skills=()): "skills_observed": dict(skills), "basis": "native ObservationEvent fields; registration alone is not usage"} +def summary_value(value): + """Numbers, booleans, errno-name counts and port lists only; anything else becomes None.""" + if value is None or type(value) in (bool, int): + return value + if isinstance(value, dict): + return {key: count for key, count in value.items() + if isinstance(key, str) and re.fullmatch(r"[A-Za-z_<>]{1,32}", key) and type(count) is int} + if isinstance(value, list) and all(type(item) is int for item in value): + return value + return None + + +def isolation_summary(result): + """The host-written probe receipt without its network/container IDs or run id.""" + try: + probe = json.loads(read_bounded(Path(result) / "isolation-probe.json", limit=1024 * 1024)) + except (OSError, ValueError): + return {"status": "absent"} + if not isinstance(probe, dict): + return {"status": "unreadable"} + try: + time_value(probe.get("verified_at")) + verified_at = probe["verified_at"] if isinstance(probe["verified_at"], str) else None + except (ValueError, TypeError, OverflowError, OSError): + verified_at = None + codes = probe.get("exit_codes") if isinstance(probe.get("exit_codes"), dict) else {} + summary = {"status": "observed", + "mechanism": PROBE_MECHANISM if probe.get("mechanism") == PROBE_MECHANISM else "", + "verified_at": verified_at, "passed": probe.get("passed") is True, + "exit_codes": {role: summary_value(codes.get(role)) for role in ("gw", "int")}} + for section, keys in PROBE_FIELDS.items(): + values = probe.get(section) if isinstance(probe.get(section), dict) else {} + summary[section] = {key: summary_value(values[key]) for key in keys if key in values} + return summary + + def create_receipt(result, database=None): result = Path(result) window = read_json(result / "window.json") @@ -197,16 +243,16 @@ def create_receipt(result, database=None): and checked.get("conversion_exit_code") == 0 and window.get("worker_exit_code") == 0) termination = window.get("agent_termination") usage = summarize_gateway(rows, selection) - def removals(pattern): + def removals(*patterns): found = [] - for path in sorted(result.glob(pattern)): + for path in sorted(path for pattern in patterns for path in result.glob(pattern)): try: found.append(read_json(path).get("confirmed_removed") is True) except (OSError, ValueError, AttributeError): found.append(False) return {"attempts": len(found), "confirmed_removed": sum(found), "complete": bool(found) and all(found)} return { - "schema_version": 5, "evidence_class": "SDK inference adapter with official SWE-bench grading", + "schema_version": 6, "evidence_class": "SDK inference adapter with official SWE-bench grading", **{k: selection[k] for k in ("arm", "base_url", "gateway_upstream", "requested_model", "gateway_model", "gateway_path", "compression_combo")}, "header_names": window.get("header_names", sorted(selection["headers"])), @@ -218,7 +264,7 @@ def removals(pattern): "agent_termination": termination if termination in AGENT_TERMINATIONS else None, "agent_termination_basis": ("agent-server REST status and latest ConversationErrorEvent; the server shares " "the model terminal's UID and store, so this selects exit 1 or 3 only"), - "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"preflight", "prepare", "skills", "qmd_setup", "agent", "export", "grader", "start", "wait", "result", "deadline"} else None, + "failure_stage": window.get("failure_stage") if window.get("failure_stage") in {"preflight", "prepare", "skills", "qmd_setup", "agent", "export", "grader", "start", "probe", "wait", "result", "deadline"} else None, "task_passed": task_passed, # The terminal shares the SDK's UID and writable persistence. Source: # SDK@fcc102a tools/terminal/terminal/subprocess_terminal.py:157-170. @@ -227,9 +273,11 @@ def removals(pattern): "independent_trace_required": True, "not_collected_reason": NOT_COLLECTED_REASON, "skills_listed_at_start": NOT_COLLECTED, "skill_listing_matches_manifest": NOT_COLLECTED, - # host.cleanup_container and host.cleanup_network records, counted separately. - "container_cleanup": removals("*.log.cleanup.json"), + # host.cleanup_container and host.cleanup_network records, counted + # separately; probe containers log beside their output directories. + "container_cleanup": removals("*.log.cleanup.json", "probes/*/*.log.cleanup.json"), "network_cleanup": removals("network-*.cleanup.json"), + "isolation": isolation_summary(result), "gateway": { "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, **usage, "entry_port": selection["gateway_port"], diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 075af46b7..5f9dac869 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -8,7 +8,9 @@ import asyncio import contextlib +from datetime import datetime, timedelta, timezone import hashlib +import http.server import io import json import os @@ -16,11 +18,13 @@ from pathlib import Path import re import shutil +import socket import sqlite3 import stat import subprocess import sys import tempfile +import threading from types import SimpleNamespace import unittest import uuid @@ -669,6 +673,71 @@ def test_preflight_failure_still_has_status_receipt_and_setup_exit(self): ({"failure_stage": "preflight"}, 3)): self.assertEqual(host.result_exit(result), expected) + def run_to_probe(self, host, probe_passed): + """host.run through prepare and the P0-P2 probe, with every host and Docker step mocked.""" + prefix, state = self.result / "prefix", self.result / "state" + (prefix / "venv/bin").mkdir(parents=True) + (prefix / "venv/bin/python").write_text("") + state.mkdir() + pins = json.loads((RECIPE / "pins.json").read_text()) + (state / "installation.json").write_text(json.dumps({"exit_code": 0, "requirements_sha256": pins["requirements_sha256"]})) + task_file = self.result / "task.json" + task_file.write_text("[]") + stack = self.result / "stack" + (stack / "blueprints/runtime-workers/skills").mkdir(parents=True) + (stack / "blueprints/runtime-workers/skills/manifest.json").write_text("{}") + task = {"instance_id": self.INSTANCE, "repo": "django/django", "base_commit": "a" * 40} + output = io.StringIO() + with contextlib.ExitStack() as stack_context: + enter = stack_context.enter_context + enter(patch.dict(os.environ, {"OPENHANDS_TASK_FILE": str(task_file), "OPENHANDS_TASK_SHA256": "0" * 64, + "OPENHANDS_STACK_ROOT": str(stack)})) + for name in ("OPENHANDS_ARM", "OPENHANDS_MODEL", "OPENHANDS_BASE_URL", "OPENHANDS_COMPRESSION"): + os.environ.pop(name, None) + enter(patch.object(host, "preflight", return_value=( + pins, {"variables": {"HOST_PATH": "/usr/bin"}, "mcp_readonly_mounts": [], "qmd_collections": {}}, {}))) + enter(patch.object(host, "pinned_image_identity", return_value="containerd")) + enter(patch.object(host, "load_task", return_value=task)) + enter(patch.object(host, "clone_command", return_value=["true"])) + enter(patch.object(host, "logged_command", return_value=0)) + enter(patch.object(host.subprocess, "check_output", + side_effect=lambda argv, **kwargs: "" if "tag" in argv else "a" * 40 + "\n")) + enter(patch.object(host, "install_workspace_skills")) + enter(patch.object(host, "workspace_skills", return_value={})) + enter(patch.object(host, "worker_instruction", return_value="task")) + enter(patch.object(host, "model_visible")) + enter(patch.object(host, "execute_container", return_value=0)) + prepare = enter(patch.object(host, "prepare_native_dispatch")) + probe = enter(patch.object(host, "run_probe", return_value=probe_passed)) + teardown = enter(patch.object(host, "teardown_attempt", return_value=True)) + enter(patch.object(host, "create_receipt", side_effect=self.absent_gateway_receipt())) + enter(contextlib.redirect_stdout(output)) + code = host.run(prefix, state, run_id="rw-openhands-probe", arm="control", prepare_only=True) + return SimpleNamespace(code=code, prepare=prepare, probe=probe, teardown=teardown, prefix=prefix, state=state, + pins=pins, result=state / "runs/rw-openhands-probe/control", + output=json.loads(output.getvalue())) + + def test_probe_failure_in_run_tears_down_once_and_exits_3(self): + # Plan E1/G7: a failed containment probe leaves no prepared attempt. + host = load_recipe_module("host.py") + run = self.run_to_probe(host, probe_passed=False) + self.assertEqual(run.code, 3) + run.prepare.assert_called_once() + run.probe.assert_called_once_with(run.state, "rw-openhands-probe", "control", run.prefix, run.pins) + run.teardown.assert_called_once_with(run.state, "rw-openhands-probe", "control") + status = json.loads((run.result / "status.json").read_text()) + self.assertEqual((status["status"], status["failure_stage"]), ("failed", "probe")) + self.assertEqual(json.loads((run.result / "receipt.json").read_text())["failure_stage"], "probe") + self.assertEqual(run.output["failure_stage"], "probe") + + def test_probe_pass_in_run_leaves_the_attempt_prepared(self): + host = load_recipe_module("host.py") + run = self.run_to_probe(host, probe_passed=True) + self.assertEqual(run.code, 0) + run.probe.assert_called_once() + run.teardown.assert_not_called() + self.assertIsNone(run.output["failure_stage"]) + def test_agent_limit_terminations_are_graded_but_never_success(self): # F16: an officially graded partial patch after an agent limit exits 1; # grading infrastructure failures keep exit 3. @@ -1194,6 +1263,591 @@ def test_teardown_keeps_key_files_while_removal_is_unconfirmed(self): self.assertIs(json.loads((self.result / "network-gw.cleanup.json").read_text())["confirmed_removed"], True) +class OpenHandsProbeTests(unittest.TestCase): + """Locally composed P0-P2 probe and dispatch gate (plan section 1, E1, G7). + + Our integration checks with Docker mocked, not upstream tests. Nothing here + creates a container or network or sends a gateway request; the HTTP + fixture is an in-process server on 127.0.0.1. The live probe sequence in + README "Security posture" is the evidence for the topology itself. + """ + OWNER = {"com.native-agent-stack.owner": "gpt6-omniroute-framework-integration"} + ISOLATED = "com.docker.network.bridge.gateway_mode_ipv4" + # The plan's P1 denied list (section 1), in its order. + PLAN_DENIED = ( + *(("GET", target) for target in ( + "/api/settings", "/api/providers", "/api/keys", "/dashboard", "/v1/ws", "/v1/alpha/search", + "/v1/files", "/V1/models", "/v1/models/", "/v1/responses/x", "/v1/../api/settings", + "/v1/%2e%2e/api/settings", "/v1/models/../../api/keys", "//api/settings", "/codex/responses", + "/api/v1/responses", "/v1/models?x=1", "/v1/models?0")), + *(("POST", target) for target in ( + "/responses", "/chat/completions", "/api/v1/responses", "/api/settings/require-login", + "/v1/responses?provider=x")), + ("DELETE", "/v1/models"), ("PUT", "/v1/responses"), ("GET", "/v1/responses")) + # iproute2 output shapes; 192.0.2.20 is an RFC 5737 documentation address. + SS = ("LISTEN 0 4096 127.0.0.1:20128 0.0.0.0:*\n" + "LISTEN 0 511 [::1]:3000 [::]:*\n" + "LISTEN 0 4096 *:8080 *:*\n" + "LISTEN 0 128 127.0.0.53%lo:53 0.0.0.0:*\n" + "LISTEN 0 4096 [::ffff:127.0.0.1]:9000 *:*\n" + "LISTEN 0 4096 127.0.0.1:3730 0.0.0.0:*\n" + "LISTEN 0 4096 0.0.0.0:20128 0.0.0.0:*\n") + IP = ("1: lo inet 127.0.0.1/8 scope host lo\\ valid_lft forever preferred_lft forever\n" + "1: lo inet 10.255.255.254/32 brd 10.255.255.254 scope global lo\\ valid_lft forever preferred_lft forever\n" + "2: eth0 inet 192.0.2.20/24 brd 192.0.2.255 scope global noprefixroute eth0\\ valid_lft forever preferred_lft forever\n" + "3: docker0 inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0\\ valid_lft forever preferred_lft forever\n") + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.state = Path(self.tmp.name).resolve() + self.run_id, self.arm = "rw-openhands-fixture", "control" + self.stem = self.run_id + "-" + self.arm + self.result = self.state / "runs" / self.run_id / self.arm + self.result.mkdir(parents=True) + self.pins = json.loads((RECIPE / "pins.json").read_text()) + + @staticmethod + def closed_port(): + with socket.socket() as probe: + probe.bind(("127.0.0.1", 0)) + return probe.getsockname()[1] + + def serve(self, replies): + """In-process HTTP fixture on 127.0.0.1:0 recording each raw request line.""" + seen = [] + + class Handler(http.server.BaseHTTPRequestHandler): + def reply(self): + # self.path rewrites a leading "//" (CPython gh-87389); the raw + # request line keeps the target byte-identical. + method, target, _ = self.requestline.split(" ", 2) + length = int(self.headers.get("Content-Length") or 0) + body = self.rfile.read(length) if length else b"" + seen.append((method, target, body, self.headers.get("Content-Type"))) + status, route = replies.get((method, target), (403, None)) + payload = b"fixture-body-never-recorded" + self.send_response(status) + if route: + self.send_header("x-omniroute-route-class", route) + self.send_header("Set-Cookie", "session=fixture-cookie") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + do_GET = do_POST = do_PUT = do_DELETE = reply + + def log_message(self, *args): + pass + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + self.addCleanup(server.server_close) + self.addCleanup(server.shutdown) + return server.server_address[1], seen + + def test_netprobe_lists_are_the_plan_lists_and_import_does_no_work(self): + with patch("socket.socket", side_effect=AssertionError("socket at import")), \ + patch("socket.getaddrinfo", side_effect=AssertionError("dns at import")), \ + patch("socket.create_connection", side_effect=AssertionError("connect at import")): + netprobe = load_recipe_module("e2e/netprobe.py") + self.assertEqual(netprobe.DENIED, self.PLAN_DENIED) + self.assertEqual(len(netprobe.DENIED), 26) + self.assertEqual(netprobe.P1_EXPECTED, (("gw", 8081, "GET", "/v1/models", 200, "CLIENT_API"), + *(("gw", 8081, m, t, 403, None) for m, t in self.PLAN_DENIED))) + self.assertEqual(netprobe.p0_expected(20129), (("10.0.2.2", 20129, "GET", "/v1/models", 200, "CLIENT_API"), + ("10.0.2.2", 20129, "GET", "/api/settings", None, "MANAGEMENT"))) + self.assertEqual(netprobe.dns_expected(self.stem + "-server"), ( + ("example.com", False), ("github.com", False), ("host.docker.internal", False), + ("gw", True), (self.stem + "-server", True))) + self.assertEqual(netprobe.FIXED_ADDRESSES, ("10.0.2.2", "10.0.2.3", "172.17.0.1", "10.0.0.1", "10.255.255.254")) + self.assertEqual(netprobe.EXTRA_PORTS, (53,)) + # P3 is a documented skeleton; it is never run in phase 2. + with self.assertRaises(NotImplementedError): + netprobe.p3_control_call() + + def test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies(self): + netprobe = load_recipe_module("e2e/netprobe.py") + port, seen = self.serve({("GET", "/v1/models"): (200, "CLIENT_API"), ("GET", "/management-class"): (200, "MANAGEMENT"), + ("GET", "/other-class"): (403, "SOMETHING-ELSE")}) + pairs = (("GET", "/v1/models"), ("GET", "/management-class"), ("GET", "/other-class"), *self.PLAN_DENIED) + records = [netprobe.http_probe("127.0.0.1", port, method, target) for method, target in pairs] + self.assertEqual([(method, target) for method, target, _, _ in seen], list(pairs)) + for raw in ("/v1/%2e%2e/api/settings", "//api/settings", "/v1/models?0", "/V1/models", "/v1/../api/settings"): + self.assertIn(("GET", raw), [(method, target) for method, target, _, _ in seen]) + self.assertEqual([(body, kind) for method, _, body, kind in seen if method == "POST"], + [(b"{}", "application/json")] * 5) + self.assertEqual([(r["status"], r["route_class"]) for r in records[:3]], + [(200, "CLIENT_API"), (200, "MANAGEMENT"), (403, "")]) + self.assertTrue(all(r["status"] == 403 and r["route_class"] is None and r["error"] is None for r in records[3:])) + self.assertEqual(set(records[0]), {"host", "port", "method", "target", "status", "route_class", "error"}) + text = json.dumps(records) + for marker in ("fixture-body-never-recorded", "fixture-cookie", "Set-Cookie"): + self.assertNotIn(marker, text) + refused = netprobe.http_probe("127.0.0.1", self.closed_port(), "GET", "/v1/models") + self.assertEqual((refused["status"], refused["route_class"], refused["error"]), (None, None, "ECONNREFUSED")) + + def test_netprobe_connects_resolves_and_reads_ipv6_without_defaults(self): + netprobe = load_recipe_module("e2e/netprobe.py") + listener = socket.socket() + self.addCleanup(listener.close) + listener.bind(("127.0.0.1", 0)) + listener.listen() + open_port, closed = listener.getsockname()[1], self.closed_port() + records = netprobe.connect_all([("127.0.0.1", open_port), ("127.0.0.1", closed)], timeout=2, workers=2) + self.assertEqual([(r["address"], r["port"], r["connected"], r["error"]) for r in records], + [("127.0.0.1", open_port, True, None), ("127.0.0.1", closed, False, "ECONNREFUSED")]) + with patch.object(netprobe.socket, "getaddrinfo", side_effect=socket.gaierror(socket.EAI_NONAME, "fixture")): + self.assertEqual(netprobe.resolve("example.com"), {"name": "example.com", "resolved": False, "error": "EAI_NONAME"}) + with patch.object(netprobe.socket, "getaddrinfo", + return_value=[(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("172.30.0.3", 0))]): + self.assertEqual(netprobe.resolve("gw"), {"name": "gw", "resolved": True, "error": None}) + # /proc/net/if_inet6: address, ifindex, prefix, scope, flags, name. + lo = "00000000000000000000000000000001 01 80 10 80 lo\n" + link = "fe80000000000000004200fffeac1e02 02 40 20 80 eth0\n" + other = "20010db8000000000000000000000005 02 40 00 00 eth0\n" + self.assertEqual(netprobe.ipv6_summary(lo), {"available": True, "loopback": 1, "non_loopback": 0, "link_local": 0}) + self.assertEqual(netprobe.ipv6_summary(lo + link), {"available": True, "loopback": 1, "non_loopback": 1, "link_local": 1}) + self.assertEqual(netprobe.ipv6_summary(lo + link + other)["non_loopback"], 2) + with self.assertRaises(ValueError): + netprobe.ipv6_summary("garbage\n") + + def expected_http(self, host_name, port, method, target): + status = 200 if (method, target) == ("GET", "/v1/models") else 403 + return {"host": host_name, "port": port, "method": method, "target": target, "status": status, + "route_class": "CLIENT_API" if status == 200 else None, "error": None} + + def test_netprobe_stops_at_the_first_unexpected_result(self): + netprobe = load_recipe_module("e2e/netprobe.py") + out = self.result / "probe-output" + out.mkdir() + target = out / "observations.json" + + def forwarded(host_name, port, method, path, **kwargs): + record = self.expected_http(host_name, port, method, path) + return dict(record, status=404) if (method, path) == ("GET", "/v1/files") else record + previous = os.umask(0o077) + self.addCleanup(os.umask, previous) + with patch.object(netprobe, "http_probe", side_effect=forwarded) as http, \ + patch.object(netprobe, "connect_all") as connects, patch.object(netprobe, "resolve") as resolve: + code = netprobe.main(["int", "--out", str(target), "--server", self.stem + "-server", + "--addresses", "10.0.2.2", "--ports", "20128"]) + self.assertEqual(code, 1) + stop = self.PLAN_DENIED.index(("GET", "/v1/files")) + 2 + data = json.loads(target.read_text()) + self.assertEqual(len(data["p1"]), stop) + self.assertEqual(http.call_count, stop) + connects.assert_not_called() + resolve.assert_not_called() + self.assertNotIn("p2", data) + self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o644) + + def test_netprobe_int_mode_passes_only_when_everything_is_refused(self): + netprobe = load_recipe_module("e2e/netprobe.py") + server = self.stem + "-server" + for label, connected, resolvable, ipv6 in (("contained", False, False, 0), ("connect", True, False, 0), + ("dns", False, True, 0), ("ipv6", False, False, 1)): + with self.subTest(label): + target = self.result / f"{label}.json" + + def connects(pairs, **kwargs): + return [{"address": a, "port": p, "connected": connected, "error": None if connected else "ENETUNREACH"} + for a, p in pairs] + + def resolve(name): + ok = name in {"gw", server} or resolvable + return {"name": name, "resolved": ok, "error": None if ok else "EAI_NONAME"} + with patch.object(netprobe, "http_probe", side_effect=self.expected_http), \ + patch.object(netprobe, "connect_all", side_effect=connects) as batch, \ + patch.object(netprobe, "resolve", side_effect=resolve), \ + patch.object(netprobe, "read_ipv6", return_value={ + "available": True, "loopback": 1, "non_loopback": ipv6, "link_local": ipv6}): + code = netprobe.main(["int", "--out", str(target), "--server", server, + "--addresses", "10.0.2.2,192.0.2.20", "--ports", "53,20128"]) + self.assertEqual(code, 0 if label == "contained" else 1) + self.assertEqual(batch.call_args.args[0], [("10.0.2.2", 53), ("10.0.2.2", 20128), + ("192.0.2.20", 53), ("192.0.2.20", 20128)]) + data = json.loads(target.read_text()) + verdict = netprobe.evaluate_p2(data.get("p2"), ["10.0.2.2", "192.0.2.20"], [53, 20128], server) + self.assertEqual(verdict["passed"], label == "contained") + for argv in (["int", "--out", str(self.result / "x.json"), "--server", "other", "--addresses", "10.0.2.2", "--ports", "53"], + ["int", "--out", str(self.result / "y.json"), "--server", server, "--addresses", "gw", "--ports", "53"], + ["int", "--out", str(self.result / "z.json"), "--server", server, "--addresses", "10.0.2.2", "--ports", "0"]): + with self.subTest(argv=argv), patch.object(netprobe, "http_probe") as http, self.assertRaises(ValueError): + netprobe.main(argv) + http.assert_not_called() + + def test_probe_targets_come_from_ss_ip_and_the_attempt_networks(self): + host = load_recipe_module("host.py") + self.assertEqual(host.listener_ports(self.SS), [53, 3000, 3730, 8080, 9000, 20128]) + # Container loopback is its own namespace: 127.0.0.0/8 is excluded by address. + self.assertEqual(host.host_ipv4_addresses(self.IP), ["10.255.255.254", "172.17.0.1", "192.0.2.20"]) + views = self.views() + networks = {"int": views[("network", self.stem + "-int")], "gw": views[("network", self.stem + "-gw")]} + addresses, ports = host.probe_targets(networks, self.SS, self.IP) + self.assertEqual(addresses, ["10.0.0.1", "10.0.2.2", "10.0.2.3", "10.255.255.254", "172.17.0.1", + "172.30.0.1", "172.31.0.1", "192.0.2.20"]) + self.assertEqual(ports, [53, 3000, 3730, 8080, 9000, 20128]) + no_gateway = dict(networks, int=dict(networks["int"], ipam=[{"Subnet": "172.29.0.0/16"}])) + self.assertIn("172.29.0.1", host.probe_targets(no_gateway, self.SS, self.IP)[0]) + for bad in ("LISTEN 0 4096 127.0.0.1:notaport 0.0.0.0:*\n", "garbage\n", "LISTEN 0 4096 127.0.0.1:70000 0.0.0.0:*\n"): + with self.subTest(bad=bad), self.assertRaises(ValueError): + host.listener_ports(bad) + with self.assertRaises(ValueError): + host.probe_targets(dict(networks, int=dict(networks["int"], ipam=[])), self.SS, self.IP) + + def test_probe_containers_use_the_agent_image_hardening_and_only_their_mounts(self): + host = load_recipe_module("host.py") + prefix = self.state / "prefix" + for role, network in (("gw", self.stem + "-gw"), ("int", self.stem + "-int")): + with self.subTest(role=role): + output = self.result / "probes" / role + name, argv = host.probe_command(self.pins, prefix, self.stem, role, output, ["--upstream-port", "20128"]) + self.assertEqual(name, f"{self.stem}-probe-{role}") + self.assertEqual(argv[:len(host.DOCKER) + 2], [*host.DOCKER, "run", "--rm"]) + for flag in ("--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--pull=never", + "--network=" + network): + self.assertIn(flag, argv) + self.assertEqual(argv[argv.index("--user") + 1], "10001:10001") + mounts = [argv[i + 1] for i, arg in enumerate(argv) if arg == "--mount"] + self.assertEqual(mounts, [f"type=bind,src={prefix}/venv,dst={prefix}/venv,readonly", + f"type=bind,src={host.HERE},dst=/recipe,readonly", + f"type=bind,src={output},dst=/probe-output"]) + self.assertFalse(any(arg == "--publish" or arg.startswith(("-p", "--publish=", "--env-file")) + for arg in argv)) + self.assertEqual(argv[argv.index("--entrypoint") + 1], f"{prefix}/venv/bin/python") + tail = argv[argv.index(self.pins["image"]["ref"]):] + self.assertEqual(tail, [self.pins["image"]["ref"], "/recipe/e2e/netprobe.py", role, + "--out", "/probe-output/observations.json", "--upstream-port", "20128"]) + for role in ("server", "proxy", "gw-int"): + with self.subTest(role=role), self.assertRaises(ValueError): + host.probe_command(self.pins, prefix, self.stem, role, self.result, []) + + def views(self): + """Selected-field inspect views (host.NETWORK_FORMAT, host.CONTAINER_FORMAT) of one attempt.""" + names = {role: f"{self.stem}-{role}" for role in ("int", "gw", "server", "proxy")} + ids = {role: hashlib.sha256(name.encode()).hexdigest() for role, name in names.items()} + + def network(role, prefix, internal): + return {"id": ids[role], "name": names[role], "driver": "bridge", "internal": internal, "ipv6": False, + "options": {self.ISOLATED: "isolated"} if internal else {}, "labels": dict(self.OWNER), + "ipam": [{"Subnet": prefix + ".0.0/16", "Gateway": prefix + ".0.1"}]} + + def endpoint(role, address, aliases=None): + return {"NetworkID": ids[role], "IPAddress": address, "Aliases": aliases} + return { + ("network", names["int"]): network("int", "172.30", True), + ("network", names["gw"]): network("gw", "172.31", False), + ("container", names["server"]): { + "id": ids["server"], "name": "/" + names["server"], "running": True, "labels": dict(self.OWNER), + "networks": {names["int"]: endpoint("int", "172.30.0.2")}}, + ("container", names["proxy"]): { + "id": ids["proxy"], "name": "/" + names["proxy"], "running": True, "labels": dict(self.OWNER), + "networks": {names["gw"]: endpoint("gw", "172.31.0.2"), + names["int"]: endpoint("int", "172.30.0.3", ["gw"])}}, + } + + def commands(self, host, views): + """subprocess.check_output fake: selected-field inspects, ss and ip; nothing else.""" + templates = {"network": host.NETWORK_FORMAT, "container": host.CONTAINER_FORMAT} + + def check_output(argv, **kwargs): + argv = list(argv) + if argv[:len(host.DOCKER)] == host.DOCKER: + kind, verb, flag, template, name = argv[len(host.DOCKER):] + # Never a full inspect: the server's Config.Env holds the session key. + self.assertEqual((verb, flag, template), ("inspect", "--format", templates[kind])) + return json.dumps(views[(kind, name)]) + if argv == ["ss", "-ltnH"]: + return self.SS + if argv == ["ip", "-4", "-o", "addr"]: + return self.IP + raise AssertionError(argv) + return check_output + + def prepared(self, port=3740): + """A prepared attempt's host files, as host.prepare_native_dispatch writes them.""" + host = load_recipe_module("host.py") + selection = load_recipe_module("recipe.py").arm_config(self.arm) + _, sha = host.write_proxy_config(self.result, host.render_proxy_config(selection, self.run_id)) + (self.result / "status.json").write_text(json.dumps({ + "run_id": self.run_id, "arm": self.arm, "status": "prepared", "server_name": self.stem + "-server", + "proxy_name": self.stem + "-proxy", "port": port, "proxy_config_sha256": sha})) + return host + + @staticmethod + def option(argv, flag): + return argv[argv.index(flag) + 1] + + def observations(self, netprobe, argv): + """What netprobe writes when every result is the expected one.""" + mode = argv[argv.index("/recipe/e2e/netprobe.py") + 1] + + def record(item): + host_name, port, method, target, status, route = item + return {"host": host_name, "port": port, "method": method, "target": target, + "status": 200 if status is None else status, "route_class": route, "error": None} + if mode == "gw": + return {"probe": "netprobe-v1", "mode": mode, + "p0": [record(item) for item in netprobe.p0_expected(int(self.option(argv, "--upstream-port")))]} + addresses = self.option(argv, "--addresses").split(",") + ports = [int(port) for port in self.option(argv, "--ports").split(",")] + return {"probe": "netprobe-v1", "mode": mode, "p1": [record(item) for item in netprobe.P1_EXPECTED], + "p2": {"connects": [{"address": a, "port": p, "connected": False, "error": "ENETUNREACH"} + for a in addresses for p in ports], + "dns": [{"name": name, "resolved": ok, "error": None if ok else "EAI_NONAME"} + for name, ok in netprobe.dns_expected(self.option(argv, "--server"))], + "ipv6": {"available": True, "loopback": 1, "non_loopback": 0, "link_local": 0}}} + + def probe(self, host, views=None, mutate=None, codes=None): + """host.run_probe with the two probe containers faked through execute_container.""" + netprobe = load_recipe_module("e2e/netprobe.py") + runs = [] + + def execute(argv, name, logfile, timeout): + runs.append((name, list(argv))) + mounts = [dict(part.split("=", 1) for part in argv[i + 1].split(",") if "=" in part) + for i, arg in enumerate(argv) if arg == "--mount"] + output = Path(next(m["src"] for m in mounts if m["dst"] == "/probe-output")) + data = self.observations(netprobe, argv) + if mutate: + mutate(data) + (output / "observations.json").write_text(json.dumps(data)) + return (codes or {}).get(data["mode"], 0) + with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views or self.views())), \ + patch.object(host, "execute_container", side_effect=execute): + passed = host.run_probe(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) + return passed, runs + + def gate(self, host, now, views=None): + with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views or self.views())): + return host.verify_isolation(self.state, self.run_id, self.arm, 3740, now=now) + + def test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate(self): + host = self.prepared() + previous = os.umask(0o022) + self.addCleanup(os.umask, previous) + passed, runs = self.probe(host) + self.assertTrue(passed) + path = self.result / "isolation-probe.json" + self.assertEqual(stat.S_IMODE(path.stat().st_mode), 0o600) + receipt = json.loads(path.read_text()) + views = self.views() + self.assertEqual({key: receipt[key] for key in ( + "mechanism", "run_id", "arm", "run_network_id", "gw_network_id", "server_container_id", + "proxy_container_id", "proxy_image", "proxy_config_sha256", "probe_script_sha256", "upstream", + "host_ingress", "exit_codes", "passed")}, { + "mechanism": "internal-isolated+nginx-v1-allowlist", "run_id": self.run_id, "arm": self.arm, + "run_network_id": views[("network", self.stem + "-int")]["id"], + "gw_network_id": views[("network", self.stem + "-gw")]["id"], + "server_container_id": views[("container", self.stem + "-server")]["id"], + "proxy_container_id": views[("container", self.stem + "-proxy")]["id"], + "proxy_image": self.pins["gateway_proxy"]["ref"], + "proxy_config_sha256": hashlib.sha256((self.result / "proxy/nginx.conf").read_bytes()).hexdigest(), + "probe_script_sha256": hashlib.sha256((RECIPE / "e2e/netprobe.py").read_bytes()).hexdigest(), + "upstream": "10.0.2.2:20128", "host_ingress": "127.0.0.1:3740", "exit_codes": {"gw": 0, "int": 0}, + "passed": True}) + self.assertEqual((receipt["p0"]["requests"], receipt["p1"]["requests"]), (2, 27)) + self.assertEqual(receipt["targets"], {"addresses": 8, "ports": [53, 3000, 3730, 8080, 9000, 20128], "pairs": 48}) + self.assertEqual((receipt["p2"]["connects"], receipt["p2"]["connected"], receipt["p2"]["dns_matched"]), (48, 0, 5)) + self.assertTrue(all(receipt[section]["passed"] for section in ("p0", "p1", "p2"))) + for address in ("192.0.2.20", "172.30.0.2", "172.30.0.3", "10.255.255.254"): + self.assertNotIn(address, path.read_text()) + # One container per network, each with an output directory of its own. + self.assertEqual([name for name, _ in runs], [self.stem + "-probe-gw", self.stem + "-probe-int"]) + self.assertIn("--network=" + self.stem + "-gw", runs[0][1]) + self.assertIn("--network=" + self.stem + "-int", runs[1][1]) + outputs = [arg for _, argv in runs for arg in argv if "dst=/probe-output" in arg] + self.assertEqual(len(set(outputs)), 2) + self.assertEqual(self.option(runs[0][1], "--upstream-port"), "20128") + self.assertEqual(self.option(runs[1][1], "--ports"), "53,3000,3730,8080,9000,20128") + self.assertEqual(self.option(runs[1][1], "--server"), self.stem + "-server") + # The gate accepts the real writer's output while the live IDs match. + verified = host.time_value(receipt["verified_at"]) + self.assertEqual(self.gate(host, verified + timedelta(seconds=900))["passed"], True) + + def test_gate_refuses_stale_future_mismatched_or_rebound_receipts(self): + host = self.prepared() + passed, _ = self.probe(host) + self.assertTrue(passed) + path = self.result / "isolation-probe.json" + original = json.loads(path.read_text()) + verified = host.time_value(original["verified_at"]) + soon = verified + timedelta(seconds=60) + self.gate(host, soon) + rebound = self.views() + new_id = hashlib.sha256(b"re-created network").hexdigest() + rebound[("network", self.stem + "-int")]["id"] = new_id + for key in (("container", self.stem + "-server"), ("container", self.stem + "-proxy")): + rebound[key]["networks"][self.stem + "-int"]["NetworkID"] = new_id + for label, now, views in (("901 s old", verified + timedelta(seconds=901), None), + ("future-dated", verified - timedelta(seconds=1), None), + ("re-created network", soon, rebound)): + with self.subTest(label), self.assertRaises(ValueError): + self.gate(host, now, views) + for label, change in (("cross-arm upstream", {"upstream": "10.0.2.2:20129"}), + ("other arm", {"arm": "engines-on"}), + ("not passed", {"passed": False}), + ("p2 not passed", {"p2": dict(original["p2"], passed=False)}), + ("other mechanism", {"mechanism": "docker-user-v1"}), + ("other ingress", {"host_ingress": "127.0.0.1:3730"}), + ("other probe script", {"probe_script_sha256": "0" * 64}), + ("other proxy image", {"proxy_image": "nginx:latest"}), + ("other server", {"server_container_id": "f" * 64})): + with self.subTest(label): + host.write_private_json(path, {**original, **change}) + with self.assertRaises(ValueError): + self.gate(host, soon) + host.write_private_json(path, original) + self.gate(host, soon) + config = self.result / "proxy/nginx.conf" + saved = config.read_text() + config.write_text(saved + "# changed\n") + with self.assertRaises(ValueError): + self.gate(host, soon) + config.write_text(saved) + status = json.loads((self.result / "status.json").read_text()) + (self.result / "status.json").write_text(json.dumps({**status, "proxy_config_sha256": "0" * 64})) + with self.assertRaises(ValueError): + self.gate(host, soon) + (self.result / "status.json").write_text(json.dumps(status)) + path.chmod(0o644) + with self.assertRaises(ValueError): + self.gate(host, soon) + path.unlink() + with self.assertRaises(OSError): + self.gate(host, soon) + + def test_any_unexpected_observation_fails_the_probe_and_the_gate(self): + host = self.prepared() + + def inside(change): + return lambda data: change(data) if data["mode"] == "int" else None + cases = ( + ("host port reachable", inside(lambda d: d["p2"]["connects"][5].update(connected=True)), None), + ("denied path forwarded", inside(lambda d: d["p1"][3].update(status=404)), None), + ("management answered", inside(lambda d: d["p1"][2].update(route_class="MANAGEMENT")), None), + ("public name resolved", inside(lambda d: d["p2"]["dns"][0].update(resolved=True)), None), + ("server unresolvable", inside(lambda d: d["p2"]["dns"][4].update(resolved=False)), None), + ("ipv6 link-local", inside(lambda d: d["p2"]["ipv6"].update(non_loopback=1, link_local=1)), None), + ("truncated connects", inside(lambda d: d["p2"]["connects"].pop()), None), + ("reordered denied list", inside(lambda d: d["p1"].reverse()), None), + ("probe exit code", None, {"int": 1}), + ) + for label, mutate, codes in cases: + with self.subTest(label): + passed, runs = self.probe(host, mutate=mutate, codes=codes) + self.assertFalse(passed) + self.assertEqual(len(runs), 2) + receipt = json.loads((self.result / "isolation-probe.json").read_text()) + self.assertFalse(receipt["passed"]) + with self.assertRaises(ValueError): + self.gate(host, host.time_value(receipt["verified_at"])) + # A blind negative control stops before the internal probe runs. + blind = lambda data: data["p0"][1].update(route_class=None) if data["mode"] == "gw" else None + passed, runs = self.probe(host, mutate=blind) + self.assertFalse(passed) + self.assertEqual([name for name, _ in runs], [self.stem + "-probe-gw"]) + receipt = json.loads((self.result / "isolation-probe.json").read_text()) + self.assertEqual((receipt["p0"]["passed"], receipt["p1"]["observed"], receipt["exit_codes"]["int"]), + (False, 0, None)) + + def test_probe_refuses_targets_that_are_its_own_attempt_containers(self): + # If IPAM does not reserve .1 in isolated mode, a container could hold it. + host = self.prepared() + views = self.views() + views[("container", self.stem + "-server")]["networks"][self.stem + "-int"]["IPAddress"] = "172.30.0.1" + with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views)), \ + patch.object(host, "execute_container") as execute, self.assertRaises(ValueError) as caught: + host.run_probe(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) + self.assertEqual(str(caught.exception), "probe_target_is_attempt_container") + execute.assert_not_called() + + def test_probe_requires_each_attempt_container_on_its_own_networks(self): + host = self.prepared() + server, proxy = ("container", self.stem + "-server"), ("container", self.stem + "-proxy") + internal, gateway = self.stem + "-int", self.stem + "-gw" + cases = ( + ("proxy lacks the gw alias", proxy, lambda v: v["networks"][internal].update(Aliases=["other"])), + ("server also on the gateway network", server, + lambda v: v["networks"].update({gateway: {"NetworkID": "e" * 64, "IPAddress": "172.31.0.9"}})), + ("proxy not on the internal network", proxy, lambda v: v["networks"].pop(internal)), + ("server stopped", server, lambda v: v.update(running=False)), + ("proxy unlabelled", proxy, lambda v: v.update(labels={})), + ("other name", server, lambda v: v.update(name="/other")), + ("endpoint on another network", server, lambda v: v["networks"][internal].update(NetworkID="f" * 64)), + ("short id", proxy, lambda v: v.update(id="abc")), + ) + for label, key, update in cases: + with self.subTest(label): + views = self.views() + update(views[key]) + with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views)), \ + patch.object(host, "execute_container") as execute, self.assertRaises(ValueError): + host.run_probe(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) + execute.assert_not_called() + # moby@464cd50c api/swagger.yaml:5637-5643: the name "may be" prefixed with "/". + views = self.views() + views[server]["name"] = self.stem + "-server" + self.assertTrue(self.probe(host, views=views)[0]) + # Only a prepared attempt is probed. + status = json.loads((self.result / "status.json").read_text()) + (self.result / "status.json").write_text(json.dumps({**status, "status": "running"})) + with patch.object(host.subprocess, "check_output") as docker, self.assertRaises(ValueError): + host.run_probe(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) + docker.assert_not_called() + + def test_probe_action_needs_an_explicit_prepared_attempt(self): + host = load_recipe_module("host.py") + output = io.StringIO() + (self.result / "status.json").write_text(json.dumps({"run_id": self.run_id, "arm": self.arm, "status": "running"})) + with patch.object(host, "run_probe") as probe, patch.object(host, "teardown_attempt") as teardown, \ + patch.object(host, "preflight") as preflight, contextlib.redirect_stdout(output): + self.assertEqual(host.probe_action(self.state / "prefix", self.state, None, self.arm), 3) + self.assertEqual(host.probe_action(self.state / "prefix", self.state, "rw-openhands-absent", self.arm), 3) + self.assertEqual(host.probe_action(self.state / "prefix", self.state, self.run_id, self.arm), 3) + probe.assert_not_called() + teardown.assert_not_called() + preflight.assert_not_called() + self.assertEqual(json.loads((self.result / "status.json").read_text())["status"], "running") + self.assertFalse((self.state / "runs" / "rw-openhands-absent").exists()) + self.assertEqual([json.loads(line)["failure_stage"] for line in output.getvalue().splitlines()], + ["preflight", "probe", "probe"]) + args = host.build_parser().parse_args(["probe", "--prefix", "/p", "--state", "/s", "--run-id", self.run_id]) + self.assertEqual((args.action, args.run_id), ("probe", self.run_id)) + + def test_probe_action_failure_tears_down_and_records_the_probe_stage(self): + host = self.prepared() + status = json.loads((self.result / "status.json").read_text()) + (self.result / "window.json").write_text(json.dumps({ + "run_id": self.run_id, "arm": self.arm, "started_at": "2026-09-28T18:00:00Z", "finished_at": None})) + (self.result / "check.json").write_text(json.dumps({"upstream_resolved": None, "grader_exit_code": None})) + receipts = load_recipe_module("receipt.py") + for label, outcome in (("passed", {"return_value": True}), ("failed", {"return_value": False}), + ("raised", {"side_effect": ValueError("attempt_container_contract_mismatch")})): + with self.subTest(label): + (self.result / "status.json").write_text(json.dumps(status)) + with patch.object(host, "preflight", return_value=(self.pins, {}, {})), \ + patch.object(host, "run_probe", **outcome) as probe, \ + patch.object(host, "teardown_attempt", return_value=True) as teardown, \ + patch.object(host, "create_receipt", side_effect=lambda result: receipts.create_receipt( + result, database=result / "absent.sqlite")), \ + contextlib.redirect_stdout(io.StringIO()): + code = host.probe_action(self.state / "prefix", self.state, self.run_id, self.arm) + probe.assert_called_once_with(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) + current = json.loads((self.result / "status.json").read_text()) + if label == "passed": + self.assertEqual(code, 0) + teardown.assert_not_called() + self.assertEqual(current, status) + continue + self.assertEqual(code, 3) + teardown.assert_called_once_with(self.state, self.run_id, self.arm) + self.assertEqual((current["status"], current["failure_stage"]), ("failed", "probe")) + self.assertEqual(json.loads((self.result / "receipt.json").read_text())["failure_stage"], "probe") + + class OpenHandsReceiptTests(unittest.TestCase): def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): module = load_recipe_module("receipt.py") @@ -1388,7 +2042,7 @@ def test_receipt_records_proxy_base_url_and_selected_compression_combo(self): **window, "base_url": "http://gw:8081/v1", "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z"})) receipt = module.create_receipt(result, database=result / "absent.sqlite") - self.assertEqual(receipt["schema_version"], 5) + self.assertEqual(receipt["schema_version"], 6) self.assertEqual(receipt["base_url"], "http://gw:8081/v1") self.assertEqual(receipt["gateway_upstream"], f"http://10.0.2.2:{port}/v1") self.assertEqual(receipt["compression_combo"], combo) @@ -1408,6 +2062,44 @@ def test_receipt_counts_network_removal_separately_from_containers(self): self.assertEqual(receipt["container_cleanup"], {"attempts": 2, "confirmed_removed": 2, "complete": True}) self.assertEqual(receipt["network_cleanup"], {"attempts": 2, "confirmed_removed": 1, "complete": False}) + def test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses(self): + module = load_recipe_module("receipt.py") + ids = {key: hashlib.sha256(key.encode()).hexdigest() + for key in ("run_network_id", "gw_network_id", "server_container_id", "proxy_container_id")} + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp).resolve() + (result / "check.json").write_text("{}") + (result / "window.json").write_text(json.dumps({ + "arm": "control", "failure_stage": "probe", + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:01Z"})) + receipt = module.create_receipt(result, database=result / "absent.sqlite") + self.assertEqual(receipt["failure_stage"], "probe") + self.assertEqual(receipt["isolation"], {"status": "absent"}) + probe = {"mechanism": "internal-isolated+nginx-v1-allowlist", **ids, "run_id": "rw-openhands-fixture", + "verified_at": "2026-09-28T17:59:00.000Z", "passed": True, "upstream": "10.0.2.2:20128", + "host_ingress": "127.0.0.1:3730", "exit_codes": {"gw": 0, "int": 0}, + "p0": {"requests": 2, "observed": 2, "matched": 2, "passed": True}, + "p1": {"requests": 27, "observed": 27, "matched": 27, "passed": True}, + "p2": {"connects": 16, "observed": 16, "connected": 0, "errors": {"ENETUNREACH": 16}, + "dns": 5, "dns_matched": 5, "ipv6_non_loopback": 0, "ipv6_link_local": 0, + "ipv6_loopback": 1, "passed": True}, + "targets": {"addresses": 8, "ports": [53, 20128], "pairs": 16}} + (result / "isolation-probe.json").write_text(json.dumps(probe)) + (result / "probes/probe-fixture").mkdir(parents=True) + (result / "probes/probe-fixture/gw.log.cleanup.json").write_text( + json.dumps({"confirmed_removed": True, "error_type": None})) + receipt = module.create_receipt(result, database=result / "absent.sqlite") + self.assertEqual(receipt["isolation"], { + "status": "observed", "mechanism": "internal-isolated+nginx-v1-allowlist", + "verified_at": "2026-09-28T17:59:00.000Z", "passed": True, "exit_codes": {"gw": 0, "int": 0}, + "p0": probe["p0"], "p1": probe["p1"], "p2": probe["p2"], "targets": probe["targets"]}) + text = json.dumps(receipt) + for value in ids.values(): + self.assertNotIn(value, text) + self.assertNotIn("rw-openhands-fixture", text) + # Probe containers count with the attempt's other container removals. + self.assertEqual(receipt["container_cleanup"], {"attempts": 1, "confirmed_removed": 1, "complete": True}) + class OpenHandsDispatchTests(unittest.TestCase): """Synthetic native REST transport, not an agent-server/model execution.""" @@ -1429,6 +2121,7 @@ def test_start_wait_result_use_native_routes_and_deterministic_host_files(self): replies = [{"id": FIXTURE_CONVERSATION_ID}, {"execution_status": "finished"}, {"response": "Done"}] output = io.StringIO() with patch.object(dispatch, "api_request", side_effect=replies) as api, \ + patch.object(dispatch, "verify_isolation", return_value={"passed": True}) as gate, \ patch.object(dispatch, "compression_snapshot", return_value={"totalRequests": 5, "totalTokensSaved": 12}), \ patch.object(dispatch, "finish_result", return_value={"task_passed": True, "evidence_complete": False, "failure_stage": None}), \ contextlib.redirect_stdout(output): @@ -1439,11 +2132,67 @@ def test_start_wait_result_use_native_routes_and_deterministic_host_files(self): self.assertEqual(routes, [("POST", "/api/conversations"), ("GET", f"/api/conversations/{FIXTURE_CONVERSATION_ID}"), ("GET", f"/api/conversations/{FIXTURE_CONVERSATION_ID}/agent_final_response")]) + # Only start is gated on the probe receipt (plan E1, G7). + gate.assert_called_once_with(self.state, self.run_id, self.arm, 3730) for row in output.getvalue().splitlines(): self.assertEqual(json.loads(row)["receipt"], str(self.result / "receipt.json")) self.assertEqual(json.loads((self.result / "status.json").read_text())["status"], "collected") self.assertNotIn(FIXTURE_CONVERSATION_ID, (self.result / "receipt.json").read_text()) + def test_start_requires_a_fresh_owner_only_probe_receipt(self): + # Plan E1/G7: refused before the serial lock, with nothing mutated. + dispatch = load_recipe_module("dispatch.py") + host = load_recipe_module("host.py") + status = {"run_id": self.run_id, "arm": self.arm, "status": "prepared", "port": 3740, + "proxy_config_sha256": "0" * 64} + receipt = self.result / "isolation-probe.json" + now = datetime.now(timezone.utc) + for label, verified_at, mode in (("missing", None, None), + ("901 s old", (now - timedelta(seconds=901)).isoformat(), 0o600), + ("mode 0644", now.isoformat(), 0o644)): + with self.subTest(label): + receipt.unlink(missing_ok=True) + if verified_at: + host.write_private_json(receipt, {"mechanism": "internal-isolated+nginx-v1-allowlist", + "verified_at": verified_at, "passed": True}) + receipt.chmod(mode) + (self.result / "status.json").write_text(json.dumps(status)) + (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) + (self.result / "start.json").write_text("{}") + output = io.StringIO() + with patch.object(dispatch, "api_request") as api, patch.object(dispatch, "stop_server") as stop, \ + patch("subprocess.check_output") as docker, contextlib.redirect_stdout(output): + self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 3) + api.assert_not_called() + stop.assert_not_called() + docker.assert_not_called() + self.assertEqual(json.loads((self.result / "status.json").read_text()), status) + self.assertFalse((self.state / "active-dispatch.json").exists()) + self.assertFalse((self.result / "receipt.json").exists()) + self.assertEqual(json.loads(output.getvalue())["failure_stage"], "probe") + + def test_native_routes_pair_each_path_with_its_one_method(self): + # Plan E3: POST only creates or interrupts a conversation; GET only reads. + dispatch = load_recipe_module("dispatch.py") + base = f"/api/conversations/{FIXTURE_CONVERSATION_ID}" + allowed = (("POST", "/api/conversations"), ("POST", base + "/interrupt"), ("GET", base), + ("GET", base + "/agent_final_response"), ("GET", base + self.ERROR_SEARCH)) + refused = (("GET", "/api/conversations"), ("POST", base), ("POST", base + "/agent_final_response"), + ("GET", base + "/interrupt"), ("POST", base + self.ERROR_SEARCH), ("DELETE", base), + ("PUT", "/api/conversations"), ("PATCH", base), ("get", base), ("HEAD", base), + ("GET", "/api/conversations/../settings"), ("POST", "/api/conversations?x=1")) + headers = self.result / "headers" + with patch.object(dispatch, "curl_json", return_value={}) as curl: + for method, path in allowed: + dispatch.api_request(method, path, headers=headers, port=3740) + self.assertEqual([(call.kwargs["method"], call.args[0]) for call in curl.call_args_list], + [(method, "http://127.0.0.1:3740" + path) for method, path in allowed]) + curl.reset_mock() + for method, path in refused: + with self.subTest(method=method, path=path), self.assertRaises(ValueError): + dispatch.api_request(method, path, headers=headers, port=3740) + curl.assert_not_called() + def test_wait_deadline_interrupts_and_emits_setup_failure(self): dispatch = load_recipe_module("dispatch.py") (self.result / "status.json").write_text(json.dumps({"status": "running", "conversation_id": FIXTURE_CONVERSATION_ID, "deadline": 0})) @@ -1787,7 +2536,7 @@ def test_selected_port_flows_from_prepare_to_dispatch(self): (self.result / "window.json").write_text(json.dumps({"arm": self.arm, "run_id": self.run_id})) headers = prepared.host.session_files(self.state, self.run_id, self.arm)[1] with patch.object(dispatch, "api_request", return_value={"id": FIXTURE_CONVERSATION_ID}) as api, \ - patch.object(dispatch, "verify_isolation", return_value=None, create=True), \ + patch.object(dispatch, "verify_isolation", return_value={"passed": True}), \ patch.object(dispatch, "compression_snapshot", return_value=None), contextlib.redirect_stdout(io.StringIO()): self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 0) self.assertEqual(api.call_args.kwargs["port"], 3740) From a0049a56be19157f4cb6be1c32c2ea4c97b497c5 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 15:30:01 -0400 Subject: [PATCH 11/22] OpenHands phase 2 (4a/4): teardown action and README for O1, the probe and the gate - host.py teardown: removes a prepared or failed attempt by exact name without starting a conversation; refuses with no Docker call while a conversation may be live or the serial reservation names the attempt; a prepared status becomes torn_down, which start and probe refuse. - README: session-key files and the container environment (E2), proxy ingress and method pairing (E3), the O1 topology, the locally composed nginx allowlist with its nginx.org and nginx source citations, the P0-P2 probe and its receipt, the dispatch gate, P3-P5 as documented steps, and the coordinator's live probe sequence. G7's P3 half is not enforced in code and is documented as the coordinator's duty. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 593 ++++++++++++++---- blueprints/runtime-workers/openhands/host.py | 56 +- tests/test_runtime_worker_openhands.py | 73 +++ 3 files changed, 599 insertions(+), 123 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 1d5a852da..83c6b4598 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -8,10 +8,18 @@ ran offline contract tests only. No installation, container, model, gateway request or official grading run was performed. The 2026-09-28 phase-1 repair added offline fixes and pulled the pinned image only to scan its digest. It started no container and made no model or gateway request -([research.md](research.md#takeover-phase-1-corrections-2026-09-28)). - -**Host acceptance is still open.** A verified firewall is required before model -execution. The model's terminal runs under the server's UID, so it can write +([research.md](research.md#takeover-phase-1-corrections-2026-09-28)). The +2026-09-28 phase-2 build replaced the DOCKER-USER firewall contract with the +O1 topology: a per-attempt internal network in gateway mode `isolated` and a +pinned nginx allowlist proxy ([Security posture](#security-posture); the +[decision record](../../../docs/decisions/2026-09-28-openhands-resolver-isolation.md)). +It ran offline tests with Docker mocked and pulled the nginx image by digest. +It created no container or network and made no model or gateway request +([research.md](research.md#takeover-phase-2-corrections-2026-09-28)). + +**Host acceptance is still open.** Before any model runs, a P0-P2 isolation +probe must pass on the live topology; it has not run yet +([Isolation probe](#isolation-probe-p0-p2-and-the-dispatch-gate)). The model's terminal runs under the server's UID, so it can write both `/run-output` and the server's persisted event store. Receipts therefore record trace, skill, MCP and version fields as `not_collected`, with the reason, instead of reading them from either place (SDK@fcc102a @@ -136,16 +144,32 @@ Failed attempts are retained. The coordinator runs installation and acceptance. ## Arms -`OPENHANDS_ARM=control|engines-on` defaults to `control`. `--arm` overrides it. -`OPENHANDS_MODEL` and `OPENHANDS_BASE_URL` are optional explicit overrides that -must match the selected arm. The control arm retains the existing `cx/gpt-6-*` -variant support; the engines-on arm admits only the exact one-slash route below. -LiteLLM prepends `openai/` for its provider selection. - -| Arm | Container base URL | Requested model | Compression header | -| --- | --- | --- | --- | -| control | `http://10.0.2.2:20128/v1` | `cx/gpt-6-astra-max` | absent | -| engines-on | `http://10.0.2.2:20129/v1` | `sharedgw/gpt-6-astra-max` | `x-omniroute-compression: allow-lossy` | +`OPENHANDS_ARM=control|engines-on` defaults to `control`, and `--arm` overrides +it. Control stays the default until the #431 A/B selects the engines arm. +`OPENHANDS_MODEL`, `OPENHANDS_BASE_URL` and `OPENHANDS_COMPRESSION` are +optional explicit overrides that must match the selected arm. The control arm +retains the existing `cx/gpt-6-*` variant support; the engines-on arm admits +only the exact one-slash route below. LiteLLM prepends `openai/` for its +provider selection. + +Both arms give the agent the same base URL, `http://gw:8081/v1`: the attempt's +proxy on the internal run network ([Security posture](#security-posture)). +Each attempt's proxy forwards to exactly one gateway, rendered from the arm: + +| Arm | Agent base URL | Proxy upstream | Requested model | Compression header | +| --- | --- | --- | --- | --- | +| control | `http://gw:8081/v1` | `http://10.0.2.2:20128/v1` | `cx/gpt-6-astra-max` | absent | +| engines-on | `http://gw:8081/v1` | `http://10.0.2.2:20129/v1` | `sharedgw/gpt-6-astra-max` | `x-omniroute-compression: `, default `allow-lossy` | + +engines-on accepts one of the seven combos recorded for the 20129 gateway +(`recipe.COMPRESSION_COMBOS`): `allow-lossy`, `fw-ccr`, `fw-codex-responses`, +`fw-headroom`, `fw-lite`, `fw-rtk` and `fw-session-dedup`. An unrecorded combo +is refused, and so is any compression value on control. The proxy, not the SDK, +sends the header: its allowlist replaces whatever the agent sends for +`x-omniroute-compression`, `X-Correlation-Id` and `x-omniroute-session` with the +attempt's fixed values. The agent therefore cannot switch arm or combo, or forge +the usage join. The LLM key stays the placeholder `local-loopback`, and +`OMNIROUTE_API_KEY` is never sent. Both arms use Responses, `reasoning_effort=max`, native function tools, omitted temperature, a 40-iteration limit and a 1,200-second conversation deadline. @@ -157,18 +181,18 @@ The exact switches are: ```sh export OPENHANDS_ARM=control export OPENHANDS_MODEL=cx/gpt-6-astra-max -export OPENHANDS_BASE_URL=http://10.0.2.2:20128/v1 +export OPENHANDS_BASE_URL=http://gw:8081/v1 ``` ```sh export OPENHANDS_ARM=engines-on export OPENHANDS_MODEL=sharedgw/gpt-6-astra-max -export OPENHANDS_BASE_URL=http://10.0.2.2:20129/v1 +export OPENHANDS_BASE_URL=http://gw:8081/v1 +export OPENHANDS_COMPRESSION=allow-lossy # optional; any recorded combo ``` -No separate compression toggle is needed. The arm adds/removes the header. -The receipt stores the arm, base URL, requested/routed model, expected path and -**header names only**. A stable `x-omniroute-session` binds the conversation and +The receipt stores the arm, base URL, proxy upstream, requested/routed model, +compression combo, expected path and **header names only**. A stable `x-omniroute-session` binds the conversation and condenser. `server_transport.py` loads through the native `--import-modules` option and supplies a fresh Idempotency-Key per logical `generate/agenerate` call, retaining upstream retries and exact native LLM types. It also captures @@ -181,12 +205,13 @@ Sources: SDK@fcc102a `llm/llm.py:442-446,1130-1138,1588-1642`, ## Workflow dispatch The coordinator first installs the recipe, supplies the frozen task and image -pin, completes the network gate described below, and prepares **one arm at a -time**. Preparation clones the task at its SWE-bench base-commit branch, -installs the shared project skills, builds the scoped lexical QMD index, -serializes the native request and starts the authenticated native server. -Preparation can be long and belongs in a background Bash task. The skills -manifest/installer PR must be available; there is no global-skill fallback. +pin, and prepares **one arm at a time**. Preparation clones the task at its +SWE-bench base-commit branch, installs the shared project skills, builds the +scoped lexical QMD index and serializes the native request. It then builds the +attempt's O1 topology, starts the authenticated native server behind the proxy +and runs the P0-P2 isolation probe. Preparation can be long and belongs in a +background Bash task. The skills manifest/installer PR must be available; +there is no global-skill fallback. Set these private inputs outside all worktrees: @@ -194,23 +219,53 @@ Set these private inputs outside all worktrees: - `OPENHANDS_STACK_ROOT`: stack checkout with the shared skill installer/manifest. - `OPENHANDS_TASK_FILE`, `OPENHANDS_TASK_SHA256`: frozen original SWE-bench row. - `OPENHANDS_GRADER_IMAGE_FILE`, `OPENHANDS_GRADER_IMAGE_SHA256`: frozen image pin. -- `OPENHANDS_SERVER_ENV`: mode-0600 file containing only this attempt's - `OH_SESSION_API_KEYS_0` setting. No shared service/provider secrets. - Preflight refuses the attempt unless a `OH_SESSION_API_KEYS_0=` line names - that variable. It parses Docker's env-file syntax for the name only and never - extracts, prints or logs the value (docker/cli@v29.8.1 - `pkg/kvfile/kvfile.go:92-124`). A bare name, which Docker would fill from the - caller's environment, is refused. An empty value (`OH_SESSION_API_KEYS_0=`) - still passes. This residual follows from never reading values. SDK@fcc102a - `openhands-agent-server/openhands/agent_server/env_parser.py:183-197` then - yields a key list holding one empty string, so the server treats - authentication as enabled (`__main__.py:30-48`). The coordinator must write a - non-empty key for each attempt. The server listens on every container - interface only when a session key is set (SDK@fcc102a - `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, - `config.py:24`). -- `OPENHANDS_HEADERS`: mode-0600 file containing the corresponding - `X-Session-API-Key` header. Commands use curl's `-H @file`; no inline key. + +The coordinator does not supply the agent-server's session key; each attempt +generates its own (plan E2). After both attempt networks exist, +`host.generate_session_files` takes one value from Python's `secrets` module +and writes two files in +`$HOME/.local/state/native-agent-stack/runtime-workers/openhands/secrets/` +(mode 0700). Each file is created with `O_CREAT|O_EXCL|O_NOFOLLOW` at mode +0600, so an existing file or a planted symlink is refused: + +- `-.server.env`: the single line `OH_SESSION_API_KEYS_0=` + in Docker env-file syntax, passed to the server as `--env-file`; +- `-.headers`: `X-Session-API-Key: `, which curl reads + with `-H @file`. + +The value is never in argv, output or logs. The server is never read with a +full `docker inspect`, because its Config.Env holds the key; containers are +read only through fixed `--format` templates. `teardown_attempt` deletes both +files once the proxy and server are confirmed removed. While either removal is +unconfirmed the files stay, so the server they belong to can still be reached +and cleaned up. The server listens on every container interface only when a +key is set (SDK@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, +`config.py:24`), and it checks the `X-Session-API-Key` header +(`dependencies.py:19`). Before the server starts, the env file is re-checked +for the variable name only, using Docker's env-file rules (docker/cli@v29.8.1 +`pkg/kvfile/kvfile.go:92-124`); no value is extracted, printed or logged. The +inventory row is `openhands-session` in +[docs/secret-storage.md](../../../docs/secret-storage.md) and +[adoption/credential-inventory.json](../../../adoption/credential-inventory.json). + +**Deviation from the plan (E2).** The plan kept the pointer variables +`OPENHANDS_SERVER_ENV` and `OPENHANDS_HEADERS`. They are retired. Both paths +derive from the validated run id and arm alone, so neither a caller's +environment nor `status.json` can redirect the key files, and a generated key +is never empty (the phase-1 empty-value residual no longer applies). + +Container environment, and only these names: `HOME=/state/home`, `PATH`, +`PYTHONDONTWRITEBYTECODE`, `UV_PYTHON_DOWNLOADS`, `OPENHANDS_OWNED_CONTAINER`, +`OPENHANDS_RUN_ID`, `OPENHANDS_ARM`, `OPENHANDS_MODEL`, `OPENHANDS_BASE_URL`, +`OPENHANDS_COMPRESSION`, `OH_ENABLE_VSCODE`, `OH_CONVERSATIONS_PATH`, +`OH_WORKSPACE_PATH` and `OH_BASH_EVENTS_DIR`, plus `OH_SESSION_API_KEYS_0` from +the env file on the server only. Never `OMNIROUTE_API_KEY`, and never a `GH_*` +or `GITHUB_*` name. **Deviation from the plan:** its closed list has no +`OPENHANDS_COMPRESSION`. It is added so the request serializer and the +server's preload rebuild the same arm selection the host checked. Without it +an engines-on attempt with a non-default combo would render a different +selection in the container. The value is a recorded combo name or empty, never +a credential, and the proxy overwrites the header in any case. ```sh RECIPE="$PWD/blueprints/runtime-workers/openhands" @@ -221,14 +276,33 @@ rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" prepare \ --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" ``` -`--port` selects the published loopback port. It must be an integer in -3730..3799, the range PR #428's crawl4ai recipe enforces (`host.py:44-45` at -3ad8ba2), and defaults to 3730 for this SWE-bench worker. The resolver uses -3740 (`--port 3740`). Preparation records the port in the host-owned -`status.json`; `dispatch.py` has no port flag, reads it from there and checks -the range again before any request. +`--port` selects the proxy's published loopback port, the host's only way into +the attempt (`127.0.0.1:` to the proxy's 8080, which forwards to the +server's 8000). It must be an integer in 3730..3799, the range PR #428's +crawl4ai recipe enforces (`host.py:44-45` at 3ad8ba2), and defaults to 3730 for +this SWE-bench worker. The resolver uses 3740 (`--port 3740`). Preparation +records the port in the host-owned `status.json`; `dispatch.py` has no port +flag, reads it from there and checks the range again before any request. + +Preparation exits 0 only when the probe passed. `dispatch.py start` accepts the +probe receipt for 900 seconds. If more time passes, re-run the probe on the +same prepared attempt; it never generates a run id: + +```sh +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" probe \ + --prefix "$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" \ + --state "$HOME/.local/state/native-agent-stack/runtime-workers/openhands" \ + --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +``` + +A prepared attempt that will not start a conversation now is removed with +`host.py teardown` and the same identity flags +([live probe sequence](#live-probe-sequence-coordinator-not-run-yet)). -The workflow child runs exactly these three Bash commands after preparation: +No conversation starts until the coordinator has run and recorded P3 on the +running gateway build (plan gate G7; the code checks only the P0-P2 half, see +[the gate](#isolation-probe-p0-p2-and-the-dispatch-gate)). After that, the +workflow child runs exactly these three Bash commands after preparation: ```sh rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/dispatch.py" start --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" @@ -248,12 +322,19 @@ $HOME/.local/state/native-agent-stack/runtime-workers/openhands/runs//`: POST `/api/conversations`, GET -`/api/conversations/`, then GET +`http://127.0.0.1:`, the proxy's host-side listener (8080), which +forwards everything to `--server:8000` on the internal network: +POST `/api/conversations`, GET `/api/conversations/`, then GET `/api/conversations//agent_final_response`. After an `error` status it also calls GET `/api/conversations//events/search` with `kind=openhands.sdk.event.conversation_error.ConversationErrorEvent`, -`sort_order=TIMESTAMP_DESC` and `limit=1`. Start is asynchronous. The +`sort_order=TIMESTAMP_DESC` and `limit=1`. At the deadline it calls POST +`/api/conversations//interrupt`. Each route is paired with its one +method (plan E3): POST starts or interrupts a conversation, and GET only +reads. Any other method/path pair is refused before curl runs. `start` first +requires a fresh probe receipt bound to the live topology +([gate](#isolation-probe-p0-p2-and-the-dispatch-gate)); a refusal prints +`failure_stage: probe`, exits 3 and changes nothing. Start is asynchronous. The native SDK constructs `StartConversationRequest` and calls `model_dump(exclude_defaults=True, mode="json", context={"expose_secrets": True})`; only the fixed keyless gateway placeholder is exposed. It includes workspace, @@ -265,9 +346,10 @@ Sources: SDK@fcc102a `conversation_router.py:72-86,163-228,257-287`, `sdk/utils/pydantic_secrets.py:24-37,48-68`. Polling treats only finished/error/stuck as terminal; idle is not completion. -At the deadline it calls the native interrupt route and removes the exact owned -server. Result captures the native final response privately, confirms container -removal, then exports the candidate patch and invokes official grading. +At the deadline it calls the native interrupt route and tears the attempt down: +the proxy, the server and both attempt networks, each by exact name. Result +captures the native final response privately, confirms container removal, then +exports the candidate patch and invokes official grading. A native final message alone is never a task verdict. Sources: SDK@fcc102a `conversation_router.py:304-321`; SWE-bench@v4.1.0 `reporting.py:127-157`. @@ -321,10 +403,11 @@ Preparation/start/wait exit 0 means that operation succeeded, not that the task passed. Duplicate starts do not resubmit a native POST. Retrying a collected result returns its retained receipt without another API or grader call. A host reservation serializes dispatches from this recipe. It does not exclude unrelated gateway -clients. Interrupted attempts retain their files; cleanup removes named owned -containers only, never uses prune, and records whether removal was confirmed. -The coordinator must reclaim a stale reservation only after confirming its -recorded container is gone. The selected port must be available. PR #428's +clients. Interrupted attempts retain their files; cleanup removes the named +owned containers and attempt networks only, never uses prune, and records +whether each removal was confirmed. The coordinator must reclaim a stale +reservation only after confirming its recorded containers and networks are +gone. The selected port must be available. PR #428's crawl4ai recipe defaults to 3730, 3731, 3732 and 3734 (its README at 3ad8ba2); stop it or choose another port in the range before preparing this recipe. @@ -394,74 +477,337 @@ Sources: SDK@fcc102a `agent_server/docker/Dockerfile:7-9,301-305,343`; Git@v2.43.0 `Documentation/git.txt:708-724`, `diff-options.txt:830-840`. MCP allowlists are not network security. ai-memory HTTP and socraticode are -**disabled** because their host services would bypass the framework's tool -filter. context-mode, Serena, jCodeMunch and lexical QMD remain container-local; -headroom remains disabled. The host mount allowlist accepts only selected +**disabled**. Their host services would bypass the framework's tool filter, +and under O1 they are unreachable by design: the internal run network has no +route to host services, and the proxy forwards only three `/v1` gateway routes +([config/mcp-policy.json](config/mcp-policy.json)). The host template still +fixes `AI_MEMORY_URL` and `EMBED_URL` because the MCP template renders them. +Neither address is reachable from the internal network; P2 checks every host +listener. context-mode, Serena, jCodeMunch and lexical QMD remain +container-local; headroom remains disabled. The host mount allowlist accepts only selected adoption tool roots and the four document roots, resolves symlinks before checking, and rejects authentication stores and every ancestor of the host home. No tool can execute directly on the host through this registration. -Model launches require `rw-openhands-egress-control` or -`rw-openhands-egress-engines-on`. The supported policy mechanism is Docker's -**DOCKER-USER iptables chain**, with explicit destination/port rules before -Docker accepts forwarding. Host INPUT must also deny traffic from the worker -bridge; IPv6 is disabled. Only the selected gateway endpoint is allowed. Plain -`--internal` alone is insufficient because an internal bridge may still reach -host listeners. Source: docker/docs@4e9a5751518ed8223a8dcde53693badddd72604f -`content/manuals/engine/network/firewall-iptables.md:22-24,48-95` and -`port-publishing.md:186-192`. - -The coordinator must provision and **independently probe** that policy in the -rootless daemon's network namespace. The repair does not change daemon-wide -firewalls. `model_network` fails closed without a fresh, host-owned mode-0600 -policy receipt whose network ID matches Docker inspect and whose allowed TCP -endpoint is exactly the selected arm. The host template has per-arm receipt -paths. The receipt schema is: - -```json -{ - "mechanism": "docker-user-iptables", - "network_id": "", - "verified_at": "", - "allowed_tcp": ["10.0.2.2:20128"], - "default_deny": true, - "host_input_denied": true, - "ipv6_disabled": true, - "gateway_reachable": true, - "other_host_ports_denied": true -} -``` - -For engines-on only the endpoint changes to port 20129. This schema is an -operator evidence gate, not a firewall implementation or proof by itself. Keep -the actual rule dump and probe outputs privately; reject access to the other -arm, ai-memory, Qdrant, embedder and other worker listeners before any model run. - -Host loopback is already reachable from containers on the reference host, so -nothing needs to be switched on for the gateway. Its rootless `docker.service` -sets `DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false`, and the running -rootlesskit has no `--disable-host-loopback` flag (read-only checks, -2026-09-28). The earlier finding F19, that loopback access still had to be -enabled, is **overturned** for that host. The upstream default does disable it -(moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`, identical to -the installed 29.8.1 script), so a new host must set the variable to `false` -and repeat both checks. With that setting, a model container can address -every host loopback listener through 10.0.2.2 unless the egress policy above -blocks it; the missing piece is that limit, not loopback access. No packet probe -was run in this repair. Rule placement in the rootless network namespace, packet -behavior and post-restart policy persistence remain unverified. Official grader -containers retain upstream -options; the coordinator must separately qualify their host-service isolation -without misrepresenting altered test conditions as an unchanged environment. - Every authoritative status/window/verdict file is outside model mounts. Receipt creation reads nothing from the worker output or the server state directory; fields that would need them are `not_collected`. The only file the host reads back from worker output is the serialized request, copied with a bounded regular-file read (O_NOFOLLOW on every path component) before any model runs. The server-reported agent-limit classification can never produce a pass, -and nothing can cause `evidence_complete=true`. This round does not invent an -isolation mechanism unsupported by the pin. +and nothing can cause `evidence_complete=true`. The network isolation below is +composed locally from documented Docker network options and nginx directives; +no upstream ships it. Until the live P0-P2 probe has run on this host, it is a +design with offline tests, not host evidence. + +### O1 topology + +The phase-2 plan (section 1, item E1) replaces the earlier DOCKER-USER +firewall contract with a topology built per attempt. Every resource is named +from `S=-` and carries the owner label +`com.native-agent-stack.owner=gpt6-omniroute-framework-integration`. Below, +`D` is `docker --context rootless`: + +```sh +D network create --internal --ipv6=false -o com.docker.network.bridge.gateway_mode_ipv4=isolated --label $S-int +D network create --ipv6=false --label $S-gw +D run --detach --name $S-server --network=$S-int --env-file /secrets/$S.server.env ... # no --publish +D create --pull=never --name $S-proxy --platform linux/amd64 --label --network=$S-gw \ + --publish 127.0.0.1::8080 --read-only --tmpfs /tmp:rw,nosuid,nodev,mode=1777 --cap-drop=ALL \ + --security-opt=no-new-privileges --pids-limit 64 --memory 256m \ + --mount type=bind,src=/proxy/nginx.conf,dst=/etc/nginx/nginx.conf,readonly \ + --entrypoint nginx -g 'daemon off;' +D network connect --alias gw $S-int $S-proxy +D start $S-proxy +``` + +- **Server.** The agent-server joins `$S-int` only and publishes nothing. Its + hardening is unchanged: UID/GID 10001, all capabilities dropped, + no-new-privileges, a read-only root, a tmpfs home, and CPU, memory and PID + limits. Its only peer is the proxy, which it reaches as `gw`. +- **Order.** The proxy is created after the server starts, and connected to + `$S-int` before it starts, so nginx resolves `$S-server` when it loads. + A created container joins its configured networks when it runs, and + `network connect --alias` adds a network-scoped name (docs.docker.com + `container run` and `network connect`, fetched 2026-09-28). +- **Read-back.** `create_topology` reads each network back through a fixed + `--format` template. It requires the bridge driver, `Internal` true on + `$S-int` and false on `$S-gw`, `EnableIPv6` false, the owner label, a 64-hex + ID, and gateway mode `isolated` on `$S-int`. +- **Fail closed.** If `network create` rejects the isolated option, the + attempt fails with `attempt_int_network_create_failed`. It never retries + with plain `--internal`. +- **Why isolated.** docker/docs@4e9a5751518ed8223a8dcde53693badddd72604f + `content/manuals/engine/network/port-publishing.md:186-192` states the + problem and the remedy: "Mode `isolated` can only be used when the network is + also created with CLI flag `--internal`, or equivalent. An address is + normally assigned to the bridge device in an `internal` network. So, + processes on the Docker host can access the network, and containers in the + network can access host services listening on that bridge address (including + services listening on "any" host address, `0.0.0.0` or `::`). No address is + assigned to the bridge when the network is created with gateway mode + `isolated`." The options are listed at `:121-131`. How rootless Docker + 29.8.1 applies the mode has only been source-reviewed; P2 is the evidence. +- **Teardown.** The proxy, the server, `$S-int` and `$S-gw` are removed by + exact name, never by prune, with a confirmed-removal record for each. + Container logs are kept first; the proxy's access log records the denied + requests. + +Host loopback stays reachable from containers on the reference host, and O1 +relies on it: the proxy reaches the gateway through 10.0.2.2. The host's +rootless `docker.service` sets +`DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false`, and the running +rootlesskit has no `--disable-host-loopback` flag (read-only checks, +2026-09-28). The upstream default does disable it (moby/moby@a46e6fa7 +`contrib/dockerd-rootless.sh:23-24,170-173`, identical to the installed 29.8.1 +script), so a new host must set the variable to `false` and repeat both +checks. Per the phase-2 synthesis, any container on a normal bridge network, +the proxy included, can then address every host-loopback listener through +10.0.2.2. The internal network is what keeps the agent-server and its tools +away from them. A daemon-wide `--disable-host-loopback` was rejected. It would +cut off every container, the proxy included, and leave the host's +non-loopback addresses reachable +([decision record](../../../docs/decisions/2026-09-28-openhands-resolver-isolation.md)). + +Official grader containers keep their upstream options and are not on the +attempt networks. The coordinator must qualify their host-service isolation +separately, without presenting altered test conditions as an unchanged +environment. + +### Gateway proxy allowlist + +[config/proxy-nginx.conf](config/proxy-nginx.conf) is **composed locally** +from documented nginx directives. No upstream ships this file, and its tests +are our integration checks, not upstream acceptance. The closest upstream +reference for a `/v1`-only front is OmniRoute's split-port bridge allowlist +(`src/lib/apiBridgeServer.ts:16-24,198-207` at 045aa81f3). That reference is +cited as reviewed in the phase-2 synthesis; this build did not re-read it. The +template narrows the idea to three exact routes with fixed methods and fixed +upstream URIs: + +| Agent request to `gw:8081` | Forwarded as | +| --- | --- | +| POST `/v1/responses` | POST `http://10.0.2.2:/v1/responses` | +| POST `/v1/chat/completions` | POST `http://10.0.2.2:/v1/chat/completions` | +| GET or HEAD `/v1/models` | the same method to `http://10.0.2.2:/v1/models` | +| any other path or method, or any query string | 403 from the proxy | + +`` is 20128 on control and 20129 on engines-on; one proxy serves one +arm. + +- **Paths.** `location =` is an exact match that ends the search. Locations + are matched against the normalized URI, after decoding `%XX`, resolving `.` + and `..` and compressing adjacent slashes (nginx.org `ngx_http_core_module`, + `location`, fetched 2026-09-28). A `proxy_pass` that carries a URI replaces + the matching part of the normalized URI with its own (`ngx_http_proxy_module`, + `proxy_pass`). Each route therefore forwards one fixed upstream path, + whatever the request's raw form. Dot-segment and `%2e` forms that normalize + to `/api/*` fall through to the 403 fallback. +- **Methods.** Each route has one `limit_except` block with `deny all`. + "Allowing the GET method makes the HEAD method also allowed" + (`ngx_http_core_module`, `limit_except`). +- **Query strings.** A `proxy_pass` with a URI still appends the request's + arguments (nginx/nginx@release-1.30.5, 4556c714, + `src/http/modules/ngx_http_proxy_module.c:1240-1251,1365-1384`). The + server-level `if ($is_args) { return 403; }` refuses every query string. + `$is_args` is `?` exactly when arguments exist + (`src/http/ngx_http_variables.c:1609-1624`). A variable condition is false + only when its value is an empty string or "0" (nginx.org + `ngx_http_rewrite_module`, `if`, fetched 2026-09-28), so `?0` is refused too. +- **Headers.** The template sets `proxy_pass_request_headers off`; the + directive "Indicates whether the header fields of the original request are + passed to the proxied server" (`ngx_http_proxy_module`). Only the fields the + template sets reach the gateway: + - a fixed `Host`; + - `Content-Type`, `Accept` and `Idempotency-Key`, copied from the request; + - `Authorization: Bearer local-loopback`; + - `X-Correlation-Id` and `x-omniroute-session`, fixed to the run id; + - `x-omniroute-compression`, fixed to the arm's combo. + + "If the value of a header field is an empty string then this field will not + be passed" (`proxy_set_header`; source + `ngx_http_proxy_module.c:1301-1303,1417-1418`), so control's empty combo + sends no compression header. The agent cannot forge the usage join, choose + a compression plan, or send `X-Forwarded-For`, `X-Real-IP` or any other + `x-omniroute-*` control header. +- **Host side.** Port 8080 forwards every path to `$S-server:8000` and + refuses no arguments, because the driver's error-event search carries a + query string. Only the host reaches it, through `127.0.0.1:`. The agent + can reach its own server as `gw:8080`, which gives it nothing it lacks. +- **Rendering.** `render_proxy_config` fills `@PORT@`, `@SERVER@`, `@RUN@` and + `@COMPRESSION@` from `recipe.arm_config` and the attempt identity. It refuses: + - a selection that differs from `arm_config`'s own output; + - any value outside its pattern (`2012[89]`, the attempt's server name, the + run-id pattern, or a recorded combo or empty); + - a template whose placeholder set changed. + + The rendered file is host-owned at `runs///proxy/nginx.conf`, + outside every model mount. It is written with `O_EXCL|O_NOFOLLOW` at mode + 0644, because the image's non-root user reads the bind mount. Its SHA-256 + goes into `status.json` and the probe receipt, and both the probe and the + gate recompute it. +- **Image.** `pins.json` `gateway_proxy` is + `ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e`, + an OCI index (tag observed `1.30.5-alpine`), with linux/amd64 manifest + `sha256:f4522a5f…`, config `sha256:61640a44…` (user 101) and source + nginx/docker-nginx-unprivileged@588b4cbc. It was verified three ways: + - the SHA-256 of the registry's index, manifest and config bytes equals each + digest; + - `install` pulls it by digest; + - `pinned_image_identity` finds it in the containerd store. + + Proxy launches use `--pull=never` ([evidence](evidence/phase2-commands.json)). + The digest has not been vulnerability-scanned (plan gate G2). + +Proxy residuals: +- The proxy can reach every host-loopback port by design, so a compromised + nginx would have host-loopback reach. It runs a static, pinned, non-root, + read-only configuration. +- A `/v1` request body reaches the gateway unchanged and can select any model + the arm serves (plan gate G5, open). +- Three things are unprobed, and P3 and P5 cover them: `--entrypoint nginx` + under `--read-only`; body framing and streaming under the header allowlist; + and gateway paths the SDK might use beyond the three routes. + +### Isolation probe (P0-P2) and the dispatch gate + +The vehicle is [e2e/netprobe.py](e2e/netprobe.py), a locally composed +standard-library script. It runs in the pinned agent-server image with the +same hardening as every model container (`docker_args`), as `$S-probe-gw` on +`$S-gw` and `$S-probe-int` on `$S-int`. Its only mounts are the SDK venv and +the recipe (read-only) and a private output directory. It sends raw request +targets through `http.client`. It records only the status, the +`x-omniroute-route-class` value and errno names, never reads a response body, +and exits nonzero at the first unexpected result. The internal probe runs only +after P0 passed. + +| Probe | From | Passes when | +| --- | --- | --- | +| P0, negative control | `$S-gw` | GET `http://10.0.2.2:/v1/models` returns 200 with route class `CLIENT_API`, and GET `/api/settings` returns a response with route class `MANAGEMENT`. This shows the vehicle can see the gateway and its management surface. | +| P1, management isolation | `$S-int` | GET `http://gw:8081/v1/models` returns 200 with `CLIENT_API`. Each of the plan's 26 denied method/target pairs returns 403 with no route-class header, so the proxy refused it rather than the gateway. The pairs cover management paths, dot-segment and `%2e` variants, case and slash variants, query strings and wrong methods. | +| P2, direct reachability | `$S-int` | No address and port pair accepts a TCP connect. `example.com`, `github.com` and `host.docker.internal` do not resolve, while `gw` and `$S-server` do. The container has no non-loopback IPv6 address. | + +P2's targets are computed on the host at probe time: +- **Addresses:** + - 10.0.2.2, 10.0.2.3, 172.17.0.1, 10.0.0.1 and 10.255.255.254; + - every non-loopback host IPv4 address from `ip -4 -o addr`; + - each attempt network's subnet `.1` and IPAM gateway. +- **Ports:** every TCP listener from `ss -ltnH`, plus 53. + +The probe refuses to run if a target is the address of its own attempt's +server or proxy. Network inspect values are context only; the failed connects +are the evidence. + +The receipt is `runs///isolation-probe.json`: mode 0600, +host-owned and outside every model mount. It holds: +- mechanism `internal-isolated+nginx-v1-allowlist`, `run_id` and `arm`; +- `run_network_id`, `gw_network_id`, `server_container_id` and + `proxy_container_id`; +- `proxy_image`, `proxy_config_sha256` and `probe_script_sha256`; +- `upstream` (`10.0.2.2:`), `host_ingress` (`127.0.0.1:`) and + `verified_at`; +- the probe containers' exit codes, target counts, the P0/P1/P2 counts and + booleans, and `passed`. + +`dispatch.py start`, including the `host.py run` path, calls `verify_isolation` +before it takes the serial reservation. The gate requires: +- an owner-only regular file (0600, no symlink, outside the checkout); +- `verified_at` between 0 and 900 seconds old; +- the expected mechanism, identity, upstream, ingress and proxy image; +- the current rendered config's hash, which must also equal `status.json`'s, + and the current netprobe hash; +- passed P0, P1 and P2; +- the four IDs equal to the live selected-field reads. + +A refusal prints `failure_stage: probe`, exits 3 and changes nothing. The run +receipt (schema 6) carries an `isolation` summary without IDs, addresses or +the run id. + +**Plan gate G7 is only half enforced in code.** G7 requires P0-P2 within 900 s +of the start *and* a passed P3 on the running gateway build. The dispatch gate +checks P0-P2 only. Until the coordinator has run and recorded P3, do not use +`dispatch.py start` or `host.py run`. Prepare, read the probe receipt and tear +down instead. The engines-on arm also waits for P4 and P5. + +### P3-P5: documented, not run + +P3-P5 are live model calls. This build wrote them as the steps below and a +skeleton, `netprobe.p3_control_call`, which raises `NotImplementedError`. None +of them has run. + +- **P3, control arm.** One SDK LLM call with one tool definition, from a + throwaway container on `$S-int` with the recipe's LLM config, through `gw` to + 20128. The call also sends a forged `X-Correlation-Id`, an `X-Forwarded-For` + and one `x-omniroute-*` control header. A second call adds a query string; it + must get the proxy's 403 and leave no `call_logs` row. The first call's row + must show: + - `/v1/responses`, status 200 and model `cx/gpt-6-astra-max`; + - effort max, both requested and sent upstream; + - the run id as the correlation value, not the forged one. + + The streamed tool call must parse, body framing must work under the header + allowlist, and the usage must equal the row. Record the client peer the + gateway logged (F10). The proxy access log must show only that allowlisted + request. +- **P4, engines-on arm.** The same through a proxy bound to 20129, plus these + checks: + - a stacked lossy plan appears in the compression analytics; + - reasoning max appears in 20128's outbound request; + - which headers survive nginx and the 20129-to-20128 hop; + - whether `prompt_cache_key` is forwarded; + - the cached-input ratio against control. +- **P5, SDK compatibility.** Encrypted reasoning include, `prompt_cache_key` + and the tool schema. After one full short conversation, the proxy access log + must show no denied path, or each denied path must be triaged. LiteLLM paths + such as `/v1/responses/{id}` or `input_tokens` are unaudited. + +### Live probe sequence (coordinator; not run yet) + +**Prerequisites.** The probe runs only through the full preparation path: +clone, skills, QMD setup, server and proxy. It therefore needs: +- a successful `host.py install` of this revision, so both images are present + by digest; +- #429's skills manifest under `OPENHANDS_STACK_ROOT`, or preparation stops + at `skills_program_pending_pr`; +- the private inputs above, including a frozen task row and grader image pin; +- a free port. 3740 stays clear of crawl4ai's defaults. + +P0 sends two live GET requests to the selected gateway port (`/v1/models` and +`/api/settings`) by design. P1 sends one allowlisted GET `/v1/models` through +the proxy. + +```sh +export PATH="$HOME/.local/share/codex-ecosystem/tools/docker-rootless-29.8.1/bin:$PATH" +RECIPE="$PWD/blueprints/runtime-workers/openhands" +PREFIX="$HOME/.local/share/codex-ecosystem/tools/openhands-1.49.6" +STATE="$HOME/.local/state/native-agent-stack/runtime-workers/openhands" +OWNER=label=com.native-agent-stack.owner=gpt6-omniroute-framework-integration +export OPENHANDS_RUN_ID=rw-openhands-probe-001 OPENHANDS_ARM=control +# 1. Nothing owned is left over: both lists are empty. +docker --context rootless ps -a --filter "$OWNER" --format '{{.Names}}' +docker --context rootless network ls --filter "$OWNER" --format '{{.Name}}' +# 2. Topology, server and proxy, then P0-P2. Exit 0 only when the probe passed. +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" prepare --prefix "$PREFIX" --state "$STATE" \ + --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" --port 3740 +# 3. The verdict and counts only; the IDs stay private. +python3 -c 'import json, sys; r = json.load(open(sys.argv[1])); print(json.dumps({k: r.get(k) for k in ("passed", "exit_codes", "targets", "p0", "p1", "p2")}))' \ + "$STATE/runs/$OPENHANDS_RUN_ID/$OPENHANDS_ARM/isolation-probe.json" +# 4. Tear down without a conversation. Exit 0 only when all four removals are confirmed. +rtk env PYTHONDONTWRITEBYTECODE=1 python3 "$RECIPE/host.py" teardown --prefix "$PREFIX" --state "$STATE" \ + --run-id "$OPENHANDS_RUN_ID" --arm "$OPENHANDS_ARM" +# 5. Both lists are empty again, and neither key file exists. +docker --context rootless ps -a --filter "$OWNER" --format '{{.Names}}' +docker --context rootless network ls --filter "$OWNER" --format '{{.Name}}' +for kind in server.env headers; do test ! -e "$STATE/secrets/$OPENHANDS_RUN_ID-$OPENHANDS_ARM.$kind" && echo "$kind deleted"; done +``` + +A failed probe exits 3 from `prepare` and tears the attempt down itself. Step 3 +still reads its receipt if the probe got far enough to write one, and step 4 +retries any unconfirmed removal. +`host.py teardown` refuses, without any Docker call, while `status.json` shows +`starting`, `running` or `terminal`, or while the serial reservation names the +attempt. It turns a `prepared` status into `torn_down`, which both +`dispatch.py start` and `host.py probe` refuse. ## Official grading and remaining host gates @@ -487,11 +833,14 @@ before a worker verdict is accepted. `e2e/check.py` relays the actual report; an error/incomplete bucket is infrastructure, not a model-negative tally. Host work remaining: native installation and import acceptance; shared skills -PR; frozen task/image selection; rootless firewall proof and port availability; -native server health/auth/OpenAPI/preload startup; actual tool names/skills/MCP -use; response-header capture for streaming and condenser calls; entry-database -matching and max effort; official positive/negative grading controls; independent -trace observation; cancellation/cleanup recovery. No A/B or savings result is +PR; frozen task/image selection; the live P0-P2 probe on the O1 topology +([sequence](#live-probe-sequence-coordinator-not-run-yet)), then P3, and P4 +and P5 before any engines-on run; a vulnerability scan of both image digests +(plan gate G2); port availability; native server health/auth/OpenAPI/preload +startup through the proxy; actual tool names/skills/MCP use; response-header +capture for streaming and condenser calls; entry-database matching and max +effort; official positive/negative grading controls; independent trace +observation; cancellation/cleanup recovery. No A/B or savings result is claimed until these observations exist. [hello]: https://github.com/OpenHands/software-agent-sdk/blob/fcc102a697874d54a357e36004e02c95040dbdc0/examples/01_standalone_sdk/01_hello_world.py#L10-L29 diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index e4af38002..25058ef83 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -1013,6 +1013,56 @@ def refuse(stage, receipt=None): return refuse("probe", str(result / "receipt.json")) +def teardown_action(state, run_id, arm): + """`host.py teardown`: remove an attempt that will not start a conversation now. + + For example after a probe-only acceptance run, or to retry an unconfirmed + cleanup. It refuses, with no Docker call, while a conversation may be live + or uncollected (status starting, running or terminal, whose teardown + belongs to dispatch wait and result) or while the serial reservation names + this attempt. teardown_attempt keeps the proxy and server logs first (the + proxy access log holds the denied requests), removes the four resources by + exact name and deletes the key files after confirmed container removal. A + prepared status becomes "torn_down", which dispatch start and the probe + refuse. Exit 0 only when all four removals are confirmed. + """ + def report(outcome, code, **extra): + print(json.dumps({"run_id": run_id, "arm": arm, "teardown": outcome, **extra})) + return code + try: + attempt_stem(run_id, arm) + except ValueError: + return report("refused", 3, reason="owned_attempt_identity_required") + result = Path(state) / "runs" / run_id / arm + if not result.is_dir() or result.resolve() != result.absolute(): + return report("refused", 3, reason="owned_attempt_directory_required") + try: + status = json.loads(read_bounded(result / "status.json")) + except FileNotFoundError: + status = None + except (OSError, ValueError): + return report("refused", 3, reason="status_unreadable") + if status is not None and (not isinstance(status, dict) or status.get("status") in {"starting", "running", "terminal"}): + return report("refused", 3, reason="conversation_may_be_live") + try: + reservation = json.loads(read_bounded(Path(state) / "active-dispatch.json")) + except FileNotFoundError: + reservation = None + except (OSError, ValueError): + return report("refused", 3, reason="reservation_unreadable") + if reservation == {"run_id": run_id, "arm": arm}: + return report("refused", 3, reason="reservation_names_attempt") + try: + removed = teardown_attempt(state, run_id, arm) is True + except (Exception, KeyboardInterrupt): + removed = False # The cleanup records keep the uncertainty. + if status is not None and status.get("status") == "prepared": + status = {**status, "status": "torn_down"} + write_json(result / "status.json", status) + return report("confirmed" if removed else "unconfirmed", 0 if removed else 3, + status=None if status is None else status.get("status")) + + def clone_command(prefix, task, workspace): # SWE-bench@v4.1.0 test_spec/python.py:271-277, including branch exceptions. code = ("import json,sys; from importlib.metadata import version; " @@ -1271,7 +1321,7 @@ def run(prefix, state, *, run_id=None, arm=None, prepare_only=False, port=DEFAUL def build_parser(): parser = argparse.ArgumentParser() - parser.add_argument("action", choices=("install", "run", "prepare", "probe")) + parser.add_argument("action", choices=("install", "run", "prepare", "probe", "teardown")) parser.add_argument("--prefix", required=True, type=Path) parser.add_argument("--state", required=True, type=Path) parser.add_argument("--run-id") @@ -1293,6 +1343,10 @@ def main(): # An existing attempt only: no run id is ever generated for a probe. return probe_action(args.prefix, args.state, args.run_id or os.environ.get("OPENHANDS_RUN_ID"), args.arm or os.environ.get("OPENHANDS_ARM", "control")) + if args.action == "teardown": + # Likewise an existing attempt only; it needs no prefix or preflight. + return teardown_action(args.state, args.run_id or os.environ.get("OPENHANDS_RUN_ID"), + args.arm or os.environ.get("OPENHANDS_ARM", "control")) return run(args.prefix, args.state, run_id=args.run_id, arm=args.arm, prepare_only=args.action == "prepare", port=args.port) diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 5f9dac869..95c835a8e 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -1847,6 +1847,79 @@ def test_probe_action_failure_tears_down_and_records_the_probe_stage(self): self.assertEqual((current["status"], current["failure_stage"]), ("failed", "probe")) self.assertEqual(json.loads((self.result / "receipt.json").read_text())["failure_stage"], "probe") + def test_teardown_action_refuses_while_a_conversation_may_be_live(self): + host = self.prepared() + status = json.loads((self.result / "status.json").read_text()) + lock = self.state / "active-dispatch.json" + linked = self.state / "runs" / "rw-openhands-linked" + linked.mkdir() + (linked / self.arm).symlink_to(self.result, target_is_directory=True) + cases = [(self.run_id, json.dumps({**status, "status": value}), None) + for value in ("starting", "running", "terminal")] + cases += [(self.run_id, json.dumps(status), json.dumps({"run_id": self.run_id, "arm": self.arm})), + (self.run_id, json.dumps(status), "{not json"), + (self.run_id, "{not json", None), + (self.run_id, json.dumps(["prepared"]), None), + ("rw-openhands-absent", json.dumps(status), None), + ("rw-openhands-linked", json.dumps(status), None), + ("not-an-owned-run", json.dumps(status), None)] + for run_id, text, reservation in cases: + with self.subTest(run_id=run_id, status=text, reservation=reservation): + (self.result / "status.json").write_text(text) + lock.unlink(missing_ok=True) + if reservation is not None: + lock.write_text(reservation) + output = io.StringIO() + with patch.object(host, "teardown_attempt") as teardown, patch.object(host.subprocess, "run") as run, \ + patch.object(host.subprocess, "check_output") as check_output, contextlib.redirect_stdout(output): + self.assertEqual(host.teardown_action(self.state, run_id, self.arm), 3) + teardown.assert_not_called() + run.assert_not_called() + check_output.assert_not_called() + self.assertEqual((self.result / "status.json").read_text(), text) + self.assertEqual(json.loads(output.getvalue())["teardown"], "refused") + self.assertFalse((self.state / "runs" / "rw-openhands-absent").exists()) + + def test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status(self): + host = self.prepared() + status = json.loads((self.result / "status.json").read_text()) + dispatch = load_recipe_module("dispatch.py") + path = self.result / "status.json" + (self.result / "window.json").write_text(json.dumps({"run_id": self.run_id, "arm": self.arm})) + # Another attempt's serial reservation does not block this teardown. + (self.state / "active-dispatch.json").write_text(json.dumps({"run_id": "rw-openhands-other", "arm": self.arm})) + for label, before, outcome, code, after in ( + ("confirmed", status, {"return_value": True}, 0, "torn_down"), + ("unconfirmed", status, {"return_value": False}, 3, "torn_down"), + ("raised", status, {"side_effect": subprocess.SubprocessError()}, 3, "torn_down"), + ("failed attempt", {**status, "status": "failed", "failure_stage": "probe"}, + {"return_value": True}, 0, "failed"), + ("repeat", {**status, "status": "torn_down"}, {"return_value": True}, 0, "torn_down"), + ("no status", None, {"return_value": True}, 0, None)): + with self.subTest(label): + path.unlink(missing_ok=True) + if before is not None: + path.write_text(json.dumps(before)) + output = io.StringIO() + with patch.object(host, "teardown_attempt", **outcome) as teardown, contextlib.redirect_stdout(output): + self.assertEqual(host.teardown_action(self.state, self.run_id, self.arm), code) + teardown.assert_called_once_with(self.state, self.run_id, self.arm) + self.assertEqual(json.loads(output.getvalue())["teardown"], "confirmed" if code == 0 else "unconfirmed") + if after is None: + self.assertFalse(path.exists()) + continue + self.assertEqual(json.loads(path.read_text()), {**before, "status": after}) + # A retired attempt can neither start a conversation nor refresh its probe. + path.write_text(json.dumps({**status, "status": "torn_down"})) + with patch.object(dispatch, "verify_isolation") as gate, patch.object(host, "run_probe") as probe, \ + contextlib.redirect_stdout(io.StringIO()): + self.assertEqual(dispatch.execute("start", self.state, self.run_id, self.arm), 3) + self.assertEqual(host.probe_action(self.state / "prefix", self.state, self.run_id, self.arm), 3) + gate.assert_not_called() + probe.assert_not_called() + args = host.build_parser().parse_args(["teardown", "--prefix", "/p", "--state", "/s", "--run-id", self.run_id]) + self.assertEqual((args.action, args.run_id), ("teardown", self.run_id)) + class OpenHandsReceiptTests(unittest.TestCase): def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): From 6f1b5d60c2c936481c7484dfe21e3f341bb018cd Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 15:43:35 -0400 Subject: [PATCH 12/22] OpenHands phase 2 (4b/4): E5 decision record, research corrections, evidence record - docs/decisions/2026-09-28-openhands-resolver-isolation.md (E5): O1 against split-port, DOCKER-USER, O2, O3/O4, O5, O7 and the daemon-wide loopback switch, with overturn conditions; the agent-branch ruleset variants; the scoped narrowing of the 2026-09-25 host-request-lane clause for the resolver lane only; a pointer to the follow-up PR for the GitHub harness; the build's deviations from the plan. - research.md: takeover phase 2 corrections and open items. - evidence/phase2-commands.json: red, failed-attempt and green unit runs for all four slices, and the read-only host observations behind the nginx pin (registry byte hashes, pull by digest, image identity) and the selected-field network template; a test ties it to pins.json and scans it with the repository's private-content patterns. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../openhands/evidence/phase2-commands.json | 545 ++++++++++++++++++ .../runtime-workers/openhands/research.md | 48 ++ ...2026-09-28-openhands-resolver-isolation.md | 226 ++++++++ tests/test_runtime_worker_openhands.py | 25 + 4 files changed, 844 insertions(+) create mode 100644 blueprints/runtime-workers/openhands/evidence/phase2-commands.json create mode 100644 docs/decisions/2026-09-28-openhands-resolver-isolation.md diff --git a/blueprints/runtime-workers/openhands/evidence/phase2-commands.json b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json new file mode 100644 index 000000000..9f59fb76b --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json @@ -0,0 +1,545 @@ +{ + "evidence_class": "Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), plus read-only host observations: registry byte hashes of the pinned proxy image, one pull by digest, image identity checks, one selected-field inspect of the default bridge network, the credential inventory tests and the value-free credential status. No attempt network or container was created and no gateway or model request was made.", + "sanitization": "The worktree prefix is shown as , the scratch directory as , the unit TMPDIR as inside outputs, and the home directory as ~ or $HOME. The module prefix `tests.test_runtime_worker_openhands.` is dropped from test names. Red and failed-attempt outputs are excerpts: the summary lines, each non-ok test with its count of non-ok lines (subtests) when above one, and each distinct first exception line with its count, cut at 220 characters; tracebacks are omitted, and rerunning the command on the named tree reproduces them. Green outputs keep the summary lines. Host outputs keep only the lines this record cites; the network ID is truncated and other credential rows are omitted.", + "base": "efa73f40453911d3ac9583218530273677983053", + "commits": { + "slice_1": "90bd4a5ae953f91dfb089ff478dfb165d5cf88cb", + "slice_2": "0fbe40eed51f4d5b1f1e29bf8e8e75039a9b32f8", + "slice_3": "a144708b2f47034f11c9696579fd391f52a31ef1", + "slice_4a": "6cd13aa344870bab07ff8818a4f064cf9095c0ce", + "slice_4b": "the commit that adds this record" + }, + "decisions": "Coordinator amendment of 2026-09-28: build the phase-2 plan's section 1 (isolation design) and items E1, E2, E3 and E5; E4 (resolver mode) goes to a follow-up PR. Fail closed if network create rejects gateway mode isolated. P3-P5 are documented steps and a skeleton, not run. No push, rebase, manifests/evidence.json change, container, network or gateway request.", + "item_tests": { + "arm_contract_and_mcp": [ + "OpenHandsRecipeTests.test_gateway_model_and_container_topology", + "OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate", + "OpenHandsRecipeTests.test_environment_selection_maps_empty_compression_to_unset", + "OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter", + "OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture", + "OpenHandsRecipeTests.test_mcp_limits", + "OpenHandsReceiptTests.test_receipt_records_proxy_base_url_and_selected_compression_combo", + "OpenHandsReceiptTests.test_skills_and_mcp_require_observations" + ], + "E1_topology_and_proxy": [ + "OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_per_attempt", + "OpenHandsRecipeTests.test_host_template_has_no_firewall_receipt_contract", + "OpenHandsRecipeTests.test_immutable_artifacts", + "OpenHandsIsolationTests.test_topology_creates_an_isolated_internal_network_and_a_gateway_network", + "OpenHandsIsolationTests.test_topology_fails_closed_when_the_isolated_gateway_mode_is_rejected", + "OpenHandsIsolationTests.test_topology_refuses_networks_whose_inspect_differs", + "OpenHandsIsolationTests.test_proxy_template_is_the_three_route_v1_allowlist", + "OpenHandsIsolationTests.test_proxy_render_fills_each_placeholder_from_the_arm_selection", + "OpenHandsIsolationTests.test_proxy_render_refuses_values_outside_their_patterns", + "OpenHandsIsolationTests.test_rendered_proxy_config_is_host_owned_0644_outside_model_mounts", + "OpenHandsIsolationTests.test_proxy_launch_is_create_connect_start_with_loopback_ingress_only", + "OpenHandsIsolationTests.test_teardown_removes_proxy_then_server_then_networks_by_exact_name", + "OpenHandsReceiptTests.test_receipt_counts_network_removal_separately_from_containers", + "OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server", + "OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch", + "OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores", + "OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container" + ], + "E1_probe_and_gate": [ + "OpenHandsProbeTests.test_netprobe_lists_are_the_plan_lists_and_import_does_no_work", + "OpenHandsProbeTests.test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies", + "OpenHandsProbeTests.test_netprobe_connects_resolves_and_reads_ipv6_without_defaults", + "OpenHandsProbeTests.test_netprobe_stops_at_the_first_unexpected_result", + "OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused", + "OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks", + "OpenHandsProbeTests.test_probe_containers_use_the_agent_image_hardening_and_only_their_mounts", + "OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts", + "OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate", + "OpenHandsProbeTests.test_probe_refuses_targets_that_are_its_own_attempt_containers", + "OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks", + "OpenHandsProbeTests.test_probe_action_needs_an_explicit_prepared_attempt", + "OpenHandsProbeTests.test_probe_action_failure_tears_down_and_records_the_probe_stage", + "OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses", + "OpenHandsDispatchTests.test_start_requires_a_fresh_owner_only_probe_receipt", + "OpenHandsUpstreamAdapterTests.test_probe_failure_in_run_tears_down_once_and_exits_3", + "OpenHandsUpstreamAdapterTests.test_probe_pass_in_run_leaves_the_attempt_prepared" + ], + "E1_teardown_action": [ + "OpenHandsProbeTests.test_teardown_action_refuses_while_a_conversation_may_be_live", + "OpenHandsProbeTests.test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status" + ], + "E2_session_key": [ + "OpenHandsUpstreamAdapterTests.test_session_key_is_generated_per_attempt_and_never_returned", + "OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only", + "OpenHandsIsolationTests.test_teardown_keeps_key_files_while_removal_is_unconfirmed" + ], + "E3_method_pairing": [ + "OpenHandsDispatchTests.test_native_routes_pair_each_path_with_its_one_method", + "OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files" + ], + "E5_decision_record": [], + "evidence_record": [ + "OpenHandsRecipeTests.test_proxy_image_evidence_matches_the_pin_and_is_publishable" + ] + }, + "item_notes": { + "arm_contract_and_mcp": "Slice 1 (commands 01-02).", + "E1_topology_and_proxy": "Slice 2 (commands 03-05). The slice-2 red run stashed the new proxy template, the pins.json gateway_proxy entry and the host-template change, so part of its red comes from missing data (KeyError 'gateway_proxy') rather than missing code.", + "E1_probe_and_gate": "Slice 3 (commands 08-10).", + "E1_teardown_action": "Slice 4a (commands 11-12). Not a plan item: it removes a prepared attempt after a probe-only acceptance run without starting a conversation.", + "E2_session_key": "Slice 2. The inventory row is checked by commands 06-07; credential_status reports it missing because no attempt exists.", + "E3_method_pairing": "Slice 3.", + "E5_decision_record": "Documentation only: docs/decisions/2026-09-28-openhands-resolver-isolation.md, README.md and research.md.", + "evidence_record": "Slice 4b (commands 13-14): ties this record's registry and identity outputs to pins.json gateway_proxy and scans it with the repository's private-content patterns.", + "host_observations": "Commands 15-17 are read-only host observations on this machine on 2026-09-28. They are not a live probe of the topology, which has not run." + }, + "commands": [ + { + "id": "01-slice1-red", + "tree": "efa73f40 with the slice-1 test edits only; recipe files unchanged.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 75 tests in 0.875s", + "FAILED (failures=5, errors=7)" + ], + "non_ok_tests": [ + "OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server", + "OpenHandsReceiptTests.test_receipt_records_proxy_base_url_and_selected_compression_combo x3", + "OpenHandsReceiptTests.test_skills_and_mcp_require_observations", + "OpenHandsRecipeTests.test_environment_selection_maps_empty_compression_to_unset", + "OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate", + "OpenHandsRecipeTests.test_gateway_model_and_container_topology", + "OpenHandsRecipeTests.test_mcp_limits x2", + "OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter", + "OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture" + ], + "first_exception_lines": [ + "ERROR OpenHandsReceiptTests.test_receipt_records_proxy_base_url_and_selected_compression_combo => ValueError: base_url_must_match_arm (x3)", + "ERROR OpenHandsRecipeTests.test_environment_selection_maps_empty_compression_to_unset => AttributeError: module 'openhands_recipe_test_recipe' has no attribute 'environment_selection'", + "ERROR OpenHandsRecipeTests.test_explicit_arms_keep_one_slash_routes_and_compression_separate => AttributeError: module 'openhands_recipe_test_recipe' has no attribute 'COMPRESSION_COMBOS'", + "ERROR OpenHandsRecipeTests.test_mcp_limits => KeyError: 'enabled' (x2)", + "FAIL OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server => AssertionError: 'OPENHANDS_BASE_URL=http://gw:8081/v1' not found in ['docker', '--context', 'rootless', 'run', '--rm', '--pull=never', '--name', 'rw-openhands-fixture-engines-on-request', '--platform', 'linux/amd64', '--", + "FAIL OpenHandsReceiptTests.test_skills_and_mcp_require_observations => AssertionError: {'jcodemunch_order': 1, 'ai-memory_memory_query': 1} != {'jcodemunch_order': 1}", + "FAIL OpenHandsRecipeTests.test_gateway_model_and_container_topology => AssertionError: 'http://127.0.0.1:20128/v1' != 'http://gw:8081/v1'", + "FAIL OpenHandsRecipeTests.test_runtime_model_override_and_native_tool_filter => AssertionError: 'http://10.0.2.2:20128/v1' != 'http://gw:8081/v1'", + "FAIL OpenHandsRecipeTests.test_worker_arm_headers_and_response_correlation_capture => AssertionError: 'http://10.0.2.2:20128/v1' != 'http://gw:8081/v1'" + ] + }, + { + "id": "02-slice1-green", + "tree": "Commit 90bd4a5a.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "summary": [ + "Ran 75 tests in 0.877s", + "OK" + ] + }, + { + "id": "03-slice2-red", + "tree": "90bd4a5a with the slice-2 test edits only; the new proxy template, pins.json entry and host-template change were stashed for this run and no code edits existed yet.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 85 tests in 0.870s", + "FAILED (failures=2, errors=90)" + ], + "non_ok_tests": [ + "OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server", + "OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt", + "OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch", + "OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload", + "OpenHandsIsolationTests.test_proxy_launch_is_create_connect_start_with_loopback_ingress_only", + "OpenHandsIsolationTests.test_proxy_render_fills_each_placeholder_from_the_arm_selection x3", + "OpenHandsIsolationTests.test_proxy_render_refuses_values_outside_their_patterns x17", + "OpenHandsIsolationTests.test_proxy_template_is_the_three_route_v1_allowlist", + "OpenHandsIsolationTests.test_rendered_proxy_config_is_host_owned_0644_outside_model_mounts", + "OpenHandsIsolationTests.test_teardown_keeps_key_files_while_removal_is_unconfirmed", + "OpenHandsIsolationTests.test_teardown_removes_proxy_then_server_then_networks_by_exact_name", + "OpenHandsIsolationTests.test_topology_creates_an_isolated_internal_network_and_a_gateway_network", + "OpenHandsIsolationTests.test_topology_fails_closed_when_the_isolated_gateway_mode_is_rejected", + "OpenHandsIsolationTests.test_topology_refuses_networks_whose_inspect_differs x11", + "OpenHandsReceiptTests.test_receipt_counts_network_removal_separately_from_containers", + "OpenHandsRecipeTests.test_host_template_has_no_firewall_receipt_contract", + "OpenHandsRecipeTests.test_immutable_artifacts", + "OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_per_attempt x4", + "OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores", + "OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock", + "OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container x30", + "OpenHandsUpstreamAdapterTests.test_session_key_is_generated_per_attempt_and_never_returned", + "OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only x10" + ], + "first_exception_lines": [ + "ERROR OpenHandsDispatchTests.test_prepare_serializes_body_offline_then_launches_native_server => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'create_topology'", + "ERROR OpenHandsDispatchTests.test_request_serialization_failure_cannot_leave_prepared_receipt => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'create_topology'", + "ERROR OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'create_topology'", + "ERROR OpenHandsDispatchTests.test_server_preserves_native_entrypoint_and_uses_supported_preload => ValueError: explicit_scoped_model_network_required", + "ERROR OpenHandsIsolationTests.test_proxy_launch_is_create_connect_start_with_loopback_ingress_only => AttributeError: module 'openhands_recipe_test_host' has no attribute 'proxy_commands'. Did you mean: 'clone_command'?", + "ERROR OpenHandsIsolationTests.test_proxy_render_fills_each_placeholder_from_the_arm_selection => AttributeError: module 'openhands_recipe_test_host' has no attribute 'render_proxy_config' (x3)", + "ERROR OpenHandsIsolationTests.test_proxy_render_refuses_values_outside_their_patterns => AttributeError: module 'openhands_recipe_test_host' has no attribute 'render_proxy_config' (x16)", + "ERROR OpenHandsIsolationTests.test_proxy_render_refuses_values_outside_their_patterns => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/config/proxy-nginx.conf'", + "ERROR OpenHandsIsolationTests.test_proxy_template_is_the_three_route_v1_allowlist => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/config/proxy-nginx.conf'", + "ERROR OpenHandsIsolationTests.test_rendered_proxy_config_is_host_owned_0644_outside_model_mounts => AttributeError: module 'openhands_recipe_test_host' has no attribute 'write_proxy_config'", + "ERROR OpenHandsIsolationTests.test_teardown_keeps_key_files_while_removal_is_unconfirmed => AttributeError: module 'openhands_recipe_test_host' has no attribute 'generate_session_files'", + "ERROR OpenHandsIsolationTests.test_teardown_removes_proxy_then_server_then_networks_by_exact_name => AttributeError: module 'openhands_recipe_test_host' has no attribute 'generate_session_files'", + "ERROR OpenHandsIsolationTests.test_topology_creates_an_isolated_internal_network_and_a_gateway_network => AttributeError: module 'openhands_recipe_test_host' has no attribute 'create_topology'", + "ERROR OpenHandsIsolationTests.test_topology_fails_closed_when_the_isolated_gateway_mode_is_rejected => AttributeError: module 'openhands_recipe_test_host' has no attribute 'create_topology'", + "ERROR OpenHandsIsolationTests.test_topology_refuses_networks_whose_inspect_differs => AttributeError: module 'openhands_recipe_test_host' has no attribute 'create_topology' (x11)", + "ERROR OpenHandsReceiptTests.test_receipt_counts_network_removal_separately_from_containers => KeyError: 'network_cleanup'", + "ERROR OpenHandsRecipeTests.test_immutable_artifacts => KeyError: 'gateway_proxy'", + "ERROR OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_per_attempt => ValueError: explicit_scoped_model_network_required (x3)", + "ERROR OpenHandsUpstreamAdapterTests.test_install_accepts_the_pinned_image_on_containerd_and_classic_stores => KeyError: 'gateway_proxy'", + "ERROR OpenHandsUpstreamAdapterTests.test_install_actually_uses_narrow_mounts_and_grader_lock => KeyError: 'gateway_proxy'", + "ERROR OpenHandsUpstreamAdapterTests.test_install_refuses_each_image_identity_mismatch_before_any_container => KeyError: 'gateway_proxy' (x30)", + "ERROR OpenHandsUpstreamAdapterTests.test_session_key_is_generated_per_attempt_and_never_returned => AttributeError: module 'openhands_recipe_test_host' has no attribute 'generate_session_files'", + "ERROR OpenHandsUpstreamAdapterTests.test_session_key_preflight_checks_the_variable_name_only => TypeError: check_server_env() takes 0 positional arguments but 1 was given (x10)", + "FAIL OpenHandsRecipeTests.test_host_template_has_no_firewall_receipt_contract => AssertionError: 'egress' unexpectedly found in {'variables': {'ECO_ROOT': '', 'HOST_PATH': '', 'AI_MEMORY_URL': '10.0.2.2:49", + "FAIL OpenHandsRecipeTests.test_model_container_is_nonroot_and_network_is_per_attempt => AssertionError: ValueError not raised" + ] + }, + { + "id": "04-slice2-failed-attempt", + "tree": "Slice-2 code before one error string was renamed.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 1, + "summary": [ + "Ran 85 tests in 0.997s", + "FAILED (failures=1)" + ], + "non_ok_tests": [ + "OpenHandsRecipeTests.test_host_template_has_no_firewall_receipt_contract" + ], + "first_exception_lines": [ + "FAIL OpenHandsRecipeTests.test_host_template_has_no_firewall_receipt_contract => AssertionError: 'model_network' unexpectedly found in '\"\"\"Owned lifecycle for the upstream SDK-in-image recipe; no work on import.\\n\\nHost actions: frozen task checkout/patch transport, supported upstream grader\\ninstall" + ], + "note": "The phase-1 error string explicit_scoped_model_network_required contained the retired name model_network, which a new test forbids in host.py; it became attempt_scoped_network_required." + }, + { + "id": "05-slice2-green", + "tree": "Slice-2 recipe and tests as committed in 0fbe40ee; the two inventory files were edited afterwards and are checked by command 06.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "summary": [ + "Ran 85 tests in 0.983s", + "OK" + ] + }, + { + "id": "06-inventory-tests", + "tree": "Commit 0fbe40ee.", + "command": "python3 -m json.tool adoption/credential-inventory.json > /dev/null && echo json-ok && TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_secret_path_guard tests.test_credential_status tests.test_host_requests tests.test_codex_worker_lane", + "exit_code": 0, + "summary": [ + "json-ok", + "Ran 153 tests in 27.969s", + "OK (skipped=9)" + ], + "note": "The unittest output also carried one ResourceWarning from a still-running subprocess; omitted." + }, + { + "id": "07-credential-status", + "tree": "Commit 0fbe40ee.", + "command": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/credential_status.py --root .", + "exit_code": 0, + "output_excerpt": [ + "missing openhands-session generated_local $HOME/.local/state/native-agent-stack/runtime-workers/openhands/secrets/-.server.env findings=-" + ], + "note": "Value-free. Only this recipe's row is kept; missing is expected while no attempt exists." + }, + { + "id": "08-slice3-red", + "tree": "0fbe40ee with the slice-3 test edits only; e2e/netprobe.py and the code changes did not exist yet.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 104 tests in 1.200s", + "FAILED (failures=16, errors=43)" + ], + "non_ok_tests": [ + "OpenHandsDispatchTests.test_native_routes_pair_each_path_with_its_one_method x11", + "OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch", + "OpenHandsDispatchTests.test_start_requires_a_fresh_owner_only_probe_receipt x3", + "OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files", + "OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate x10", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts", + "OpenHandsProbeTests.test_netprobe_connects_resolves_and_reads_ipv6_without_defaults", + "OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused", + "OpenHandsProbeTests.test_netprobe_lists_are_the_plan_lists_and_import_does_no_work", + "OpenHandsProbeTests.test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies", + "OpenHandsProbeTests.test_netprobe_stops_at_the_first_unexpected_result", + "OpenHandsProbeTests.test_probe_action_failure_tears_down_and_records_the_probe_stage x3", + "OpenHandsProbeTests.test_probe_action_needs_an_explicit_prepared_attempt", + "OpenHandsProbeTests.test_probe_containers_use_the_agent_image_hardening_and_only_their_mounts x5", + "OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate", + "OpenHandsProbeTests.test_probe_refuses_targets_that_are_its_own_attempt_containers", + "OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks x9", + "OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks", + "OpenHandsReceiptTests.test_receipt_records_proxy_base_url_and_selected_compression_combo x3", + "OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses", + "OpenHandsUpstreamAdapterTests.test_probe_failure_in_run_tears_down_once_and_exits_3", + "OpenHandsUpstreamAdapterTests.test_probe_pass_in_run_leaves_the_attempt_prepared" + ], + "first_exception_lines": [ + "ERROR OpenHandsDispatchTests.test_selected_port_flows_from_prepare_to_dispatch => AttributeError: /blueprints/runtime-workers/openhands/dispatch.py'> does not have the attribute 'verify_isolation'", + "ERROR OpenHandsDispatchTests.test_start_requires_a_fresh_owner_only_probe_receipt => AttributeError: module 'openhands_recipe_test_host' has no attribute 'write_private_json' (x2)", + "ERROR OpenHandsDispatchTests.test_start_wait_result_use_native_routes_and_deterministic_host_files => AttributeError: /blueprints/runtime-workers/openhands/dispatch.py'> does not have the attribute 'verify_isolation'", + "ERROR OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py' (x10)", + "ERROR OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_netprobe_connects_resolves_and_reads_ipv6_without_defaults => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_netprobe_lists_are_the_plan_lists_and_import_does_no_work => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_netprobe_stops_at_the_first_unexpected_result => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_probe_action_failure_tears_down_and_records_the_probe_stage => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'run_probe' (x3)", + "ERROR OpenHandsProbeTests.test_probe_action_needs_an_explicit_prepared_attempt => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'run_probe'", + "ERROR OpenHandsProbeTests.test_probe_containers_use_the_agent_image_hardening_and_only_their_mounts => AttributeError: module 'openhands_recipe_test_host' has no attribute 'probe_command'. Did you mean: 'clone_command'? (x5)", + "ERROR OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_probe_refuses_targets_that_are_its_own_attempt_containers => AttributeError: module 'openhands_recipe_test_host' has no attribute 'run_probe'", + "ERROR OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks => AttributeError: module 'openhands_recipe_test_host' has no attribute 'run_probe' (x8)", + "ERROR OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/e2e/netprobe.py'", + "ERROR OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks => AttributeError: module 'openhands_recipe_test_host' has no attribute 'listener_ports'", + "ERROR OpenHandsUpstreamAdapterTests.test_probe_failure_in_run_tears_down_once_and_exits_3 => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'run_probe'", + "ERROR OpenHandsUpstreamAdapterTests.test_probe_pass_in_run_leaves_the_attempt_prepared => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribute 'run_probe'", + "FAIL OpenHandsDispatchTests.test_native_routes_pair_each_path_with_its_one_method => AssertionError: ValueError not raised (x10)", + "FAIL OpenHandsDispatchTests.test_native_routes_pair_each_path_with_its_one_method => Calls: [call('http://127.0.0.1:3740/api/conversations', method='GET', headers=PosixPath('/tmpznbir4t6/runs/rw-openhands-fixture/engines-on/headers'), body=None),", + "FAIL OpenHandsDispatchTests.test_start_requires_a_fresh_owner_only_probe_receipt => Calls: [call('POST', '/api/conversations', body=PosixPath('/tmp9z8wte10/runs/rw-openhands-fixture/engines-on/start.json'), port=3740, headers=PosixPath('/tmp9z8wte10/secrets/rw-openhands-fixture-engines-o", + "FAIL OpenHandsReceiptTests.test_receipt_records_proxy_base_url_and_selected_compression_combo => AssertionError: 5 != 6 (x3)", + "FAIL OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses => AssertionError: None != 'probe'" + ] + }, + { + "id": "09-slice3-failed-attempt", + "tree": "Slice-3 code with a fixture bug in a new test.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 104 tests in 1.977s", + "FAILED (failures=1)" + ], + "non_ok_tests": [ + "OpenHandsProbeTests.test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies" + ], + "first_exception_lines": [ + "FAIL OpenHandsProbeTests.test_netprobe_sends_raw_targets_verbatim_and_never_records_bodies => AssertionError: False is not true" + ], + "note": "Test bug, not a code defect: the fixture's reply map answered GET /api/settings, which the same test also sends as a denied target, with 200 and route class MANAGEMENT. The MANAGEMENT case moved to its own target, /management-class." + }, + { + "id": "10-slice3-green", + "tree": "Commit a144708b.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "summary": [ + "Ran 104 tests in 1.940s", + "OK" + ] + }, + { + "id": "11-slice4a-red", + "tree": "a144708b with the two teardown-action tests only.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 1, + "summary": [ + "Ran 106 tests in 2.082s", + "FAILED (errors=17)" + ], + "non_ok_tests": [ + "OpenHandsProbeTests.test_teardown_action_refuses_while_a_conversation_may_be_live x10", + "OpenHandsProbeTests.test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status x7" + ], + "first_exception_lines": [ + "ERROR OpenHandsProbeTests.test_teardown_action_refuses_while_a_conversation_may_be_live => AttributeError: module 'openhands_recipe_test_host' has no attribute 'teardown_action'. Did you mean: 'teardown_attempt'? (x10)", + "ERROR OpenHandsProbeTests.test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status => AttributeError: module 'openhands_recipe_test_host' has no attribute 'teardown_action'. Did you mean: 'teardown_attempt'? (x6)", + "ERROR OpenHandsProbeTests.test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status => SystemExit: 2" + ] + }, + { + "id": "12-slice4a-green", + "tree": "Commit 6cd13aa3 (host.py teardown_action; README not yet rewritten).", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": [ + "Ran 106 tests in 2.081s", + "OK" + ] + }, + { + "id": "13-slice4b-red", + "tree": "6cd13aa3 with the evidence-record test only; this record did not exist yet.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 1, + "summary": [ + "Ran 107 tests in 2.030s", + "FAILED (errors=1)" + ], + "non_ok_tests": [ + "OpenHandsRecipeTests.test_proxy_image_evidence_matches_the_pin_and_is_publishable" + ], + "first_exception_lines": [ + "ERROR OpenHandsRecipeTests.test_proxy_image_evidence_matches_the_pin_and_is_publishable => FileNotFoundError: [Errno 2] No such file or directory: '/blueprints/runtime-workers/openhands/evidence/phase2-commands.json'" + ] + }, + { + "id": "13b-slice4b-failed-attempt", + "tree": "Slice-4b files with this record present, before one misplaced line was moved back.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": [ + "Ran 107 tests in 2.054s", + "FAILED (errors=1)" + ], + "non_ok_tests": [ + "OpenHandsRecipeTests.test_proxy_image_evidence_matches_the_pin_and_is_publishable" + ], + "first_exception_lines": [ + "ERROR OpenHandsRecipeTests.test_proxy_image_evidence_matches_the_pin_and_is_publishable => NameError: name 'name' is not defined" + ], + "note": "Insertion error: the new test was inserted one line too early, so it inherited the grype receipt test's last line (a README check on that test's own variable). The line went back to the grype test, and the new test gained its own README and research.md link check. Command 13 ran with the same misplacement, so the grype test ran without that one assertion there." + }, + { + "id": "14-final-green", + "tree": "Final tree: 6cd13aa3 plus the slice-4b changes (research.md section, the decision record, this record and its test), before this entry and entries 19-20 were filled in.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "summary": [ + "Ran 107 tests in 2.077s", + "OK" + ], + "note": "107 lines ending in ok. After this entry and entries 13b, 19 and 20 were filled in, the same command ran again on the committed tree before the commit; its summary is in the coordinator report, because recording it here would change the file it checks." + }, + { + "id": "15-registry-bytes", + "tree": "Not applicable (registry read).", + "command": "python3 -B /registry_verify.py ghcr.io nginx/nginx-unprivileged sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e --layers", + "exit_code": 0, + "output_excerpt": { + "top_media_type": "application/vnd.oci.image.index.v1+json", + "top_sha256_of_bytes": "sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e", + "top_docker_content_digest": "sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e", + "top_bytes_match_reference": true, + "amd64_manifest": { + "digest": "sha256:f4522a5f23f9caebcae9be6e3940ea0599a2b9eb05222c7118d01fc887e848d6", + "sha256_of_bytes": "sha256:f4522a5f23f9caebcae9be6e3940ea0599a2b9eb05222c7118d01fc887e848d6", + "bytes_match": true, + "media_type": "application/vnd.oci.image.manifest.v1+json" + }, + "config": { + "digest": "sha256:61640a44f99dcfd7f1af9f144f023195e82fdefd2f5aba7f917548867d4f3a6f", + "sha256_of_bytes": "sha256:61640a44f99dcfd7f1af9f144f023195e82fdefd2f5aba7f917548867d4f3a6f", + "bytes_match": true, + "os_arch": "linux/amd64", + "User": "101", + "Entrypoint": [ + "/docker-entrypoint.sh" + ], + "Cmd": [ + "nginx", + "-g", + "daemon off;" + ], + "ExposedPorts": [ + "8080/tcp" + ], + "version_env": [ + "NGINX_VERSION=1.30.5" + ], + "labels": { + "org.opencontainers.image.created": "2026-09-28T01:38:21.026Z", + "org.opencontainers.image.revision": "588b4cbc88065a313909f47a02510639e01cc508", + "org.opencontainers.image.source": "https://github.com/nginx/docker-nginx-unprivileged", + "org.opencontainers.image.version": "1.30.5-alpine" + } + }, + "files_present": { + "bin/busybox": "file", + "bin/sh": "symlink->/bin/busybox", + "usr/bin/getent": "file", + "usr/sbin/nginx": "file", + "usr/bin/curl": "file" + } + }, + "note": "registry_verify.py is a local standard-library helper, not committed, written because no skopeo, crane or buildx was installed. It takes an anonymous pull token, fetches the index, the linux/amd64 manifest and the config, and compares the SHA-256 of the returned bytes with each digest (the OCI content-addressing rule); --layers lists a few files from the layers. Commands 16-17 are the native evidence." + }, + { + "id": "16-bridge-template", + "tree": "host.py NETWORK_FORMAT as committed in 0fbe40ee.", + "command": "D=\"$HOME/.local/share/codex-ecosystem/tools/docker-rootless-29.8.1/bin/docker\"; \"$D\" --context rootless version --format '{{.Server.Version}} {{.Client.Version}}'; \"$D\" --context rootless network inspect --format '' bridge | ; echo \"rc=$?\"; \"$D\" --context rootless network ls --format '{{.Name}}' | wc -l", + "exit_code": 0, + "output": [ + "29.8.1 29.8.1", + "{\"id\": \"fedad086b6d4…\", \"name\": \"bridge\", \"driver\": \"bridge\", \"internal\": false, \"ipv6\": false, \"options\": {\"com.docker.network.bridge.default_bridge\": \"true\", \"com.docker.network.bridge.enable_icc\": \"true\", \"com.docker.network.bridge.enable_ip_masquerade\": \"true\", \"com.docker.network.bridge.host_binding_ipv4\": \"127.0.0.1\", \"com.docker.network.bridge.name\": \"docker0\", \"com.docker.network.driver.mtu\": \"1500\"}, \"labels\": {}, \"ipam\": [{\"Gateway\": \"172.17.0.1\", \"Subnet\": \"172.17.0.0/16\"}]}", + "rc=0", + "3" + ], + "note": "Read-only: shows the selected-field template renders on this daemon. CONTAINER_FORMAT has no live check yet." + }, + { + "id": "17-pull-and-identity", + "tree": "host.py pinned_image_identity as committed in 0fbe40ee.", + "command": "export PATH=\"$HOME/.local/share/codex-ecosystem/tools/docker-rootless-29.8.1/bin:$PATH\"; docker --context rootless pull --platform linux/amd64 ; python3 -c ''; docker --context rootless ps -aq | wc -l", + "exit_code": 0, + "output": [ + "pull rc=0", + "Digest: sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e", + "Status: Image is up to date for ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e", + "gateway_proxy store=containerd", + "image store=containerd", + "1" + ], + "note": "The container count of 1 is a container this work did not create. The pull ran no container and created no network." + }, + { + "id": "18-diff-check", + "command": "git diff --check", + "exit_code": 0, + "output": "", + "note": "Run before each slice commit." + }, + { + "id": "19-validate", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 scripts/validate.py", + "exit_code": 1, + "output_excerpt": { + "first_line": "Publication validation failed:", + "sha256_and_byte_count_mismatch": [ + "adoption/credential-inventory.json", + "blueprints/runtime-workers/openhands/README.md", + "blueprints/runtime-workers/openhands/config/host.example.json", + "blueprints/runtime-workers/openhands/config/mcp-policy.json", + "blueprints/runtime-workers/openhands/config/worker.json", + "blueprints/runtime-workers/openhands/dispatch.py", + "blueprints/runtime-workers/openhands/host.py", + "blueprints/runtime-workers/openhands/pins.json", + "blueprints/runtime-workers/openhands/receipt.py", + "blueprints/runtime-workers/openhands/recipe.py", + "blueprints/runtime-workers/openhands/research.md", + "blueprints/runtime-workers/openhands/worker.py", + "docs/secret-storage.md", + "tests/test_runtime_worker_openhands.py" + ], + "file_missing_by_index": [ + "files[1024]", + "files[1029]", + "files[1030]", + "files[1031]", + "files[1032]", + "files[1034]" + ], + "file_missing_by_path": [ + "blueprints/runtime-workers/openhands/config/skills.lock.json", + "blueprints/runtime-workers/openhands/e2e/fixture-repo/README.md", + "blueprints/runtime-workers/openhands/e2e/fixture-repo/range_utils.py", + "blueprints/runtime-workers/openhands/e2e/fixture-repo/tests/test_ranges.py", + "blueprints/runtime-workers/openhands/e2e/frozen.json", + "blueprints/runtime-workers/openhands/e2e/task.txt" + ], + "other_lines": 0 + }, + "note": "Expected until the coordinator re-registers manifests/evidence.json, which this worker must not touch. validate() collects every error, including the credential-inventory check and the publication scan of registered files, before it fails. The only errors are the SHA-256 and byte-count pairs for the 14 registered files this takeover changed, and the index and path entries for the six phase-1 orphans deleted in phase 1. New unregistered files are outside that scan; command 20 scans them." + }, + { + "id": "20-private-content-scan", + "command": "python3 scripts/validate.py --scan-file ; then the same scan of a planted file containing a home-directory path", + "exit_code": 0, + "output": "{\"scanned_files\": 18, \"status\": \"passed\"}", + "note": "The planted negative control exited 1 with a personal-home-path finding, so the scan detects what it claims to. The changed files were also checked for the host user name: no matches." + } + ] +} diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index f65b40161..b2ca8c2f7 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -392,3 +392,51 @@ Open finding from this phase, not changed here: - The module-qualified event kind string is source-reviewed only. If the live server used a different kind, the agent-limit check would fail closed to exit 3. + +## Takeover phase 2 corrections (2026-09-28) + +This phase is an offline build on efa73f40 following the coordinator's +phase-2 plan: section 1 (isolation design) and items E1, E2, E3 and E5. Item +E4, resolver mode, goes to a follow-up PR. The phase ran unit tests with Docker +mocked plus read-only host observations. It created no attempt network or +container and made no gateway or model request. +- Commands and outputs: + [evidence/phase2-commands.json](evidence/phase2-commands.json). +- The choice and its alternatives: + [decision record](../../../docs/decisions/2026-09-28-openhands-resolver-isolation.md). +- Full detail: README.md, [Security posture](README.md#security-posture). + +| Finding | Correction and source | +| --- | --- | +| The phase-1 contract was a DOCKER-USER rule plus an operator receipt (R12 and the host-loopback row in the round-2 log above). An L4 rule to the gateway port cannot separate `/v1/*` from `/api/*` on the same port, and its placement in the rootlesskit namespace was never probed. | **Replaced by O1 (E1).** `$S-int` is created with `--internal`, IPv6 off and gateway mode `isolated`; `$S-gw` is a normal bridge; a pinned proxy joins both. The same docker/docs@4e9a5751 page that R12 cites for the caveat also gives the remedy: `port-publishing.md:186-192` ("No address is assigned to the bridge when the network is created with gateway mode `isolated`"), with the options at `:121-131`. If the option is rejected, the attempt fails closed with no retry on plain `--internal`. Networks are read back through a fixed `--format` template. R12's DOCKER-USER mechanism is superseded; the row stays as history. | +| F19 (host loopback), overturned for this host in phase 1. | No longer a design input. O1 relies on host loopback for the proxy's upstream and keeps the agent-server off it through the internal network. | +| Each arm's base URL pointed at its gateway port directly. | **Both arms now use `http://gw:8081/v1`.** The proxy's upstream is `10.0.2.2:20128` for control or `10.0.2.2:20129` for engines-on, taken from `recipe.arm_config`. Engines-on accepts only the seven recorded combos (`allow-lossy`, the default, plus `fw-ccr`, `fw-codex-responses`, `fw-headroom`, `fw-lite`, `fw-rtk` and `fw-session-dedup`); control sends no compression header. Control stays the default arm until the #431 A/B. | +| New: the gateway proxy. | **Composed locally; no upstream ships it.** Directive sources are nginx.org `ngx_http_core_module` (`location` matching on the normalized URI, `=` exact match, `limit_except`, where GET also allows HEAD), `ngx_http_proxy_module` (`proxy_pass` with a URI, `proxy_pass_request_headers`, and `proxy_set_header`, where an empty value is not passed) and `ngx_http_rewrite_module` (`if`, false only for "" or "0"), all fetched 2026-09-28. Source lines are from nginx/nginx@release-1.30.5 (4556c714): `ngx_http_proxy_module.c:1240-1251,1365-1384` shows arguments are appended even to a fixed URI, hence `if ($is_args) { return 403; }`; `:1301-1303,1417-1418` shows empty headers are skipped; `ngx_http_variables.c:1609-1624` defines `$is_args`. | +| The proxy image needed a pin. | `ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1f…`, tag observed `1.30.5-alpine`. The SHA-256 of the registry's index, linux/amd64 manifest (`f4522a5f…`) and config (`61640a44…`, user 101, revision 588b4cbc) bytes equals each digest. A pull by index digest reported "Image is up to date", and `pinned_image_identity` returned the containerd store. The digest has not been vulnerability-scanned (plan gate G2). | +| The session key came from a coordinator-supplied file (E2). | **Generated per attempt** with Python's `secrets` module. Two files, `.server.env` (Docker env-file syntax) and `.headers`, are written with `O_CREAT\|O_EXCL\|O_NOFOLLOW` at 0600 under `/secrets/` (0700). The key is never in argv, and no full inspect is run. The files are deleted after confirmed container removal and kept while removal is unconfirmed. **Deviation:** the pointer variables `OPENHANDS_SERVER_ENV` and `OPENHANDS_HEADERS` are retired, because the paths derive from the validated identity. This also ends the phase-1 empty-value residual. The inventory row is `openhands-session` (`generated_local`). | +| The container environment list. | **Deviation:** `OPENHANDS_COMPRESSION` was added to the plan's closed list, so the request serializer and the server's preload rebuild the arm selection the host checked. | +| Native routes were checked by path only (E3). | Each route is now paired with exactly one method: POST `/api/conversations` and `.../interrupt`, and GET for the conversation, the final response and the error-event search. Any other pair is refused before curl runs. | +| No proof of containment before a model runs. | **P0-P2 probe and dispatch gate.** `e2e/netprobe.py` runs in the pinned agent image with the model-container hardening. The host writes the receipt `isolation-probe.json` at 0600, bound to the two network IDs and the two container IDs. `dispatch.py start` requires it to be at most 900 s old and to match the live IDs, the rendered-config hash and the probe-script hash. The run receipt moves to schema 6, with an `isolation` summary carrying no IDs or addresses. | +| MCP endpoints on the host. | ai-memory and socraticode stay disabled, now with the reason that they are unreachable by design under O1. The proxy has no route to either. | +| Addition, not a plan item. | `host.py teardown` removes a prepared attempt after a probe-only run and retries unconfirmed removals. It refuses while a conversation may be live or the serial reservation names the attempt. | + +Open items from this phase: + +- **Not run:** the live P0-P2 probe, and the P3-P5 model calls, which exist as + documented steps and a skeleton. G7's P3 half is not enforced in code. +- **Unprobed:** + - gateway mode `isolated` under rootless Docker 29.8.1; + - nginx under `--read-only` with `--entrypoint nginx`; + - body framing and streaming under the header allowlist; + - the container `--format` template, checked live only for its network + counterpart. +- **P2 is strict** about IPv6. Any non-loopback IPv6 address, including a + link-local one, fails it. An attempt network's subnet `.1` that belongs to + its own server or proxy makes the probe refuse to run rather than report. +- **Still open from the plan:** + - G2, the image scans; + - G5, the model surface: a `/v1` body can pick any model the arm serves; + - F10, the client peer the gateway logs; + - normal-bridge containers such as cognee-live still reach host loopback. +- **Outside this recipe's paths:** the repository's secret-path guard does not + yet cover the new `secrets/` directory. diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md new file mode 100644 index 000000000..2b1c25665 --- /dev/null +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -0,0 +1,226 @@ +# OpenHands worker isolation: per-attempt internal network and a pinned /v1 allowlist proxy (2026-09-28) + +**Scope.** This record covers three things: +- network isolation for the OpenHands runtime worker + ([README, Security posture](../../blueprints/runtime-workers/openhands/README.md#security-posture)), + in the SWE-bench mode built by the PR #425 takeover (phase 2) and in the resolver mode that a + follow-up PR adds; +- the ruleset variants for the resolver's agent branches; +- a scoped narrowing of one clause of the 2026-09-25 host-request-lane decision, for the resolver + lane only. + +It replaces the phase-1 DOCKER-USER firewall contract. The source is the coordinator's phase-2 +research synthesis of 2026-09-28, which is not committed. Each finding used here is restated with +its own source, and a source the build did not re-read is labelled as reviewed in that synthesis. + +**Chosen: O1.** Each attempt `S=-` gets its own resources: +- **`$S-int`,** created with `--internal`, IPv6 off and gateway mode `isolated`. Only the + agent-server and the P1/P2 probe container join it. +- **`$S-gw`,** a normal bridge network used only by the proxy's upstream side and the P0 probe + container. +- **A pinned nginx-unprivileged proxy** on both networks. As `gw:8081` it forwards three exact + `/v1` routes with fixed methods, fixed upstream URIs and a fixed header set to one arm's gateway + port on 10.0.2.2. The host's loopback ingress (`127.0.0.1:`) reaches the proxy's 8080, + which forwards to the server. +- **OmniRoute stays unchanged:** no login, password or management key is added. The model's + bearer stays the placeholder `local-loopback`, and `OMNIROUTE_API_KEY` is never sent. +- **The only secret is a per-attempt agent-server session key.** It is generated by the host, + kept in two owner-only files and deleted after confirmed removal (plan item E2). +- **Before the first conversation,** a P0-P2 probe receipt must be at most 900 seconds old and + bound to the live network and container IDs. The dispatch gate enforces this. The coordinator + must also have recorded P3 on the running gateway build; code does not enforce that half. + +Isolation then holds by construction, whatever OmniRoute's authentication state. It also covers +the other host-loopback listeners, not only OmniRoute. Gateway settings cannot provide it. The +synthesis reviewed these points at OmniRoute@045aa81f3, and this build did not re-read them: +- With `requireLogin=false`, every management route that is not always-protected is anonymous for + loopback peers (`policies/management.ts:99-103,253-266`; `apiAuth.ts:451`). +- With `requireLogin=true` and no password, a loopback caller can still post `requireLogin: false` + (`require-login/route.ts:40-42,89-93`). +- `OMNIROUTE_API_KEY` is checked only after that bypass (`apiKeys.ts:1374-1399`; + `management.ts:261-266`). + +In addition, the container reaches host-loopback ports as 10.0.2.2 on this host +(`DOCKERD_ROOTLESS_ROOTLESSKIT_DISABLE_HOST_LOOPBACK=false`; moby/moby@a46e6fa7 +`contrib/dockerd-rootless.sh:23-24,170-173` shows the upstream default). SDK v1.49.6 makes its LLM +calls inside the container. + +**Alternatives considered:** +- **OmniRoute split-port mode** (`API_PORT`/`API_HOST`; `.env.example:199-203`; + `src/lib/apiBridgeServer.ts:16-24,44-56,184-207,244`, as reviewed in the synthesis). Not + selected, for four reasons: + - the dashboard listener stays on `127.0.0.1:PORT`, which containers reach as 10.0.2.2; + - the bridge forwards the raw request URL to a loopback hop after a prefix check, so + `/v1/../api/settings` is untested; + - it also passes `/api/oauth/*` and `/callback`; + - enabling it restarts the shared gateways. + + It remains a candidate upstream target behind the proxy, as defence in depth. +- **DOCKER-USER rule to `10.0.2.2:`** (the phase-1 contract; docker/docs@4e9a5751 + `content/manuals/engine/network/firewall-iptables.md:22-24,48-95`). Rejected as the primary + control for three reasons: + - the same port serves `/api/*`, so a layer-4 rule cannot separate the client API from + management; + - where the rule sits in the rootlesskit namespace is unprobed; + - the rule is lost on a daemon restart. +- **O2, a third OmniRoute in a container on the run network.** A fallback only, as a second + layer behind O1. +- **O3/O4, `requireLogin=true` or an environment key on the gateways.** Rejected. The loopback + bootstrap write disables login again, and only a password closes that. Both options also change + the shared gateways for every other client. +- **O5, LLM calls made from the host.** Not supported at SDK v1.49.6. +- **O7, X-Forwarded-For marking alone.** Rejected on its own. It could be added as a later layer + once the gateways' IP-filter state has been read. Today the proxy passes no X-Forwarded-For. +- **Daemon-wide `--disable-host-loopback`.** Rejected. It would cut off every container, the + proxy and cognee-live included, and leave the host's non-loopback addresses reachable. + +**Overturn (O1):** +- **Stop and reopen this choice, with no fallback to plain `--internal`,** if either of these + happens: + - `network create` rejects gateway mode `isolated` under rootless Docker 29.8.1; + - P0-P2 fail on the live topology. +- **P3 or P5 contradicts the allowlist.** This covers two cases: + - P3 shows the header allowlist breaking request-body framing or streaming; + - P5's access log shows that the SDK needs a path outside the three routes. + + In either case, add only a probed route, or reopen the choice. +- **Split-port proves simpler.** Reopen if a probe shows split-port plus a layer-4 rule that + allows only `10.0.2.2:`, and that combination blocks `/api/*` for the agent, + including dot-segment and `%2e` variants, with fewer moving parts than O1. + +## Agent-branch ruleset (resolver mode; applied by the owner) + +**Chosen:** one repository ruleset, with main ruleset 23739774 and tag rulesets 23829417 and +23859358 unchanged: + +```json +{ + "name": "Agent branches: no history rewrite", + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": {"ref_name": {"include": ["refs/heads/openhands/*", "refs/heads/openhands/**/*"], "exclude": []}}, + "rules": [{"type": "non_fast_forward"}] +} +``` + +The resolver acts through the admin owner login, so any bypass would free it too. Only rules +with no bypass bind it. Two patterns are needed. "Because GitHub uses the File::FNM_PATHNAME +flag for the File.fnmatch syntax, the * wildcard does not match directory separators (/)", and +`qa/**/*` is the documented form for deeper names (docs.github.com +creating-rulesets-for-a-repository, fetched 2026-09-28). Whether `**/*` also matches single-level +names is unverified. The owner creates the ruleset with one POST and edits it later with PUT by +id, following the repository convention (`docs/github-automation.md:479-481,493-494`). + +**Alternatives considered:** +- **A deletion rule with no bypass.** Not chosen. It would stop the resolver deleting and + recreating a branch, but it would also block post-merge automatic deletion and the owner's + cleanup: "Branch protection rules and repository rules can also prevent branches being + automatically deleted" (docs.github.com managing-the-automatic-deletion-of-branches, fetched + 2026-09-28). That path is closed on the client side instead: no ref DELETE, `--delete` or + `:ref` refspec. +- **A deletion rule with an admin bypass.** Rejected, because it protects nothing: the resolver + is the admin. +- **Approval counts, last-push approval or update restrictions.** Rejected. Owner and resolver + are the same identity, so these rules either block the owner or, with a bypass, free the + resolver. +- **Push rulesets with path restrictions.** Unavailable here: "Push rulesets are available for + the GitHub Team plan in internal and private repositories" (docs.github.com + available-rules-for-rulesets, fetched 2026-09-28). The hard-deny paths live in the resolver's + patch validator instead. + +**Overturn:** +- **A receipt shows the resolver deleting or recreating an `openhands/*` ref.** Add a deletion + rule with no bypass and accept manual cleanup. +- **Never-merge, no-ready, no-tag or no-workflow-edit must hold on the server.** Move to a + dedicated GitHub App identity without the workflows permission, plus a pull-request approval + rule on main with an owner bypass. That is not a remedy for container egress. +- **Push rulesets become available for this repository.** Move the hard-deny paths to the server. + +## Scoped narrowing of the host-request-lane clause (resolver lane only) + +`docs/decisions/2026-09-25-host-request-lane.md:176-178` rejects "Auto-executing requests with +`claude -p`. That would feed untrusted issue text to a model that has tools. Anyone can open an +issue on a public repository, and even the owner's text is data, not an instruction. Execution +stays coordinator-run." + +The resolver lane does not simply conform. Even when the coordinator starts each run, a model +with tools acts on owner-authored issue text. This record therefore narrows the clause for this +lane only. Owner-authored issue text may reach a model with tools only when all three conditions +hold: +1. the coordinator starts the run and declares its scope; +2. the model's tools are contained by the O1 topology above and by the resolver's GitHub harness, + so they have no network beyond the proxy's three routes, no credential and no host access; +3. a validated patch is the only output. + +**Overturn, reverting to coordinator-only execution,** if any of these happens: +- a containment probe or check fails: P0-P2, or the resolver acceptance check A7; +- an outgoing-text guard finding or an unowned path appears in any run; +- the lane gains a trigger other than an explicit coordinator invocation. A timer, label or + webhook needs its own dated record. + +The narrowing takes effect only when this record merges, and the owner may reject it. The +SWE-bench mode reads no issue text and needs no narrowing. + +## GitHub harness (follow-up PR) + +The resolver's GitHub side is compared and built in the follow-up PR that adds resolver mode +(plan item E4). That PR compares the existing native `gh` sign-in, driving a fixed operation +allowlist from an allowlisted child environment, against exporting a token into the environment, +an extension's secret forwarding, a GitHub App and a fine-grained personal access token. This +change implements none of it. + +## Deviations from the phase-2 plan in this build + +- **E2 pointer variables retired.** The plan kept `OPENHANDS_SERVER_ENV` and + `OPENHANDS_HEADERS`. The build derives both key-file paths from the validated run id and arm, + so neither a caller's environment nor `status.json` can redirect them. +- **`OPENHANDS_COMPRESSION` added to the container environment.** The plan's closed list omits + it. The request serializer and the server's preload need the same arm selection the host + checked. The value is a recorded combo name or empty, and the proxy overwrites the header in + any case. +- **`host.py teardown` added.** It is not a plan item. It removes a prepared attempt after a + probe-only acceptance run and retries unconfirmed removals. It refuses while a conversation may + be live. +- **Plan gate G7 is only half enforced in code.** The dispatch gate checks P0-P2. P3 is a live + model call written as documented steps and a skeleton, and it has not run. The coordinator must + record P3 before any conversation starts. + +## Evidence class + +The evidence so far has two parts: +- offline unit checks with Docker mocked, which are our integration checks and not upstream + tests; +- read-only host observations: registry byte hashes of the pinned proxy image, one pull by + digest, image identity checks and one selected-field inspect of the default bridge network + ([evidence/phase2-commands.json](../../blueprints/runtime-workers/openhands/evidence/phase2-commands.json)). + +No attempt network or container was created. No gateway or model request was made. The first +host evidence for this design is the live P0-P2 probe +([sequence](../../blueprints/runtime-workers/openhands/README.md#live-probe-sequence-coordinator-not-run-yet)). + +## Sources + +- docker/docs@4e9a5751518ed8223a8dcde53693badddd72604f + `content/manuals/engine/network/port-publishing.md:121-131,186-192` (gateway modes; the + internal-network caveat and the `isolated` remedy) and `firewall-iptables.md:22-24,48-95` + (DOCKER-USER). +- docs.docker.com `container run` (`--env-file` syntax) and `network connect` (`--alias`), + fetched 2026-09-28. +- nginx.org `ngx_http_core_module` (`location`, `limit_except`), `ngx_http_proxy_module` + (`proxy_pass`, `proxy_pass_request_headers`, `proxy_set_header`) and `ngx_http_rewrite_module` + (`if`), fetched 2026-09-28. nginx/nginx@release-1.30.5 (4556c714) + `src/http/modules/ngx_http_proxy_module.c:1240-1251,1301-1303,1365-1384,1417-1418` and + `src/http/ngx_http_variables.c:1609-1624`. +- `ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e`, + source nginx/docker-nginx-unprivileged@588b4cbc (`pins.json` `gateway_proxy`). +- moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`. +- OpenHands/software-agent-sdk@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, + `config.py:24` and `dependencies.py:19`. +- OmniRoute@045aa81f3, as reviewed in the phase-2 synthesis and not re-read here: + `src/lib/apiBridgeServer.ts`, `.env.example`, `policies/management.ts`, `apiAuth.ts`, + `require-login/route.ts`, `apiKeys.ts`, at the lines cited above. +- docs.github.com creating-rulesets-for-a-repository, available-rules-for-rulesets and + managing-the-automatic-deletion-of-branches, fetched 2026-09-28. +- Repository: `docs/decisions/2026-09-25-host-request-lane.md:176-178`, + `docs/github-automation.md:479-481,493-494`, `.github/tag-ruleset.json` (the JSON shape). diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 95c835a8e..46b89d311 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -490,6 +490,31 @@ def test_agent_server_image_scan_receipt_matches_the_pinned_digest(self): self.assertIsNone(pattern.search(text), description) self.assertIn(name, (RECIPE / "README.md").read_text()) + def test_proxy_image_evidence_matches_the_pin_and_is_publishable(self): + # Phase 2: the gateway_proxy pin rests on registry byte hashes, a pull + # by digest and pinned_image_identity (evidence/phase2-commands.json). + from scripts.validate import PRIVATE_CONTENT + pins = self.read_json("pins.json")["gateway_proxy"] + record = self.read_json("evidence/phase2-commands.json") + commands = {item["id"]: item for item in record["commands"]} + self.assertEqual(len(commands), len(record["commands"])) + registry = commands["15-registry-bytes"] + index = pins["ref"].rsplit("@", 1)[1] + self.assertIn(index, registry["command"]) + found = registry["output_excerpt"] + self.assertEqual((found["top_sha256_of_bytes"], found["top_bytes_match_reference"]), (index, True)) + self.assertEqual((found["amd64_manifest"]["sha256_of_bytes"], found["amd64_manifest"]["bytes_match"]), + ("sha256:" + pins["manifest_sha256"], True)) + self.assertEqual((found["config"]["sha256_of_bytes"], found["config"]["bytes_match"], found["config"]["os_arch"]), + ("sha256:" + pins["config_sha256"], True, pins["platform"])) + self.assertIn("gateway_proxy store=containerd", commands["17-pull-and-identity"]["output"]) + self.assertEqual(record["base"], "efa73f40453911d3ac9583218530273677983053") + text = json.dumps(record, ensure_ascii=False) + for description, pattern in PRIVATE_CONTENT: + self.assertIsNone(pattern.search(text), description) + for document in ("README.md", "research.md"): + self.assertIn("evidence/phase2-commands.json", (RECIPE / document).read_text()) + class OpenHandsUpstreamAdapterTests(unittest.TestCase): """Synthetic transport controls from SWE-bench 4.1.0 reporting.py. From 39aaebe96b1e4c3c37b99a5ba93a524f7a17c7e6 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 15:51:38 -0400 Subject: [PATCH 13/22] OpenHands phase 2: correct $S-int membership and host-side reach wording The decision record said only the agent-server and the P1/P2 probe join $S-int; the proxy also joins it as gw. The README said only the host reaches the proxy's 8080 while the next sentence said the agent reaches it as gw:8080. Both now state the actual membership and reach. Co-Authored-By: Claude Opus 5.5 (1M context) --- blueprints/runtime-workers/openhands/README.md | 9 ++++++--- .../decisions/2026-09-28-openhands-resolver-isolation.md | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 83c6b4598..d724f1bc1 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -524,7 +524,9 @@ D start $S-proxy - **Server.** The agent-server joins `$S-int` only and publishes nothing. Its hardening is unchanged: UID/GID 10001, all capabilities dropped, no-new-privileges, a read-only root, a tmpfs home, and CPU, memory and PID - limits. Its only peer is the proxy, which it reaches as `gw`. + limits. Its only standing peer is the proxy, which it reaches as `gw`; the + P1/P2 probe container joins `$S-int` only while a probe runs (`prepare` or + `host.py probe`). - **Order.** The proxy is created after the server starts, and connected to `$S-int` before it starts, so nginx resolves `$S-server` when it loads. A created container joins its configured networks when it runs, and @@ -632,8 +634,9 @@ arm. `x-omniroute-*` control header. - **Host side.** Port 8080 forwards every path to `$S-server:8000` and refuses no arguments, because the driver's error-event search carries a - query string. Only the host reaches it, through `127.0.0.1:`. The agent - can reach its own server as `gw:8080`, which gives it nothing it lacks. + query string. The host reaches it through `127.0.0.1:`. The agent can + also reach it as `gw:8080`, which leads only to the agent's own server and + gives it nothing it lacks. - **Rendering.** `render_proxy_config` fills `@PORT@`, `@SERVER@`, `@RUN@` and `@COMPRESSION@` from `recipe.arm_config` and the attempt identity. It refuses: - a selection that differs from `arm_config`'s own output; diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index 2b1c25665..e09a127df 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -14,8 +14,8 @@ research synthesis of 2026-09-28, which is not committed. Each finding used here its own source, and a source the build did not re-read is labelled as reviewed in that synthesis. **Chosen: O1.** Each attempt `S=-` gets its own resources: -- **`$S-int`,** created with `--internal`, IPv6 off and gateway mode `isolated`. Only the - agent-server and the P1/P2 probe container join it. +- **`$S-int`,** created with `--internal`, IPv6 off and gateway mode `isolated`. Besides the + proxy, which joins it as `gw`, only the agent-server and the P1/P2 probe container join it. - **`$S-gw`,** a normal bridge network used only by the proxy's upstream side and the P0 probe container. - **A pinned nginx-unprivileged proxy** on both networks. As `gw:8081` it forwards three exact From 1ee13eb053c187493b094aa4830ce1506008c8ec Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 16:13:29 -0400 Subject: [PATCH 14/22] OpenHands phase 2: record the repository checks and their base control Adds entries 21-24 to evidence/phase2-commands.json: the post-fix recipe suite, the validate job's read-only check commands and the full unit suite, both run in an unprivileged network namespace with only loopback, and a base-versus-head control. Every failure also occurs at efa73f40: the stale evidence manifest, credential ancestor checks under the user namespace, and a worktree under /tmp. The record now names the 4b and review-fix commits, and the decision record and research note describe the added evidence part. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../openhands/evidence/phase2-commands.json | 111 +++++++++++++++++- .../runtime-workers/openhands/research.md | 4 +- ...2026-09-28-openhands-resolver-isolation.md | 9 +- 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/blueprints/runtime-workers/openhands/evidence/phase2-commands.json b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json index 9f59fb76b..e7ac75952 100644 --- a/blueprints/runtime-workers/openhands/evidence/phase2-commands.json +++ b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json @@ -1,5 +1,5 @@ { - "evidence_class": "Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), plus read-only host observations: registry byte hashes of the pinned proxy image, one pull by digest, image identity checks, one selected-field inspect of the default bridge network, the credential inventory tests and the value-free credential status. No attempt network or container was created and no gateway or model request was made.", + "evidence_class": "Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), plus read-only host observations: registry byte hashes of the pinned proxy image, one pull by digest, image identity checks, one selected-field inspect of the default bridge network, the credential inventory tests and the value-free credential status. No attempt network or container was created and no gateway or model request was made. Entries 21-24 were added after review at commit 34d43059: the validate workflow's read-only check commands and the full unit suite, run inside an unprivileged user and network namespace whose only interface is loopback, so no gateway, registry or other network request could leave it, plus a base-versus-head control for every check that failed. Owned containers and networks were 0 afterwards.", "sanitization": "The worktree prefix is shown as , the scratch directory as , the unit TMPDIR as inside outputs, and the home directory as ~ or $HOME. The module prefix `tests.test_runtime_worker_openhands.` is dropped from test names. Red and failed-attempt outputs are excerpts: the summary lines, each non-ok test with its count of non-ok lines (subtests) when above one, and each distinct first exception line with its count, cut at 220 characters; tracebacks are omitted, and rerunning the command on the named tree reproduces them. Green outputs keep the summary lines. Host outputs keep only the lines this record cites; the network ID is truncated and other credential rows are omitted.", "base": "efa73f40453911d3ac9583218530273677983053", "commits": { @@ -7,7 +7,9 @@ "slice_2": "0fbe40eed51f4d5b1f1e29bf8e8e75039a9b32f8", "slice_3": "a144708b2f47034f11c9696579fd391f52a31ef1", "slice_4a": "6cd13aa344870bab07ff8818a4f064cf9095c0ce", - "slice_4b": "the commit that adds this record" + "slice_4b": "4f6a6741ff4fa0fd352d2dc345eff2bfda295cc4", + "review_fix": "34d4305933180d9160d6477dc0b942d67aaccede", + "record_addendum": "the commit that adds entries 21-24" }, "decisions": "Coordinator amendment of 2026-09-28: build the phase-2 plan's section 1 (isolation design) and items E1, E2, E3 and E5; E4 (resolver mode) goes to a follow-up PR. Fail closed if network create rejects gateway mode isolated. P3-P5 are documented steps and a skeleton, not run. No push, rebase, manifests/evidence.json change, container, network or gateway request.", "item_tests": { @@ -540,6 +542,111 @@ "exit_code": 0, "output": "{\"scanned_files\": 18, \"status\": \"passed\"}", "note": "The planted negative control exited 1 with a personal-home-path finding, so the scan detects what it claims to. The changed files were also checked for the host user name: no matches." + }, + { + "id": "21-post-fix-green", + "tree": "Commit 34d43059.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 0, + "summary": [ + "Ran 107 tests in 2.091s", + "OK" + ], + "note": "Rerun after the review fix, which corrects two sentences: the decision record now says the proxy also joins $S-int as gw, and the README says the host reaches the proxy's 8080 through loopback while the agent can also reach it as gw:8080. git diff --check exited 0 and the private-content scan of both edited files passed." + }, + { + "id": "22-validate-job-offline", + "tree": "Commit 34d43059.", + "command": "unshare --user --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- sh \"$0\" \"$1\" \"$2\"' /p2/ci_local.sh /p2/runs/ci-local-head", + "script": "ci_local.sh sets TMPDIR=/var/tmp/claude-425-p2 and PYTHONDONTWRITEBYTECODE=1, then runs the validate job's read-only check commands from .github/workflows/validate.yml:80-121 in order and prints each exit code. The earlier steps (example workflow identity, zizmor, actionlint) and the node step at lines 142-143 check paths this build does not touch and were not run.", + "output": [ + "01 rc=1 | python3 scripts/validate.py", + "02 rc=0 | python3 scripts/host_receipts.py validate", + "03 rc=0 | python3 scripts/release_due.py --strict-if-repinned origin/main", + "04 rc=0 | python3 scripts/validate_catalogs.py", + "05 rc=0 | python3 scripts/validate_foundation.py --root . --json", + "06 rc=0 | python3 scripts/landscape.py --root .", + "07 rc=0 | python3 blueprints/blind-catalog-convergence/audit_reports.py --check", + "08 rc=1 | python3 scripts/validate_convergence.py --all-recorded --root . --json", + "09 rc=0 | python3 tools/sota-convergence/build_verdicts.py --check", + "10 rc=0 | python3 scripts/component_matrix.py --check", + "11 rc=0 | python3 scripts/new_host_grand_list.py --check", + "12 rc=0 | python3 tools/sota-convergence/gap_crosswalk.py build --check", + "13 rc=0 | python3 tools/sota-convergence/gap_wave_ledger.py --wave gap-wave2-20260923 --wave gap-wave3-20260923 --owner gap-resolution --check", + "14 rc=0 | python3 scripts/build_ecosystem.py --check" + ], + "failed_checks": { + "01": "After the first line, the 40 lines are the 14 SHA-256 and byte-count pairs and the 12 file-missing lines listed under entry 19, and nothing else.", + "08": "{\"records\": [{\"errors\": [\"record discovery: invalid, changed or empty evidence manifest\"], \"observations\": 0, \"valid\": false}], \"valid\": false}" + }, + "base_control": { + "command": "git archive efa73f40453911d3ac9583218530273677983053 | tar -x -C /p2/base-tree; then, in that tree, TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 unshare --user --map-current-user --net python3 scripts/validate.py, and the same for python3 scripts/validate_convergence.py --all-recorded --root . --json", + "exit_codes": { + "validate.py": 1, + "validate_convergence.py": 1 + }, + "validate_excerpt": "24 lines after the first: the same 12 file-missing lines, and the SHA-256 and byte-count pairs of README.md, dispatch.py, host.py, receipt.py and research.md under blueprints/runtime-workers/openhands/ and of tests/test_runtime_worker_openhands.py. Every base line also appears at head; the 16 head-only lines are the pairs of the eight registered files this build changed that the manifest still matched at base.", + "validate_convergence_output": "Identical to check 08 at head." + }, + "note": "Both failures come from manifests/evidence.json, which the coordinator re-registers after #429 lands, and the base already fails both checks the same way. The other twelve checks exit 0." + }, + { + "id": "23-full-suite-offline", + "tree": "Commit 34d43059.", + "command": "TMPDIR=/var/tmp/claude-425-p2 PYTHONDONTWRITEBYTECODE=1 unshare --user --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -m unittest'", + "exit_code": 1, + "summary": [ + "Ran 6881 tests in 658.496s", + "FAILED (failures=29, errors=45, skipped=765)" + ], + "non_ok_lines_per_module": { + "test_adaptive_market_research": 7, + "test_adaptive_paper_credential_race": 22, + "test_adaptive_paper_credential_source": 19, + "test_adaptive_paper_runner": 6, + "test_catalog_freshness_propose": 1, + "test_gpt6_family_tiering_20260926": 1, + "test_native_faults_min": 16, + "test_order_throughput": 1, + "test_path_safety": 1 + }, + "distinct_first_exception_lines": [ + "safety.SafetyError: credential_file_permissions:ancestor (x28)", + "credential_guard.CredentialGuardError: credential_file_permissions:ancestor (x17)", + "market_research.ResearchError: credential_file_permissions:ancestor (x11)", + "AssertionError: 'credential_file_permissions:ancestor' != 'alpaca_paper_only:live_host' (x4)", + "AssertionError: 'credential_file_permissions:ancestor' != 'alpaca_paper_only:not_paper_host' (x4)", + "AssertionError: Regex didn't match: 'credential_file_permissions:missing' not found in 'credential_file_permissions:ancestor' (x2)", + "market_research_bound_check.ResearchError: credential_file_permissions:ancestor (x1)", + "ValueError: refused (x1)", + "AssertionError: {'disable_hardlink_check': {'mutation': 'd[21452 chars]\\n'}} != {} (x1)", + "AssertionError: 'credential_file_permissions:ancestor' != 'duplicate_credential_variable' (x1)", + "AssertionError: Tuples differ: (1, 'credential_file_permissions:ancestor') != (1, 'alpaca_paper_only:not_paper_host') (x1)", + "AssertionError: SafetyError('credential_file_permissions:ancestor') is not None (x1)", + "AssertionError: 1 != 0 : Publication validation failed: (x1)", + "AssertionError: 2 != 5 : run_arm: the temporary directory /tmp must lie outside the state directory and every repository (x1)" + ], + "note": "This is the validate job's `python3 -m unittest` step (.github/workflows/validate.yml:140-141), run offline and without the promotion-gate venv that the job provisions first. tests.test_runtime_worker_openhands and every module not listed had no failure or error. Entry 24 finds each of the 74 non-ok lines at the base as well; that control, not the reading that follows, is the attribution. The exception lines name three conditions of this run: the user namespace maps only the invoking user, so root-owned ancestors such as / and /var appear with the overflow owner and the credential-file ancestor checks refuse them (most lines); the publication validator fails on the stale evidence manifest of entry 22; and the tiering runner refuses /tmp as its temporary directory because this worktree lies under /tmp." + }, + { + "id": "24-suite-failure-attribution", + "command": "git clone -q --shared --no-checkout /p2/base-clone && git -C /p2/base-clone checkout -q efa73f40453911d3ac9583218530273677983053; then, in the same namespace as entry 23, /p2/attrib.sh runs `python3 -m unittest` over the nine modules of entry 23 in the base clone and in at commit 34d43059", + "exit_codes": { + "base": 1, + "head": 1 + }, + "summary": { + "base": [ + "Ran 541 tests in 100.811s", + "FAILED (failures=29, errors=45, skipped=71)" + ], + "head": [ + "Ran 541 tests in 101.273s", + "FAILED (failures=29, errors=45, skipped=71)" + ] + }, + "non_ok_lines_per_module": "Identical at base and head, and equal to entry 23's counts.", + "comparison": "The sorted FAIL and ERROR header lines are identical at base and head (74 each, empty difference both ways), and so are the distinct first exception lines. None of the 74 is introduced by this build." } ] } diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index b2ca8c2f7..9ee89bf94 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -398,7 +398,9 @@ Open finding from this phase, not changed here: This phase is an offline build on efa73f40 following the coordinator's phase-2 plan: section 1 (isolation design) and items E1, E2, E3 and E5. Item E4, resolver mode, goes to a follow-up PR. The phase ran unit tests with Docker -mocked plus read-only host observations. It created no attempt network or +mocked plus read-only host observations, then the repository's validate-job +checks and full unit suite in a network namespace with only loopback. Each +failure there also occurs at the base. It created no attempt network or container and made no gateway or model request. - Commands and outputs: [evidence/phase2-commands.json](evidence/phase2-commands.json). diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index e09a127df..f901f237d 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -188,12 +188,15 @@ change implements none of it. ## Evidence class -The evidence so far has two parts: +The evidence so far has three parts +([evidence/phase2-commands.json](../../blueprints/runtime-workers/openhands/evidence/phase2-commands.json)): - offline unit checks with Docker mocked, which are our integration checks and not upstream tests; - read-only host observations: registry byte hashes of the pinned proxy image, one pull by - digest, image identity checks and one selected-field inspect of the default bridge network - ([evidence/phase2-commands.json](../../blueprints/runtime-workers/openhands/evidence/phase2-commands.json)). + digest, image identity checks and one selected-field inspect of the default bridge network; +- the repository's own validate-job checks and full unit suite, run in a network namespace with + only loopback. Every failure there also occurs at the base, and the evidence entries show that + comparison. No attempt network or container was created. No gateway or model request was made. The first host evidence for this design is the live P0-P2 probe From a3a8818089e2c6306b39f2418ea7b879748eaac6 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 16:15:48 -0400 Subject: [PATCH 15/22] OpenHands phase 2: bound the namespace claim and mark masked suite results The record said no network request could leave the namespace; a network namespace isolates TCP/IP only, so it now says no TCP or UDP traffic could leave, that path-based unix sockets stayed reachable, and what bounded them (no Docker CLI on PATH, zero owned resources afterwards). Entry 24 now says the base control shows no new failure but not a pass at head, and names the one failing test that reads changed paths. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/evidence/phase2-commands.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blueprints/runtime-workers/openhands/evidence/phase2-commands.json b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json index e7ac75952..5ee3dea1b 100644 --- a/blueprints/runtime-workers/openhands/evidence/phase2-commands.json +++ b/blueprints/runtime-workers/openhands/evidence/phase2-commands.json @@ -1,5 +1,5 @@ { - "evidence_class": "Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), plus read-only host observations: registry byte hashes of the pinned proxy image, one pull by digest, image identity checks, one selected-field inspect of the default bridge network, the credential inventory tests and the value-free credential status. No attempt network or container was created and no gateway or model request was made. Entries 21-24 were added after review at commit 34d43059: the validate workflow's read-only check commands and the full unit suite, run inside an unprivileged user and network namespace whose only interface is loopback, so no gateway, registry or other network request could leave it, plus a base-versus-head control for every check that failed. Owned containers and networks were 0 afterwards.", + "evidence_class": "Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), plus read-only host observations: registry byte hashes of the pinned proxy image, one pull by digest, image identity checks, one selected-field inspect of the default bridge network, the credential inventory tests and the value-free credential status. No attempt network or container was created and no gateway or model request was made. Entries 21-24 were added after review at commit 34d43059: the validate workflow's read-only check commands and the full unit suite, run inside an unprivileged user and network namespace whose only interface was loopback with nothing listening, so no TCP or UDP traffic could leave it. The namespace does not isolate path-based unix sockets, such as the rootless Docker daemon's or the user bus; the Docker CLI was not on PATH, and owned containers and networks were 0 afterwards. A base-versus-head control covers every check that failed.", "sanitization": "The worktree prefix is shown as , the scratch directory as , the unit TMPDIR as inside outputs, and the home directory as ~ or $HOME. The module prefix `tests.test_runtime_worker_openhands.` is dropped from test names. Red and failed-attempt outputs are excerpts: the summary lines, each non-ok test with its count of non-ok lines (subtests) when above one, and each distinct first exception line with its count, cut at 220 characters; tracebacks are omitted, and rerunning the command on the named tree reproduces them. Green outputs keep the summary lines. Host outputs keep only the lines this record cites; the network ID is truncated and other credential rows are omitted.", "base": "efa73f40453911d3ac9583218530273677983053", "commits": { @@ -646,7 +646,7 @@ ] }, "non_ok_lines_per_module": "Identical at base and head, and equal to entry 23's counts.", - "comparison": "The sorted FAIL and ERROR header lines are identical at base and head (74 each, empty difference both ways), and so are the distinct first exception lines. None of the 74 is introduced by this build." + "comparison": "The sorted FAIL and ERROR header lines are identical at base and head (74 each, empty difference both ways), and so are the distinct first exception lines. None of the 74 is introduced by this build. The control does not show that these tests pass at head: most stop at the ancestor check, so their later assertions never ran. Neither the nine modules nor the sources they exercise read a path this build changed, with one exception. test_catalog_freshness_propose's publication-validator test runs scripts/validate.py over a copy of the repository, which reads the registered files this build changed; it fails on the stale manifest at base and head, and its first real run comes after the manifest is re-registered." } ] } From 153de40abde13a59885244834ac6c03f01e6e129 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 17:02:35 -0400 Subject: [PATCH 16/22] OpenHands repair R1: grade with network mode none and inspect every grader container The official grader containers had upstream network settings (the default bridge), so grading could reach host-loopback listeners through 10.0.2.2. e2e/docker_grader.py now: - forces network_mode "none" on every ContainerCollection.create spec and refuses any upstream network, network_mode, networking_config or network_disabled option (docker-py 7.1.0 models/containers.py:686-694); - refuses any SDK POST /containers/create body whose HostConfig.NetworkMode is not "none" (api/container.py:445-457, types/containers.py:351); - refuses to grade when the created container's inspect shows another mode or any network besides "none", and removes that container (moby docker-v29.8.1 daemon/create.go:251, container_operations.go:363-406). main() installs the transport through install_transport, which the tests drive with fake docker modules. README and the decision record scope containment to grading, document the fail-closed offline-verdict trade-off (SWE-bench v4.1.0 test_spec/python.py:443-444, test_spec.py:55-60) and the overturn condition: a separately probed internal network for graders. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 39 ++++- .../openhands/e2e/docker_grader.py | 107 ++++++++++---- ...2026-09-28-openhands-resolver-isolation.md | 16 +++ tests/test_runtime_worker_openhands.py | 133 +++++++++++++++++- 4 files changed, 261 insertions(+), 34 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index d724f1bc1..b21d595fc 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -571,10 +571,34 @@ cut off every container, the proxy included, and leave the host's non-loopback addresses reachable ([decision record](../../../docs/decisions/2026-09-28-openhands-resolver-isolation.md)). -Official grader containers keep their upstream options and are not on the -attempt networks. The coordinator must qualify their host-service isolation -separately, without presenting altered test conditions as an unchanged -environment. +Official grader containers are not on the attempt networks and have **no +network at all**. `e2e/docker_grader.py` creates each one with network mode +`none`, "No networking for this container" (docker/docker-py@7.1.0, the grader +lock's SDK, `docker/models/containers.py:686-694`). It enforces this at three +points: +- it refuses any upstream network, network-mode, networking-config or + network-disabled option; +- it refuses any SDK create body whose `HostConfig.NetworkMode` is not `none` + (`api/container.py:445-457`, `types/containers.py:351`); +- it refuses to grade when the created container's inspect shows a mode other + than `none` or any network besides `none`, and removes that container. + +moby/moby@docker-v29.8.1 records mode `none` as the single network entry `none` +at create (`daemon/create.go:251`, `daemon/container_operations.go:363-406`). +It also refuses to connect such a container to another network (`:202-204`). +Grading therefore has no path to the host's loopback listeners, the gateways or +the internet. + +This is a fail-closed deviation from the upstream grading environment. The +evaluation script re-runs the repository's install command +(SWE-bench@v4.1.0 `test_spec/python.py:443-444`), and the script sets +`-uxo pipefail` but not `-e` (`test_spec/test_spec.py:55-60`). An install step +that must download therefore fails, and the tests run against the image's +preinstalled environment. A verdict that depends on network access during +evaluation is an offline verdict, not an unchanged-environment verdict. The +official gold and known-negative controls for the selected instance run under +the same condition. **Overturn:** give graders a separately probed internal +network, with its own P0-P2-style receipt, and record that before using it. ### Gateway proxy allowlist @@ -829,10 +853,11 @@ rechecks that identity; mutable pulls and builds are refused. Sources: SWE-bench@v4.1.0 `test_spec/test_spec.py:106-120`, `docker_build.py:516-524`; docker/docker-py@7.1.0 `docker/models/resource.py:28-33`. -`e2e/docker_grader.py` adapts names and ownership labels only. It adds **no memory, +`e2e/docker_grader.py` adapts names, ownership labels and the network mode +(`none`; see [Security posture](#security-posture)) only. It adds **no memory, CPU, PID, capability or user changes** to the official grading container. Gold -patch and known-negative official controls must run under the frozen image -before a worker verdict is accepted. `e2e/check.py` relays the actual report; an +patch and known-negative official controls must run under the frozen image, with +no network, before a worker verdict is accepted. `e2e/check.py` relays the actual report; an error/incomplete bucket is infrastructure, not a model-negative tally. Host work remaining: native installation and import acceptance; shared skills diff --git a/blueprints/runtime-workers/openhands/e2e/docker_grader.py b/blueprints/runtime-workers/openhands/e2e/docker_grader.py index 6635108f0..26965d93e 100644 --- a/blueprints/runtime-workers/openhands/e2e/docker_grader.py +++ b/blueprints/runtime-workers/openhands/e2e/docker_grader.py @@ -1,11 +1,23 @@ """Run the unchanged official SWE-bench CLI with owned Docker transport. Reference: swebench==4.1.0 harness/docker_build.py:516 and -harness/run_evaluation.py:580. Only Docker creation names/labels are adapted. -No tests, scoring, patch application or report-writing logic is replaced. +harness/run_evaluation.py:580. Only Docker creation names, labels and the +network mode are adapted. No tests, scoring, patch application or +report-writing logic is replaced. + +Every grader container is created with network mode "none", which +docker/docker-py@7.1.0 documents as "No networking for this container" +(docker/models/containers.py:686-694). The SDK builds one HostConfig +(types/containers.py:351) and posts it through create_container_from_config +(api/container.py:445-457), which is checked as well. moby/moby@docker-v29.8.1 +records the mode as the single NetworkSettings.Networks entry "none" at create +(daemon/create.go:251; daemon/container_operations.go:363-406) and refuses to +connect such a container to another network (container_operations.go:202-204). +A created container whose inspect shows anything else is removed, not graded. """ from __future__ import annotations +import contextlib import importlib.metadata import json import os @@ -16,6 +28,8 @@ OWNER_KEY = "com.native-agent-stack.owner" OWNER = "gpt6-omniroute-framework-integration" +NETWORK_MODE = "none" +NETWORK_KEYS = ("network", "network_mode", "networking_config", "network_disabled") def checked_image(collection, image, expected_id): @@ -33,13 +47,77 @@ def owned_container_options(options): name = result.get("name", "") if not re.fullmatch(r"sweb\.eval\.[a-z0-9_.-]+", name): raise ValueError("unexpected_upstream_container_name") - if any(result.get(key) for key in ("ports", "publish_all_ports", "volumes", "mounts", "network", "network_mode")): + if any(result.get(key) for key in ("ports", "publish_all_ports", "volumes", "mounts")): + raise ValueError("grader_requires_no_ports_volumes_or_custom_networks") + requested = {key: result[key] for key in NETWORK_KEYS if result.get(key)} + if requested not in ({}, {"network_mode": NETWORK_MODE}): raise ValueError("grader_requires_no_ports_volumes_or_custom_networks") result["name"] = "rw-openhands-" + name result["labels"] = {**result.get("labels", {}), OWNER_KEY: OWNER} + result["network_mode"] = NETWORK_MODE return result +def checked_create_config(config): + """The body of every SDK POST /containers/create must carry mode none.""" + host = config.get("HostConfig") if isinstance(config, dict) else None + if not isinstance(host, dict) or host.get("NetworkMode") != NETWORK_MODE or config.get("NetworkingConfig"): + raise ValueError("grader_container_requires_network_none") + return config + + +def checked_network_none(container): + """The created container's inspect: mode none and no other network.""" + attrs = getattr(container, "attrs", None) or {} + mode = (attrs.get("HostConfig") or {}).get("NetworkMode") + networks = (attrs.get("NetworkSettings") or {}).get("Networks") + if mode != NETWORK_MODE or not isinstance(networks, dict) or set(networks) != {NETWORK_MODE}: + raise ValueError("grader_container_network_not_none") + return container + + +def owned_create(create, collection, args, kwargs, image_id, record): + options = owned_container_options(kwargs) + image = options.get("image", args[0] if args else None) + checked_image(collection, image, image_id) + # A separate host supervisor retains these exact names for cleanup. + with Path(record).open("a") as stream: + stream.write(json.dumps({"name": options["name"]}) + "\n") + # docker-py 7.1.0 create() returns self.get(Id), a fresh inspect + # (models/containers.py:913-936). + container = create(collection, *args, **options) + try: + return checked_network_none(container) + except ValueError: + # The host's grade() also removes this recorded name afterwards. + with contextlib.suppress(Exception): + container.remove(force=True) + raise + + +def install_transport(containers, api, images, volumes, networks, image_id, record="docker-created.jsonl"): + create = containers.create + post = api.create_container_from_config + + def created(collection, *args, **kwargs): + return owned_create(create, collection, args, kwargs, image_id, record) + + def posted(client, config, *args, **kwargs): + return post(client, checked_create_config(config), *args, **kwargs) + + def forbidden(*args, **kwargs): + raise RuntimeError("use_prebuilt_images_without_new_volumes_or_networks") + + containers.create = created + api.create_container_from_config = posted + # Namespace swebench uses prebuilt images. Never create unlabelled build + # intermediates or additional resources if the selected upstream changes. + api.build = forbidden + images.pull = forbidden + volumes.create = forbidden + networks.create = forbidden + + def main(): if importlib.metadata.version("swebench") != "4.1.0": raise RuntimeError("exact_upstream_grader_version_required") @@ -49,27 +127,8 @@ def main(): from docker.models.images import ImageCollection from docker.api.client import APIClient - create = ContainerCollection.create - - def owned_create(collection, *args, **kwargs): - kwargs = owned_container_options(kwargs) - image = kwargs.get("image", args[0] if args else None) - checked_image(collection, image, os.environ.get("OPENHANDS_GRADER_IMAGE_ID")) - # A separate host supervisor retains these exact names for cleanup. - with Path("docker-created.jsonl").open("a") as stream: - stream.write(json.dumps({"name": kwargs["name"]}) + "\n") - return create(collection, *args, **kwargs) - - def forbidden(*args, **kwargs): - raise RuntimeError("use_prebuilt_images_without_new_volumes_or_networks") - - ContainerCollection.create = owned_create - # Namespace swebench uses prebuilt images. Never create unlabelled build - # intermediates or additional resources if the selected upstream changes. - APIClient.build = forbidden - ImageCollection.pull = forbidden - VolumeCollection.create = forbidden - NetworkCollection.create = forbidden + install_transport(ContainerCollection, APIClient, ImageCollection, VolumeCollection, NetworkCollection, + os.environ.get("OPENHANDS_GRADER_IMAGE_ID")) runpy.run_module("swebench.harness.run_evaluation", run_name="__main__") diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index f901f237d..9519015db 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -29,6 +29,15 @@ its own source, and a source the build did not re-read is labelled as reviewed i - **Before the first conversation,** a P0-P2 probe receipt must be at most 900 seconds old and bound to the live network and container IDs. The dispatch gate enforces this. The coordinator must also have recorded P3 on the running gateway build; code does not enforce that half. +- **Official grading has no network.** Every grader container is created with network mode + `none`, "No networking for this container" (docker/docker-py@7.1.0 + `docker/models/containers.py:686-694`). The adapter refuses any other mode, any SDK create body + without it, and any created container whose inspect shows another network; that container is + removed, not graded. moby/moby@docker-v29.8.1 records the mode as the single network `none` + and refuses to connect such a container elsewhere (`daemon/create.go:251`, + `daemon/container_operations.go:202-204,363-406`). This fails closed: an evaluation step that + needs to download fails, so the verdict is an offline verdict + (SWE-bench@v4.1.0 `test_spec/python.py:443-444`, `test_spec/test_spec.py:55-60`). Isolation then holds by construction, whatever OmniRoute's authentication state. It also covers the other host-loopback listeners, not only OmniRoute. Gateway settings cannot provide it. The @@ -87,6 +96,8 @@ calls inside the container. - **Split-port proves simpler.** Reopen if a probe shows split-port plus a layer-4 rule that allows only `10.0.2.2:`, and that combination blocks `/api/*` for the agent, including dot-segment and `%2e` variants, with fewer moving parts than O1. +- **Graders need a network.** Replace mode `none` only with a separately probed internal network + for graders, backed by its own P0-P2-style receipt. ## Agent-branch ruleset (resolver mode; applied by the owner) @@ -218,6 +229,11 @@ host evidence for this design is the live P0-P2 probe - `ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e`, source nginx/docker-nginx-unprivileged@588b4cbc (`pins.json` `gateway_proxy`). - moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`. +- docker/docker-py@7.1.0 (a3652028, the SDK in OpenHands/benchmarks@405bae7 `uv.lock`) + `docker/models/containers.py:686-694,913-936`, `docker/api/container.py:445-457` and + `docker/types/containers.py:351`; moby/moby@docker-v29.8.1 (464cd50c) `daemon/create.go:251` + and `daemon/container_operations.go:202-204,363-406`; SWE-bench@v4.1.0 (726c5461) + `swebench/harness/test_spec/python.py:443-444` and `test_spec/test_spec.py:55-60`. - OpenHands/software-agent-sdk@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, `config.py:24` and `dependencies.py:19`. - OmniRoute@045aa81f3, as reviewed in the phase-2 synthesis and not re-read here: diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 46b89d311..937fbb90d 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -641,12 +641,132 @@ def test_official_grader_container_transport_preserves_upstream_settings(self): self.assertEqual(changed["labels"]["com.native-agent-stack.owner"], "gpt6-omniroute-framework-integration") self.assertEqual(changed["image"], original["image"]) - self.assertEqual(set(changed) - set(original), {"labels"}) + # docker-py 7.1.0 models/containers.py:686-694: network_mode "none" is + # "No networking for this container". + self.assertEqual(set(changed) - set(original), {"labels", "network_mode"}) + self.assertEqual(changed["network_mode"], "none") self.assertEqual(original["name"], "sweb.eval.django__django-11333.attempt") - for update in ({"ports": {"8000/tcp": 3710}}, {"name": "unowned"}, {"volumes": {"unowned": {}}}): + self.assertEqual(module.owned_container_options({**original, "network_mode": "none"})["network_mode"], "none") + for update in ({"ports": {"8000/tcp": 3710}}, {"name": "unowned"}, {"volumes": {"unowned": {}}}, + {"network_mode": "bridge"}, {"network_mode": "host"}, {"network_mode": "default"}, + {"network_mode": "container:rw-openhands-other"}, {"network": "bridge"}, + {"networking_config": {"bridge": {}}}, {"network_disabled": True}): with self.subTest(update=update), self.assertRaises(ValueError): module.owned_container_options({**original, **update}) + def networked(self, mode, networks): + removed = [] + container = SimpleNamespace(attrs={"HostConfig": {"NetworkMode": mode}, + "NetworkSettings": {"Networks": networks}}, + remove=lambda force: removed.append(force)) + return container, removed + + def test_grader_container_is_created_with_network_none_and_inspected(self): + module = load_recipe_module("e2e/docker_grader.py") + image_id = "sha256:" + "a" * 64 + collection = SimpleNamespace(client=SimpleNamespace(images=SimpleNamespace( + get=lambda image: SimpleNamespace(id=image_id)))) + record = self.result / "docker-created.jsonl" + spec = {"name": "sweb.eval.django__django-11333.attempt", "image": "swebench/example:latest", + "detach": True, "command": "tail -f /dev/null"} + created = [] + # moby docker-v29.8.1 daemon/create.go:251 and container_operations.go:363-406: + # mode "none" is recorded as the single NetworkSettings.Networks entry "none". + good, removed = self.networked("none", {"none": {"IPAddress": ""}}) + + def create(collection, *args, **kwargs): + created.append(kwargs) + return good + self.assertIs(module.owned_create(create, collection, (), spec, image_id, record), good) + self.assertEqual(created[0]["network_mode"], "none") + self.assertEqual(created[0]["name"], "rw-openhands-" + spec["name"]) + self.assertEqual(json.loads(record.read_text()), {"name": "rw-openhands-" + spec["name"]}) + self.assertEqual(removed, []) + # A spec that asks for any network is refused before the Docker call. + for update in ({"network_mode": "bridge"}, {"network": "rw-openhands-other"}): + with self.subTest(update=update), self.assertRaises(ValueError): + module.owned_create(create, collection, (), {**spec, **update}, image_id, record) + self.assertEqual(len(created), 1) + # Refuse to grade when the created container's inspect shows another network. + for mode, networks in (("default", {"bridge": {"IPAddress": "172.17.0.2"}}), + ("none", {"none": {}, "bridge": {}}), ("none", {}), ("none", None), + ("bridge", {"none": {}})): + bad, removed = self.networked(mode, networks) + with self.subTest(mode=mode, networks=networks): + with self.assertRaisesRegex(ValueError, "grader_container_network_not_none"): + module.owned_create(lambda collection, *a, **k: bad, collection, (), spec, image_id, record) + self.assertEqual(removed, [True]) + + def test_grader_sdk_create_body_requires_network_none(self): + module = load_recipe_module("e2e/docker_grader.py") + # docker-py 7.1.0 types/containers.py:351 writes NetworkMode (default "default"); + # api/container.py:445-457 posts that body to /containers/create. + config = {"Image": "swebench/example:latest", "HostConfig": {"NetworkMode": "none"}} + self.assertIs(module.checked_create_config(config), config) + for bad in ({"HostConfig": {"NetworkMode": "default"}}, {"HostConfig": {"NetworkMode": "bridge"}}, + {"HostConfig": {"NetworkMode": "host"}}, {"HostConfig": {}}, {}, None, + {"HostConfig": {"NetworkMode": "none"}, "NetworkingConfig": {"EndpointsConfig": {"bridge": {}}}}): + with self.subTest(bad=bad), self.assertRaisesRegex(ValueError, "grader_container_requires_network_none"): + module.checked_create_config(bad) + + def test_grader_main_installs_the_network_none_transport(self): + module = load_recipe_module("e2e/docker_grader.py") + image_id = "sha256:" + "a" * 64 + posted, created = [], [] + + class APIClient: + def create_container_from_config(self, config, name=None, platform=None): + posted.append(config) + return {"Id": "fixture"} + + def build(self, *args, **kwargs): + return "built" + + class ContainerCollection: + client = SimpleNamespace(images=SimpleNamespace(get=lambda image: SimpleNamespace(id=image_id))) + + def create(self, image, command=None, **kwargs): + created.append(kwargs) + return SimpleNamespace(attrs={"HostConfig": {"NetworkMode": kwargs.get("network_mode")}, + "NetworkSettings": {"Networks": {"none": {}}}}) + + class Other: + def pull(self, *args, **kwargs): + return "pulled" + + def create(self, *args, **kwargs): + return "created" + + modules = {name: SimpleNamespace() for name in ( + "docker", "docker.models", "docker.api", "docker.models.containers", "docker.models.networks", + "docker.models.volumes", "docker.models.images", "docker.api.client")} + modules["docker.models.containers"].ContainerCollection = ContainerCollection + modules["docker.api.client"].APIClient = APIClient + networks, volumes, images = type("N", (Other,), {}), type("V", (Other,), {}), type("I", (Other,), {}) + modules["docker.models.networks"].NetworkCollection = networks + modules["docker.models.volumes"].VolumeCollection = volumes + modules["docker.models.images"].ImageCollection = images + ran = [] + with patch.dict(sys.modules, modules), \ + patch.object(module.importlib.metadata, "version", return_value="4.1.0"), \ + patch.object(module.runpy, "run_module", side_effect=lambda *a, **k: ran.append(a)), \ + patch.dict(os.environ, {"OPENHANDS_GRADER_IMAGE_ID": image_id}), \ + contextlib.chdir(self.result): + module.main() + ContainerCollection().create("swebench/example:latest", name="sweb.eval.fixture.run") + with self.assertRaises(ValueError): + ContainerCollection().create("swebench/example:latest", name="sweb.eval.fixture.run", + network_mode="bridge") + with self.assertRaisesRegex(ValueError, "grader_container_requires_network_none"): + APIClient().create_container_from_config({"HostConfig": {"NetworkMode": "default"}}) + APIClient().create_container_from_config({"HostConfig": {"NetworkMode": "none"}}) + for forbidden in (APIClient().build, images().pull, volumes().create, networks().create): + with self.subTest(forbidden=forbidden), self.assertRaises(RuntimeError): + forbidden() + self.assertEqual(ran, [("swebench.harness.run_evaluation",)]) + self.assertEqual([options["network_mode"] for options in created], ["none"]) + self.assertEqual(posted, [{"HostConfig": {"NetworkMode": "none"}}]) + def test_task_adapter_hides_oracle_and_requires_frozen_original_bytes(self): module = load_recipe_module("e2e/task.py") row = {"instance_id": self.INSTANCE, "repo": "django/django", "base_commit": "a" * 40, @@ -1020,7 +1140,14 @@ def test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags(self): module.checked_image(collection, "swebench/example:latest", "sha256:" + "b" * 64) with self.assertRaises(ValueError): module.checked_image(collection, "swebench/example:latest", None) - self.assertIn("ImageCollection.pull = forbidden", (RECIPE / "e2e/docker_grader.py").read_text()) + # Pulls stay forbidden once the transport is installed (see + # test_grader_main_installs_the_network_none_transport for main()). + images = type("ImageCollection", (), {"pull": lambda self, *a, **k: "pulled"}) + others = [type(name, (), {"create": lambda self, *a, **k: "created"}) for name in ("C", "V", "N")] + api = type("APIClient", (), {"build": lambda self: "built", "create_container_from_config": lambda self, c: c}) + module.install_transport(others[0], api, images, others[1], others[2], "sha256:" + "a" * 64) + with self.assertRaisesRegex(RuntimeError, "use_prebuilt_images"): + images().pull("swebench/example:latest") def test_frozen_grader_digest_is_checked_before_retagging(self): host = load_recipe_module("host.py") From 03480d3f52175bcdadbbd790ada1b9ecf9d35e1a Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 17:12:48 -0400 Subject: [PATCH 17/22] OpenHands repair R2, R4 and addendum b: probe targets, route evidence and resolver class R2. probe_targets added each network's subnet .1 and run_probe refused when an attempt container held a target. moby docker-v29.8.1 allocates no gateway in isolated mode (bridge_linux.go:700-713, network.go:1594-1602), so the server always holds .1 on $S-int and every live probe would have refused. Targets now come from recorded IPAM gateways only; the attempt containers' own addresses (server_int, proxy_int, proxy_gw) are excluded and recorded by role in targets.excluded, never a refusal. The unit fixtures now model the isolated network (no int gateway, server at .1). R4. P2 now: - starts with a positive control, a TCP connect to gw:8081; - requires every pair outside the run subnet to fail with ENETUNREACH or EHOSTUNREACH, not a timeout (the host passes --subnets and classifies from the expected pairs); - sends one DNS datagram (RFC 1035 4.1.1-4.1.2) to 10.0.2.3:53, which must get no answer and fail for want of a route; - reads /proc/net/route (linux v6.18 fib_trie.c:2940-3000) and requires no default route and no RTF_GATEWAY route. Addendum (b). The P2 summary and receipt keep dns_errors (EAI_NONAME vs EAI_AGAIN); the pass condition is still that no address resolved. README probe section and research.md:436 are corrected to match. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 26 +- .../runtime-workers/openhands/e2e/netprobe.py | 176 ++++++++++-- blueprints/runtime-workers/openhands/host.py | 68 +++-- .../runtime-workers/openhands/receipt.py | 8 +- .../runtime-workers/openhands/research.md | 9 +- tests/test_runtime_worker_openhands.py | 260 +++++++++++++++--- 6 files changed, 455 insertions(+), 92 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index b21d595fc..7339efc40 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -712,18 +712,30 @@ after P0 passed. | --- | --- | --- | | P0, negative control | `$S-gw` | GET `http://10.0.2.2:/v1/models` returns 200 with route class `CLIENT_API`, and GET `/api/settings` returns a response with route class `MANAGEMENT`. This shows the vehicle can see the gateway and its management surface. | | P1, management isolation | `$S-int` | GET `http://gw:8081/v1/models` returns 200 with `CLIENT_API`. Each of the plan's 26 denied method/target pairs returns 403 with no route-class header, so the proxy refused it rather than the gateway. The pairs cover management paths, dot-segment and `%2e` variants, case and slash variants, query strings and wrong methods. | -| P2, direct reachability | `$S-int` | No address and port pair accepts a TCP connect. `example.com`, `github.com` and `host.docker.internal` do not resolve, while `gw` and `$S-server` do. The container has no non-loopback IPv6 address. | +| P2, direct reachability | `$S-int` | A positive control first: a TCP connect to `gw:8081` succeeds. Then no address and port pair accepts a TCP connect, and every pair outside the run subnet fails for want of a route (`ENETUNREACH` or `EHOSTUNREACH`), not by timing out. One DNS datagram to 10.0.2.3:53 gets no answer, and fails the same way. `example.com`, `github.com` and `host.docker.internal` do not resolve, while `gw` and `$S-server` do. The container has no non-loopback IPv6 address, and `/proc/net/route` holds no default route and no route via a gateway. | P2's targets are computed on the host at probe time: - **Addresses:** - 10.0.2.2, 10.0.2.3, 172.17.0.1, 10.0.0.1 and 10.255.255.254; - every non-loopback host IPv4 address from `ip -4 -o addr`; - - each attempt network's subnet `.1` and IPAM gateway. + - each gateway that an attempt network's IPAM actually records. - **Ports:** every TCP listener from `ss -ltnH`, plus 53. -The probe refuses to run if a target is the address of its own attempt's -server or proxy. Network inspect values are context only; the failed connects -are the evidence. +`$S-int` records no gateway. moby@464cd50c (docker-v29.8.1) skips gateway +allocation in isolated mode (`daemon/libnetwork/drivers/bridge/bridge_linux.go:700-713`, +`daemon/libnetwork/network.go:1594-1602`), so the server, the first endpoint, +holds the subnet's `.1`. The attempt containers' own addresses (the server's +and the proxy's on `$S-int`, the proxy's on `$S-gw`) are removed from the +targets. The receipt records, by role, any that coincided with a target; they +never stop the probe. The host passes the run subnet to the probe, and the +host-side verdict classifies each pair as inside or outside it from the +expected list, never from the probe's own record. + +The route-table check reads the main table only, as the kernel writes it +(linux@v6.18 `net/ipv4/fib_trie.c:2940-3000`). For the three names that must +not resolve, the receipt keeps the resolver's error class, such as `EAI_NONAME` +or `EAI_AGAIN`. The pass condition is still only that no address resolved. +Network inspect values are context only; the failed connects are the evidence. The receipt is `runs///isolation-probe.json`: mode 0600, host-owned and outside every model mount. It holds: @@ -733,8 +745,8 @@ host-owned and outside every model mount. It holds: - `proxy_image`, `proxy_config_sha256` and `probe_script_sha256`; - `upstream` (`10.0.2.2:`), `host_ingress` (`127.0.0.1:`) and `verified_at`; -- the probe containers' exit codes, target counts, the P0/P1/P2 counts and - booleans, and `passed`. +- the probe containers' exit codes, target counts and excluded roles, the + P0/P1/P2 counts, error-name counts and booleans, and `passed`. `dispatch.py start`, including the `host.py run` path, calls `verify_isolation` before it takes the serial reservation. The gate requires: diff --git a/blueprints/runtime-workers/openhands/e2e/netprobe.py b/blueprints/runtime-workers/openhands/e2e/netprobe.py index da5bf4ed4..c154fedc0 100644 --- a/blueprints/runtime-workers/openhands/e2e/netprobe.py +++ b/blueprints/runtime-workers/openhands/e2e/netprobe.py @@ -10,9 +10,14 @@ (GET /api/settings -> MANAGEMENT). - mode int, on -int: P1, only the proxy's three /v1 routes answer and every other method or target gets the proxy's own 403 with no route-class - header; then P2, every TCP connect to a host-side address fails, public - names and host.docker.internal do not resolve, gw and the server do, and - no interface other than lo has an IPv6 address. + header; then P2. Its positive control, a TCP connect to gw:8081, must + succeed. Every TCP connect to a host-side address must fail, and one + outside the run subnet must fail for want of a route (ENETUNREACH or + EHOSTUNREACH), not by timing out. One DNS datagram to 10.0.2.3:53 must get + no answer. Public names and host.docker.internal must not resolve, while gw + and the server do; the resolver's error name is kept. No interface other + than lo may have an IPv6 address, and /proc/net/route may hold no default + or gateway route. Raw request targets go through http.client, which sends them verbatim (the equivalent of curl --path-as-is). Only the status code, the route-class @@ -35,6 +40,7 @@ from pathlib import Path import re import socket +import struct # OmniRoute's response header separating gateway answers from proxy denials @@ -62,12 +68,22 @@ # attempt networks' gateways (host.probe_targets). FIXED_ADDRESSES = ("10.0.2.2", "10.0.2.3", "172.17.0.1", "10.0.0.1", "10.255.255.254") EXTRA_PORTS = (53,) +# With no route, connect() fails at once with one of these; a timeout means a +# route exists and something else dropped the packet. +UNREACHABLE = ("ENETUNREACH", "EHOSTUNREACH") +# slirp4netns's DNS address (rootlesskit@v3.1.0, the running version, docs/network.md:81-82). +UDP_TARGET = ("10.0.2.3", 53) +# RFC 1035 section 4.1.1-4.1.2: ID, RD set, one question: example.com A IN. +DNS_QUERY = (struct.pack("!6H", 0x4E41, 0x0100, 1, 0, 0, 0) + b"\x07example\x03com\x00" + + struct.pack("!2H", 1, 1)) SERVER_NAME = re.compile(r"rw-openhands-[a-z0-9-]{1,64}-(?:control|engines-on)-server") UPSTREAM_PORTS = (20128, 20129) HTTP_TIMEOUT = 10.0 CONNECT_TIMEOUT = 2.0 WORKERS = 64 IF_INET6 = "/proc/net/if_inet6" +ROUTES = "/proc/net/route" +RTF_GATEWAY = 0x0002 # linux@v6.18 include/uapi/linux/route.h:52 def p0_expected(upstream_port): @@ -135,6 +151,38 @@ def connect_all(pairs, *, timeout=CONNECT_TIMEOUT, workers=WORKERS): return list(pool.map(lambda pair: connect_probe(*pair, timeout=timeout), pairs)) +def control_probe(*, timeout=CONNECT_TIMEOUT): + """The P2 positive control: one TCP connect to gw:8081 by name, which must succeed. + + It shows that the connect path works from this container, so the refused + connects that follow are not an artifact of the probe itself. + """ + record = {"host": PROXY_HOST, "port": PROXY_PORT, "connected": False, "error": None} + try: + with socket.create_connection((PROXY_HOST, PROXY_PORT), timeout=timeout): + record["connected"] = True + except OSError as exc: + record["error"] = error_name(exc) + return record + + +def udp_probe(address, port, *, timeout=CONNECT_TIMEOUT): + """One DNS query datagram to an IPv4 literal; only whether anything answered is kept.""" + record = {"address": address, "port": port, "answered": False, "error": None} + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(timeout) + try: + # connect() on a datagram socket looks up the route, so a missing + # route fails here rather than as a silent drop. + sock.connect((address, port)) + sock.send(DNS_QUERY) + sock.recv(512) + record["answered"] = True + except OSError as exc: + record["error"] = error_name(exc) + return record + + def resolve(name): try: found = socket.getaddrinfo(name, None, proto=socket.IPPROTO_TCP) @@ -161,6 +209,35 @@ def read_ipv6(): return {"available": False, "loopback": 0, "non_loopback": 0, "link_local": 0} +def route_summary(text): + """Count /proc/net/route rows, the default ones and those via a gateway. + + linux@v6.18 net/ipv4/fib_trie.c:2940-3000 writes the main table: a header, + then iface, destination, gateway, flags, refcnt, use, metric, mask, mtu, + window, irtt, with destination, gateway and mask as %08X and flags as %04X. + A default route has destination and mask 0; RTF_GATEWAY marks a route via + a gateway (fib_trie.c:2916-2932). No address is returned. + """ + lines = [line.split() for line in text.splitlines() if line.strip()] + if not lines or lines[0][:2] != ["Iface", "Destination"]: + raise ValueError("unexpected_proc_net_route_format") + rows = lines[1:] + if any(len(row) != 11 or not all(re.fullmatch(r"[0-9A-F]{8}", row[index]) for index in (1, 2, 7)) + or not re.fullmatch(r"[0-9A-F]{4}", row[3]) for row in rows): + raise ValueError("unexpected_proc_net_route_format") + return {"available": True, "routes": len(rows), + "default": sum(int(row[1], 16) == 0 and int(row[7], 16) == 0 for row in rows), + "gateway": sum(bool(int(row[3], 16) & RTF_GATEWAY) or int(row[2], 16) != 0 for row in rows)} + + +def read_routes(): + try: + with open(ROUTES) as stream: + return route_summary(stream.read()) + except FileNotFoundError: + return {"available": False, "routes": 0, "default": 0, "gateway": 0} + + def http_matches(record, expected): host, port, method, target, status, route = expected return (isinstance(record, dict) and record.get("host") == host and record.get("port") == port @@ -180,36 +257,71 @@ def count(value): return value if type(value) is int and value >= 0 else None -def evaluate_p2(p2, addresses, ports, server): +def error_counts(records, flag): + """Error names of the records whose flag is False; anything unexpected is "".""" + return dict(sorted(Counter( + record["error"] if isinstance(record.get("error"), str) and re.fullmatch(r"[A-Za-z_]{1,32}", record["error"]) + else "" for record in records if isinstance(record, dict) and record.get(flag) is False).items())) + + +def outside(address, subnets): + """True unless address is an IPv4 literal inside one of the run network's subnets.""" + try: + value = ipaddress.IPv4Address(address) + except ValueError: + return True + return not any(value in subnet for subnet in subnets) + + +def evaluate_p2(p2, addresses, ports, server, subnets): p2 = p2 if isinstance(p2, dict) else {} + subnets = [ipaddress.IPv4Network(value) for value in subnets] pairs = [(address, port) for address in addresses for port in ports] connects = p2.get("connects") if isinstance(p2.get("connects"), list) else [] aligned = len(connects) == len(pairs) and all( isinstance(record, dict) and (record.get("address"), record.get("port")) == pair for record, pair in zip(connects, pairs)) connected = sum(not isinstance(record, dict) or record.get("connected") is not False for record in connects) - errors = Counter(record.get("error") if isinstance(record, dict) and isinstance(record.get("error"), str) - and re.fullmatch(r"[A-Za-z_]{1,32}", record["error"]) else "" - for record in connects if isinstance(record, dict) and record.get("connected") is False) + # Off-subnet is decided from the expected pairs, never from the record itself. + off_subnet = sum(outside(address, subnets) for address, _ in pairs) + unreachable = sum(isinstance(record, dict) and record.get("connected") is False + and record.get("error") in UNREACHABLE + for record, (address, _) in zip(connects, pairs) if outside(address, subnets)) + control = p2.get("control") if isinstance(p2.get("control"), dict) else {} + control_connected = (control.get("host"), control.get("port"), control.get("connected")) == ( + PROXY_HOST, PROXY_PORT, True) + udp = p2.get("udp") if isinstance(p2.get("udp"), dict) else {} + udp_answered = udp.get("answered") if type(udp.get("answered")) is bool else None + udp_passed = ((udp.get("address"), udp.get("port")) == UDP_TARGET and udp_answered is False + and (not outside(UDP_TARGET[0], subnets) or udp.get("error") in UNREACHABLE)) expected = dns_expected(server) dns = p2.get("dns") if isinstance(p2.get("dns"), list) else [] dns_matched = sum(isinstance(record, dict) and record.get("name") == name and record.get("resolved") is ok for record, (name, ok) in zip(dns, expected)) ipv6 = p2.get("ipv6") if isinstance(p2.get("ipv6"), dict) else {} non_loopback = count(ipv6.get("non_loopback")) - return {"connects": len(pairs), "observed": len(connects), "connected": connected, - "errors": dict(sorted(errors.items())), "dns": len(expected), "dns_matched": dns_matched, + routes = p2.get("routes") if isinstance(p2.get("routes"), dict) else {} + routes_available = routes.get("available") is True + default_routes, gateway_routes = count(routes.get("default")), count(routes.get("gateway")) + return {"control_connected": control_connected, "connects": len(pairs), "observed": len(connects), + "connected": connected, "errors": error_counts(connects, "connected"), "off_subnet": off_subnet, + "off_subnet_unreachable": unreachable, "udp_answered": udp_answered, + "udp_errors": error_counts([udp], "answered"), "dns": len(expected), "dns_matched": dns_matched, + # Addendum (b): the resolver's error class is kept; the pass condition is still "not resolved". + "dns_errors": error_counts(dns, "resolved"), "ipv6_non_loopback": non_loopback, "ipv6_link_local": count(ipv6.get("link_local")), - "ipv6_loopback": count(ipv6.get("loopback")), - "passed": (aligned and connected == 0 and len(dns) == len(expected) == dns_matched - and non_loopback == 0)} + "ipv6_loopback": count(ipv6.get("loopback")), "routes_available": routes_available, + "routes": count(routes.get("routes")), "default_routes": default_routes, "gateway_routes": gateway_routes, + "passed": (aligned and control_connected and connected == 0 and unreachable == off_subnet + and udp_passed and len(dns) == len(expected) == dns_matched and non_loopback == 0 + and routes_available and default_routes == 0 and gateway_routes == 0)} -def evaluate(gw, internal, upstream_port, addresses, ports, server): +def evaluate(gw, internal, upstream_port, addresses, ports, server, subnets): """The host's verdict, re-derived from both observation files.""" p0 = evaluate_http(gw.get("p0") if isinstance(gw, dict) else None, p0_expected(upstream_port)) p1 = evaluate_http(internal.get("p1") if isinstance(internal, dict) else None, P1_EXPECTED) - p2 = evaluate_p2(internal.get("p2") if isinstance(internal, dict) else None, addresses, ports, server) + p2 = evaluate_p2(internal.get("p2") if isinstance(internal, dict) else None, addresses, ports, server, subnets) return {"p0": p0, "p1": p1, "p2": p2, "passed": p0["passed"] and p1["passed"] and p2["passed"]} @@ -239,11 +351,22 @@ def run_http(observations, key, expected): return 0 -def run_p2(observations, addresses, ports, server): +def run_p2(observations, addresses, ports, server, subnets): p2 = observations.setdefault("p2", {}) + # The positive control first: if gw:8081 is unreachable, no refusal below means anything. + p2["control"] = control_probe() + if p2["control"].get("connected") is not True: + return 1 # One bounded batch, recorded whole and evaluated in order. - p2["connects"] = connect_all([(address, port) for address in addresses for port in ports]) - if any(record.get("connected") is not False for record in p2["connects"]): + pairs = [(address, port) for address in addresses for port in ports] + p2["connects"] = connect_all(pairs) + if any(record.get("connected") is not False + or (outside(address, subnets) and record.get("error") not in UNREACHABLE) + for record, (address, _) in zip(p2["connects"], pairs)): + return 1 + p2["udp"] = udp_probe(*UDP_TARGET) + if p2["udp"].get("answered") is not False or ( + outside(UDP_TARGET[0], subnets) and p2["udp"].get("error") not in UNREACHABLE): return 1 p2["dns"] = [] for name, resolvable in dns_expected(server): @@ -252,7 +375,11 @@ def run_p2(observations, addresses, ports, server): if record.get("resolved") is not resolvable: return 1 p2["ipv6"] = read_ipv6() - return 0 if p2["ipv6"].get("non_loopback") == 0 else 1 + if p2["ipv6"].get("non_loopback") != 0: + return 1 + p2["routes"] = read_routes() + routes = p2["routes"] + return 0 if routes.get("available") is True and routes.get("default") == 0 and routes.get("gateway") == 0 else 1 def parse_addresses(text): @@ -271,6 +398,14 @@ def parse_ports(text): return ports +def parse_subnets(text): + """The run network's IPv4 subnets; strict, so a CIDR with host bits set is refused.""" + subnets = [ipaddress.IPv4Network(part) for part in text.split(",") if part] + if not subnets: + raise ValueError("probe_subnets_required") + return subnets + + def write_observations(path, observations): """0644 whatever the umask, so the host user can read what UID 10001 wrote.""" descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644) @@ -287,17 +422,20 @@ def main(argv=None): parser.add_argument("--server") parser.add_argument("--addresses", default="") parser.add_argument("--ports", default="") + parser.add_argument("--subnets", default="") args = parser.parse_args(argv) if args.mode == "gw": expected = p0_expected(args.upstream_port) else: dns_expected(args.server) addresses, ports = parse_addresses(args.addresses), parse_ports(args.ports) + subnets = parse_subnets(args.subnets) observations = {"probe": "netprobe-v1", "mode": args.mode} try: if args.mode == "gw": return run_http(observations, "p0", expected) - return run_http(observations, "p1", P1_EXPECTED) or run_p2(observations, addresses, ports, args.server) + return (run_http(observations, "p1", P1_EXPECTED) + or run_p2(observations, addresses, ports, args.server, subnets)) finally: write_observations(args.out, observations) diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index 25058ef83..fb43f1cd1 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -787,26 +787,50 @@ def host_ipv4_addresses(text): return [str(address) for address in sorted(found)] -def probe_targets(networks, listeners, interfaces): +ATTEMPT_ROLES = ("server_int", "proxy_int", "proxy_gw") + + +def ipv4_entries(network): + entries = [entry for entry in network.get("ipam") or [] + if isinstance(entry, dict) and ":" not in str(entry.get("Subnet", ""))] + if not entries: + raise ValueError("attempt_network_ipam_required") + return entries + + +def ipv4_subnets(network): + """The network's IPv4 subnets as strict CIDR strings, from its selected-field IPAM view.""" + return [str(ipaddress.IPv4Network(entry["Subnet"])) for entry in ipv4_entries(network)] + + +def probe_targets(networks, listeners, interfaces, attempt_addresses): """P2 targets (plan section 1): addresses times ports, both sorted and deduplicated. - Addresses: netprobe.FIXED_ADDRESSES, every non-loopback host IPv4 address, - and each attempt network's IPAM gateway and subnet .1. Ports: every host - TCP listener at probe time plus netprobe.EXTRA_PORTS (53). + Addresses: netprobe.FIXED_ADDRESSES, every non-loopback host IPv4 address + and each gateway the attempt networks' IPAM actually records. The isolated + $S-int records none: moby@464cd50c (docker-v29.8.1) skips gateway + allocation in isolated mode (daemon/libnetwork/drivers/bridge/ + bridge_linux.go:700-713, network.go:1594-1602), so its first endpoint, the + server, holds the subnet's .1. The attempt containers' own addresses are + removed: a connect there reaches the attempt, not the host. The roles + removed are returned so the receipt can record them; they never cause a + refusal. Ports: every host TCP listener at probe time plus + netprobe.EXTRA_PORTS (53). """ + if set(attempt_addresses) != set(ATTEMPT_ROLES): + raise ValueError("attempt_container_addresses_required") + own = {role: ipaddress.IPv4Address(attempt_addresses[role]) for role in ATTEMPT_ROLES} addresses = {ipaddress.IPv4Address(value) for value in netprobe.FIXED_ADDRESSES} addresses |= {ipaddress.IPv4Address(value) for value in host_ipv4_addresses(interfaces)} for kind in ("int", "gw"): - entries = [entry for entry in networks[kind].get("ipam") or [] - if isinstance(entry, dict) and ":" not in str(entry.get("Subnet", ""))] - if not entries: - raise ValueError("attempt_network_ipam_required") - for entry in entries: - addresses.add(ipaddress.IPv4Network(entry["Subnet"]).network_address + 1) + for entry in ipv4_entries(networks[kind]): + ipaddress.IPv4Network(entry["Subnet"]) if entry.get("Gateway"): addresses.add(ipaddress.IPv4Address(entry["Gateway"])) + excluded = [role for role in ATTEMPT_ROLES if own[role] in addresses] + addresses -= set(own.values()) ports = sorted(set(listener_ports(listeners)) | set(netprobe.EXTRA_PORTS)) - return [str(address) for address in sorted(addresses)], ports + return [str(address) for address in sorted(addresses)], ports, excluded def checked_container(name, networks, aliases=None): @@ -882,19 +906,21 @@ def run_probe(state, run_id, arm, prefix, pins): if status.get("proxy_config_sha256") != config: raise ValueError("proxy_config_changed_since_prepare") networks, server, proxy = attempt_bindings(stem) + internal, gateway = networks["int"]["name"], networks["gw"]["name"] + own = {"server_int": server["networks"][internal].get("IPAddress"), + "proxy_int": proxy["networks"][internal].get("IPAddress"), + "proxy_gw": proxy["networks"][gateway].get("IPAddress")} listeners = subprocess.check_output(["ss", "-ltnH"], text=True, timeout=30) interfaces = subprocess.check_output(["ip", "-4", "-o", "addr"], text=True, timeout=30) - addresses, ports = probe_targets(networks, listeners, interfaces) - # A target holding one of the attempt's own containers would connect and - # read as a containment failure; refuse the probe instead. - internal = networks["int"]["name"] - if {view["networks"][internal].get("IPAddress") for view in (server, proxy)} & set(addresses): - raise ValueError("probe_target_is_attempt_container") + # An attempt container's own address is no host target; it is excluded and + # recorded by role (repair R2), never a reason to refuse. + addresses, ports, excluded = probe_targets(networks, listeners, interfaces, own) + subnets = ipv4_subnets(networks["int"]) private_directory(result / "probes") directory = Path(tempfile.mkdtemp(prefix="probe-", dir=result / "probes")) arguments = {"gw": ["--upstream-port", str(upstream)], "int": ["--server", stem + "-server", "--addresses", ",".join(addresses), - "--ports", ",".join(map(str, ports))]} + "--ports", ",".join(map(str, ports)), "--subnets", ",".join(subnets)]} observed, codes = {"gw": None, "int": None}, {"gw": None, "int": None} for role in ("gw", "int"): output = directory / role @@ -908,7 +934,8 @@ def run_probe(state, run_id, arm, prefix, pins): observed["gw"].get("p0") if isinstance(observed["gw"], dict) else None, netprobe.p0_expected(upstream))["passed"]): break - verdict = netprobe.evaluate(observed["gw"], observed["int"], upstream, addresses, ports, stem + "-server") + verdict = netprobe.evaluate(observed["gw"], observed["int"], upstream, addresses, ports, stem + "-server", + subnets) passed = verdict["passed"] and codes == {"gw": 0, "int": 0} write_private_json(result / "isolation-probe.json", { "mechanism": PROBE_MECHANISM, "run_id": run_id, "arm": arm, @@ -918,7 +945,8 @@ def run_probe(state, run_id, arm, prefix, pins): "probe_script_sha256": digest(HERE / "e2e/netprobe.py"), "upstream": f"{netprobe.GATEWAY_HOST}:{upstream}", "host_ingress": f"127.0.0.1:{port}", "verified_at": utc_now(), "exit_codes": codes, - "targets": {"addresses": len(addresses), "ports": ports, "pairs": len(addresses) * len(ports)}, + "targets": {"addresses": len(addresses), "ports": ports, "pairs": len(addresses) * len(ports), + "excluded": {role: 1 for role in excluded}}, "p0": verdict["p0"], "p1": verdict["p1"], "p2": verdict["p2"], "passed": passed, }) return passed diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 2710090b6..1821ddaf1 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -26,9 +26,11 @@ PROBE_FIELDS = { "p0": ("requests", "observed", "matched", "passed"), "p1": ("requests", "observed", "matched", "passed"), - "p2": ("connects", "observed", "connected", "errors", "dns", "dns_matched", "ipv6_non_loopback", - "ipv6_link_local", "ipv6_loopback", "passed"), - "targets": ("addresses", "ports", "pairs"), + "p2": ("control_connected", "connects", "observed", "connected", "errors", "off_subnet", + "off_subnet_unreachable", "udp_answered", "udp_errors", "dns", "dns_matched", "dns_errors", + "ipv6_non_loopback", "ipv6_link_local", "ipv6_loopback", "routes_available", "routes", + "default_routes", "gateway_routes", "passed"), + "targets": ("addresses", "ports", "pairs", "excluded"), } COLUMNS = ( "timestamp", "path", "status", "model", "reasoning_effort_requested", diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index 9ee89bf94..fcbe6fe46 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -433,8 +433,13 @@ Open items from this phase: - the container `--format` template, checked live only for its network counterpart. - **P2 is strict** about IPv6. Any non-loopback IPv6 address, including a - link-local one, fails it. An attempt network's subnet `.1` that belongs to - its own server or proxy makes the probe refuse to run rather than report. + link-local one, fails it. *(Corrected in the repair round.)* This build also + added each network's subnet `.1` as a target and refused to probe when an + attempt container held one. On the isolated `$S-int` the server always holds + `.1`, because moby allocates no gateway there, so every live probe would + have refused. Targets now come only from recorded IPAM gateways. The attempt + containers' own addresses are excluded and recorded by role, and never + cause a refusal. - **Still open from the plan:** - G2, the image scans; - G5, the model surface: a `/v1` body can pick any model the arm serves; diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 937fbb90d..ebd2da5da 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -9,6 +9,7 @@ import asyncio import contextlib from datetime import datetime, timedelta, timezone +import errno import hashlib import http.server import io @@ -1515,6 +1516,13 @@ def test_netprobe_lists_are_the_plan_lists_and_import_does_no_work(self): ("gw", True), (self.stem + "-server", True))) self.assertEqual(netprobe.FIXED_ADDRESSES, ("10.0.2.2", "10.0.2.3", "172.17.0.1", "10.0.0.1", "10.255.255.254")) self.assertEqual(netprobe.EXTRA_PORTS, (53,)) + # Repair R4: off-subnet connects must fail for want of a route, not time out. + self.assertEqual(netprobe.UNREACHABLE, ("ENETUNREACH", "EHOSTUNREACH")) + self.assertEqual(netprobe.UDP_TARGET, ("10.0.2.3", 53)) + self.assertEqual(netprobe.ROUTES, "/proc/net/route") + # RFC 1035 section 4.1.1-4.1.2: one recursive A/IN question for example.com. + self.assertEqual(netprobe.DNS_QUERY[2:12], bytes.fromhex("01000001000000000000")) + self.assertEqual(netprobe.DNS_QUERY[12:], b"\x07example\x03com\x00\x00\x01\x00\x01") # P3 is a documented skeleton; it is never run in phase 2. with self.assertRaises(NotImplementedError): netprobe.p3_control_call() @@ -1552,6 +1560,9 @@ def test_netprobe_connects_resolves_and_reads_ipv6_without_defaults(self): [("127.0.0.1", open_port, True, None), ("127.0.0.1", closed, False, "ECONNREFUSED")]) with patch.object(netprobe.socket, "getaddrinfo", side_effect=socket.gaierror(socket.EAI_NONAME, "fixture")): self.assertEqual(netprobe.resolve("example.com"), {"name": "example.com", "resolved": False, "error": "EAI_NONAME"}) + # Addendum (b): a temporary failure stays distinguishable from "no such name". + with patch.object(netprobe.socket, "getaddrinfo", side_effect=socket.gaierror(socket.EAI_AGAIN, "fixture")): + self.assertEqual(netprobe.resolve("github.com"), {"name": "github.com", "resolved": False, "error": "EAI_AGAIN"}) with patch.object(netprobe.socket, "getaddrinfo", return_value=[(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("172.30.0.3", 0))]): self.assertEqual(netprobe.resolve("gw"), {"name": "gw", "resolved": True, "error": None}) @@ -1565,6 +1576,74 @@ def test_netprobe_connects_resolves_and_reads_ipv6_without_defaults(self): with self.assertRaises(ValueError): netprobe.ipv6_summary("garbage\n") + @staticmethod + def closed_udp_port(): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe: + probe.bind(("127.0.0.1", 0)) + return probe.getsockname()[1] + + def test_netprobe_reads_routes_sends_one_udp_query_and_runs_the_positive_control(self): + netprobe = load_recipe_module("e2e/netprobe.py") + # linux@v6.18 net/ipv4/fib_trie.c:2940-3000 writes /proc/net/route from the main table: + # iface, destination, gateway, flags, refcnt, use, metric, mask, mtu, window, irtt. + header = "Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT".ljust(127) + "\n" + subnet = "eth0\t00001EAC\t00000000\t0001\t0\t0\t0\t0000FFFF\t0\t0\t0".ljust(127) + "\n" + default = "eth0\t00000000\t01001EAC\t0003\t0\t0\t0\t00000000\t0\t0\t0".ljust(127) + "\n" + via = "eth0\t0002000A\t01001EAC\t0003\t0\t0\t0\t00FFFFFF\t0\t0\t0".ljust(127) + "\n" + self.assertEqual(netprobe.route_summary(header + subnet), + {"available": True, "routes": 1, "default": 0, "gateway": 0}) + self.assertEqual(netprobe.route_summary(header + subnet + default), + {"available": True, "routes": 2, "default": 1, "gateway": 1}) + self.assertEqual(netprobe.route_summary(header + subnet + via), + {"available": True, "routes": 2, "default": 0, "gateway": 1}) + self.assertEqual(netprobe.route_summary(header), {"available": True, "routes": 0, "default": 0, "gateway": 0}) + for bad in ("", "garbage\n", header + "eth0\t0\t0\n", header + subnet.replace("\t0001\t", "\tzz01\t")): + with self.subTest(bad=bad), self.assertRaises(ValueError): + netprobe.route_summary(bad) + with patch.object(netprobe, "ROUTES", str(self.result / "absent-route-table")): + self.assertEqual(netprobe.read_routes(), {"available": False, "routes": 0, "default": 0, "gateway": 0}) + # One DNS datagram: answered, refused by ICMP, and unroutable. + answering = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self.addCleanup(answering.close) + answering.bind(("127.0.0.1", 0)) + received = [] + + def answer(): + data, peer = answering.recvfrom(512) + received.append(data) + answering.sendto(data[:2] + b"\x81\x80" + data[4:], peer) + thread = threading.Thread(target=answer, daemon=True) + thread.start() + port = answering.getsockname()[1] + self.assertEqual(netprobe.udp_probe("127.0.0.1", port, timeout=2), + {"address": "127.0.0.1", "port": port, "answered": True, "error": None}) + thread.join(2) + self.assertEqual(received, [netprobe.DNS_QUERY]) + # Whether a closed loopback port reports ICMP refusal or stays silent differs + # between hosts (this WSL2 host times out); both are "no answer". + silent = netprobe.udp_probe("127.0.0.1", self.closed_udp_port(), timeout=0.5) + self.assertEqual(silent["answered"], False) + self.assertIn(silent["error"], ("timeout", "ECONNREFUSED")) + with patch.object(netprobe.socket.socket, "recv", side_effect=ConnectionRefusedError(errno.ECONNREFUSED, "fixture")): + refused = netprobe.udp_probe("127.0.0.1", self.closed_udp_port()) + self.assertEqual((refused["answered"], refused["error"]), (False, "ECONNREFUSED")) + with patch.object(netprobe.socket.socket, "connect", side_effect=OSError(errno.ENETUNREACH, "fixture")): + self.assertEqual(netprobe.udp_probe("10.0.2.3", 53), + {"address": "10.0.2.3", "port": 53, "answered": False, "error": "ENETUNREACH"}) + # The positive control connects to gw:8081 by name; here a local listener stands in. + listener = socket.socket() + self.addCleanup(listener.close) + listener.bind(("127.0.0.1", 0)) + listener.listen() + with patch.object(netprobe, "PROXY_HOST", "127.0.0.1"), \ + patch.object(netprobe, "PROXY_PORT", listener.getsockname()[1]): + self.assertEqual(netprobe.control_probe(timeout=2), {"host": "127.0.0.1", "port": listener.getsockname()[1], + "connected": True, "error": None}) + closed = self.closed_port() + with patch.object(netprobe, "PROXY_HOST", "127.0.0.1"), patch.object(netprobe, "PROXY_PORT", closed): + self.assertEqual(netprobe.control_probe(timeout=2), {"host": "127.0.0.1", "port": closed, + "connected": False, "error": "ECONNREFUSED"}) + def expected_http(self, host_name, port, method, target): status = 200 if (method, target) == ("GET", "/v1/models") else 403 return {"host": host_name, "port": port, "method": method, "target": target, "status": status, @@ -1582,51 +1661,94 @@ def forwarded(host_name, port, method, path, **kwargs): previous = os.umask(0o077) self.addCleanup(os.umask, previous) with patch.object(netprobe, "http_probe", side_effect=forwarded) as http, \ + patch.object(netprobe, "control_probe") as control, patch.object(netprobe, "udp_probe") as udp, \ patch.object(netprobe, "connect_all") as connects, patch.object(netprobe, "resolve") as resolve: code = netprobe.main(["int", "--out", str(target), "--server", self.stem + "-server", - "--addresses", "10.0.2.2", "--ports", "20128"]) + "--addresses", "10.0.2.2", "--ports", "20128", "--subnets", "172.30.0.0/16"]) self.assertEqual(code, 1) stop = self.PLAN_DENIED.index(("GET", "/v1/files")) + 2 data = json.loads(target.read_text()) self.assertEqual(len(data["p1"]), stop) self.assertEqual(http.call_count, stop) - connects.assert_not_called() - resolve.assert_not_called() + for later in (control, udp, connects, resolve): + later.assert_not_called() self.assertNotIn("p2", data) self.assertEqual(stat.S_IMODE(target.stat().st_mode), 0o644) def test_netprobe_int_mode_passes_only_when_everything_is_refused(self): netprobe = load_recipe_module("e2e/netprobe.py") server = self.stem + "-server" - for label, connected, resolvable, ipv6 in (("contained", False, False, 0), ("connect", True, False, 0), - ("dns", False, True, 0), ("ipv6", False, False, 1)): + routes = {"available": True, "routes": 1, "default": 0, "gateway": 0} + cases = {"contained": {}, "connect": {"connected": True}, "off-subnet timeout": {"error": "timeout"}, + "dns": {"resolvable": True}, "ipv6": {"ipv6": 1}, "control refused": {"control": False}, + "udp answered": {"answered": True}, "udp timeout": {"udp_error": "timeout"}, + "default route": {"routes": dict(routes, default=1, gateway=1)}, + "gateway route": {"routes": dict(routes, gateway=1)}, + "no route table": {"routes": {"available": False, "routes": 0, "default": 0, "gateway": 0}}} + for label, case in cases.items(): with self.subTest(label): - target = self.result / f"{label}.json" + target = self.result / (label.replace(" ", "-") + ".json") + connected, reached = case.get("connected", False), case.get("control", True) def connects(pairs, **kwargs): - return [{"address": a, "port": p, "connected": connected, "error": None if connected else "ENETUNREACH"} - for a, p in pairs] + return [{"address": a, "port": p, "connected": connected, + "error": None if connected else case.get("error", "ENETUNREACH")} for a, p in pairs] def resolve(name): - ok = name in {"gw", server} or resolvable - return {"name": name, "resolved": ok, "error": None if ok else "EAI_NONAME"} + ok = name in {"gw", server} or case.get("resolvable", False) + return {"name": name, "resolved": ok, "error": None if ok else "EAI_AGAIN"} + control = {"host": "gw", "port": 8081, "connected": reached, "error": None if reached else "ECONNREFUSED"} + udp = {"address": "10.0.2.3", "port": 53, "answered": case.get("answered", False), + "error": None if case.get("answered") else case.get("udp_error", "ENETUNREACH")} with patch.object(netprobe, "http_probe", side_effect=self.expected_http), \ + patch.object(netprobe, "control_probe", return_value=control), \ patch.object(netprobe, "connect_all", side_effect=connects) as batch, \ + patch.object(netprobe, "udp_probe", return_value=udp) as datagram, \ patch.object(netprobe, "resolve", side_effect=resolve), \ patch.object(netprobe, "read_ipv6", return_value={ - "available": True, "loopback": 1, "non_loopback": ipv6, "link_local": ipv6}): + "available": True, "loopback": 1, "non_loopback": case.get("ipv6", 0), + "link_local": case.get("ipv6", 0)}), \ + patch.object(netprobe, "read_routes", return_value=case.get("routes", routes)): code = netprobe.main(["int", "--out", str(target), "--server", server, - "--addresses", "10.0.2.2,192.0.2.20", "--ports", "53,20128"]) + "--addresses", "10.0.2.2,192.0.2.20", "--ports", "53,20128", + "--subnets", "172.30.0.0/16"]) self.assertEqual(code, 0 if label == "contained" else 1) - self.assertEqual(batch.call_args.args[0], [("10.0.2.2", 53), ("10.0.2.2", 20128), - ("192.0.2.20", 53), ("192.0.2.20", 20128)]) + if reached: + self.assertEqual(batch.call_args.args[0], [("10.0.2.2", 53), ("10.0.2.2", 20128), + ("192.0.2.20", 53), ("192.0.2.20", 20128)]) + else: # The positive control failed, so nothing negative is attempted. + batch.assert_not_called() data = json.loads(target.read_text()) - verdict = netprobe.evaluate_p2(data.get("p2"), ["10.0.2.2", "192.0.2.20"], [53, 20128], server) + verdict = netprobe.evaluate_p2(data.get("p2"), ["10.0.2.2", "192.0.2.20"], [53, 20128], server, + ["172.30.0.0/16"]) self.assertEqual(verdict["passed"], label == "contained") - for argv in (["int", "--out", str(self.result / "x.json"), "--server", "other", "--addresses", "10.0.2.2", "--ports", "53"], - ["int", "--out", str(self.result / "y.json"), "--server", server, "--addresses", "gw", "--ports", "53"], - ["int", "--out", str(self.result / "z.json"), "--server", server, "--addresses", "10.0.2.2", "--ports", "0"]): - with self.subTest(argv=argv), patch.object(netprobe, "http_probe") as http, self.assertRaises(ValueError): + if label == "contained": + datagram.assert_called_once_with("10.0.2.3", 53) + self.assertEqual({key: verdict[key] for key in ( + "control_connected", "off_subnet", "off_subnet_unreachable", "udp_answered", "udp_errors", + "dns_errors", "routes_available", "routes", "default_routes", "gateway_routes")}, { + "control_connected": True, "off_subnet": 4, "off_subnet_unreachable": 4, + "udp_answered": False, "udp_errors": {"ENETUNREACH": 1}, "dns_errors": {"EAI_AGAIN": 3}, + "routes_available": True, "routes": 1, "default_routes": 0, "gateway_routes": 0}) + # A target inside the run subnet (a host address that collides with it) may time out, never connect. + inside = {"control": {"host": "gw", "port": 8081, "connected": True, "error": None}, + "connects": [{"address": "10.0.2.2", "port": 53, "connected": False, "error": "ENETUNREACH"}, + {"address": "172.30.0.9", "port": 53, "connected": False, "error": "timeout"}], + "udp": {"address": "10.0.2.3", "port": 53, "answered": False, "error": "EHOSTUNREACH"}, + "dns": [{"name": name, "resolved": ok, "error": None if ok else "EAI_NONAME"} + for name, ok in netprobe.dns_expected(server)], + "ipv6": {"available": True, "loopback": 1, "non_loopback": 0, "link_local": 0}, "routes": routes} + verdict = netprobe.evaluate_p2(inside, ["10.0.2.2", "172.30.0.9"], [53], server, ["172.30.0.0/16"]) + self.assertEqual((verdict["passed"], verdict["off_subnet"], verdict["off_subnet_unreachable"]), (True, 1, 1)) + inside["connects"][1].update(connected=True, error=None) + self.assertFalse(netprobe.evaluate_p2(inside, ["10.0.2.2", "172.30.0.9"], [53], server, ["172.30.0.0/16"])["passed"]) + base = ["--server", server, "--addresses", "10.0.2.2", "--ports", "53", "--subnets", "172.30.0.0/16"] + for index, change in enumerate(({"--server": "other"}, {"--addresses": "gw"}, {"--ports": "0"}, + {"--subnets": ""}, {"--subnets": "172.30.0.1/16"}, {"--subnets": "gw"})): + argv = ["int", "--out", str(self.result / f"refused-{index}.json")] + for flag, value in zip(base[::2], base[1::2]): + argv += [flag, change.get(flag, value)] + with self.subTest(change=change), patch.object(netprobe, "http_probe") as http, self.assertRaises(ValueError): netprobe.main(argv) http.assert_not_called() @@ -1637,17 +1759,33 @@ def test_probe_targets_come_from_ss_ip_and_the_attempt_networks(self): self.assertEqual(host.host_ipv4_addresses(self.IP), ["10.255.255.254", "172.17.0.1", "192.0.2.20"]) views = self.views() networks = {"int": views[("network", self.stem + "-int")], "gw": views[("network", self.stem + "-gw")]} - addresses, ports = host.probe_targets(networks, self.SS, self.IP) + own = {"server_int": "172.30.0.1", "proxy_int": "172.30.0.2", "proxy_gw": "172.31.0.2"} + addresses, ports, excluded = host.probe_targets(networks, self.SS, self.IP, own) + # Repair R2: only recorded IPAM gateways are added. The isolated $S-int has none, + # and its first endpoint, the server, holds 172.30.0.1. self.assertEqual(addresses, ["10.0.0.1", "10.0.2.2", "10.0.2.3", "10.255.255.254", "172.17.0.1", - "172.30.0.1", "172.31.0.1", "192.0.2.20"]) + "172.31.0.1", "192.0.2.20"]) self.assertEqual(ports, [53, 3000, 3730, 8080, 9000, 20128]) - no_gateway = dict(networks, int=dict(networks["int"], ipam=[{"Subnet": "172.29.0.0/16"}])) - self.assertIn("172.29.0.1", host.probe_targets(no_gateway, self.SS, self.IP)[0]) + self.assertEqual(excluded, []) + self.assertEqual(host.ipv4_subnets(networks["int"]), ["172.30.0.0/16"]) + gatewayed = dict(networks, int=dict(networks["int"], ipam=[{"Subnet": "172.29.0.0/16", "Gateway": "172.29.0.1"}])) + self.assertIn("172.29.0.1", host.probe_targets(gatewayed, self.SS, self.IP, own)[0]) + # An attempt container's own address is excluded and recorded by role, never refused. + addresses, _, excluded = host.probe_targets(networks, self.SS, self.IP, dict(own, proxy_gw="192.0.2.20")) + self.assertNotIn("192.0.2.20", addresses) + self.assertEqual(excluded, ["proxy_gw"]) + addresses, _, excluded = host.probe_targets(networks, self.SS, self.IP, dict(own, proxy_gw="172.31.0.1")) + self.assertEqual((excluded, "172.31.0.1" in addresses), (["proxy_gw"], False)) for bad in ("LISTEN 0 4096 127.0.0.1:notaport 0.0.0.0:*\n", "garbage\n", "LISTEN 0 4096 127.0.0.1:70000 0.0.0.0:*\n"): with self.subTest(bad=bad), self.assertRaises(ValueError): host.listener_ports(bad) with self.assertRaises(ValueError): - host.probe_targets(dict(networks, int=dict(networks["int"], ipam=[])), self.SS, self.IP) + host.probe_targets(dict(networks, int=dict(networks["int"], ipam=[])), self.SS, self.IP, own) + for role in own: + with self.subTest(role=role), self.assertRaises(ValueError): + host.probe_targets(networks, self.SS, self.IP, dict(own, **{role: ""})) + with self.assertRaises(ValueError): + host.probe_targets(networks, self.SS, self.IP, {"server_int": "172.30.0.1"}) def test_probe_containers_use_the_agent_image_hardening_and_only_their_mounts(self): host = load_recipe_module("host.py") @@ -1682,9 +1820,12 @@ def views(self): ids = {role: hashlib.sha256(name.encode()).hexdigest() for role, name in names.items()} def network(role, prefix, internal): + # moby@464cd50c (docker-v29.8.1): gateway mode isolated allocates no gateway + # (bridge_linux.go:700-713, network.go:1594-1602), so the first endpoint takes .1. + ipam = {"Subnet": prefix + ".0.0/16"} if internal else {"Subnet": prefix + ".0.0/16", "Gateway": prefix + ".0.1"} return {"id": ids[role], "name": names[role], "driver": "bridge", "internal": internal, "ipv6": False, "options": {self.ISOLATED: "isolated"} if internal else {}, "labels": dict(self.OWNER), - "ipam": [{"Subnet": prefix + ".0.0/16", "Gateway": prefix + ".0.1"}]} + "ipam": [ipam]} def endpoint(role, address, aliases=None): return {"NetworkID": ids[role], "IPAddress": address, "Aliases": aliases} @@ -1693,11 +1834,11 @@ def endpoint(role, address, aliases=None): ("network", names["gw"]): network("gw", "172.31", False), ("container", names["server"]): { "id": ids["server"], "name": "/" + names["server"], "running": True, "labels": dict(self.OWNER), - "networks": {names["int"]: endpoint("int", "172.30.0.2")}}, + "networks": {names["int"]: endpoint("int", "172.30.0.1")}}, ("container", names["proxy"]): { "id": ids["proxy"], "name": "/" + names["proxy"], "running": True, "labels": dict(self.OWNER), "networks": {names["gw"]: endpoint("gw", "172.31.0.2"), - names["int"]: endpoint("int", "172.30.0.3", ["gw"])}}, + names["int"]: endpoint("int", "172.30.0.2", ["gw"])}}, } def commands(self, host, views): @@ -1746,11 +1887,14 @@ def record(item): addresses = self.option(argv, "--addresses").split(",") ports = [int(port) for port in self.option(argv, "--ports").split(",")] return {"probe": "netprobe-v1", "mode": mode, "p1": [record(item) for item in netprobe.P1_EXPECTED], - "p2": {"connects": [{"address": a, "port": p, "connected": False, "error": "ENETUNREACH"} + "p2": {"control": {"host": "gw", "port": 8081, "connected": True, "error": None}, + "connects": [{"address": a, "port": p, "connected": False, "error": "ENETUNREACH"} for a in addresses for p in ports], + "udp": {"address": "10.0.2.3", "port": 53, "answered": False, "error": "ENETUNREACH"}, "dns": [{"name": name, "resolved": ok, "error": None if ok else "EAI_NONAME"} for name, ok in netprobe.dns_expected(self.option(argv, "--server"))], - "ipv6": {"available": True, "loopback": 1, "non_loopback": 0, "link_local": 0}}} + "ipv6": {"available": True, "loopback": 1, "non_loopback": 0, "link_local": 0}, + "routes": {"available": True, "routes": 1, "default": 0, "gateway": 0}}} def probe(self, host, views=None, mutate=None, codes=None): """host.run_probe with the two probe containers faked through execute_container.""" @@ -1801,10 +1945,16 @@ def test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate(self): "upstream": "10.0.2.2:20128", "host_ingress": "127.0.0.1:3740", "exit_codes": {"gw": 0, "int": 0}, "passed": True}) self.assertEqual((receipt["p0"]["requests"], receipt["p1"]["requests"]), (2, 27)) - self.assertEqual(receipt["targets"], {"addresses": 8, "ports": [53, 3000, 3730, 8080, 9000, 20128], "pairs": 48}) - self.assertEqual((receipt["p2"]["connects"], receipt["p2"]["connected"], receipt["p2"]["dns_matched"]), (48, 0, 5)) + self.assertEqual(receipt["targets"], {"addresses": 7, "ports": [53, 3000, 3730, 8080, 9000, 20128], "pairs": 42, + "excluded": {}}) + self.assertEqual((receipt["p2"]["connects"], receipt["p2"]["connected"], receipt["p2"]["dns_matched"]), (42, 0, 5)) + self.assertEqual({key: receipt["p2"][key] for key in ( + "control_connected", "off_subnet", "off_subnet_unreachable", "udp_answered", "dns_errors", + "routes_available", "default_routes", "gateway_routes")}, { + "control_connected": True, "off_subnet": 42, "off_subnet_unreachable": 42, "udp_answered": False, + "dns_errors": {"EAI_NONAME": 3}, "routes_available": True, "default_routes": 0, "gateway_routes": 0}) self.assertTrue(all(receipt[section]["passed"] for section in ("p0", "p1", "p2"))) - for address in ("192.0.2.20", "172.30.0.2", "172.30.0.3", "10.255.255.254"): + for address in ("192.0.2.20", "172.30.0.1", "172.30.0.2", "172.31.0.2", "10.255.255.254", "172.30.0.0"): self.assertNotIn(address, path.read_text()) # One container per network, each with an output directory of its own. self.assertEqual([name for name, _ in runs], [self.stem + "-probe-gw", self.stem + "-probe-int"]) @@ -1815,6 +1965,7 @@ def test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate(self): self.assertEqual(self.option(runs[0][1], "--upstream-port"), "20128") self.assertEqual(self.option(runs[1][1], "--ports"), "53,3000,3730,8080,9000,20128") self.assertEqual(self.option(runs[1][1], "--server"), self.stem + "-server") + self.assertEqual(self.option(runs[1][1], "--subnets"), "172.30.0.0/16") # The gate accepts the real writer's output while the live IDs match. verified = host.time_value(receipt["verified_at"]) self.assertEqual(self.gate(host, verified + timedelta(seconds=900))["passed"], True) @@ -1886,6 +2037,14 @@ def inside(change): ("truncated connects", inside(lambda d: d["p2"]["connects"].pop()), None), ("reordered denied list", inside(lambda d: d["p1"].reverse()), None), ("probe exit code", None, {"int": 1}), + ("off-subnet connect timed out", inside(lambda d: d["p2"]["connects"][0].update(error="timeout")), None), + ("default route", inside(lambda d: d["p2"]["routes"].update(default=1, gateway=1)), None), + ("gateway route", inside(lambda d: d["p2"]["routes"].update(gateway=1)), None), + ("route table unread", inside(lambda d: d["p2"]["routes"].update(available=False)), None), + ("positive control refused", inside(lambda d: d["p2"]["control"].update(connected=False)), None), + ("positive control missing", inside(lambda d: d["p2"].pop("control")), None), + ("udp answered", inside(lambda d: d["p2"]["udp"].update(answered=True, error=None)), None), + ("udp timed out", inside(lambda d: d["p2"]["udp"].update(error="timeout")), None), ) for label, mutate, codes in cases: with self.subTest(label): @@ -1905,15 +2064,31 @@ def inside(change): self.assertEqual((receipt["p0"]["passed"], receipt["p1"]["observed"], receipt["exit_codes"]["int"]), (False, 0, None)) - def test_probe_refuses_targets_that_are_its_own_attempt_containers(self): - # If IPAM does not reserve .1 in isolated mode, a container could hold it. + def test_probe_excludes_and_records_its_own_attempt_containers(self): + # Repair R2: in isolated mode the server holds 172.30.0.1, which is no target + # and never a refusal. host = self.prepared() views = self.views() - views[("container", self.stem + "-server")]["networks"][self.stem + "-int"]["IPAddress"] = "172.30.0.1" + self.assertEqual(views[("container", self.stem + "-server")]["networks"][self.stem + "-int"]["IPAddress"], + "172.30.0.1") + passed, runs = self.probe(host, views=views) + self.assertTrue(passed) + self.assertNotIn("172.30.0.1", self.option(runs[1][1], "--addresses").split(",")) + # A host address that an attempt container also holds is excluded and recorded by role. + views[("container", self.stem + "-proxy")]["networks"][self.stem + "-gw"]["IPAddress"] = "192.0.2.20" + passed, runs = self.probe(host, views=views) + self.assertTrue(passed) + self.assertNotIn("192.0.2.20", self.option(runs[1][1], "--addresses").split(",")) + path = self.result / "isolation-probe.json" + receipt = json.loads(path.read_text()) + self.assertEqual(receipt["targets"], {"addresses": 6, "ports": [53, 3000, 3730, 8080, 9000, 20128], + "pairs": 36, "excluded": {"proxy_gw": 1}}) + self.assertNotIn("192.0.2.20", path.read_text()) + # A container with no address on its own network breaks the live contract. + views[("container", self.stem + "-server")]["networks"][self.stem + "-int"]["IPAddress"] = "" with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views)), \ - patch.object(host, "execute_container") as execute, self.assertRaises(ValueError) as caught: + patch.object(host, "execute_container") as execute, self.assertRaises(ValueError): host.run_probe(self.state, self.run_id, self.arm, self.state / "prefix", self.pins) - self.assertEqual(str(caught.exception), "probe_target_is_attempt_container") execute.assert_not_called() def test_probe_requires_each_attempt_container_on_its_own_networks(self): @@ -2305,10 +2480,13 @@ def test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses(self): "host_ingress": "127.0.0.1:3730", "exit_codes": {"gw": 0, "int": 0}, "p0": {"requests": 2, "observed": 2, "matched": 2, "passed": True}, "p1": {"requests": 27, "observed": 27, "matched": 27, "passed": True}, - "p2": {"connects": 16, "observed": 16, "connected": 0, "errors": {"ENETUNREACH": 16}, - "dns": 5, "dns_matched": 5, "ipv6_non_loopback": 0, "ipv6_link_local": 0, - "ipv6_loopback": 1, "passed": True}, - "targets": {"addresses": 8, "ports": [53, 20128], "pairs": 16}} + "p2": {"control_connected": True, "connects": 16, "observed": 16, "connected": 0, + "errors": {"ENETUNREACH": 16}, "off_subnet": 16, "off_subnet_unreachable": 16, + "udp_answered": False, "udp_errors": {"ENETUNREACH": 1}, "dns": 5, "dns_matched": 5, + "dns_errors": {"EAI_AGAIN": 3}, "ipv6_non_loopback": 0, "ipv6_link_local": 0, + "ipv6_loopback": 1, "routes_available": True, "routes": 1, "default_routes": 0, + "gateway_routes": 0, "passed": True}, + "targets": {"addresses": 8, "ports": [53, 20128], "pairs": 16, "excluded": {"proxy_gw": 1}}} (result / "isolation-probe.json").write_text(json.dumps(probe)) (result / "probes/probe-fixture").mkdir(parents=True) (result / "probes/probe-fixture/gw.log.cleanup.json").write_text( From 5f29b5fb320c5ea991cc5fba2e4b65c05c94e059 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 17:17:22 -0400 Subject: [PATCH 18/22] OpenHands repair R6/G7 and probe-receipt trust: re-derive the verdict, require recorded stage gates Probe-receipt trust (GPT-6 host.py:953). verify_isolation trusted the receipt's passed booleans. checked_probe_counts now re-derives the verdict from the recorded counts and refuses contradictions: both exit codes 0; P0 and P1 fully observed and matched; a non-empty target set with pairs equal to addresses times ports; every P2 pair observed, failed with a named error and, off-subnet, unreachable; the positive control connected; no DNS answer; all five names matched; no IPv6 address, default route or gateway route. R6/G7. verify_stage_gates requires the host-owned (0600, outside the checkout) /stage-gates.json that the coordinator records live: passed G2 scans of both pinned image refs, P3 (plus P4 and P5 for engines-on) recorded under the pinned proxy image and the current proxy template digest with a gateway build, and the arm's G5 record, none dated after the check. The recipe never writes the file, so dispatch start and host.py run refuse until it exists. README, research.md and the decision record now describe the gate and its limit (the declared build is not checked against the running build). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 41 +++++-- blueprints/runtime-workers/openhands/host.py | 106 +++++++++++++++++- .../runtime-workers/openhands/research.md | 4 +- ...2026-09-28-openhands-resolver-isolation.md | 15 ++- tests/test_runtime_worker_openhands.py | 94 +++++++++++++++- 5 files changed, 238 insertions(+), 22 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 7339efc40..828c1711e 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -299,9 +299,10 @@ A prepared attempt that will not start a conversation now is removed with `host.py teardown` and the same identity flags ([live probe sequence](#live-probe-sequence-coordinator-not-run-yet)). -No conversation starts until the coordinator has run and recorded P3 on the -running gateway build (plan gate G7; the code checks only the P0-P2 half, see -[the gate](#isolation-probe-p0-p2-and-the-dispatch-gate)). After that, the +No conversation starts until the coordinator has run P3 on the running gateway +build and recorded it, with G2 and G5, in the host-owned stage-gates file (plan +gate G7). `dispatch.py start` refuses without that file; see +[the gate](#isolation-probe-p0-p2-and-the-dispatch-gate). After that, the workflow child runs exactly these three Bash commands after preparation: ```sh @@ -755,18 +756,40 @@ before it takes the serial reservation. The gate requires: - the expected mechanism, identity, upstream, ingress and proxy image; - the current rendered config's hash, which must also equal `status.json`'s, and the current netprobe hash; -- passed P0, P1 and P2; +- passed P0, P1 and P2, re-derived from the recorded counts rather than the + `passed` flags: both exit codes 0, every request observed and matched, a + non-empty target set, every pair observed and failed with a named error, + every off-subnet pair unreachable, the positive control connected, no DNS + answer, every name matched, and no IPv6 address, default route or gateway + route. A contradictory receipt is refused; +- the recorded stage gates below; - the four IDs equal to the live selected-field reads. A refusal prints `failure_stage: probe`, exits 3 and changes nothing. The run receipt (schema 6) carries an `isolation` summary without IDs, addresses or the run id. -**Plan gate G7 is only half enforced in code.** G7 requires P0-P2 within 900 s -of the start *and* a passed P3 on the running gateway build. The dispatch gate -checks P0-P2 only. Until the coordinator has run and recorded P3, do not use -`dispatch.py start` or `host.py run`. Prepare, read the probe receipt and tear -down instead. The engines-on arm also waits for P4 and P5. +**Stage gates (plan G7's P3 half, G2 and G5).** The gate also requires +`/stage-gates.json`. The coordinator writes it after observing those +gates live; this recipe only reads it. Until it exists, `dispatch.py start` +and `host.py run` refuse, so prepare, read the probe receipt and tear down +instead. The file must be owner-only (0600) and outside the checkout: + +```json +{"schema": "openhands-stage-gates-v1", + "g2": {"": {"passed": true, "recorded_at": ""}, + "": {"passed": true, "recorded_at": ""}}, + "probes": {"p3": {"passed": true, "recorded_at": "", "gateway_build": "", + "proxy_image": "", + "proxy_template_sha256": ""}, + "p4": "", "p5": ""}, + "g5": {"control": {"passed": true, "recorded_at": ""}, "engines-on": ""}} +``` + +The control arm needs P3; engines-on also needs P4 and P5. No record may be +dated after the check. A probe record binds to the pinned proxy image and the +current proxy template, so changing either needs new records. The gateway +build is recorded, but this code cannot tell which build is running. ### P3-P5: documented, not run diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index fb43f1cd1..9c6fa0b4c 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -67,6 +67,13 @@ # Plan section 1 and E1: a P0-P2 receipt gates dispatch start for at most 900 s # and only for the topology whose network and container IDs it records. PROBE_MAX_AGE_SECONDS = 900 +# Plan G7's P3 half, G2 (both image digests scanned and triaged) and G5, as the +# coordinator records them live in /stage-gates.json (repair R6). This +# recipe never writes that file. The engines-on arm also waits for P4 and P5 +# (README "P3-P5"). +STAGE_GATES = "stage-gates.json" +STAGE_GATES_SCHEMA = "openhands-stage-gates-v1" +STAGE_PROBES = {"control": ("p3",), "engines-on": ("p3", "p4", "p5")} def owned_port(value): @@ -952,28 +959,115 @@ def run_probe(state, run_id, arm, prefix, pins): return passed +def checked_probe_counts(receipt, upstream_port): + """Re-derive the probe verdict from the recorded counts; a passed flag alone is not evidence. + + Requires both probe exit codes 0, fully observed and matched P0 and P1, + a non-empty target set whose pair count is addresses times ports, and a + P2 in which every pair was observed and failed with a named error, every + off-subnet pair failed for want of a route, the positive control + connected, the DNS datagram got no answer, every expected name matched, + and no IPv6 address, default route or gateway route was found. Any + contradiction refuses. + """ + section = {name: receipt.get(name) if isinstance(receipt.get(name), dict) else {} + for name in ("p0", "p1", "p2", "targets")} + p2, targets = section["p2"], section["targets"] + addresses, ports, pairs = targets.get("addresses"), targets.get("ports"), targets.get("pairs") + errors = p2.get("errors") if isinstance(p2.get("errors"), dict) else {} + off_subnet = p2.get("off_subnet") + consistent = ( + receipt.get("exit_codes") == {"gw": 0, "int": 0} + and all(section[name].get("requests") == section[name].get("observed") == section[name].get("matched") + == expected and section[name].get("passed") is True + for name, expected in (("p0", len(netprobe.p0_expected(upstream_port))), + ("p1", len(netprobe.P1_EXPECTED)))) + and type(addresses) is int and addresses > 0 and isinstance(ports, list) and bool(ports) + and all(type(value) is int for value in ports) and pairs == addresses * len(ports) + and p2.get("connects") == p2.get("observed") == pairs and p2.get("connected") == 0 + and all(type(value) is int for value in errors.values()) and sum(errors.values()) == pairs + and type(off_subnet) is int and 0 < off_subnet <= pairs and p2.get("off_subnet_unreachable") == off_subnet + and p2.get("control_connected") is True and p2.get("udp_answered") is False + and p2.get("dns") == p2.get("dns_matched") == len(netprobe.UNRESOLVABLE) + 2 + and p2.get("ipv6_non_loopback") == 0 and p2.get("routes_available") is True + and p2.get("default_routes") == 0 and p2.get("gateway_routes") == 0 and p2.get("passed") is True) + if not consistent: + raise ValueError("isolation_probe_contradiction") + return receipt + + +def recorded(entry, now, **fields): + """A stage-gate entry: passed, recorded no later than now, and carrying these exact fields.""" + if not isinstance(entry, dict) or entry.get("passed") is not True: + return False + if any(entry.get(key) != value for key, value in fields.items()): + return False + try: + return time_value(entry.get("recorded_at")) <= now + except (ValueError, TypeError, OverflowError, OSError): + return False + + +def verify_stage_gates(state, arm, *, now): + """The coordinator's live records that code cannot observe (repair R6). + + /stage-gates.json must be an owner-only regular file outside the + checkout. It must hold passed G2 scans of both pinned image references + and each STAGE_PROBES entry for this arm, recorded under the pinned proxy + image and the current proxy template, with the gateway build it ran on. + It must also hold this arm's G5 record. Only the declared build is kept; + this check cannot tell which build is running. The file stays absent + until the gates are observed, so dispatch start refuses until then. + """ + arm_config(arm) + path = Path(state) / STAGE_GATES + if not path.exists(): + raise ValueError("stage_gates_not_recorded") + gates = json.loads(read_bounded(private_file(path), limit=1024 * 1024)) + if not isinstance(gates, dict) or gates.get("schema") != STAGE_GATES_SCHEMA: + raise ValueError("stage_gates_schema") + pins = read_json(HERE / "pins.json") + scans = gates.get("g2") if isinstance(gates.get("g2"), dict) else {} + if not all(recorded(scans.get(ref), now) for ref in (pins["image"]["ref"], pins["gateway_proxy"]["ref"])): + raise ValueError("stage_gate_g2_not_recorded") + probes = gates.get("probes") if isinstance(gates.get("probes"), dict) else {} + template = digest(PROXY_TEMPLATE) + if not all(recorded(probes.get(name), now, proxy_image=pins["gateway_proxy"]["ref"], + proxy_template_sha256=template) + and re.fullmatch(r"[0-9a-f]{7,40}", str(probes[name].get("gateway_build"))) + for name in STAGE_PROBES[arm]): + raise ValueError("stage_gate_probe_not_recorded") + surfaces = gates.get("g5") if isinstance(gates.get("g5"), dict) else {} + if not recorded(surfaces.get(arm), now): + raise ValueError("stage_gate_g5_not_recorded") + return gates + + def verify_isolation(state, run_id, arm, port, *, now=None): """The dispatch-start gate (plan E1, G7): a fresh passed probe bound to the live attempt. Refuses unless /isolation-probe.json is an owner-only regular file written at most PROBE_MAX_AGE_SECONDS ago, for this arm's upstream, this ingress port, the pinned proxy image, the rendered config and this probe - script, with every section passed, and unless the live network and - container IDs still match. File and time checks come before any Docker - read. A pure check: it changes nothing. + script, whose counts re-derive a pass (checked_probe_counts); unless the + coordinator's stage gates are recorded (verify_stage_gates); and unless the + live network and container IDs still match. File and time checks come + before any Docker read. A pure check: it changes nothing. """ stem = attempt_stem(run_id, arm) + now = now or datetime.now(timezone.utc) result = Path(state) / "runs" / run_id / arm receipt = json.loads(read_bounded(private_file(result / "isolation-probe.json"), limit=1024 * 1024)) if not isinstance(receipt, dict): raise ValueError("isolation_probe_receipt_shape") - age = ((now or datetime.now(timezone.utc)) - time_value(receipt.get("verified_at"))).total_seconds() + age = (now - time_value(receipt.get("verified_at"))).total_seconds() if not 0 <= age <= PROBE_MAX_AGE_SECONDS: raise ValueError("isolation_probe_not_fresh") status = json.loads(read_bounded(result / "status.json")) config = digest(result / "proxy/nginx.conf") + upstream = arm_config(arm)["gateway_port"] expected = {"mechanism": PROBE_MECHANISM, "run_id": run_id, "arm": arm, - "upstream": f"{netprobe.GATEWAY_HOST}:{arm_config(arm)['gateway_port']}", + "upstream": f"{netprobe.GATEWAY_HOST}:{upstream}", "host_ingress": f"127.0.0.1:{owned_port(port)}", "proxy_image": read_json(HERE / "pins.json")["gateway_proxy"]["ref"], "proxy_config_sha256": config, "probe_script_sha256": digest(HERE / "e2e/netprobe.py"), @@ -983,6 +1077,8 @@ def verify_isolation(state, run_id, arm, port, *, now=None): or any(not isinstance(receipt.get(section), dict) or receipt[section].get("passed") is not True for section in ("p0", "p1", "p2"))): raise ValueError("isolation_probe_mismatch") + checked_probe_counts(receipt, upstream) + verify_stage_gates(state, arm, now=now) networks, server, proxy = attempt_bindings(stem) live = {"run_network_id": networks["int"]["id"], "gw_network_id": networks["gw"]["id"], "server_container_id": server["id"], "proxy_container_id": proxy["id"]} diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index fcbe6fe46..cd85cdf28 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -425,7 +425,9 @@ container and made no gateway or model request. Open items from this phase: - **Not run:** the live P0-P2 probe, and the P3-P5 model calls, which exist as - documented steps and a skeleton. G7's P3 half is not enforced in code. + documented steps and a skeleton. G7's P3 half was not enforced in code; + *(repair round)* the dispatch gate now requires the coordinator's recorded + stage-gates file. - **Unprobed:** - gateway mode `isolated` under rootless Docker 29.8.1; - nginx under `--read-only` with `--entrypoint nginx`; diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index 9519015db..765cea72b 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -27,8 +27,12 @@ its own source, and a source the build did not re-read is labelled as reviewed i - **The only secret is a per-attempt agent-server session key.** It is generated by the host, kept in two owner-only files and deleted after confirmed removal (plan item E2). - **Before the first conversation,** a P0-P2 probe receipt must be at most 900 seconds old and - bound to the live network and container IDs. The dispatch gate enforces this. The coordinator - must also have recorded P3 on the running gateway build; code does not enforce that half. + bound to the live network and container IDs, and its counts must re-derive a pass. The + dispatch gate enforces this. It also requires the host-owned `/stage-gates.json`, in + which the coordinator records P3 (and P4 and P5 for engines-on), the G2 scans of both pinned + images and G5. The file stays absent until those gates are observed live, so dispatch start + refuses until then. The gateway build a probe ran on is recorded, not checked against the + running build. - **Official grading has no network.** Every grader container is created with network mode `none`, "No networking for this container" (docker/docker-py@7.1.0 `docker/models/containers.py:686-694`). The adapter refuses any other mode, any SDK create body @@ -193,9 +197,10 @@ change implements none of it. - **`host.py teardown` added.** It is not a plan item. It removes a prepared attempt after a probe-only acceptance run and retries unconfirmed removals. It refuses while a conversation may be live. -- **Plan gate G7 is only half enforced in code.** The dispatch gate checks P0-P2. P3 is a live - model call written as documented steps and a skeleton, and it has not run. The coordinator must - record P3 before any conversation starts. +- **Plan gate G7's P3 half is enforced through a recorded file (repair R6).** P3 is a live model + call written as documented steps and a skeleton, and it has not run. The dispatch gate reads the + coordinator's `stage-gates.json` record of it rather than observing P3 itself. The same file + carries G2 and G5, and the engines-on arm's P4 and P5. ## Evidence class diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index ebd2da5da..a2349299c 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -1867,8 +1867,24 @@ def prepared(self, port=3740): (self.result / "status.json").write_text(json.dumps({ "run_id": self.run_id, "arm": self.arm, "status": "prepared", "server_name": self.stem + "-server", "proxy_name": self.stem + "-proxy", "port": port, "proxy_config_sha256": sha})) + self.stage_gates(host) return host + def stage_gates(self, host, **changes): + """/stage-gates.json as the coordinator records it after live P3-P5, G2 and G5 (repair R6).""" + when = (datetime.now(timezone.utc) - timedelta(hours=1)).isoformat() + probe = {"passed": True, "recorded_at": when, "gateway_build": "045aa81f3", + "proxy_image": self.pins["gateway_proxy"]["ref"], + "proxy_template_sha256": hashlib.sha256((RECIPE / "config/proxy-nginx.conf").read_bytes()).hexdigest()} + gates = {"schema": "openhands-stage-gates-v1", + "g2": {ref: {"passed": True, "recorded_at": when} + for ref in (self.pins["image"]["ref"], self.pins["gateway_proxy"]["ref"])}, + "probes": {name: dict(probe) for name in ("p3", "p4", "p5")}, + "g5": {arm: {"passed": True, "recorded_at": when} for arm in ("control", "engines-on")}} + gates.update(changes) + host.write_private_json(self.state / "stage-gates.json", gates) + return gates + @staticmethod def option(argv, flag): return argv[argv.index(flag) + 1] @@ -1989,15 +2005,36 @@ def test_gate_refuses_stale_future_mismatched_or_rebound_receipts(self): ("re-created network", soon, rebound)): with self.subTest(label), self.assertRaises(ValueError): self.gate(host, now, views) + p2, targets = original["p2"], original["targets"] for label, change in (("cross-arm upstream", {"upstream": "10.0.2.2:20129"}), ("other arm", {"arm": "engines-on"}), ("not passed", {"passed": False}), - ("p2 not passed", {"p2": dict(original["p2"], passed=False)}), + ("p2 not passed", {"p2": dict(p2, passed=False)}), ("other mechanism", {"mechanism": "docker-user-v1"}), ("other ingress", {"host_ingress": "127.0.0.1:3730"}), ("other probe script", {"probe_script_sha256": "0" * 64}), ("other proxy image", {"proxy_image": "nginx:latest"}), - ("other server", {"server_container_id": "f" * 64})): + ("other server", {"server_container_id": "f" * 64}), + # The gate re-derives the verdict from the counts; passed flags alone are not trusted. + ("a connect succeeded", {"p2": dict(p2, connected=1)}), + ("no observations", {"p2": dict(p2, connects=0, observed=0, errors={}), + "targets": dict(targets, addresses=0, pairs=0)}), + ("probe exit code", {"exit_codes": {"gw": 0, "int": 1}}), + ("probe exit code missing", {"exit_codes": {"gw": 0}}), + ("p0 unobserved", {"p0": {"passed": True}}), + ("p1 short", {"p1": dict(original["p1"], observed=26, matched=26)}), + ("pairs inconsistent", {"targets": dict(targets, pairs=1)}), + ("connects short", {"p2": dict(p2, observed=41)}), + ("errors do not cover every connect", {"p2": dict(p2, errors={"ENETUNREACH": 1})}), + ("an off-subnet connect timed out", {"p2": dict(p2, off_subnet_unreachable=41)}), + ("nothing off-subnet", {"p2": dict(p2, off_subnet=0, off_subnet_unreachable=0)}), + ("positive control failed", {"p2": dict(p2, control_connected=False)}), + ("udp answered", {"p2": dict(p2, udp_answered=True)}), + ("dns short", {"p2": dict(p2, dns_matched=4)}), + ("ipv6 present", {"p2": dict(p2, ipv6_non_loopback=1)}), + ("default route", {"p2": dict(p2, default_routes=1)}), + ("gateway route", {"p2": dict(p2, gateway_routes=1)}), + ("route table unread", {"p2": dict(p2, routes_available=False)})): with self.subTest(label): host.write_private_json(path, {**original, **change}) with self.assertRaises(ValueError): @@ -2022,6 +2059,59 @@ def test_gate_refuses_stale_future_mismatched_or_rebound_receipts(self): with self.assertRaises(OSError): self.gate(host, soon) + def test_gate_requires_the_host_recorded_stage_gates(self): + # Repair R6 (plan G7's P3 half, G2 and G5): a host-owned file the coordinator records live. + host = self.prepared() + passed, _ = self.probe(host) + self.assertTrue(passed) + soon = host.time_value(json.loads((self.result / "isolation-probe.json").read_text())["verified_at"]) + soon += timedelta(seconds=60) + self.gate(host, soon) + gates = json.loads((self.state / "stage-gates.json").read_text()) + agent, proxy = self.pins["image"]["ref"], self.pins["gateway_proxy"]["ref"] + later = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat() + + def probe(**change): + return dict(gates, probes={**gates["probes"], "p3": dict(gates["probes"]["p3"], **change)}) + cases = (("other schema", dict(gates, schema="openhands-stage-gates-v0")), + ("agent image not scanned", dict(gates, g2={proxy: gates["g2"][proxy]})), + ("proxy scan not passed", dict(gates, g2={**gates["g2"], proxy: {"passed": False, + "recorded_at": gates["g2"][proxy]["recorded_at"]}})), + ("scan recorded in the future", dict(gates, g2={**gates["g2"], agent: {"passed": True, "recorded_at": later}})), + ("p3 not passed", probe(passed=False)), + ("p3 under another proxy template", probe(proxy_template_sha256="0" * 64)), + ("p3 under another proxy image", probe(proxy_image="nginx:latest")), + ("p3 recorded in the future", probe(recorded_at=later)), + ("p3 without a gateway build", probe(gateway_build="main")), + ("p3 absent", dict(gates, probes={"p4": gates["probes"]["p4"], "p5": gates["probes"]["p5"]})), + ("g5 for the other arm only", dict(gates, g5={"engines-on": gates["g5"]["engines-on"]})), + ("not an object", [])) + for label, value in cases: + with self.subTest(label): + host.write_private_json(self.state / "stage-gates.json", value) + with self.assertRaises(ValueError): + self.gate(host, soon) + # P4 and P5 bind only the engines-on arm. + host.write_private_json(self.state / "stage-gates.json", dict(gates, probes={"p3": gates["probes"]["p3"]})) + self.gate(host, soon) + with self.assertRaises(ValueError): + host.verify_stage_gates(self.state, "engines-on", now=soon) + host.write_private_json(self.state / "stage-gates.json", gates) + self.assertEqual(host.verify_stage_gates(self.state, "engines-on", now=soon), gates) + # Owner-only and present, or refused. + (self.state / "stage-gates.json").chmod(0o644) + with self.assertRaises(ValueError): + self.gate(host, soon) + (self.state / "stage-gates.json").unlink() + with self.assertRaisesRegex(ValueError, "stage_gates_not_recorded"): + self.gate(host, soon) + # The recipe only reads the file; the resolver PR's live run records it. + self.assertEqual(host.STAGE_GATES, "stage-gates.json") + for source in RECIPE.rglob("*.py"): + with self.subTest(source=source.name): + self.assertNotRegex(source.read_text(), + r"(?:write\w*|open|replace|rename|touch)\([^)\n]*(?:STAGE_GATES|stage-gates)") + def test_any_unexpected_observation_fails_the_probe_and_the_gate(self): host = self.prepared() From 30d2cd71047b85b559e88212735ff54f3733f4c4 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 17:25:03 -0400 Subject: [PATCH 19/22] OpenHands repair R3 (G5): read each reachable gateway store read-only and allowlist its providers verify_isolation now runs verify_gateway_providers before any Docker read. For each OmniRoute store the arm reaches (control: 20128; engines-on: 20129 and the 20128 store it forwards to), gateway_surface opens sqlite with mode=ro and query_only and runs four statements only: SELECT provider FROM provider_connections (no credential column) SELECT count(*) FROM combos the values of settings keys blockedProviders and noAuthFallbackDisabledProviders (no other settings key) check_gateway_surface refuses a provider row outside the host file's per-arm allowlist (gateway_providers: control codex, engines-on openai-compatible-responses-*; only a trailing * wildcard), any routing combo, and any no-auth or anonymous-fallback provider that settings do not disable by id or alias. Extension beyond the brief: OmniRoute 045aa81f3 and dd6e9607e serve those providers with a synthetic credential and no provider_connections row (src/sse/services/auth.ts:739-800,1193-1201), and they include the subprocess-backed devin-cli-agentic, auggie and zcode, so a row-only check would pass while they stay reachable. read_host_file is factored out of preflight, which now also validates the allowlists. Tests use fixture stores only (trace-checked statements, mode=ro, unchanged bytes, no secret in the output); an audited suite run recorded no access under ~/.local/share/omniroute*. README (new G5 section), research.md and the decision record describe the check, its consequence (the live stores refuse until those providers are disabled) and its limits (codex app-server opt-in unread, inactive rows count, combos refused not resolved). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 65 ++++- .../openhands/config/host.example.json | 4 + blueprints/runtime-workers/openhands/host.py | 143 +++++++++-- .../runtime-workers/openhands/research.md | 6 +- ...2026-09-28-openhands-resolver-isolation.md | 19 ++ tests/test_runtime_worker_openhands.py | 224 +++++++++++++++++- 6 files changed, 441 insertions(+), 20 deletions(-) diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 828c1711e..9c7ee6ba7 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -215,7 +215,9 @@ there is no global-skill fallback. Set these private inputs outside all worktrees: -- `OPENHANDS_HOST_FILE`: filled mode-0600 [host template](config/host.example.json). +- `OPENHANDS_HOST_FILE`: filled mode-0600 [host template](config/host.example.json), + including the per-arm `gateway_providers` allowlists; `dispatch.py start` + also reads it ([G5](#g5-gateway-provider-preflight)). - `OPENHANDS_STACK_ROOT`: stack checkout with the shared skill installer/manifest. - `OPENHANDS_TASK_FILE`, `OPENHANDS_TASK_SHA256`: frozen original SWE-bench row. - `OPENHANDS_GRADER_IMAGE_FILE`, `OPENHANDS_GRADER_IMAGE_SHA256`: frozen image pin. @@ -692,11 +694,68 @@ Proxy residuals: nginx would have host-loopback reach. It runs a static, pinned, non-root, read-only configuration. - A `/v1` request body reaches the gateway unchanged and can select any model - the arm serves (plan gate G5, open). + the arm serves. The G5 preflight below bounds which providers that can be. - Three things are unprobed, and P3 and P5 cover them: `--entrypoint nginx` under `--read-only`; body framing and streaming under the header allowlist; and gateway paths the SDK might use beyond the three routes. +### G5: gateway provider preflight + +Some OmniRoute executors on the `/v1` path spawn local processes (plan N6; +`open-sse/executors/devin-cli.ts:29`, `devin-cli-agentic.ts:1`, +`auggie.ts:26`, `zcodeProtocol.ts:1`, `open-sse/services/qoderCli.ts:1` and +`open-sse/vendor/codex-chatgpt-web/process.ts:2` at 045aa81f3). A model +reaching one of them would reach the host. So at every `dispatch.py start`, +before any Docker read, the gate checks which providers each gateway store the +arm reaches can serve: +- **Stores.** Control reads the 20128 store + (`~/.local/share/omniroute/storage.sqlite`). Engines-on reads the 20129 store + (`omniroute-fw`) and also the 20128 store, because 20129 forwards to 20128. + Each store is checked against its own arm's allowlist. +- **Allowlists.** They come from the host file's `gateway_providers`: control + `codex`, engines-on `openai-compatible-responses-*`. Only a trailing `*` is + a wildcard. `dispatch.py start` therefore also needs `OPENHANDS_HOST_FILE`. +- **Reads.** The store is opened with sqlite `mode=ro` and `query_only`. Four + statements run: `SELECT provider FROM provider_connections`, which reads no + credential column; `SELECT count(*) FROM combos`; and one value read each for + the settings keys `blockedProviders` and `noAuthFallbackDisabledProviders`. + The settings namespace also holds secrets, so no other key is read. +- **Refusals.** The gate refuses when: + - a provider row falls outside the allowlist; + - any routing combo exists (its targets are not read); + - a no-auth provider is named, by id or alias, in neither the allowlist nor + `blockedProviders`; + - an anonymous-fallback provider is named in neither the allowlist nor + `noAuthFallbackDisabledProviders`. + +The settings keys matter because a provider row is not the only way in. +OmniRoute serves its ten no-auth providers with a synthetic credential and no +row, unless `blockedProviders` names them (`src/sse/services/auth.ts:1193-1201`, +`noAuthProviderSettings.ts:5-18`, `src/shared/utils/noAuthProviders.ts:19-31`). +Those ten include the subprocess-backed devin-cli-agentic, auggie and zcode, +plus codex-app-server. Four anonymous-fallback providers (opencode-zen, +opencode-go, pollinations and kilocode) are also served without a row, unless +`noAuthFallbackDisabledProviders` names them (`auth.ts:739-800`). A row-only +check would pass while all of these stay reachable. Both lists are copied from +OmniRoute 045aa81f3 (20128) and dd6e9607e (20129), where the files are +identical. A gateway upgrade must re-read them. + +**Consequence.** The gate refuses until the operator disables those providers +in each store's OmniRoute settings. The coordinator's read-only observation on +2026-09-28 found six codex rows on 20128 and one +`openai-compatible-responses-` node on 20129. It did not read settings or +combos, so whether the live stores pass is unknown. The unit tests use fixture +stores only. + +**Limits.** +- A codex connection can opt into an app-server transport + (`providerSpecificData.codexTransport`, behind + `OMNIROUTE_CODEX_APP_SERVER_ENABLED`; `open-sse/executors/codex.ts:415-439`). + G5 does not read `provider_specific_data`, which may hold key material, so it + cannot see that opt-in. +- Inactive rows count as served. +- Routing combos are refused, not resolved. + ### Isolation probe (P0-P2) and the dispatch gate The vehicle is [e2e/netprobe.py](e2e/netprobe.py), a locally composed @@ -763,6 +822,8 @@ before it takes the serial reservation. The gate requires: answer, every name matched, and no IPv6 address, default route or gateway route. A contradictory receipt is refused; - the recorded stage gates below; +- the [G5 provider preflight](#g5-gateway-provider-preflight) for every store + the arm reaches; - the four IDs equal to the live selected-field reads. A refusal prints `failure_stage: probe`, exits 3 and changes nothing. The run diff --git a/blueprints/runtime-workers/openhands/config/host.example.json b/blueprints/runtime-workers/openhands/config/host.example.json index dd5781861..155d7168c 100644 --- a/blueprints/runtime-workers/openhands/config/host.example.json +++ b/blueprints/runtime-workers/openhands/config/host.example.json @@ -15,5 +15,9 @@ "foundation-adoption": "", "us-equities-foundation": "", "us-equities-catalog": "" + }, + "gateway_providers": { + "control": ["codex"], + "engines-on": ["openai-compatible-responses-*"] } } diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index 9c6fa0b4c..a85c970dd 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -9,6 +9,7 @@ import argparse import contextlib +import fnmatch import hashlib from datetime import datetime, timezone import json @@ -20,6 +21,7 @@ import secrets import shutil import signal +import sqlite3 import stat import subprocess import sys @@ -31,7 +33,7 @@ render_mcp) from e2e import netprobe from e2e.task import load_task, worker_instruction -from receipt import PROBE_MECHANISM, create_receipt, read_bounded, time_value +from receipt import PROBE_MECHANISM, create_receipt, gateway_database, read_bounded, time_value DOCKER = ["docker", "--context", "rootless"] @@ -74,6 +76,28 @@ STAGE_GATES = "stage-gates.json" STAGE_GATES_SCHEMA = "openhands-stage-gates-v1" STAGE_PROBES = {"control": ("p3",), "engines-on": ("p3", "p4", "p5")} +# Plan G5 (repair R3). OmniRoute@045aa81f3 (20128) and @dd6e9607e (20129), +# whose provider constants and settings helpers are identical: these +# providers are served with a synthetic credential and no provider_connections +# row. No-auth ones (src/shared/constants/providers/noauth.ts, noAuth: true at +# :14,32,51,67,86,100,120,138,162,181) are refused only when +# settings.blockedProviders names their id or alias +# (src/sse/services/auth.ts:1193-1201 at 045aa81f3; noAuthProviderSettings.ts:5-18; +# src/shared/utils/noAuthProviders.ts:19-31). Anonymous-fallback ones +# (anonymousFallback: true at apikey/gateways.ts:741,756, +# apikey/specialty-media.ts:56 and oauth.ts:243) are refused only when +# settings.noAuthFallbackDisabledProviders names them (auth.ts:739-800). +# devin-cli-agentic, auggie and zcode have subprocess executors +# (open-sse/executors/devin-cli-agentic.ts:1, auggie.ts:26, zcodeProtocol.ts:1). +NO_AUTH_PROVIDERS = {"devin-cli-agentic": "dva", "opencode": "oc", "duckduckgo-web": "ddgw", + "cloudflare-playground": "cfp", "veoaifree-web": "veo-free", "auggie": "aug", "zcode": "zc", + "codex-app-server": "cxa", "uncloseai": "unc", "aihorde": "horde"} +ANONYMOUS_FALLBACK_PROVIDERS = {"opencode-zen": "opencode-zen", "opencode-go": "opencode-go", + "pollinations": "pol", "kilocode": "kc"} +# The stores each arm's requests can reach, with the allowlist that applies to +# each: the engines-on gateway forwards to 20128 (README "P3-P5", P4). +GATEWAY_CHAIN = {"control": ("control",), "engines-on": ("engines-on", "control")} +ALLOWLIST_PATTERN = re.compile(r"[a-z0-9][a-z0-9-]{0,63}\*?") def owned_port(value): @@ -198,19 +222,8 @@ def selected_mcp_roots(eco): )] -def preflight(prefix, state): - pins = read_json(HERE / "pins.json") - if digest(HERE / "requirements.lock") != pins["requirements_sha256"]: - raise ValueError("runtime_lock_hash_mismatch") - if digest(HERE / "build-requirements.lock") != pins["build_requirements_sha256"]: - raise ValueError("build_lock_hash_mismatch") - expected_prefix = Path.home() / ".local/share/codex-ecosystem/tools" / ("openhands-" + pins["version"]) - expected_state = Path.home() / ".local/state/native-agent-stack/runtime-workers/openhands" - if prefix != expected_prefix or state != expected_state: - raise ValueError("unexpected_owned_prefix") - for path in (prefix, state): - if path.resolve() != path: - raise ValueError("owned_path_must_not_follow_symlinks") +def read_host_file(): + """The private OPENHANDS_HOST_FILE: owned, mode 0600, outside the checkout, placeholders filled.""" private_name = os.environ.get("OPENHANDS_HOST_FILE") if not private_name: raise ValueError("OPENHANDS_HOST_FILE_required") @@ -224,6 +237,101 @@ def preflight(prefix, state): host = read_json(path) if "<" in json.dumps(host): raise ValueError("host_file_has_unfilled_placeholders") + return host + + +def gateway_allowlists(host): + """Plan G5: the provider ids each arm's gateway may serve (host file gateway_providers). + + Exactly the two arms, each a non-empty list of lowercase ids, of which only + a trailing "*" may be a wildcard, so no pattern can match every provider. + """ + lists = host.get("gateway_providers") if isinstance(host, dict) else None + if (not isinstance(lists, dict) or set(lists) != {"control", "engines-on"} + or any(not isinstance(patterns, list) or not patterns + or any(not isinstance(pattern, str) or not ALLOWLIST_PATTERN.fullmatch(pattern) + for pattern in patterns) + for patterns in lists.values())): + raise ValueError("gateway_provider_allowlists_required") + return {arm: tuple(patterns) for arm, patterns in lists.items()} + + +def gateway_surface(database): + """Plan G5 inputs from one OmniRoute store, read-only (sqlite URI mode=ro and query_only). + + Four statements: the provider column of provider_connections and never a + credential column (OmniRoute src/lib/db/core.ts:220), the row count of + the routing combos table (:291-298), and the values of the two named + settings keys that disable row-less providers. OmniRoute keeps each + setting as JSON under key_value namespace "settings" (src/lib/db/settings.ts: + 154,283-293). That namespace also holds secrets, so no other key is read. + A value that is not a JSON list of strings reads as empty, which refuses. + """ + database = Path(database) + if not database.is_file() or database.is_symlink(): + raise ValueError("gateway_database_must_be_existing_regular_file") + connection = sqlite3.connect(database.resolve().as_uri() + "?mode=ro", uri=True, timeout=3) + try: + connection.execute("PRAGMA query_only = ON") + providers = sorted({row[0] for row in connection.execute("SELECT provider FROM provider_connections")}) + combos = connection.execute("SELECT count(*) FROM combos").fetchone()[0] + settings = {} + for key in ("blockedProviders", "noAuthFallbackDisabledProviders"): + row = connection.execute("SELECT value FROM key_value WHERE namespace = 'settings' AND key = ?", + (key,)).fetchone() + try: + value = json.loads(row[0]) if row else [] + except (TypeError, ValueError): + value = [] + settings[key] = sorted({item for item in value if isinstance(item, str)}) if isinstance(value, list) else [] + finally: + connection.close() + return {"providers": providers, "combos": combos, **settings} + + +def check_gateway_surface(surface, allowed): + """Refuse unless every provider this store can serve is in the arm's allowlist. + + Served means a provider_connections row, or a no-auth or anonymous-fallback + provider that the settings do not disable. Any routing combo is refused: + its targets are not read. Returns counts only. + """ + def permitted(provider): + return isinstance(provider, str) and any(fnmatch.fnmatchcase(provider, pattern) for pattern in allowed) + if not all(permitted(provider) for provider in surface["providers"]): + raise ValueError("gateway_provider_outside_allowlist") + if surface["combos"] != 0: + raise ValueError("gateway_routing_combo_refused") + for providers, key, reason in ( + (NO_AUTH_PROVIDERS, "blockedProviders", "gateway_no_auth_provider_not_blocked"), + (ANONYMOUS_FALLBACK_PROVIDERS, "noAuthFallbackDisabledProviders", "gateway_anonymous_fallback_not_disabled")): + named = set(surface[key]) + if any(not permitted(provider) and not {provider, alias} & named for provider, alias in providers.items()): + raise ValueError(reason) + return {"providers": len(surface["providers"]), "combos": 0} + + +def verify_gateway_providers(arm, allowlists): + """Plan G5 at dispatch start: every store the arm's requests can reach (GATEWAY_CHAIN).""" + for store in GATEWAY_CHAIN[arm_config(arm)["arm"]]: + check_gateway_surface(gateway_surface(gateway_database(store)), allowlists[store]) + + +def preflight(prefix, state): + pins = read_json(HERE / "pins.json") + if digest(HERE / "requirements.lock") != pins["requirements_sha256"]: + raise ValueError("runtime_lock_hash_mismatch") + if digest(HERE / "build-requirements.lock") != pins["build_requirements_sha256"]: + raise ValueError("build_lock_hash_mismatch") + expected_prefix = Path.home() / ".local/share/codex-ecosystem/tools" / ("openhands-" + pins["version"]) + expected_state = Path.home() / ".local/state/native-agent-stack/runtime-workers/openhands" + if prefix != expected_prefix or state != expected_state: + raise ValueError("unexpected_owned_prefix") + for path in (prefix, state): + if path.resolve() != path: + raise ValueError("owned_path_must_not_follow_symlinks") + host = read_host_file() + gateway_allowlists(host) variables = host["variables"] # These endpoints belong to the disabled ai-memory and socraticode entries # (config/mcp-policy.json). Under O1 they are unreachable by design from @@ -1050,8 +1158,10 @@ def verify_isolation(state, run_id, arm, port, *, now=None): written at most PROBE_MAX_AGE_SECONDS ago, for this arm's upstream, this ingress port, the pinned proxy image, the rendered config and this probe script, whose counts re-derive a pass (checked_probe_counts); unless the - coordinator's stage gates are recorded (verify_stage_gates); and unless the - live network and container IDs still match. File and time checks come + coordinator's stage gates are recorded (verify_stage_gates); unless every + provider the arm's gateway stores can serve is allowlisted in the host file + (verify_gateway_providers, plan G5); and unless the live network and + container IDs still match. File, time and read-only store checks come before any Docker read. A pure check: it changes nothing. """ stem = attempt_stem(run_id, arm) @@ -1079,6 +1189,7 @@ def verify_isolation(state, run_id, arm, port, *, now=None): raise ValueError("isolation_probe_mismatch") checked_probe_counts(receipt, upstream) verify_stage_gates(state, arm, now=now) + verify_gateway_providers(arm, gateway_allowlists(read_host_file())) networks, server, proxy = attempt_bindings(stem) live = {"run_network_id": networks["int"]["id"], "gw_network_id": networks["gw"]["id"], "server_container_id": server["id"], "proxy_container_id": proxy["id"]} diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index cd85cdf28..d3535f401 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -444,7 +444,11 @@ Open items from this phase: cause a refusal. - **Still open from the plan:** - G2, the image scans; - - G5, the model surface: a `/v1` body can pick any model the arm serves; + - G5, the model surface: a `/v1` body can pick any model the arm serves. + *(Repair round)* The dispatch gate now reads each reachable store + read-only and refuses providers outside a per-arm allowlist, including + row-less no-auth and anonymous-fallback providers that settings leave + enabled. It has not run against the live stores; - F10, the client peer the gateway logs; - normal-bridge containers such as cognee-live still reach host loopback. - **Outside this recipe's paths:** the repository's secret-path guard does not diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index 765cea72b..0d9248bc1 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -42,6 +42,15 @@ its own source, and a source the build did not re-read is labelled as reviewed i `daemon/container_operations.go:202-204,363-406`). This fails closed: an evaluation step that needs to download fails, so the verdict is an offline verdict (SWE-bench@v4.1.0 `test_spec/python.py:443-444`, `test_spec/test_spec.py:55-60`). +- **G5, the gateway provider surface, is checked at every start (repair R3).** The gate reads + each OmniRoute store the arm reaches, read-only: the provider column, the routing-combo count + and two named settings keys. It refuses unless every provider the store can serve is in the + host file's allowlist for that store's arm (control `codex`; engines-on + `openai-compatible-responses-*`, plus the 20128 store it forwards to). OmniRoute serves + no-auth and anonymous-fallback providers without a provider row, unless settings disable them + (`src/sse/services/auth.ts:739-800,1193-1201` at 045aa81f3). Some of these providers have + subprocess executors, so the gate requires every one to be disabled by id or alias. Routing + combos are refused. The stage-gates file must also carry the coordinator's G5 record. Isolation then holds by construction, whatever OmniRoute's authentication state. It also covers the other host-loopback listeners, not only OmniRoute. Gateway settings cannot provide it. The @@ -102,6 +111,9 @@ calls inside the container. including dot-segment and `%2e` variants, with fewer moving parts than O1. - **Graders need a network.** Replace mode `none` only with a separately probed internal network for graders, backed by its own P0-P2-style receipt. +- **G5 needs a routing combo, or OmniRoute changes its row-less providers.** For a combo, resolve + its targets against the allowlist rather than refusing. For a gateway upgrade, re-read the + no-auth and anonymous-fallback lists from its source before the next start. ## Agent-branch ruleset (resolver mode; applied by the owner) @@ -241,6 +253,13 @@ host evidence for this design is the live P0-P2 probe `swebench/harness/test_spec/python.py:443-444` and `test_spec/test_spec.py:55-60`. - OpenHands/software-agent-sdk@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, `config.py:24` and `dependencies.py:19`. +- OmniRoute@045aa81f3 (20128) and @dd6e9607e (20129), read for G5 in the repair round: + `src/sse/services/auth.ts:739-800,1193-1201` (lines shifted by one at dd6e9607e), + `src/sse/services/noAuthProviderSettings.ts:5-18`, `src/shared/utils/noAuthProviders.ts:19-31`, + `src/shared/constants/providers/noauth.ts`, `apikey/gateways.ts:741,756`, + `apikey/specialty-media.ts:56`, `oauth.ts:243`, `src/lib/db/core.ts` and + `src/lib/db/settings.ts:154,283-293`; `open-sse/executors/codex.ts:415-439` and the + `child_process` importers of plan N6. - OmniRoute@045aa81f3, as reviewed in the phase-2 synthesis and not re-read here: `src/lib/apiBridgeServer.ts`, `.env.example`, `policies/management.ts`, `apiAuth.ts`, `require-login/route.ts`, `apiKeys.ts`, at the lines cited above. diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index a2349299c..32a7d91f6 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -49,6 +49,56 @@ def load_recipe_module(filename): return module +# Plan G5 (repair R3) fixtures. OmniRoute@045aa81f3 and @dd6e9607e (identical +# files): providers served with a synthetic credential and no +# provider_connections row, src/shared/constants/providers/noauth.ts (noAuth) +# and apikey/gateways.ts:741,756, apikey/specialty-media.ts:56, oauth.ts:243 +# (anonymousFallback). +NO_AUTH_PROVIDERS = {"devin-cli-agentic": "dva", "opencode": "oc", "duckduckgo-web": "ddgw", + "cloudflare-playground": "cfp", "veoaifree-web": "veo-free", "auggie": "aug", "zcode": "zc", + "codex-app-server": "cxa", "uncloseai": "unc", "aihorde": "horde"} +ANONYMOUS_FALLBACK_PROVIDERS = {"opencode-zen": "opencode-zen", "opencode-go": "opencode-go", + "pollinations": "pol", "kilocode": "kc"} +GATEWAY_SECRETS = ("SECRET-ACCESS", "SECRET-REFRESH", "SECRET-API-KEY", "SECRET-ID-TOKEN", "SECRET-PSD", + "SECRET-OIDC", "SECRET-COMBO") +ENGINES_NODE = "openai-compatible-responses-" + str(uuid.UUID(int=7)) + + +def gateway_store(path, providers, *, combos=0, blocked=tuple(NO_AUTH_PROVIDERS), + fallback=tuple(ANONYMOUS_FALLBACK_PROVIDERS), settings=None, tables=None): + """A fixture OmniRoute storage.sqlite (src/lib/db/core.ts schema subset), with credential columns filled.""" + connection = sqlite3.connect(path) + try: + connection.executescript(tables or """ + CREATE TABLE provider_connections (id TEXT PRIMARY KEY, provider TEXT NOT NULL, auth_type TEXT, + access_token TEXT, refresh_token TEXT, api_key TEXT, id_token TEXT, provider_specific_data TEXT, + is_active INTEGER); + CREATE TABLE key_value (namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, + PRIMARY KEY (namespace, key)); + CREATE TABLE combos (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, data TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL); + """) + for number, provider in enumerate(providers): + connection.execute("INSERT INTO provider_connections VALUES (?, ?, 'oauth', 'SECRET-ACCESS', " + "'SECRET-REFRESH', 'SECRET-API-KEY', 'SECRET-ID-TOKEN', '{\"t\":\"SECRET-PSD\"}', 1)", + (f"connection-{number}", provider)) + for number in range(combos): + connection.execute("INSERT INTO combos VALUES (?, ?, '{\"models\":[\"SECRET-COMBO\"]}', 0, 't', 't')", + (f"combo-{number}", f"combo-{number}")) + values = {"oidcClientSecret": json.dumps("SECRET-OIDC")} + if blocked is not None: + values["blockedProviders"] = json.dumps(list(blocked)) + if fallback is not None: + values["noAuthFallbackDisabledProviders"] = json.dumps(list(fallback)) + values.update(settings or {}) + for key, value in values.items(): + connection.execute("INSERT INTO key_value VALUES ('settings', ?, ?)", (key, value)) + connection.commit() + finally: + connection.close() + return path + + class OpenHandsRecipeTests(unittest.TestCase): def read_json(self, relative): return json.loads((RECIPE / relative).read_text()) @@ -1459,6 +1509,13 @@ def setUp(self): self.result = self.state / "runs" / self.run_id / self.arm self.result.mkdir(parents=True) self.pins = json.loads((RECIPE / "pins.json").read_text()) + # G5 fixtures only; no unit test opens a live gateway store. + self.stores = {"control": gateway_store(self.state / "omniroute.sqlite", ["codex"] * 6), + "engines-on": gateway_store(self.state / "omniroute-fw.sqlite", [ENGINES_NODE])} + self.host_file = self.state / "host.json" + self.host_file.write_text(json.dumps({"gateway_providers": { + "control": ["codex"], "engines-on": ["openai-compatible-responses-*"]}})) + self.host_file.chmod(0o600) @staticmethod def closed_port(): @@ -1933,9 +1990,35 @@ def execute(argv, name, logfile, timeout): return passed, runs def gate(self, host, now, views=None): - with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views or self.views())): + with patch.object(host.subprocess, "check_output", side_effect=self.commands(host, views or self.views())), \ + patch.dict(os.environ, {"OPENHANDS_HOST_FILE": str(self.host_file)}), \ + patch.object(host, "gateway_database", side_effect=lambda arm: self.stores[arm]): return host.verify_isolation(self.state, self.run_id, self.arm, 3740, now=now) + def test_gate_runs_the_g5_provider_preflight_on_fixture_stores(self): + # Repair R3: the provider surface is read at each start, before any Docker read. + host = self.prepared() + passed, _ = self.probe(host) + self.assertTrue(passed) + soon = host.time_value(json.loads((self.result / "isolation-probe.json").read_text())["verified_at"]) + soon += timedelta(seconds=60) + self.gate(host, soon) + gateway_store(self.state / "wider.sqlite", ["codex", "openai"]) + with patch.object(host, "attempt_bindings") as docker, self.assertRaisesRegex( + ValueError, "gateway_provider_outside_allowlist"): + self.stores["control"] = self.state / "wider.sqlite" + self.gate(host, soon) + docker.assert_not_called() + self.stores["control"] = gateway_store(self.state / "unblocked.sqlite", ["codex"], blocked=["opencode"]) + with self.assertRaisesRegex(ValueError, "gateway_no_auth_provider_not_blocked"): + self.gate(host, soon) + self.stores["control"] = self.state / "omniroute.sqlite" + with patch.dict(os.environ, {"OPENHANDS_HOST_FILE": ""}), \ + patch.object(host.subprocess, "check_output", side_effect=self.commands(host, self.views())), \ + patch.object(host, "gateway_database", side_effect=lambda arm: self.stores[arm]), \ + self.assertRaisesRegex(ValueError, "OPENHANDS_HOST_FILE_required"): + host.verify_isolation(self.state, self.run_id, self.arm, 3740, now=soon) + def test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate(self): host = self.prepared() previous = os.umask(0o022) @@ -2338,6 +2421,145 @@ def test_teardown_action_removes_the_attempt_once_and_retires_a_prepared_status( self.assertEqual((args.action, args.run_id), ("teardown", self.run_id)) +class OpenHandsGatewaySurfaceTests(unittest.TestCase): + """Plan G5 (repair R3): each arm's provider surface from fixture OmniRoute stores. + + Our integration checks on synthetic sqlite files, never the live stores. + """ + ALLOWLISTS = {"control": ("codex",), "engines-on": ("openai-compatible-responses-*",)} + + def setUp(self): + self.host = load_recipe_module("host.py") + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name).resolve() + + def test_provider_lists_are_the_omniroute_source_lists(self): + self.assertEqual(self.host.NO_AUTH_PROVIDERS, NO_AUTH_PROVIDERS) + self.assertEqual(self.host.ANONYMOUS_FALLBACK_PROVIDERS, ANONYMOUS_FALLBACK_PROVIDERS) + self.assertEqual(self.host.GATEWAY_CHAIN, {"control": ("control",), "engines-on": ("engines-on", "control")}) + + def test_surface_reads_only_the_provider_column_combo_count_and_two_settings_keys(self): + path = gateway_store(self.root / "store.sqlite", ["codex", "codex", ENGINES_NODE], combos=2, + blocked=["zc", "opencode"], fallback=["kc"]) + before = hashlib.sha256(path.read_bytes()).hexdigest() + statements, real = [], sqlite3.connect + + def traced(*args, **kwargs): + connection = real(*args, **kwargs) + connection.set_trace_callback(statements.append) + return connection + with patch.object(self.host.sqlite3, "connect", side_effect=traced) as connect: + surface = self.host.gateway_surface(path) + self.assertEqual(connect.call_args.args[0], path.as_uri() + "?mode=ro") + self.assertIs(connect.call_args.kwargs["uri"], True) + self.assertEqual(statements, [ + "PRAGMA query_only = ON", + "SELECT provider FROM provider_connections", + "SELECT count(*) FROM combos", + "SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'blockedProviders'", + "SELECT value FROM key_value WHERE namespace = 'settings' AND key = 'noAuthFallbackDisabledProviders'"]) + self.assertEqual(surface, {"providers": ["codex", ENGINES_NODE], "combos": 2, + "blockedProviders": ["opencode", "zc"], "noAuthFallbackDisabledProviders": ["kc"]}) + text = json.dumps(surface) + for secret in GATEWAY_SECRETS: + self.assertNotIn(secret, text) + self.assertEqual(hashlib.sha256(path.read_bytes()).hexdigest(), before) + for bad in (self.root / "absent.sqlite",): + with self.assertRaises(ValueError): + self.host.gateway_surface(bad) + link = self.root / "link.sqlite" + link.symlink_to(path) + with self.assertRaises(ValueError): + self.host.gateway_surface(link) + # A store without the combos table is refused rather than read as empty. + bare = gateway_store(self.root / "bare.sqlite", ["codex"], tables=""" + CREATE TABLE provider_connections (id TEXT PRIMARY KEY, provider TEXT NOT NULL, auth_type TEXT, + access_token TEXT, refresh_token TEXT, api_key TEXT, id_token TEXT, provider_specific_data TEXT, + is_active INTEGER); + CREATE TABLE key_value (namespace TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, + PRIMARY KEY (namespace, key));""") + with self.assertRaises(sqlite3.Error): + self.host.gateway_surface(bare) + + def test_provider_allowlist_refuses_every_other_served_provider(self): + check = self.host.check_gateway_surface + control, engines = self.ALLOWLISTS["control"], self.ALLOWLISTS["engines-on"] + + def surface(providers, **options): + return self.host.gateway_surface(gateway_store(self.root / (uuid.uuid4().hex + ".sqlite"), + providers, **options)) + self.assertEqual(check(surface(["codex"] * 6), control), {"providers": 1, "combos": 0}) + self.assertEqual(check(surface([ENGINES_NODE]), engines), {"providers": 1, "combos": 0}) + # Blocking by alias is enough (OmniRoute noAuthProviders.ts isProviderBlockedByIdOrAlias). + self.assertEqual(check(surface(["codex"], blocked=list(NO_AUTH_PROVIDERS.values()), + fallback=list(ANONYMOUS_FALLBACK_PROVIDERS.values())), control)["combos"], 0) + refusals = ( + ("other provider row", surface(["codex", "openai"]), control, "gateway_provider_outside_allowlist"), + ("chat node on engines-on", surface(["openai-compatible-chat-x"]), engines, + "gateway_provider_outside_allowlist"), + ("codex on engines-on", surface(["codex"]), engines, "gateway_provider_outside_allowlist"), + ("routing combo", surface(["codex"], combos=1), control, "gateway_routing_combo_refused"), + ("subprocess no-auth provider open", surface(["codex"], blocked=[p for p in NO_AUTH_PROVIDERS if p != "zcode"]), + control, "gateway_no_auth_provider_not_blocked"), + ("no-auth settings absent", surface(["codex"], blocked=None), control, + "gateway_no_auth_provider_not_blocked"), + ("no-auth settings not a list", surface(["codex"], blocked=None, settings={"blockedProviders": "{}"}), + control, "gateway_no_auth_provider_not_blocked"), + ("no-auth settings not JSON", surface(["codex"], blocked=None, settings={"blockedProviders": "zcode"}), + control, "gateway_no_auth_provider_not_blocked"), + ("anonymous fallback open", surface(["codex"], fallback=["opencode-zen", "opencode-go", "pol"]), + control, "gateway_anonymous_fallback_not_disabled"), + ("anonymous fallback blocked but not disabled", + surface(["codex"], blocked=[*NO_AUTH_PROVIDERS, *ANONYMOUS_FALLBACK_PROVIDERS], fallback=[]), + control, "gateway_anonymous_fallback_not_disabled"), + ) + for label, value, allowed, reason in refusals: + with self.subTest(label), self.assertRaisesRegex(ValueError, reason): + check(value, allowed) + + def test_engines_on_also_checks_the_control_store_it_forwards_to(self): + stores = {"control": gateway_store(self.root / "control.sqlite", ["codex"] * 6), + "engines-on": gateway_store(self.root / "fw.sqlite", [ENGINES_NODE])} + read = [] + + def database(arm): + read.append(arm) + return stores[arm] + with patch.object(self.host, "gateway_database", side_effect=database): + self.host.verify_gateway_providers("engines-on", self.ALLOWLISTS) + self.assertEqual(read, ["engines-on", "control"]) + read.clear() + self.host.verify_gateway_providers("control", self.ALLOWLISTS) + self.assertEqual(read, ["control"]) + stores["control"] = gateway_store(self.root / "wider.sqlite", ["codex", "openrouter"]) + for arm in ("control", "engines-on"): + with self.subTest(arm=arm), self.assertRaisesRegex(ValueError, "gateway_provider_outside_allowlist"): + self.host.verify_gateway_providers(arm, self.ALLOWLISTS) + + def test_host_file_allowlists_are_exact_arms_and_bounded_patterns(self): + template = json.loads((RECIPE / "config/host.example.json").read_text()) + self.assertEqual(self.host.gateway_allowlists(template), self.ALLOWLISTS) + for bad in ({}, {"control": ["codex"]}, {"control": ["codex"], "engines-on": []}, + {"control": ["*"], "engines-on": ["openai-compatible-responses-*"]}, + {"control": ["co*dex"], "engines-on": ["openai-compatible-responses-*"]}, + {"control": ["codex"], "engines-on": ["openai-compatible-responses-*"], "other": ["x"]}, + {"control": "codex", "engines-on": ["openai-compatible-responses-*"]}): + with self.subTest(bad=bad), self.assertRaises(ValueError): + self.host.gateway_allowlists({"gateway_providers": bad}) + path = self.root / "host.json" + path.write_text(json.dumps({"gateway_providers": {"control": ["codex"], + "engines-on": ["openai-compatible-responses-*"]}})) + for mode, expected in ((0o600, None), (0o644, "private_host_file_requires_mode_0600")): + path.chmod(mode) + with self.subTest(mode=oct(mode)), patch.dict(os.environ, {"OPENHANDS_HOST_FILE": str(path)}): + if expected: + with self.assertRaisesRegex(ValueError, expected): + self.host.read_host_file() + else: + self.assertIn("gateway_providers", self.host.read_host_file()) + + class OpenHandsReceiptTests(unittest.TestCase): def test_entry_gateway_usage_and_effort_are_counted_once_without_ids(self): module = load_recipe_module("receipt.py") From 086b7ec181e6424f0ef3b13676df4548cefa35c0 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 17:40:29 -0400 Subject: [PATCH 20/22] OpenHands repair R5 (source-checked), gw:8080 log format and key-file wording R5 deviation: receipts keep time-window attribution. At OmniRoute@045aa81f3 and @dd6e9607e the /v1/responses route passes a fresh randomUUID to handleChat (src/app/api/v1/responses/route.ts:193,213; requestId.ts:100-102; chat.ts:436), which call_logs.correlation_id stores (attemptLogging.ts:611). The recipe fixes /v1/responses, so filtering on the run id would drop every row (negative control: 'empty_window' != 'observed'). The attribution string now says why, and a regression test counts rows with gateway-generated IDs. The P3 skeleton moves its correlation check to a /v1/chat/completions call, the route that keeps a caller ID (route.ts:292-322). gw:8080 access log: an http-level ingress format (time, method, status) so a URI sent to the host side is not copied whole; 8081 keeps combined for P5 (nginx release-1.30.5 ngx_http_log_module.c:170-171,230-232). check_server_env docstring: reads the file and compares only the variable name; the value is never logged or returned (addendum a). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../openhands/config/proxy-nginx.conf | 12 ++++- .../runtime-workers/openhands/e2e/netprobe.py | 14 ++++-- blueprints/runtime-workers/openhands/host.py | 8 ++-- .../runtime-workers/openhands/receipt.py | 13 ++++- tests/test_runtime_worker_openhands.py | 47 +++++++++++++++++++ 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/blueprints/runtime-workers/openhands/config/proxy-nginx.conf b/blueprints/runtime-workers/openhands/config/proxy-nginx.conf index f27fe98b5..a5f4c8241 100644 --- a/blueprints/runtime-workers/openhands/config/proxy-nginx.conf +++ b/blueprints/runtime-workers/openhands/config/proxy-nginx.conf @@ -23,6 +23,13 @@ http { scgi_temp_path /tmp/scgi_temp; server_tokens off; access_log /dev/stdout; + # The default combined format logs the whole request line, "$request" + # (src/http/modules/ngx_http_log_module.c:230-232). The host side uses this + # one instead, so its access log does not copy a URI sent to gw:8080. + # Request-bound error-log entries still carry the request line + # (src/http/ngx_http_request.c:4103-4107). log_format is accepted only at + # http level (NGX_HTTP_MAIN_CONF, ngx_http_log_module.c:170-171). + log_format ingress '$time_iso8601 $request_method $status'; # Agent side: gw:8081, reached from the internal run network. server { @@ -62,9 +69,12 @@ http { # Host side: 127.0.0.1: is published to 8080 and forwarded to the # attempt's agent-server. The dispatch driver's event search carries a - # query string, so this block has no argument refusal. + # query string, so this block has no argument refusal. Its access log + # records time, method and status only; the agent side (8081) keeps the + # combined format, which P5 triage reads. server { listen 8080; + access_log /dev/stdout ingress; client_max_body_size 50m; proxy_read_timeout 900s; location / { proxy_pass http://@SERVER@:8000; } diff --git a/blueprints/runtime-workers/openhands/e2e/netprobe.py b/blueprints/runtime-workers/openhands/e2e/netprobe.py index c154fedc0..a31bbf868 100644 --- a/blueprints/runtime-workers/openhands/e2e/netprobe.py +++ b/blueprints/runtime-workers/openhands/e2e/netprobe.py @@ -333,10 +333,16 @@ def p3_control_call(*_args, **_kwargs): sending a forged X-Correlation-Id, X-Forwarded-For and one x-omniroute-* header; then a second call with a query string, which must get the proxy's 403 and no call_logs row. The call_logs row must show - /v1/responses, 200, cx/gpt-6-astra-max, effort max requested and sent - upstream, and the proxy's fixed run id as the correlation value. The - streamed tool call must parse and its usage equal the row; record the - client peer the gateway logged (F10) and the proxy access log lines. + /v1/responses, 200, cx/gpt-6-astra-max, and effort max requested and sent + upstream. Its correlation value is gateway-generated, neither the forged + one nor the run id: at OmniRoute@045aa81f3 and @dd6e9607e /v1/responses + ignores the caller's X-Correlation-Id (src/app/api/v1/responses/route.ts:193,213). + A third call shows the proxy's replacement: one POST /v1/chat/completions + with the same forged header. That route keeps a caller ID + (src/app/api/v1/chat/completions/route.ts:292-322), so its row, whatever + its status, must carry the run id. The streamed tool call must parse and + its usage equal the row; record the client peer the gateway logged (F10) + and the proxy access log lines. """ raise NotImplementedError("p3_is_a_documented_skeleton_not_run") diff --git a/blueprints/runtime-workers/openhands/host.py b/blueprints/runtime-workers/openhands/host.py index a85c970dd..f5c699d4c 100644 --- a/blueprints/runtime-workers/openhands/host.py +++ b/blueprints/runtime-workers/openhands/host.py @@ -122,7 +122,10 @@ def private_file(path): def check_server_env(path): - """Require the session-key variable NAME in the server env file; values stay unread. + """Require the session-key variable NAME in the server env file. + + Reads the file and compares only the variable name; the value is never + logged or returned. SDK@fcc102a agent_server/__main__.py:282-285 binds all interfaces only with a session API key (config.py:24 names OH_SESSION_API_KEYS_0). Without one the @@ -130,8 +133,7 @@ def check_server_env(path): docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124 is the --env-file format: newline-delimited lines, a BOM dropped on line one, leading whitespace trimmed, "#" comments, and the name ends at the first "=". A bare name copies - the Docker CLI's own environment, so only the NAME= form is accepted. Only - the text before "=" is compared; no value is kept, printed or logged. + the Docker CLI's own environment, so only the NAME= form is accepted. """ path = private_file(path) try: diff --git a/blueprints/runtime-workers/openhands/receipt.py b/blueprints/runtime-workers/openhands/receipt.py index 1821ddaf1..64c6f0807 100644 --- a/blueprints/runtime-workers/openhands/receipt.py +++ b/blueprints/runtime-workers/openhands/receipt.py @@ -226,6 +226,14 @@ def create_receipt(result, database=None): window.get("base_url"), window.get("compression_combo")) db = database or gateway_database(selection["arm"]) rows, db_status = [], "unavailable" + # No run-id match (repair R5, checked against source). At OmniRoute@045aa81f3 + # (20128) and @dd6e9607e (20129), whose files here are identical, /v1/responses + # hands handleChat a fresh randomUUID (src/app/api/v1/responses/route.ts:193,213; + # src/shared/utils/requestId.ts:100-102; src/sse/handlers/chat.ts:436), and + # call_logs.correlation_id stores it (open-sse/handlers/chatCore/attemptLogging.ts:611; + # src/lib/usage/callLogs.ts:713,786,801). Only /v1/chat/completions keeps a + # caller X-Correlation-Id (route.ts:292-322). arm_config fixes /v1/responses, + # so matching the proxy's run id would drop every row and report false zeros. try: rows = gateway_rows(db, window["started_at"], window["finished_at"]) db_status = "observed" if rows else "empty_window" @@ -283,7 +291,10 @@ def removals(*patterns): "gateway": { "read_mode": "read_only", "columns": list(COLUMNS), "status": db_status, **usage, "entry_port": selection["gateway_port"], - "attribution": "Time window + model + path at the arm entry gateway; concurrent callers cannot be excluded by the recipe lock.", + "attribution": ("Time window + model + path at the arm entry gateway. The proxy fixes X-Correlation-Id " + "to the run id, but at the pinned OmniRoute builds /v1/responses logs a gateway-generated " + "correlation ID instead, so rows are not matched by run id and concurrent callers of the " + "same model and path are included; the recipe lock cannot exclude them."), "usage_note": "Sum each entry row once, including failed calls; never add 20128 to 20129. Cache-read is an input subset. Missing counters keep totals null.", }, "compression": window.get("compression", {"delta": None, "status": "unavailable"}), diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 32a7d91f6..1e8eaf39a 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -1342,6 +1342,21 @@ def test_proxy_template_is_the_three_route_v1_allowlist(self): for path in re.findall(r"_temp_path\s+(\S+);", body): self.assertTrue(path.startswith("/tmp/"), path) + def test_host_side_access_log_records_no_request_line(self): + # GPT-6 review session-key-logging: the combined format logs "$request" + # (nginx release-1.30.5 src/http/modules/ngx_http_log_module.c:230-232), + # so a URI sent to gw:8080 would reach the retained proxy log whole. + _, body, agent, host_side = self.proxy_blocks() + http_level = body[:body.index("listen 8081;")] + formats = dict(re.findall(r"log_format\s+(\w+)\s+'([^']*)';", body)) + self.assertEqual(list(formats), ["ingress"]) + self.assertIn("log_format ingress", http_level) + self.assertEqual(re.findall(r"\$\w+", formats["ingress"]), ["$time_iso8601", "$request_method", "$status"]) + self.assertEqual(re.findall(r"access_log\s+([^;]+);", host_side), ["/dev/stdout ingress"]) + # P5 triages the agent side's full request lines, so 8081 keeps the combined default. + self.assertEqual(re.findall(r"access_log\s+([^;]+);", http_level), ["/dev/stdout"]) + self.assertEqual(re.findall(r"access_log\s+([^;]+);", agent), []) + def test_proxy_render_fills_each_placeholder_from_the_arm_selection(self): host = load_recipe_module("host.py") recipe = load_recipe_module("recipe.py") @@ -2728,6 +2743,38 @@ def test_gateway_reads_only_authorized_columns_and_preserves_unknown_usage(self) self.assertNotIn("sqlite_master", source) self.assertIn("?mode=ro", source) + def test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids(self): + # Repair R5, source-checked. At OmniRoute@045aa81f3 and @dd6e9607e the + # /v1/responses route hands handleChat a fresh randomUUID + # (src/app/api/v1/responses/route.ts:193,213; src/shared/utils/requestId.ts:100-102), + # and call_logs.correlation_id records it (chatCore/attemptLogging.ts:611). + # The proxy's fixed run id never reaches that column, so filtering on it + # would turn this attempt's usage into a false empty window. + module = load_recipe_module("receipt.py") + # Built at runtime, like the gateway's own IDs (publication identifier policy). + generated = (str(uuid.uuid4()), str(uuid.uuid4())) + with tempfile.TemporaryDirectory() as tmp: + result = Path(tmp).resolve() + db = result / "fixture.sqlite" + connection = sqlite3.connect(db) + connection.execute("CREATE TABLE call_logs (timestamp, path, status, model, reasoning_effort_requested, reasoning_effort_upstream, tokens_in, tokens_cache_read, tokens_reasoning, correlation_id)") + connection.executemany("INSERT INTO call_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ( + ("2026-09-28T18:00:01Z", "/v1/responses", 200, "gpt-6-astra-max", "max", "max", 30, 20, 4, generated[0]), + ("2026-09-28T18:00:02Z", "/v1/responses", 200, "gpt-6-astra-max", None, None, 10, 0, 0, generated[1]))) + connection.commit() + connection.close() + (result / "check.json").write_text("{}") + (result / "window.json").write_text(json.dumps({ + "arm": "control", "run_id": "rw-openhands-fixture", "instance_id": "django__django-11333", + "started_at": "2026-09-28T18:00:00Z", "finished_at": "2026-09-28T18:00:03Z"})) + gateway = module.create_receipt(result, database=db)["gateway"] + self.assertEqual(gateway["status"], "observed") + self.assertEqual(len(gateway["rows"]), 2) + self.assertEqual(gateway["totals"], {"tokens_in": 40, "tokens_cache_read": 20, "tokens_reasoning": 4}) + for phrase in ("/v1/responses", "gateway-generated", "concurrent callers"): + self.assertIn(phrase, gateway["attribution"]) + self.assertFalse(any(value in json.dumps(gateway) for value in generated)) + def test_skills_and_mcp_require_observations(self): # Phase 2 disables the unreachable host services in the policy, so the # fixture uses a container-local server the native filter still admits. From 62f6a22a94e9ca4abd0948869519f0b45a3a7e06 Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 18:28:28 -0400 Subject: [PATCH 21/22] OpenHands repair round docs: scope the containment claims and record red/green C9: the decision record separates what holds by construction (the agent-server's containment, grading with no network) from G5's checked gateway state, and narrowing condition 2 names the proxy routes, the session key, G5 and offline grading. G5 joins the narrowing's overturn list. GPT-6 items: every "three routes" claim now says three routes reach OmniRoute and gw:8080 reaches only the agent's own server (README, decision record, MCP policy reasons, netprobe docstring). The header bullet lists nginx's own defaults at release-1.30.5 (generated Content-Length or chunked framing, an empty and so omitted Connection; ngx_http_proxy_module.c:747-757,4210-4228) and its framing refusals (ngx_http_request.c:2034-2064). The session key is "not intentionally logged", with the retained-log residual. P0-P2 are described as bounded observations with the trusted-producer limit. R5: README usage accounting and the P3 steps state why receipts cannot match the run id on /v1/responses at the pinned OmniRoute builds; P3 expects the routed model the receipt matches. evidence/repair-round-commands.json records each code item's red run, the suite on each fixing commit (re-run from git archive), the literal-R5 negative control, the live-store audit and its planted-event control, and the pinned source reads; a new test checks its pairing and publishability. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/README.md | 125 ++++++-- .../openhands/config/mcp-policy.json | 4 +- .../runtime-workers/openhands/e2e/netprobe.py | 16 +- .../evidence/repair-round-commands.json | 296 ++++++++++++++++++ .../runtime-workers/openhands/research.md | 50 ++- ...2026-09-28-openhands-resolver-isolation.md | 52 ++- tests/test_runtime_worker_openhands.py | 25 ++ 7 files changed, 517 insertions(+), 51 deletions(-) create mode 100644 blueprints/runtime-workers/openhands/evidence/repair-round-commands.json diff --git a/blueprints/runtime-workers/openhands/README.md b/blueprints/runtime-workers/openhands/README.md index 9c7ee6ba7..563a151b5 100644 --- a/blueprints/runtime-workers/openhands/README.md +++ b/blueprints/runtime-workers/openhands/README.md @@ -168,8 +168,11 @@ is refused, and so is any compression value on control. The proxy, not the SDK, sends the header: its allowlist replaces whatever the agent sends for `x-omniroute-compression`, `X-Correlation-Id` and `x-omniroute-session` with the attempt's fixed values. The agent therefore cannot switch arm or combo, or forge -the usage join. The LLM key stays the placeholder `local-loopback`, and -`OMNIROUTE_API_KEY` is never sent. +the correlation and session values the gateway receives. On the recipe's +`/v1/responses` path the pinned gateways log their own correlation ID instead, +so usage is not joined on the run id ([Usage accounting](#usage-accounting)). +The LLM key stays the placeholder `local-loopback`, and `OMNIROUTE_API_KEY` is +never sent. Both arms use Responses, `reasoning_effort=max`, native function tools, omitted temperature, a 40-iteration limit and a 1,200-second conversation deadline. @@ -235,17 +238,25 @@ and writes two files in - `-.headers`: `X-Session-API-Key: `, which curl reads with `-H @file`. -The value is never in argv, output or logs. The server is never read with a -full `docker inspect`, because its Config.Env holds the key; containers are -read only through fixed `--format` templates. `teardown_attempt` deletes both +The driver never puts the value in argv and does not intentionally log or print +it. That is not a guarantee that no log holds it: the model can read the key +inside its container (the accepted design) and place it in any request it sends +through the proxy. The agent side's access log keeps whole request lines, and +so do request-bound error-log entries on both listeners (nginx +release-1.30.5 `src/http/ngx_http_request.c:4103-4107`). The host side's access +log keeps only time, method and status. Teardown retains the proxy log, and +deleting the key files does not remove such copies. The server is never read +with a full `docker inspect`, because its Config.Env holds the key; containers +are read only through fixed `--format` templates. `teardown_attempt` deletes both files once the proxy and server are confirmed removed. While either removal is unconfirmed the files stay, so the server they belong to can still be reached and cleaned up. The server listens on every container interface only when a key is set (SDK@fcc102a `openhands-agent-server/openhands/agent_server/__main__.py:282-285`, `config.py:24`), and it checks the `X-Session-API-Key` header -(`dependencies.py:19`). Before the server starts, the env file is re-checked -for the variable name only, using Docker's env-file rules (docker/cli@v29.8.1 -`pkg/kvfile/kvfile.go:92-124`); no value is extracted, printed or logged. The +(`dependencies.py:19`). Before the server starts, the host reads the env file +and compares only the variable name, using Docker's env-file rules +(docker/cli@v29.8.1 `pkg/kvfile/kvfile.go:92-124`); the value is never logged +or returned. The inventory row is `openhands-session` in [docs/secret-storage.md](../../../docs/secret-storage.md) and [adoption/credential-inventory.json](../../../adoption/credential-inventory.json). @@ -441,10 +452,25 @@ Never add forwarded 20128 rows to engines-on 20129 rows. Reads are restricted to timestamp, path, status, model, tokens_in, tokens_cache_read, tokens_reasoning and correlation_id, plus the two explicitly -required effort columns. IDs are discarded before returning rows. The private -native capture may lack headers on streaming paths and shares the model's trust -boundary, so receipts conservatively use **time window + model + path** at the -entry gateway. They state the concurrent-caller limitation. Per-arm totals sum +required effort columns. IDs are discarded before returning rows. Receipts use +**time window + model + path** at the entry gateway and state the +concurrent-caller limitation. Two sources could have narrowed that, and neither +can: +- The proxy's fixed `X-Correlation-Id` does not reach the row. At + OmniRoute@045aa81f3 and @dd6e9607e, whose files here are identical, + `/v1/responses` passes a fresh `randomUUID()` to `handleChat` + (`src/app/api/v1/responses/route.ts:193,213`, + `src/shared/utils/requestId.ts:100-102`, `src/sse/handlers/chat.ts:436`). That + ID is what `call_logs.correlation_id` stores + (`open-sse/handlers/chatCore/attemptLogging.ts:611`, + `src/lib/usage/callLogs.ts:713,786,801`). Only `/v1/chat/completions` keeps + a caller's ID (`src/app/api/v1/chat/completions/route.ts:292-322`). Matching + the run id on this path would drop every row and report false zeros, so + receipts do not (repair R5; a regression test pins this). +- The private native capture of returned IDs may lack headers on streaming + paths and shares the model's trust boundary. + +Per-arm totals sum each matched row once, including failed calls; if any requested counter is missing, totals remain null. Cache-read tokens are an input subset, not an additional cost. No output-token or complete-provider-cost total is invented. @@ -482,7 +508,8 @@ Git@v2.43.0 `Documentation/git.txt:708-724`, `diff-options.txt:830-840`. MCP allowlists are not network security. ai-memory HTTP and socraticode are **disabled**. Their host services would bypass the framework's tool filter, and under O1 they are unreachable by design: the internal run network has no -route to host services, and the proxy forwards only three `/v1` gateway routes +route to host services, only three `/v1` routes reach the gateway, and the +proxy's other listener, `gw:8080`, reaches only the agent's own server ([config/mcp-policy.json](config/mcp-policy.json)). The host template still fixes `AI_MEMORY_URL` and `EMBED_URL` because the MCP template renders them. Neither address is reachable from the internal network; P2 checks every host @@ -622,7 +649,10 @@ upstream URIs: | any other path or method, or any query string | 403 from the proxy | `` is 20128 on control and 20129 on engines-on; one proxy serves one -arm. +arm. These are the only routes that reach the gateway, not the only ones the +agent can reach: nginx listens on every proxy interface, so the agent can also +use the host side below as `gw:8080`, which forwards every path to its own +agent-server. - **Paths.** `location =` is an exact match that ends the search. Locations are matched against the normalized URI, after decoding `%XX`, resolving `.` @@ -645,25 +675,39 @@ arm. `ngx_http_rewrite_module`, `if`, fetched 2026-09-28), so `?0` is refused too. - **Headers.** The template sets `proxy_pass_request_headers off`; the directive "Indicates whether the header fields of the original request are - passed to the proxied server" (`ngx_http_proxy_module`). Only the fields the - template sets reach the gateway: + passed to the proxied server" (`ngx_http_proxy_module`). Of the agent's + fields, only the three the template copies reach the gateway. The gateway + receives the fields the template sets: - a fixed `Host`; - `Content-Type`, `Accept` and `Idempotency-Key`, copied from the request; - `Authorization: Bearer local-loopback`; - `X-Correlation-Id` and `x-omniroute-session`, fixed to the run id; - `x-omniroute-compression`, fixed to the arm's combo. + nginx adds its own defaults for any name the template leaves unset + (`ngx_http_proxy_module.c:747-757`, merged at `:4210-4228`). At + release-1.30.5 these are the body framing it generates itself, + `Content-Length` or `Transfer-Encoding: chunked` (`:750-751`), and empty + `Connection`, `TE`, `Keep-Alive`, `Expect` and `Upgrade` (`:749,752-755`). "If the value of a header field is an empty string then this field will not be passed" (`proxy_set_header`; source - `ngx_http_proxy_module.c:1301-1303,1417-1418`), so control's empty combo - sends no compression header. The agent cannot forge the usage join, choose - a compression plan, or send `X-Forwarded-For`, `X-Real-IP` or any other - `x-omniroute-*` control header. + `ngx_http_proxy_module.c:1301-1303,1417-1418`). So no `Connection` field is + sent; the `Connection: close` in older documentation is not the pinned + behaviour. Before proxying, nginx refuses `Transfer-Encoding` on HTTP/1.0, + `Transfer-Encoding` together with `Content-Length` (400 each) and any + transfer coding other than `chunked` (501) + (`src/http/ngx_http_request.c:2034-2064`); live framing is still P3's to + show. Control's empty combo sends no compression header either. The + agent cannot forge the correlation or session value, choose a compression + plan, or send `X-Forwarded-For`, `X-Real-IP` or any other `x-omniroute-*` + control header. - **Host side.** Port 8080 forwards every path to `$S-server:8000` and refuses no arguments, because the driver's error-event search carries a query string. The host reaches it through `127.0.0.1:`. The agent can also reach it as `gw:8080`, which leads only to the agent's own server and - gives it nothing it lacks. + gives it nothing it lacks. Its access log uses the `ingress` format (time, + method and status) instead of combined, which logs the whole request line as + `$request` (`src/http/modules/ngx_http_log_module.c:230-232`). - **Rendering.** `render_proxy_config` fills `@PORT@`, `@SERVER@`, `@RUN@` and `@COMPRESSION@` from `recipe.arm_config` and the attempt identity. It refuses: - a selection that differs from `arm_config`'s own output; @@ -697,7 +741,12 @@ Proxy residuals: the arm serves. The G5 preflight below bounds which providers that can be. - Three things are unprobed, and P3 and P5 cover them: `--entrypoint nginx` under `--read-only`; body framing and streaming under the header allowlist; - and gateway paths the SDK might use beyond the three routes. + and gateway paths the SDK might use beyond the three routes. No running + nginx has parsed the template yet, including the host side's `ingress` log + format; the first live `prepare` does. +- The retained proxy log can hold the session key if the model puts it in a + request line on the agent side, or in any request that produces a + request-bound error entry. ### G5: gateway provider preflight @@ -797,6 +846,19 @@ not resolve, the receipt keeps the resolver's error class, such as `EAI_NONAME` or `EAI_AGAIN`. The pass condition is still only that no address resolved. Network inspect values are context only; the failed connects are the evidence. +P0-P2 are bounded observations, not proof of every path: +- they run in probe containers on the attempt networks, not in the + agent-server's own network namespace; +- UDP is tried only as the one DNS datagram to 10.0.2.3:53; +- no probe sends a request body, so body framing under the header allowlist is + source-reviewed only; +- no probe makes a successful LLM request. P3 and P5 cover the last two live. + +The gate re-derives the verdict from the recorded counts and refuses a +contradictory receipt. It still trusts the host that wrote the receipt: a +consistent receipt forged, or re-dated, by the owning user would pass. No +model write path to it exists. + The receipt is `runs///isolation-probe.json`: mode 0600, host-owned and outside every model mount. It holds: - mechanism `internal-isolated+nginx-v1-allowlist`, `run_id` and `arm`; @@ -864,14 +926,19 @@ of them has run. and one `x-omniroute-*` control header. A second call adds a query string; it must get the proxy's 403 and leave no `call_logs` row. The first call's row must show: - - `/v1/responses`, status 200 and model `cx/gpt-6-astra-max`; + - `/v1/responses`, status 200 and the routed model the receipt matches, + `gpt-6-astra-max`; - effort max, both requested and sent upstream; - - the run id as the correlation value, not the forged one. - - The streamed tool call must parse, body framing must work under the header - allowlist, and the usage must equal the row. Record the client peer the - gateway logged (F10). The proxy access log must show only that allowlisted - request. + - a gateway-generated correlation value, neither the forged one nor the run + id, because `/v1/responses` ignores a caller's `X-Correlation-Id` at the + pinned builds ([Usage accounting](#usage-accounting)). + + A third call shows the proxy's replacement: one POST `/v1/chat/completions` + with the same forged header. That route keeps a caller's ID, so its row, + whatever its status, must carry the run id. The streamed tool call must + parse, body framing must work under the header allowlist, and the usage + must equal the row. Record the client peer the gateway logged (F10). The + proxy's agent-side access log must show exactly these three requests. - **P4, engines-on arm.** The same through a proxy bound to 20129, plus these checks: - a stacked lossy plan appears in the compression analytics; diff --git a/blueprints/runtime-workers/openhands/config/mcp-policy.json b/blueprints/runtime-workers/openhands/config/mcp-policy.json index 80c99a36c..7c6919ee2 100644 --- a/blueprints/runtime-workers/openhands/config/mcp-policy.json +++ b/blueprints/runtime-workers/openhands/config/mcp-policy.json @@ -12,13 +12,13 @@ "ai-memory": { "enabled": false, "enabled_tools": ["memory_query", "memory_read_page", "memory_recent", "memory_status", "memory_briefing"], - "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services, and the proxy forwards only three /v1 gateway routes (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." + "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services, only three /v1 routes reach the gateway, and the proxy's other listener, gw:8080, reaches only the agent's own server (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." }, "socraticode": { "enabled": false, "enabled_tools": ["codebase_search", "codebase_status", "codebase_list_projects", "codebase_health"], "env": {"SOCRATICODE_WATCHER": "manual"}, - "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services (Qdrant, the embedder), and the proxy forwards only three /v1 gateway routes (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." + "reason": "Disabled because it is unreachable by design under O1: the internal run network has no route to host services (Qdrant, the embedder), only three /v1 routes reach the gateway, and the proxy's other listener, gw:8080, reaches only the agent's own server (docs/decisions/2026-09-28-openhands-resolver-isolation.md)." }, "headroom": { "enabled": false, diff --git a/blueprints/runtime-workers/openhands/e2e/netprobe.py b/blueprints/runtime-workers/openhands/e2e/netprobe.py index a31bbf868..637caac80 100644 --- a/blueprints/runtime-workers/openhands/e2e/netprobe.py +++ b/blueprints/runtime-workers/openhands/e2e/netprobe.py @@ -8,9 +8,10 @@ - mode gw, on -gw: P0, the negative control. The vehicle must see the gateway (GET /v1/models -> 200 + CLIENT_API) and its management exposure (GET /api/settings -> MANAGEMENT). -- mode int, on -int: P1, only the proxy's three /v1 routes answer and - every other method or target gets the proxy's own 403 with no route-class - header; then P2. Its positive control, a TCP connect to gw:8081, must +- mode int, on -int: P1, on gw:8081 only the proxy's three /v1 routes + answer and every other method or target gets the proxy's own 403 with no + route-class header (gw:8080, which leads to the agent's own server, is not + probed); then P2. Its positive control, a TCP connect to gw:8081, must succeed. Every TCP connect to a host-side address must fail, and one outside the run subnet must fail for want of a route (ENETUNREACH or EHOSTUNREACH), not by timing out. One DNS datagram to 10.0.2.3:53 must get @@ -333,10 +334,11 @@ def p3_control_call(*_args, **_kwargs): sending a forged X-Correlation-Id, X-Forwarded-For and one x-omniroute-* header; then a second call with a query string, which must get the proxy's 403 and no call_logs row. The call_logs row must show - /v1/responses, 200, cx/gpt-6-astra-max, and effort max requested and sent - upstream. Its correlation value is gateway-generated, neither the forged - one nor the run id: at OmniRoute@045aa81f3 and @dd6e9607e /v1/responses - ignores the caller's X-Correlation-Id (src/app/api/v1/responses/route.ts:193,213). + /v1/responses, 200, the routed model the receipt matches + (gpt-6-astra-max), and effort max requested and sent upstream. Its + correlation value is gateway-generated, neither the forged one nor the + run id: at OmniRoute@045aa81f3 and @dd6e9607e /v1/responses ignores the + caller's X-Correlation-Id (src/app/api/v1/responses/route.ts:193,213). A third call shows the proxy's replacement: one POST /v1/chat/completions with the same forged header. That route keeps a caller ID (src/app/api/v1/chat/completions/route.ts:292-322), so its row, whatever diff --git a/blueprints/runtime-workers/openhands/evidence/repair-round-commands.json b/blueprints/runtime-workers/openhands/evidence/repair-round-commands.json new file mode 100644 index 000000000..4a5efd7db --- /dev/null +++ b/blueprints/runtime-workers/openhands/evidence/repair-round-commands.json @@ -0,0 +1,296 @@ +{ + "evidence_class": "The single repair round on e45c3cd1. Offline unit checks of the recipe with Docker mocked (our integration checks, not upstream tests), source reads at pinned upstream revisions, and an audit-hook run showing that the suite opened no file or sqlite store under the live OmniRoute data directories. No container or network was created and no gateway or model request was made. The G5 tests read synthetic sqlite stores only.", + "sanitization": "The worktree prefix is shown as , the scratch directory as , the unit TMPDIR as and the local OmniRoute source checkout as . The module prefix `tests.test_runtime_worker_openhands.` is dropped from test names. Red outputs are excerpts: the summary lines, each non-ok test with its count of non-ok lines (subtests) when above one, and each distinct first exception line with its count, cut at 220 characters. Tracebacks are omitted; rerunning a command on its tree reproduces them. Green outputs keep the summary lines. Full outputs stay private.", + "base": "e45c3cd1c274aff47b82129bf8d22ed374bd577b", + "reviews": { + "headless_claude": "changes-needed on e45c3cd1: R1 blocker (grading on the default bridge), R2 medium (the probe refused its own server), R3 high (G5 unenforced), R4 low (P2 weak), R5 low (correlation unused in the receipt), R6 low (holds only in documents), C9 medium (the decision record overclaimed); eight confirms.", + "gpt6": "changes-needed on e45c3cd1: containment-grader high (same as R1), session-key-logging low, probe-receipt-trust low, gw-8080 info, nginx-headers info; six confirms." + }, + "commits": { + "R1": "9302ec2319037af60f70155cbdf60691ee2de6c1", + "R2_R4_addendum_b": "92de42e8f252c5fda3808e36f8850b7a94d8c18b", + "R6_probe_receipt_trust": "185de7181ba02db8079c7f06b3a0888e7d685d03", + "R3_G5": "726849b352f23889d7d08f5ea221eb136db241d1", + "R5_gw8080_log_addendum_a": "5a0b0a54a19a65ae1ecea4950a09b40dd9e27cbf", + "docs": "the commit that adds this file" + }, + "decisions": "Coordinator repair round of 2026-09-28: new commits on e45c3cd1, a failing test first for every code item, and no push, rebase, manifests/evidence.json change, container, network or gateway request. R5 was not applied as written. At OmniRoute 045aa81f3 and dd6e9607e the recipe's /v1/responses path stores a gateway-generated correlation ID (entry 15), so passing correlation_ids={run_id} would drop every row. Entry 11 shows that change turning the fixture's usage into an empty window. Receipts keep time-window attribution and say why, and the P3 steps move the correlation check to a /v1/chat/completions call.", + "item_runs": { + "R1": ["01-R1-red", "02-R1-green"], + "R2_R4_addendum_b": ["03-R2-R4-b-red", "04-R2-R4-b-green"], + "R6_probe_receipt_trust": ["05-R6-trust-red", "06-R6-trust-green"], + "R3_G5": ["07-R3-G5-red", "08-R3-G5-green"], + "R5_gw8080_log": ["10-R5-gw8080-red", "12-R5-gw8080-green"] + }, + "item_tests": { + "R1": [ + "OpenHandsUpstreamAdapterTests.test_grader_container_is_created_with_network_none_and_inspected", + "OpenHandsUpstreamAdapterTests.test_grader_sdk_create_body_requires_network_none", + "OpenHandsUpstreamAdapterTests.test_grader_main_installs_the_network_none_transport", + "OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings", + "OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags" + ], + "R2_R4_addendum_b": [ + "OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks", + "OpenHandsProbeTests.test_probe_excludes_and_records_its_own_attempt_containers", + "OpenHandsProbeTests.test_netprobe_reads_routes_sends_one_udp_query_and_runs_the_positive_control", + "OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused", + "OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate", + "OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses" + ], + "R6_probe_receipt_trust": [ + "OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts" + ], + "R3_G5": [ + "OpenHandsGatewaySurfaceTests.test_provider_lists_are_the_omniroute_source_lists", + "OpenHandsGatewaySurfaceTests.test_surface_reads_only_the_provider_column_combo_count_and_two_settings_keys", + "OpenHandsGatewaySurfaceTests.test_provider_allowlist_refuses_every_other_served_provider", + "OpenHandsGatewaySurfaceTests.test_engines_on_also_checks_the_control_store_it_forwards_to", + "OpenHandsGatewaySurfaceTests.test_host_file_allowlists_are_exact_arms_and_bounded_patterns", + "OpenHandsProbeTests.test_gate_runs_the_g5_provider_preflight_on_fixture_stores" + ], + "R5_gw8080_log": [ + "OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids", + "OpenHandsIsolationTests.test_host_side_access_log_records_no_request_line" + ] + }, + "item_notes": { + "R1": "Grader containers are created with network mode none, the SDK create body is checked, and the created container's inspect must show only the network none; otherwise it is removed and not graded.", + "R2_R4_addendum_b": "After the red run one test changed: on this host a closed loopback UDP port times out rather than answering ECONNREFUSED, so the real-socket case accepts either and a mocked recv covers ECONNREFUSED.", + "R6_probe_receipt_trust": "After the red run one assertion changed: a per-file count of stage-gates mentions became a check that no recipe file writes, opens, replaces, renames or touches the stage-gates file.", + "R3_G5": "The red tree had no config/host.example.json change. Entries 09 and 13 show the suite touching no live store, and entry 14 shows the audit hook detecting planted live-store events.", + "R5_gw8080_log": "R5 is the source-checked deviation in `decisions`. The same commit rewords check_server_env's docstring (addendum a) and the P3 skeleton; wording has no red run." + }, + "commands": [ + { + "id": "01-R1-red", + "tree": "e45c3cd1 with the R1 test edits only; recipe files unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": ["Ran 110 tests in 2.133s", "FAILED (failures=2, errors=3)"], + "non_ok_tests": [ + "OpenHandsUpstreamAdapterTests.test_grader_container_is_created_with_network_none_and_inspected", + "OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags", + "OpenHandsUpstreamAdapterTests.test_grader_sdk_create_body_requires_network_none", + "OpenHandsUpstreamAdapterTests.test_grader_main_installs_the_network_none_transport", + "OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings" + ], + "first_exception_lines": [ + "ERROR OpenHandsUpstreamAdapterTests.test_grader_container_is_created_with_network_none_and_inspected => AttributeError: module 'openhands_recipe_test_e2e_docker_grader' has no attribute 'owned_create'", + "ERROR OpenHandsUpstreamAdapterTests.test_grader_refuses_unpinned_images_and_does_not_pull_mutable_tags => AttributeError: module 'openhands_recipe_test_e2e_docker_grader' has no attribute 'install_transport'", + "ERROR OpenHandsUpstreamAdapterTests.test_grader_sdk_create_body_requires_network_none => AttributeError: module 'openhands_recipe_test_e2e_docker_grader' has no attribute 'checked_create_config'", + "FAIL OpenHandsUpstreamAdapterTests.test_grader_main_installs_the_network_none_transport => AssertionError: ValueError not raised", + "FAIL OpenHandsUpstreamAdapterTests.test_official_grader_container_transport_preserves_upstream_settings => AssertionError: Items in the second set but not the first:" + ] + }, + { + "id": "02-R1-green", + "tree": "Commit 9302ec2319037af60f70155cbdf60691ee2de6c1, extracted with git archive.", + "command": "git archive 9302ec23 | tar -x -C /trees/9302ec23; in that tree: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": ["Ran 110 tests in 2.220s", "OK"] + }, + { + "id": "03-R2-R4-b-red", + "tree": "9302ec23 with the R2, R4 and addendum-b test edits only; recipe files unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": ["Ran 111 tests in 1.966s", "FAILED (failures=1, errors=38)"], + "non_ok_tests": [ + "OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate x18", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts", + "OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused x12", + "OpenHandsProbeTests.test_netprobe_lists_are_the_plan_lists_and_import_does_no_work", + "OpenHandsProbeTests.test_netprobe_reads_routes_sends_one_udp_query_and_runs_the_positive_control", + "OpenHandsProbeTests.test_netprobe_stops_at_the_first_unexpected_result", + "OpenHandsProbeTests.test_probe_excludes_and_records_its_own_attempt_containers", + "OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate", + "OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks", + "OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks", + "OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses" + ], + "first_exception_lines": [ + "ERROR OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate => ValueError: probe_target_is_attempt_container (x18)", + "ERROR OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts => ValueError: probe_target_is_attempt_container", + "ERROR OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused => AttributeError: /blueprints/runtime-workers/openhands/e2e/netprobe.p (x11)", + "ERROR OpenHandsProbeTests.test_netprobe_int_mode_passes_only_when_everything_is_refused => TypeError: evaluate_p2() takes 4 positional arguments but 5 were given", + "ERROR OpenHandsProbeTests.test_netprobe_lists_are_the_plan_lists_and_import_does_no_work => AttributeError: module 'openhands_recipe_test_e2e_netprobe' has no attribute 'UNREACHABLE'. Did you mean: 'UNRESOLVABLE'?", + "ERROR OpenHandsProbeTests.test_netprobe_reads_routes_sends_one_udp_query_and_runs_the_positive_control => AttributeError: module 'openhands_recipe_test_e2e_netprobe' has no attribute 'route_summary'", + "ERROR OpenHandsProbeTests.test_netprobe_stops_at_the_first_unexpected_result => AttributeError: /blueprints/runtime-workers/openhands/e2e/netprobe.py'> does no", + "ERROR OpenHandsProbeTests.test_probe_excludes_and_records_its_own_attempt_containers => ValueError: probe_target_is_attempt_container", + "ERROR OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate => ValueError: probe_target_is_attempt_container", + "ERROR OpenHandsProbeTests.test_probe_requires_each_attempt_container_on_its_own_networks => ValueError: probe_target_is_attempt_container", + "ERROR OpenHandsProbeTests.test_probe_targets_come_from_ss_ip_and_the_attempt_networks => TypeError: probe_targets() takes 3 positional arguments but 4 were given", + "FAIL OpenHandsReceiptTests.test_receipt_summarizes_the_isolation_probe_without_ids_or_addresses => AssertionError: {'sta[310 chars]{'connects': 16, 'observed': 16, 'connected': [203 chars] 16}} != {'sta[310 chars]{'contr" + ] + }, + { + "id": "04-R2-R4-b-green", + "tree": "Commit 92de42e8f252c5fda3808e36f8850b7a94d8c18b, extracted with git archive.", + "command": "git archive 92de42e8 | tar -x -C /trees/92de42e8; in that tree: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": ["Ran 111 tests in 2.983s", "OK"] + }, + { + "id": "05-R6-trust-red", + "tree": "92de42e8 with the R6 and probe-receipt-trust test edits only; recipe files unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": ["Ran 112 tests in 2.984s", "FAILED (failures=30, errors=1)"], + "non_ok_tests": [ + "OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates x13", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts x18" + ], + "first_exception_lines": [ + "ERROR OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates => AttributeError: module 'openhands_recipe_test_host' has no attribute 'verify_stage_gates'", + "FAIL OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts => AssertionError: ValueError not raised (x18)", + "FAIL OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates => AssertionError: ValueError not raised (x12)" + ] + }, + { + "id": "06-R6-trust-green", + "tree": "Commit 185de7181ba02db8079c7f06b3a0888e7d685d03, extracted with git archive.", + "command": "git archive 185de718 | tar -x -C /trees/185de718; in that tree: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": ["Ran 112 tests in 3.112s", "OK"] + }, + { + "id": "07-R3-G5-red", + "tree": "185de718 with the R3 test edits only; recipe files, including config/host.example.json, unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands -v", + "exit_code": 1, + "summary": ["Ran 118 tests in 4.675s", "FAILED (errors=26)"], + "non_ok_tests": [ + "OpenHandsGatewaySurfaceTests.test_engines_on_also_checks_the_control_store_it_forwards_to", + "OpenHandsGatewaySurfaceTests.test_host_file_allowlists_are_exact_arms_and_bounded_patterns", + "OpenHandsGatewaySurfaceTests.test_provider_allowlist_refuses_every_other_served_provider", + "OpenHandsGatewaySurfaceTests.test_provider_lists_are_the_omniroute_source_lists", + "OpenHandsGatewaySurfaceTests.test_surface_reads_only_the_provider_column_combo_count_and_two_settings_keys", + "OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate x17", + "OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts", + "OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates", + "OpenHandsProbeTests.test_gate_runs_the_g5_provider_preflight_on_fixture_stores", + "OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate" + ], + "first_exception_lines": [ + "ERROR OpenHandsGatewaySurfaceTests.test_engines_on_also_checks_the_control_store_it_forwards_to => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does", + "ERROR OpenHandsGatewaySurfaceTests.test_host_file_allowlists_are_exact_arms_and_bounded_patterns => AttributeError: module 'openhands_recipe_test_host' has no attribute 'gateway_allowlists'", + "ERROR OpenHandsGatewaySurfaceTests.test_provider_allowlist_refuses_every_other_served_provider => AttributeError: module 'openhands_recipe_test_host' has no attribute 'check_gateway_surface'", + "ERROR OpenHandsGatewaySurfaceTests.test_provider_lists_are_the_omniroute_source_lists => AttributeError: module 'openhands_recipe_test_host' has no attribute 'NO_AUTH_PROVIDERS'", + "ERROR OpenHandsGatewaySurfaceTests.test_surface_reads_only_the_provider_column_combo_count_and_two_settings_keys => AttributeError: module 'openhands_recipe_test_host' has no attribute 'sqlite3'", + "ERROR OpenHandsProbeTests.test_any_unexpected_observation_fails_the_probe_and_the_gate => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have (x17)", + "ERROR OpenHandsProbeTests.test_gate_refuses_stale_future_mismatched_or_rebound_receipts => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not hav", + "ERROR OpenHandsProbeTests.test_gate_requires_the_host_recorded_stage_gates => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have the attribu", + "ERROR OpenHandsProbeTests.test_gate_runs_the_g5_provider_preflight_on_fixture_stores => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not have t", + "ERROR OpenHandsProbeTests.test_probe_receipt_is_bound_owner_only_and_accepted_by_the_gate => AttributeError: /blueprints/runtime-workers/openhands/host.py'> does not h" + ] + }, + { + "id": "08-R3-G5-green", + "tree": "Commit 726849b352f23889d7d08f5ea221eb136db241d1, extracted with git archive.", + "command": "git archive 726849b3 | tar -x -C /trees/726849b3; in that tree: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": ["Ran 118 tests in 9.031s", "OK"] + }, + { + "id": "09-R3-G5-audit", + "tree": "726849b3's code and tests, before that commit's README, decision-record and research.md edits.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 /p3/audit_run.py (the module's suite under sys.addaudithook, recording every sqlite3.connect and every open event under the two live OmniRoute data directories)", + "exit_code": 0, + "output": "{'tests': 118, 'failures': 0, 'errors': 0, 'sqlite_connects': 92, 'live_store_accesses': []}" + }, + { + "id": "10-R5-gw8080-red", + "tree": "726849b3 with the R5 and gw:8080 test edits only; recipe files unchanged.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 1, + "summary": ["Ran 120 tests in 7.662s", "FAILED (failures=2)"], + "non_ok_tests": [ + "OpenHandsIsolationTests.test_host_side_access_log_records_no_request_line", + "OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids" + ], + "first_exception_lines": [ + "FAIL OpenHandsIsolationTests.test_host_side_access_log_records_no_request_line => AssertionError: Lists differ: [] != ['ingress']", + "FAIL OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids => AssertionError: '/v1/responses' not found in 'Time window + model + path at the arm entry gateway; concurr" + ] + }, + { + "id": "11-R5-literal-negative-control", + "tree": "5a0b0a54's content, before it was committed, with receipt.py temporarily changed to pass correlation_ids={window.get(\"run_id\")} to gateway_rows, the brief's literal R5. The file was restored afterwards: the same SHA-256 prefix before and after, and commit 5a0b0a54 holds the restored file.", + "command": "TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands.OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids", + "exit_code": 1, + "summary": ["Ran 1 test in 0.090s", "FAILED (failures=1)"], + "non_ok_tests": [ + "OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids" + ], + "first_exception_lines": [ + "FAIL OpenHandsReceiptTests.test_responses_rows_keep_window_attribution_because_the_gateway_replaces_caller_ids => AssertionError: 'empty_window' != 'observed'" + ] + }, + { + "id": "12-R5-gw8080-green", + "tree": "Commit 5a0b0a54a19a65ae1ecea4950a09b40dd9e27cbf, extracted with git archive.", + "command": "git archive 5a0b0a54 | tar -x -C /trees/5a0b0a54; in that tree: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_runtime_worker_openhands", + "exit_code": 0, + "summary": ["Ran 120 tests in 7.421s", "OK"] + }, + { + "id": "13-audit-5a0b0a54", + "tree": "Commit 5a0b0a54, extracted with git archive.", + "command": "in /trees/5a0b0a54: TMPDIR= PYTHONDONTWRITEBYTECODE=1 python3 /p3/audit_run.py", + "exit_code": 0, + "output": "{'tests': 120, 'failures': 0, 'errors': 0, 'sqlite_connects': 94, 'live_store_accesses': []}" + }, + { + "id": "14-audit-negative-control", + "tree": "Not a repository tree; /p3/audit_control.py.", + "command": "python3 /p3/audit_control.py (the same matching conditions as audit_run.py; sys.audit raises two planted events naming each live store's storage.sqlite and one naming a fixture path, and nothing is connected or opened)", + "exit_code": 0, + "output": "{'planted_live_events': 2, 'detected': 2, 'fixture_events_detected': 0}" + }, + { + "id": "15-omniroute-correlation-source", + "tree": " at 045aa81f3 (the 20128 build) and dd6e9607e (the 20129 build).", + "command": "git show : | sha256sum for each file below at both commits; git show 045aa81f3: | sed -n p", + "exit_code": 0, + "output": { + "sha256_prefix_equal_at_both_builds": { + "src/app/api/v1/responses/route.ts": "50d42226d52e", + "src/shared/utils/requestId.ts": "a64a84272495", + "src/sse/handlers/chat.ts": "6d76396caec9", + "src/app/api/v1/chat/completions/route.ts": "4a9a591e6ac8", + "src/shared/utils/correlationPreserve.ts": "bbeecb755cf9", + "open-sse/handlers/chatCore/attemptLogging.ts": "c3f112d92b2f", + "src/lib/usage/callLogs.ts": "64269db02cb0" + }, + "lines": { + "src/app/api/v1/responses/route.ts:193": "const correlationId = generateRequestId();", + "src/app/api/v1/responses/route.ts:213": "return finishAdmission(await handleChat(resolved, null, resolvedBody));", + "src/shared/utils/requestId.ts:100-102": "export function generateRequestId() { return randomUUID(); }", + "src/sse/handlers/chat.ts:436": "const reqId = correlationId || generateRequestId();", + "src/app/api/v1/chat/completions/route.ts:292-295": "#11739: preserve caller-provided X-Correlation-Id when present; generate only when absent.", + "open-sse/handlers/chatCore/attemptLogging.ts:611": "correlationId: correlationId || traceId,", + "src/lib/usage/callLogs.ts:713,786,801": "correlationId: entry.correlationId || null; the INSERT's correlation_id column takes @correlationId" + } + } + }, + { + "id": "16-nginx-release-1.30.5-bytes", + "tree": "Not a repository tree.", + "command": "curl -fsSL https://raw.githubusercontent.com/nginx/nginx/release-1.30.5/ | sha256sum for the four files below; cmp with the copies read earlier in the round; gh api repos/nginx/nginx/git/ref/tags/release-1.30.5 --jq .object.sha", + "exit_code": 0, + "output": { + "sha256_prefix": { + "src/http/modules/ngx_http_proxy_module.c": "3bd280886adfbd22", + "src/http/ngx_http_request.c": "6aa0cff0840047d8", + "src/http/modules/ngx_http_log_module.c": "26c731d6762ae2a3", + "src/http/modules/ngx_http_chunked_filter_module.c": "85274f1d726e40ee" + }, + "earlier_copies_identical": "the proxy, request and log module files; the chunked filter had no earlier copy", + "tag_object": "9afafb84841376ee288f19b66302e7883aaa16b5", + "lines_cited": "ngx_http_proxy_module.c:747-757 (default proxy headers: Connection empty at :749; Content-Length and Transfer-Encoding generated at :750-751), :1301-1303,1417-1418 (empty values skipped), :4210-4228 (defaults merged only for names the configuration leaves unset); ngx_http_log_module.c:170-171 (log_format at http level only), :230-232 (combined logs $request); ngx_http_request.c:2034-2064 (framing refusals), :4103-4107 (request-bound error entries carry the request line)" + } + } + ] +} diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index d3535f401..86ff1b780 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -384,7 +384,7 @@ green runs are in | --- | --- | | F16 forgery bound | The newest-`ConversationErrorEvent` design stays. The strict alternative, exit 3 for every `error`, would label every honest iteration-limit run an infrastructure failure. `dispatch.py` `PERMITTED_TERMINATIONS` keeps a label only as a refinement of the REST status, so exit 0 requires REST `finished`. A planted event can move exit 3 to 1 and no further. The README states the residual and the analysis rule: exit 1 and exit 3 are both non-success, and reruns re-queue only exit 3. | | `install()` image identity, open after phase 1 | Fixed. Both a plain and a `--platform linux/amd64` inspect must list the exact pinned ref in `.RepoDigests`. The plain `.Id` must be the index digest (containerd store) or the config digest (classic store). On containerd the platform `.Id` and `.Descriptor.digest` must be manifest `ec7ed86f...`; on classic the platform `.Id` must be the config and no descriptor may appear. Sources: moby docker-v29.8.1@464cd50c `daemon/containerd/image_inspect.go:28,71-73,95,97`; `daemon/images/image_inspect.go:59`, `daemon/images/image.go:160-197`, `daemon/internal/image/store.go:152,160`, `daemon/internal/image/fs.go:120`, `daemon/internal/distribution/pull_v2.go:431-434,705-747,845-867`; `api/swagger.yaml:1826-1850`. Inspect with `--platform` needs Engine API v1.49 (`api/docs/CHANGELOG.md:175-180`). On the containerd store no field reports the config digest. That covers inspect, history (`daemon/containerd/image_history.go:113-115`) and the manifest summary (`api/swagger.yaml:8216-8335`). The content-addressed platform manifest binds the config there. The Docker containerd image-store page (docker/docs@3c117d8e `content/manuals/engine/storage/containerd.md`) does not describe image IDs. | -| Empty `OH_SESSION_API_KEYS_0=` value | Documented residual, because preflight never reads values. SDK@fcc102a `env_parser.py:183-197` turns it into a one-item list holding an empty string, and `__main__.py:30-48` then treats authentication as enabled. | +| Empty `OH_SESSION_API_KEYS_0=` value | Documented residual, because preflight compares only the name: it reads the file, but never checks, logs or returns the value. SDK@fcc102a `env_parser.py:183-197` turns it into a one-item list holding an empty string, and `__main__.py:30-48` then treats authentication as enabled. | | Unused `run_worker`, `record_event`, `observations()` | Kept, because references remain: `worker.py:202,232`, and `observations` is used in `tests/test_runtime_worker_openhands.py`. | Open finding from this phase, not changed here: @@ -453,3 +453,51 @@ Open items from this phase: - normal-bridge containers such as cognee-live still reach host loopback. - **Outside this recipe's paths:** the repository's secret-path guard does not yet cover the new `secrets/` directory. + +## Repair round (2026-09-28) + +Two independent reviews of e45c3cd1 returned changes-needed: a headless Claude +review and a GPT-6 review. This single repair round adds commits on top of +e45c3cd1 and nothing else. Every code item had a failing test first. Each red run +and each fixed commit's suite, re-run from `git archive`, is in +[evidence/repair-round-commands.json](evidence/repair-round-commands.json). No +container, network, gateway or model request was made. The G5 tests read +synthetic sqlite stores, and an audit hook showed that the suite opened nothing +under the live OmniRoute data directories. + +| Finding | Correction and source | +| --- | --- | +| R1 (Claude blocker; GPT-6 containment-grader, high). The official grader ran the model's patch on Docker's default bridge, which reaches host loopback. | **Grading has no network (9302ec23).** Every grader container gets network mode `none`, "No networking for this container" (docker/docker-py@7.1.0 `docker/models/containers.py:686-694`). The adapter refuses other options, checks the SDK create body (`api/container.py:445-457`, `types/containers.py:351`) and refuses to grade a container whose inspect shows another network. moby@docker-v29.8.1 records mode `none` as the single network `none` and refuses to connect it elsewhere (`daemon/create.go:251`, `daemon/container_operations.go:202-204,363-406`). Verdicts are offline verdicts (SWE-bench@v4.1.0 `test_spec/python.py:443-444`, `test_spec/test_spec.py:55-60`). | +| R2 (Claude, medium). Every live probe would have refused its own server: the isolated network has no gateway, so the server holds `.1`, which the probe targeted. | **Targets come only from recorded IPAM gateways (92de42e8).** moby@464cd50c skips the gateway in isolated mode (`bridge_linux.go:700-713`, `network.go:1594-1602`). The attempt containers' own addresses are excluded and recorded by role, never refused. The fixtures now model the isolated network. | +| R4 (Claude, low). P2 counted timeouts as containment, tried TCP only and asserted no route table. | **Stronger P2 (92de42e8).** A positive control to `gw:8081` runs first. Off-subnet pairs must fail with `ENETUNREACH` or `EHOSTUNREACH`. One DNS datagram (RFC 1035 4.1.1-4.1.2) to 10.0.2.3:53 (rootlesskit@v3.1.0 `docs/network.md:81-82`) must get no answer. `/proc/net/route` (linux@v6.18 `net/ipv4/fib_trie.c:2940-3000`) must hold no default or gateway route. | +| Addendum b (Claude C4, info). DNS negatives did not tell `EAI_AGAIN` from `EAI_NONAME`. | The resolver's error class is recorded in the observation and the receipt (92de42e8). The pass condition stays "no address resolved". | +| R6 (Claude, low) and GPT-6 probe-receipt-trust (low). G7, G2 and G5 held only in documents, and the gate trusted `passed` flags. | **Recorded stage gates and re-derived counts (185de718).** The gate re-derives P0-P2 from the recorded counts and refuses a contradictory receipt. It also requires the host-owned `stage-gates.json` with P3 (plus P4 and P5 for engines-on), G2 for both pinned images and G5, each bound to the proxy image and template. In the docs commit the README calls P0-P2 bounded observations and states the trusted-producer limit. | +| R3/G5 (Claude, high). The request body picks the model, and nothing enforced G5. | **G5 at every start (726849b3).** The gate reads each store the arm reaches read-only and refuses providers outside the host file's per-arm allowlist. Row-less no-auth and anonymous-fallback providers must be disabled in settings, and routing combos are refused. Sources: OmniRoute@045aa81f3 and @dd6e9607e, listed in the decision record. | +| R5 (Claude, low). "Pass `correlation_ids={run_id}`." | **Not applied as written (5a0b0a54).** At both builds `/v1/responses` hands `handleChat` a fresh `randomUUID()` (`src/app/api/v1/responses/route.ts:193,213`, `src/shared/utils/requestId.ts:100-102`, `src/sse/handlers/chat.ts:436`), and `call_logs.correlation_id` stores it (`open-sse/handlers/chatCore/attemptLogging.ts:611`, `src/lib/usage/callLogs.ts:713,786,801`). Only `/v1/chat/completions` keeps a caller ID (`route.ts:292-322`). The recipe fixes `/v1/responses`, so the filter would drop every row; entry 11 of the evidence shows it turning the fixture's usage into `empty_window`. Receipts keep time-window attribution, and the attribution string now says why. A regression test counts rows with gateway-generated IDs. P3's correlation check moved to a `/v1/chat/completions` call. | +| GPT-6 session-key-logging (low). "Never in output or logs" was too strong: the model can put the key into a request line, and the combined log keeps it. | The host side (8080) logs time, method and status only (`ingress`; nginx release-1.30.5 `ngx_http_log_module.c:170-171,230-232`). The README now says the driver does not intentionally log the key. The agent side's access log and request-bound error entries (`ngx_http_request.c:4103-4107`) can still hold it, and the retained proxy log survives key-file deletion. | +| GPT-6 gw-8080 (info). "Only three routes are reachable" was false: nginx listens on every interface, so the agent reaches `gw:8080`. | Every such claim now says that three routes reach OmniRoute and that `gw:8080` reaches only the agent's own server: README, decision record, MCP policy reasons and the netprobe docstring. | +| GPT-6 nginx-headers (info). The header description omitted fields nginx generates. | The README lists the defaults (`ngx_http_proxy_module.c:747-757`, merged at `:4210-4228`). At release-1.30.5 these are the generated `Content-Length` or `Transfer-Encoding: chunked` and an empty, so omitted, `Connection`, with no `Connection: close`. It also lists the framing refusals (`ngx_http_request.c:2034-2064`). | +| C9 (Claude, medium). The decision record said isolation "holds by construction" and the narrowing said "no host access". | The record now separates three parts. The agent-server's containment and grading's lack of a network hold by construction. What the three routes reach is G5's checked gateway state. Nothing has been observed live. Narrowing condition 2 names the proxy routes, the session key, G5 and offline grading, and G5 joins the narrowing's overturn list. | +| Addendum a (GPT-6 session-key-lifecycle wording). "Values stay unread" was inaccurate. | `check_server_env`, the README and this file now say that the host reads the file and compares only the variable name, and never logs or returns the value (5a0b0a54 and the docs commit). | +| Found while editing P3. P3 expected model `cx/gpt-6-astra-max` in the row, but the receipt matches the routed model. | P3 now expects `gpt-6-astra-max`, the value `receipt.summarize_gateway` matches. | + +Open after the repair round: +- **Live and unrun:** P0-P5, G2's scans and G5 against the live stores. The + stage-gates file does not exist, so `dispatch.py start` refuses. G5 will + likely refuse the live stores until three things are done. The ten no-auth + providers must be blocked and the four anonymous-fallback providers disabled + in both gateways' settings. Routing combos must be removed. +- **Source-derived only:** that off-subnet connects on the rootless isolated + network fail with `ENETUNREACH` or `EHOSTUNREACH`, and that nginx accepts the + `ingress` log format (no running nginx has parsed the template). +- **Per-attempt usage attribution** needs a host-trusted capture of + OmniRoute's returned ID or an upstream change. The agent side's access log + could record `$upstream_http_x_correlation_id`. The streaming + `/v1/responses` route passes no extra headers to its early response, though, + so that capture may miss streamed calls. Until one is shown, receipts + include concurrent callers of the same model and path. +- **Grading is offline:** an evaluation that needs downloads fails. The gold + and negative controls run under the same condition. +- **G5's limits:** it cannot see a Codex connection's app-server opt-in + (`open-sse/executors/codex.ts:415-439`). It counts inactive rows as served. Its + provider lists are tied to the two builds read. diff --git a/docs/decisions/2026-09-28-openhands-resolver-isolation.md b/docs/decisions/2026-09-28-openhands-resolver-isolation.md index 0d9248bc1..5fb246847 100644 --- a/docs/decisions/2026-09-28-openhands-resolver-isolation.md +++ b/docs/decisions/2026-09-28-openhands-resolver-isolation.md @@ -21,7 +21,8 @@ its own source, and a source the build did not re-read is labelled as reviewed i - **A pinned nginx-unprivileged proxy** on both networks. As `gw:8081` it forwards three exact `/v1` routes with fixed methods, fixed upstream URIs and a fixed header set to one arm's gateway port on 10.0.2.2. The host's loopback ingress (`127.0.0.1:`) reaches the proxy's 8080, - which forwards to the server. + which forwards to the server. The agent can reach that listener too, as `gw:8080`; it leads + only to the agent's own server, and its access log keeps time, method and status only. - **OmniRoute stays unchanged:** no login, password or management key is added. The model's bearer stays the placeholder `local-loopback`, and `OMNIROUTE_API_KEY` is never sent. - **The only secret is a per-attempt agent-server session key.** It is generated by the host, @@ -52,9 +53,21 @@ its own source, and a source the build did not re-read is labelled as reviewed i subprocess executors, so the gate requires every one to be disabled by id or alias. Routing combos are refused. The stage-gates file must also carry the coordinator's G5 record. -Isolation then holds by construction, whatever OmniRoute's authentication state. It also covers -the other host-loopback listeners, not only OmniRoute. Gateway settings cannot provide it. The -synthesis reviewed these points at OmniRoute@045aa81f3, and this build did not re-read them: +**What this design establishes, and where it stops (scoped in the repair round, C9).** None of +it has been observed live; the P0-P2 receipt and the stage-gates records are the operational +holds. It has three parts, and only the first two hold by construction: +- **The agent-server.** Its only standing peer is the proxy; the P1/P2 probe joins `$S-int` only + while a probe runs. Three routes reach OmniRoute, and `gw:8080` reaches only the agent's own + server. This does not depend on OmniRoute's authentication state, and it covers the other + host-loopback listeners, not only OmniRoute. +- **Grading.** Grader containers have no network at all (repair R1), so a model-written patch + runs offline when it is graded. +- **What the three routes reach.** The request body selects the model, so the providers behind + the routes are bounded by G5's check of each store at every start (repair R3). That is a check + of gateway state, which can change after the check, not a property of the topology. + +Gateway settings cannot provide the first two parts. The synthesis reviewed these points at +OmniRoute@045aa81f3, and this build did not re-read them: - With `requireLogin=false`, every management route that is not always-protected is anonymous for loopback peers (`policies/management.ts:99-103,253-266`; `apiAuth.ts:451`). - With `requireLogin=true` and no password, a loopback caller can still post `requireLogin: false` @@ -176,12 +189,15 @@ with tools acts on owner-authored issue text. This record therefore narrows the lane only. Owner-authored issue text may reach a model with tools only when all three conditions hold: 1. the coordinator starts the run and declares its scope; -2. the model's tools are contained by the O1 topology above and by the resolver's GitHub harness, - so they have no network beyond the proxy's three routes, no credential and no host access; +2. the model's tools are contained by the O1 topology above, the G5 provider check and the + resolver's GitHub harness. Their only network is the proxy: three routes reach OmniRoute, + which serves only allowlisted providers when G5 passes, and `gw:8080` reaches only the + agent's own server. They hold no credential other than their own server's session key, reach + the host only through those routes, and their patch is graded with no network; 3. a validated patch is the only output. **Overturn, reverting to coordinator-only execution,** if any of these happens: -- a containment probe or check fails: P0-P2, or the resolver acceptance check A7; +- a containment probe or check fails: P0-P2, G5, or the resolver acceptance check A7; - an outgoing-text guard finding or an unowned path appears in any run; - the lane gains a trigger other than an explicit coordinator invocation. A timer, label or webhook needs its own dated record. @@ -216,8 +232,8 @@ change implements none of it. ## Evidence class -The evidence so far has three parts -([evidence/phase2-commands.json](../../blueprints/runtime-workers/openhands/evidence/phase2-commands.json)): +The evidence so far has four parts. The first three are in +[evidence/phase2-commands.json](../../blueprints/runtime-workers/openhands/evidence/phase2-commands.json): - offline unit checks with Docker mocked, which are our integration checks and not upstream tests; - read-only host observations: registry byte hashes of the pinned proxy image, one pull by @@ -226,6 +242,13 @@ The evidence so far has three parts only loopback. Every failure there also occurs at the base, and the evidence entries show that comparison. +The fourth is the repair round after two independent reviews of e45c3cd1 +([evidence/repair-round-commands.json](../../blueprints/runtime-workers/openhands/evidence/repair-round-commands.json); +[research.md](../../blueprints/runtime-workers/openhands/research.md#repair-round-2026-09-28)). It +holds a failing run for each code item and the suite on each fixing commit. It also holds source +reads at the pinned revisions, and an audit-hook run showing that the G5 tests read fixture +stores only. These are also our integration checks. + No attempt network or container was created. No gateway or model request was made. The first host evidence for this design is the live P0-P2 probe ([sequence](../../blueprints/runtime-workers/openhands/README.md#live-probe-sequence-coordinator-not-run-yet)). @@ -241,8 +264,10 @@ host evidence for this design is the live P0-P2 probe - nginx.org `ngx_http_core_module` (`location`, `limit_except`), `ngx_http_proxy_module` (`proxy_pass`, `proxy_pass_request_headers`, `proxy_set_header`) and `ngx_http_rewrite_module` (`if`), fetched 2026-09-28. nginx/nginx@release-1.30.5 (4556c714) - `src/http/modules/ngx_http_proxy_module.c:1240-1251,1301-1303,1365-1384,1417-1418` and - `src/http/ngx_http_variables.c:1609-1624`. + `src/http/modules/ngx_http_proxy_module.c:747-757,1240-1251,1301-1303,1365-1384,1417-1418,4210-4228`, + `src/http/ngx_http_variables.c:1609-1624`, and, read in the repair round, + `src/http/modules/ngx_http_log_module.c:170-171,230-232` and + `src/http/ngx_http_request.c:2034-2064,4103-4107`. - `ghcr.io/nginx/nginx-unprivileged@sha256:ed04ec1ff34502c339ee5c3ae3f855442398edc1d05591e2b98981dcbbd20b1e`, source nginx/docker-nginx-unprivileged@588b4cbc (`pins.json` `gateway_proxy`). - moby/moby@a46e6fa7 `contrib/dockerd-rootless.sh:23-24,170-173`. @@ -259,7 +284,10 @@ host evidence for this design is the live P0-P2 probe `src/shared/constants/providers/noauth.ts`, `apikey/gateways.ts:741,756`, `apikey/specialty-media.ts:56`, `oauth.ts:243`, `src/lib/db/core.ts` and `src/lib/db/settings.ts:154,283-293`; `open-sse/executors/codex.ts:415-439` and the - `child_process` importers of plan N6. + `child_process` importers of plan N6. For the receipt's correlation (R5), identical at both: + `src/app/api/v1/responses/route.ts:193,213`, `src/shared/utils/requestId.ts:100-102`, + `src/sse/handlers/chat.ts:436`, `src/app/api/v1/chat/completions/route.ts:292-322`, + `open-sse/handlers/chatCore/attemptLogging.ts:611` and `src/lib/usage/callLogs.ts:713,786,801`. - OmniRoute@045aa81f3, as reviewed in the phase-2 synthesis and not re-read here: `src/lib/apiBridgeServer.ts`, `.env.example`, `policies/management.ts`, `apiAuth.ts`, `require-login/route.ts`, `apiKeys.ts`, at the lines cited above. diff --git a/tests/test_runtime_worker_openhands.py b/tests/test_runtime_worker_openhands.py index 1e8eaf39a..a257142fa 100644 --- a/tests/test_runtime_worker_openhands.py +++ b/tests/test_runtime_worker_openhands.py @@ -566,6 +566,31 @@ def test_proxy_image_evidence_matches_the_pin_and_is_publishable(self): for document in ("README.md", "research.md"): self.assertIn("evidence/phase2-commands.json", (RECIPE / document).read_text()) + def test_repair_round_evidence_pairs_each_red_run_with_a_green_commit(self): + # The repair round on e45c3cd1: every code item's failing run, then the + # suite on the commit that fixed it. Our integration checks, publishable. + from scripts.validate import PRIVATE_CONTENT + name = "evidence/repair-round-commands.json" + record = self.read_json(name) + self.assertEqual(record["base"], "e45c3cd1c274aff47b82129bf8d22ed374bd577b") + commands = {item["id"]: item for item in record["commands"]} + self.assertEqual(len(commands), len(record["commands"])) + self.assertTrue(record["item_runs"]) + for item, (red, green) in record["item_runs"].items(): + with self.subTest(item=item): + self.assertEqual(commands[red]["exit_code"], 1) + self.assertTrue(commands[red]["summary"][-1].startswith("FAILED")) + self.assertTrue(commands[red]["non_ok_tests"]) + self.assertEqual((commands[green]["exit_code"], commands[green]["summary"][-1]), (0, "OK")) + commit = re.match(r"Commit ([0-9a-f]{40})\b", commands[green]["tree"]) + self.assertIsNotNone(commit) + self.assertIn(commit.group(1), record["commits"].values()) + text = json.dumps(record, ensure_ascii=False) + for description, pattern in PRIVATE_CONTENT: + self.assertIsNone(pattern.search(text), description) + self.assertIn(name, (RECIPE / "research.md").read_text()) + self.assertIn(name, (ROOT / "docs/decisions/2026-09-28-openhands-resolver-isolation.md").read_text()) + class OpenHandsUpstreamAdapterTests(unittest.TestCase): """Synthetic transport controls from SWE-bench 4.1.0 reporting.py. From ec229a780516edb18b5ea88f29b2f9c039aea85c Mon Sep 17 00:00:00 2001 From: Scout Date: Mon, 28 Sep 2026 18:38:27 -0400 Subject: [PATCH 22/22] Rebase onto main 83229e24: register the PR's files; research notes #468's guard coverage The rebase dropped the earlier registration commit (conflict with #468 in manifests/evidence.json, resolved to main's side), so all 55 files this PR changes are registered again with host_receipts.register_file. research.md no longer says the secret-path guard misses secrets/: #468 (11a23f1b) covers it, with docker exec/inspect recorded as the remaining gap. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../runtime-workers/openhands/research.md | 7 +- manifests/evidence.json | 272 +++++++++++++++++- 2 files changed, 271 insertions(+), 8 deletions(-) diff --git a/blueprints/runtime-workers/openhands/research.md b/blueprints/runtime-workers/openhands/research.md index 86ff1b780..a21888d4e 100644 --- a/blueprints/runtime-workers/openhands/research.md +++ b/blueprints/runtime-workers/openhands/research.md @@ -451,8 +451,11 @@ Open items from this phase: enabled. It has not run against the live stores; - F10, the client peer the gateway logs; - normal-bridge containers such as cognee-live still reach host loopback. -- **Outside this recipe's paths:** the repository's secret-path guard does not - yet cover the new `secrets/` directory. +- **Outside this recipe's paths:** the repository's secret-path guard covers + the `secrets/` directory since #468 (merged 2026-09-28, `11a23f1b`): readers, + copies and searches of it, its files and a glob in it are blocked, and the + settings template denies `Read` of it. `docker exec` into the agent-server and + a full `docker inspect` of it still show the key (recorded guard gap). ## Repair round (2026-09-28) diff --git a/manifests/evidence.json b/manifests/evidence.json index f13d66ab6..97c145b08 100644 --- a/manifests/evidence.json +++ b/manifests/evidence.json @@ -3501,8 +3501,8 @@ }, { "path": ".github/osv-scanner-lockfiles.json", - "sha256": "d1cab283114c18e009752d9489851ef733f7ac0fedaa4981426724005aeacfef", - "bytes": 9814 + "sha256": "c0025db3159bef98f27abaa32cbd1522067a7d3f114f5c339893883d6fc39dcd", + "bytes": 10060 }, { "path": ".github/osv-scanner.toml", @@ -3696,8 +3696,8 @@ }, { "path": "adoption/credential-inventory.json", - "sha256": "a37085f2a47af3e97de2ee34912cf233e1a196bc46e7e976b28bede947a1444d", - "bytes": 16338 + "sha256": "5e20ce0d08dd203b5b9d86ee2375cd03131a4c1f3c18b096bb48f66371bb6e1a", + "bytes": 17862 }, { "path": "adoption/hardware-profiles.json", @@ -8620,6 +8620,256 @@ "sha256": "df268219b74a432cfc49b1e53e9911cfd8c632fc86d07a963719472ab26022fa", "bytes": 2690 }, + { + "path": "blueprints/runtime-workers/openhands/README.md", + "sha256": "23bb0ae8f02c4d40baf0f65f20b169808f5348b3e9010e8d3a80d9be9afd46c9", + "bytes": 71896 + }, + { + "path": "blueprints/runtime-workers/openhands/build-requirements.lock", + "sha256": "32cff7dc47659683a19936944a47123160731a2bdb711aee78a7d63fbab205e2", + "bytes": 286 + }, + { + "path": "blueprints/runtime-workers/openhands/config/host.example.json", + "sha256": "25e8499980ad05e3dac6a042e23d3ba3fc2b5015791d5ffb41d1c4a8f17d9e01", + "bytes": 857 + }, + { + "path": "blueprints/runtime-workers/openhands/config/mcp-policy.json", + "sha256": "e229f8b36b2a7ffd7a023fa59e45b96e070411bf14872cde0b73f6fdc330374e", + "bytes": 1663 + }, + { + "path": "blueprints/runtime-workers/openhands/config/mcp.template.json", + "sha256": "7e452b37fa45c7d3cd653a7e43bc99ccc07168b2bd7bbdf1ca1467b1309a6a53", + "bytes": 3059 + }, + { + "path": "blueprints/runtime-workers/openhands/config/proxy-nginx.conf", + "sha256": "cbbb64f01eabbac149142e98a6697d54eabe2fad41a4f9095d782af64b1a5630", + "bytes": 4358 + }, + { + "path": "blueprints/runtime-workers/openhands/config/serena_config.yml", + "sha256": "c49c6f0e2e7b33116483b8ab10bfc5cb9056efa0f7f3b7bf05064a0491a06565", + "bytes": 395 + }, + { + "path": "blueprints/runtime-workers/openhands/config/worker.json", + "sha256": "0363f132b82aa838b45b8d612508cf6c2ff636ca891aae29b25caf65ecb73c2f", + "bytes": 1283 + }, + { + "path": "blueprints/runtime-workers/openhands/dispatch.py", + "sha256": "a579f6ce525ddffc05989c05425e485562c982ae9d0c78b92af97424691d8ca2", + "bytes": 19469 + }, + { + "path": "blueprints/runtime-workers/openhands/e2e/check.py", + "sha256": "bcb8ee028037d39c385e80884b0b2fc492f47a418bf872ba0e6dcf1b96530a7b", + "bytes": 3522 + }, + { + "path": "blueprints/runtime-workers/openhands/e2e/docker_grader.py", + "sha256": "b75fd7cbb250d24b58f788d4e465910983ecc24599b828687c213c657dde4799", + "bytes": 5871 + }, + { + "path": "blueprints/runtime-workers/openhands/e2e/netprobe.py", + "sha256": "52650f68e8c0ab48dcef57f2a7a82c4a29ea88d4ce691df83cc6219aa9e3ebcb", + "bytes": 21917 + }, + { + "path": "blueprints/runtime-workers/openhands/e2e/task.py", + "sha256": "832043a6733b112cc9cc79d5a82524849bc357374bb1eb0ba267747cf24fcaa7", + "bytes": 2956 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/agent-server-image-grype-20260928.json", + "sha256": "8eceed55832600bd1c728e47d7e3cbcaa5f081e5b8167374cd18dc5e6def6dca", + "bytes": 36999 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/expanded-tests.txt", + "sha256": "b291a1d1f4af02653c53d4f9eeb0aaecf4985d079ae5ed77c4979bb6cdb136c6", + "bytes": 3118 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/fail-first.txt", + "sha256": "499168343f4f33d8a54294f350ff38c3c69cdcb6e06c6b691eb2fd927504d76f", + "bytes": 8151 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/phase1-commands.json", + "sha256": "cbd76e3a31da13c27566b24f6c6c083b351d2cfe7e1b140a868419ff4334ec6f", + "bytes": 34970 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json", + "sha256": "56a5db9f436dd117f63e2fe98d6a203632e18cd06a185adacd8b944fe3f057be", + "bytes": 40180 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/phase2-commands.json", + "sha256": "843768ab93898fbd3e41b44d388d6d1411ad5c322c482c18066c6f1dc169b7de", + "bytes": 50331 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/publication-first.txt", + "sha256": "1f8791713a42ea92c498d4f1bd352f345b3a7c6f0d5a115462cad25fdc9527ed", + "bytes": 114 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/publication-pass.txt", + "sha256": "4c6f58444061b23b0b40dc27ffb52e3f55ffdd02e3e4195407ec57992d1e5be2", + "bytes": 212 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/receipt-red.txt", + "sha256": "fb365ecd833144fc3112e44373c1b152fe69298dd77b4dff388e68f70e0bf9e4", + "bytes": 6368 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/relock-2026-09-27.txt", + "sha256": "7e23a33b38eee053064b4152baf5543caec46d33ed33a8f683f470c618375373", + "bytes": 33599 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/repair-round-commands.json", + "sha256": "89e32d55231db27386ed16dc077fa56dbd710c9c45f80433fa02c6b42e13b54e", + "bytes": 24151 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-fail-first.txt", + "sha256": "221598a5ce0669d5521e07c4b96e510d95244491d0febfacfcc99466547f9122", + "bytes": 46977 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-publication-first.txt", + "sha256": "bb5a392ee7ea7f1a5dd1729eeef3ef7ac557e9570bead139f7eabeac088f64ad", + "bytes": 1711 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-publication.txt", + "sha256": "1f8829c06c4222a0e029d6255979db4dfba7ac9cbb5373eec7e0456c0b1a890a", + "bytes": 1570 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-static-checks.txt", + "sha256": "49efbda45452a34a6c3e4f224b0fcaeb9f47356ca3ce55db69be875aad26822a", + "bytes": 2175 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-unit-pass.txt", + "sha256": "ccf01200e5abc65a74ce8b77a78cc3db03783dd107424b10e32dbc425c3188c6", + "bytes": 4894 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round2-verification.md", + "sha256": "3b38e242c6a73000d6874013fe674e26d7edb767057c8c3e1dca7ebf5535819f", + "bytes": 4649 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round3-commands.json", + "sha256": "1df821aa25a43514b57f6cca68878a13ce47ae05d967d66f543237998c56eed6", + "bytes": 136251 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/round3-verification.md", + "sha256": "6d689203155b396bf2f9a1c9824579814886397b0e343082f94b94ea47aa9abb", + "bytes": 30470 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/serena-red.txt", + "sha256": "d3872f7b0505b7e096af32ccf7ae99d7b655ea7ac5a5e7268718fc0b0122de18", + "bytes": 3568 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/unit-pass.txt", + "sha256": "6576e0f1b0ea27a22909854a90299ffef78034239d7147629eec595f6f520de0", + "bytes": 3013 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/verification.md", + "sha256": "51c90be86ab3fb3535f42aeb9227bff1479f101a3232860b47a98765c3e37269", + "bytes": 2872 + }, + { + "path": "blueprints/runtime-workers/openhands/evidence/wheel-source-comparison.json", + "sha256": "fcbc715b373c55bdd4a14c240ecc423cd338ff1b6a44a0c387929b43e33df325", + "bytes": 388 + }, + { + "path": "blueprints/runtime-workers/openhands/host.py", + "sha256": "c6a609b51403353da7fc16bc131cd2f76904b00f498d9478e30b742e6bc89e4c", + "bytes": 84290 + }, + { + "path": "blueprints/runtime-workers/openhands/install-container.sh", + "sha256": "1b639d729d7a3e2384e39c2817269aaa786615b2806d7d8123b8e18b081f098a", + "bytes": 1291 + }, + { + "path": "blueprints/runtime-workers/openhands/install-grader.sh", + "sha256": "84c23ee6ab190562761d65aca61d50d8a4bf33e0285f180dc00a1df3a288a243", + "bytes": 2359 + }, + { + "path": "blueprints/runtime-workers/openhands/install.sh", + "sha256": "8c79bfc0a512ad31653b428e19da458309ff3866cb6ec7d1410c0d2e7109cdd8", + "bytes": 690 + }, + { + "path": "blueprints/runtime-workers/openhands/mcp_guard.py", + "sha256": "b3d6012c8b2e65e3923d64bc39a2cf6ae9ea7042de9d4f6a0c342cfbc69eed03", + "bytes": 1326 + }, + { + "path": "blueprints/runtime-workers/openhands/pins.json", + "sha256": "cbefaf39340c43d2ce7f6411e93e62fb11979f29c2a09b67c7b957ea50f7c683", + "bytes": 4460 + }, + { + "path": "blueprints/runtime-workers/openhands/qmd-setup.py", + "sha256": "f419df9eef4ba3c48d3916a86ea46a735a091ca67f2868c59190bdb02571b7f4", + "bytes": 1126 + }, + { + "path": "blueprints/runtime-workers/openhands/receipt.py", + "sha256": "f8d04162b554157cf958d72ebc80f9adbc1f07a98a3b39bed4d72a16f0b00cc8", + "bytes": 17709 + }, + { + "path": "blueprints/runtime-workers/openhands/recipe.py", + "sha256": "8de3c2159a137c477716ba2871bbb9efa5275169f746a84ba5c98c16ecabce77", + "bytes": 6670 + }, + { + "path": "blueprints/runtime-workers/openhands/requirements.lock", + "sha256": "02d0a7f058d08d28d0fb3f7344ed9b042bf5a317607454faf4bbb210af7fa394", + "bytes": 106002 + }, + { + "path": "blueprints/runtime-workers/openhands/research.md", + "sha256": "1d47c05cfc3a1bb3541b6b80aaf4f1bed2949962a2d67115aa36cd5c07ddc2d2", + "bytes": 60078 + }, + { + "path": "blueprints/runtime-workers/openhands/run-e2e.sh", + "sha256": "a14cf4b25484b77c5f5b6d3103877d9eae3dee660b087b1e264710dd93c207a4", + "bytes": 442 + }, + { + "path": "blueprints/runtime-workers/openhands/server_transport.py", + "sha256": "39a26c5588f5865a129c982d0f60c591c4e745b35547816e68328e118cb97ac4", + "bytes": 629 + }, + { + "path": "blueprints/runtime-workers/openhands/worker.py", + "sha256": "ea5d67a538f87feac8a4d0b8a66cfdcbaef44f86a8ee7cb11efc332272bb1f5c", + "bytes": 11141 + }, { "path": "blueprints/runtime-workers/skills/README.md", "sha256": "4a367abac888f8ac75da50b0cc3b37fe98f112630c1f98195f32b9f68130efac", @@ -14110,6 +14360,11 @@ "sha256": "ef93948f54a2ed1695a1d332f01423d2ba6266c5429146fe547142c16e986698", "bytes": 16692 }, + { + "path": "docs/decisions/2026-09-28-openhands-resolver-isolation.md", + "sha256": "1a2d28d0a0f998d3945ddbadb2c4ae18dd4e72838a0315a77bfea885483435ab", + "bytes": 19915 + }, { "path": "docs/decisions/2026-09-28-top-rule-templates.md", "sha256": "85002f8d31fe6ee4d333bf0ee4e2510bb34d549020ea29a6b5345907ef1c92c6", @@ -14407,8 +14662,8 @@ }, { "path": "docs/secret-storage.md", - "sha256": "cd2c4275b86d0908cb102e7676f68d18faf22a5f39401046fd9e304438985d48", - "bytes": 81526 + "sha256": "6d7a9d5e33efc186c2876daa5990713be7cc8e3f7491190de2022b4d952624f2", + "bytes": 82026 }, { "path": "docs/stack.md", @@ -42645,6 +42900,11 @@ "sha256": "d1f61612522dc3de6bb78b9bf953668d920fe3778567e935d1b609131dab87ee", "bytes": 58079 }, + { + "path": "tests/test_runtime_worker_openhands.py", + "sha256": "1103b1cd52507997cd6a3c628970d7fe46007fc0ddcf3fb43027f53f05a4b981", + "bytes": 220324 + }, { "path": "tests/test_runtime_worker_skills.py", "sha256": "e021cb1b2e615c35de09a4029dc5c0f21d49a31f5c3c72109c40d1f9acfcd25f",