Skip to content

Runtime worker: OpenHands on GPT-6 via OmniRoute (recipe, O1 isolation, reviewed repair round) - #425

Merged
seathatflowsinourveins merged 22 commits into
mainfrom
claude/runtime-worker-openhands-20260927
Sep 28, 2026
Merged

seathatflowsinourveins merged 22 commits into
mainfrom
claude/runtime-worker-openhands-20260927

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

This PR adds a runtime-worker recipe: OpenHands, powered by GPT-6 through the local OmniRoute gateway, with upstream skills, MCP tools and the token practice. Rounds 1–3 were built by GPT-6 (astra, max) and the runtime lock was then relocked. On 2026-09-28 the coordinator took the PR over and added:

  • phase 1: session-key and dispatch hardening;
  • phase 2: per-attempt network isolation (O1) and its decision record;
  • one repair round for two independent reviews.

SOTA sources

  • OpenHands/software-agent-sdk v1.49.6 @ fcc102a697874d54a357e36004e02c95040dbdc0: the standalone worker. The source archive, uv.lock and wheels are pinned by sha256, and the agent-server image by digest in pins.json. The session key is OH_SESSION_API_KEYS_0 (openhands-agent-server/openhands/agent_server/config.py:24).
  • OpenHands/OpenHands v1.24.0 @ 7dc6805406ea3c76cb4a3ce407c3c72d481b0ac6, as reference.
  • Grader: OpenHands/benchmarks @ 405bae7140d7e961a75f4910a0b2e7069731db96 with official SWE-bench 4.1.0 grading. The runner-up is harbor-framework/harbor v0.23.0.
  • Isolation (phase 2; per-claim citations in docs/decisions/2026-09-28-openhands-resolver-isolation.md and research.md):
    • moby/moby docker-v29.8.1 bridge gateway_mode_ipv4=isolated, internal networks and embedded DNS;
    • nginx release-1.30.5 exact locations, proxy_pass_request_headers off, limit_except and $is_args, through ghcr.io/nginx/nginx-unprivileged 1.30.5-alpine pinned by digest;
    • docker/docker-py 7.1.0 network_mode="none" for grader containers.
  • G5 gateway-provider gate: the OmniRoute 3.8.51 storage schema (src/lib/db/core.ts, src/lib/db/settings.ts) and its no-auth and anonymous-fallback provider lists.
  • Runtime lock: astral-sh/uv 0.12.17 environments; OSV-Scanner 2.6.0.
  • Gateway rules: OmniRoute 3.8.51 routing verdict of 2026-09-27. Effort is pinned by the -max model suffix; the recipe sends a stable x-omniroute-session and a fresh Idempotency-Key.

Status

Recipe with offline acceptance only. Nothing is installed as a service, no attempt container has run, no model has been called through the proxy, and P0–P5 have not run live. Those live gates and this host's install and receipts belong to the follow-up resolver PR.

  • O1 isolation (phase 2).
    • Each attempt gets an --internal, IPv6-off network with gateway mode isolated. Its only peer is a pinned nginx proxy. As gw:8081 the proxy forwards exactly three /v1 routes (fixed methods, URIs and header set; query strings refused) to one arm's OmniRoute port. The host's loopback ingress reaches the agent-server through the proxy's 8080.
    • OmniRoute gets no login or password.
    • The per-attempt session key lives only in two 0600 files under <state>/secrets/. It is deleted after confirmed removal, and Secret guard and template: cover the OpenHands runtime-worker session-key directory #468 put that directory under the secret-path guard.
  • Dispatch gate. Dispatch requires:
    • a fresh P0–P2 receipt bound to the live IDs, whose counts re-derive a pass;
    • the host-owned stage-gates.json (P3, G2 image scans, G5), which stays absent until those gates are observed live.
  • Grading. Every SWE-bench grader container is created with network mode none. The adapter checks this on the SDK create body and on inspect, and removes and refuses anything else.
  • G5. At dispatch start, every gateway store an arm can reach is read read-only. It must pass three conditions, or dispatch refuses:
    • every provider is in the arm's allowlist;
    • there are no routing combos;
    • the no-auth and anonymous-fallback providers are blocked in settings.
  • Arms:
    • Control: cx/gpt-6-astra-max through 127.0.0.1:20128.
    • Engines-on: sharedgw/gpt-6-astra-max through 127.0.0.1:20129, with exactly one slash.

Independent review and repair (2026-09-28)

Two reviews of e45c3cd1 ran: a headless Claude security review (claude -p, read-only tools) and a GPT-6 cross-family review (Codex CLI 0.157.1, gpt-6-astra, effort max, read-only). Both said changes-needed. The raw verdicts are posted as a PR comment. One repair round followed, rebased as:

commit findings
153de40a R1 / containment-grader (blocker): network mode none for grading
03480d3f R2: probe targets on isolated IPAM with no gateway. R4: off-subnet targets need ENETUNREACH or EHOSTUNREACH, plus a route table, a positive gw:8081 control and a UDP DNS negative. The DNS errno class is recorded
5f29b5fb R6/G7: the stage-gates.json gate. Probe-receipt trust: the verdict is re-derived from its counts
30d2cd71 R3 / G5: the provider allowlist, combo and settings gate
086b7ec1 R5, source-checked deviation (below). gw:8080 ingress log format, method and status only. Key-file wording
62f6a22a C9, the three-routes claim, the nginx header description, key-logging wording, P0–P2 described as bounded observations, and the repair-round command record
  • R5 deviation. Filtering receipt usage on correlation_ids={run_id} would drop every row. At both pinned gateway builds, /v1/responses stores a gateway-generated randomUUID() rather than the caller's X-Correlation-Id (src/app/api/v1/responses/route.ts:193,213; open-sse/handlers/chatCore/attemptLogging.ts:611). A literal application failed the new test ('empty_window' != 'observed'). Receipts keep time-window attribution, and the attribution string says why. P3's run-id check now uses a /v1/chat/completions control call, which keeps the caller's ID (route.ts:292-322).
  • Rebase. The branch was rebased onto 83229e24 to clear the conflict with Secret guard and template: cover the OpenHands runtime-worker session-key directory #468. ec229a78 re-registers the PR's 55 files and updates research.md for Secret guard and template: cover the OpenHands runtime-worker session-key directory #468's guard coverage.

Checks (head ec229a78, on main 83229e24)

  • python3 -m unittest tests.test_runtime_worker_openhands: 121 tests OK. The red→green run for each repair commit is in evidence/repair-round-commands.json.
  • tests.test_secret_path_guard and tests.test_credential_status: OK.
  • python3 scripts/validate.py: "status": "passed" (7,984 hashed files).
  • git diff --check origin/main: clean, except the verbatim captured log evidence/round2-fail-first.txt, whose trailing spaces are unittest's own output and are kept byte-exact.
  • An audit hook on the G5 tests counted sqlite connects (94 at the final repair commit) and 0 accesses to the live OmniRoute stores; a planted control confirmed it detects them.

Residuals

  • Nothing runs live yet. P0–P5, G2 and G5 have not run against the live stores or containers. Off-subnet ENETUNREACH on the rootless isolated network is derived from source, not probed. The new nginx ingress log format has not been loaded by a running nginx.
  • P3's control call assumes cx/gpt-6-astra-max over /v1/chat/completions writes a call_logs row. If it doesn't, P3 needs a fallback before it can pass.
  • G5 will refuse on today's gateways until the operator blocks the listed no-auth providers and disables the anonymous-fallback ones in both gateways' settings. It cannot see a Codex connection's app-server opt-in, counts inactive rows as served, and is a start-time check.
  • Usage attribution is by time window, model and path, so concurrent callers of the same model on the same gateway are included.
  • Key exposure. The model can still read the in-container key and write it into a request line that the retained proxy error log records. docker exec into the agent-server and a full docker inspect of it show the key, which is the guard gap recorded in Secret guard and template: cover the OpenHands runtime-worker session-key directory #468.
  • Owner trust. A consistent receipt forged by the owning user passes the gate (trusted producer).
  • Image CVEs. The agent-server image scan (evidence/agent-server-image-grype-20260928.json) lists 32 critical and 168 high findings. That is G2 input, not yet a gate result.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Sep 27, 2026
@socket-security

socket-security Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/runtime-worker-openhands-20260927 branch from 1746057 to 2fd937a Compare September 27, 2026 21:18
seathatflowsinourveins pushed a commit that referenced this pull request Sep 27, 2026
…ef's tip

On 2026-09-27 six PRs (#416, #425-#429) failed CI's validate job on three
registry checks that run in about a second locally:
- the OSV lockfile inventory;
- the blind-checkout label classification;
- the new-workflow security coverage.

scripts/git-hooks/pre-push runs those three test methods against the tip
commit of each pushed ref, in a detached worktree of exactly that commit, and
refuses the push unless all three run and pass. Like CI, which tests one
commit per pull request, it does not test the commits between a ref's old and
new tip. A skip or an expected failure counts as a failure, so the hook
refuses while zizmor is missing and there is a commit to test.
`git push --no-verify` is the visible override. The same core.hooksPath that
enables the pre-commit gitleaks gate activates it.

Scratch and cleanup:
- The scratch base defaults to /var/tmp. The checkout is about 140 MB, and
  file-hierarchy(7) keeps /tmp, usually a tmpfs, for small files.
- The inside-a-repository guard asks git's own discovery, after dropping the
  GIT_DIR that git exports to hooks. An empty .git mount point, as a sandbox
  leaves in /tmp on the WSL host, is not a repository, so it is not refused.
- HUP, INT, QUIT, PIPE and TERM exit through the EXIT trap, as Autoconf's
  configure traps 1, 2, 13 and 15. A reader that closes early
  (`git push 2>&1 | head -n 1`) no longer leaves a worktree behind.
- The runner puts the checked-out tree first on sys.path, so PYTHONSAFEPATH
  plus a PYTHONPATH naming another checkout cannot supply the tests.
- stdin is parsed before the zizmor check, so a deletion needs no zizmor.

Sources:
- githooks(5) for git 2.43.0: pre-push, and the exported GIT_DIR.
- git's templates/hooks--pre-push.sample: the all-zero oid and the read loop.
- git-rev-parse(1) --absolute-git-dir, and git(1) GIT_DIR and
  GIT_CEILING_DIRECTORIES.
- file-hierarchy(7).
- Autoconf lib/autoconf/general.m4 at 0c777e326bd9: traps 1, 2, 13 and 15.
- python3 -P and PYTHONSAFEPATH.
- The repository's pre-commit gate and its test, as the pattern.

tests/test_pre_push_gate.py has 16 tests, run on a shared clone with
plumbing-built commits. Each run sets GIT_CEILING_DIRECTORIES above its
temporary root, and one test pushes natively from a linked worktree to a
shared bare clone. Each test checks that no worktree or scratch is left behind.
Of twenty named mutations of the hook, the tests catch sixteen; the PR lists
the four defensive lines they miss.

Docs: docs/secret-storage.md step 5c (behaviour, prerequisites, override and
cleanup), docs/harness-defaults.md (an anti-pattern row), and the builder
row's Use cell in examples/claude-native/workflows/README.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/runtime-worker-openhands-20260927 branch from 2fd937a to 45d40f6 Compare September 27, 2026 23:00
@seathatflowsinourveins seathatflowsinourveins changed the title Runtime worker: OpenHands on GPT-6 via OmniRoute (recipe, round 2) Runtime worker: OpenHands on GPT-6 via OmniRoute (recipe, round 3 and lock relock) Sep 27, 2026
seathatflowsinourveins added a commit that referenced this pull request Sep 27, 2026
…ef's tip (#438)

On 2026-09-27 six PRs (#416, #425-#429) failed CI's validate job on three
registry checks that run in about a second locally:
- the OSV lockfile inventory;
- the blind-checkout label classification;
- the new-workflow security coverage.

scripts/git-hooks/pre-push runs those three test methods against the tip
commit of each pushed ref, in a detached worktree of exactly that commit, and
refuses the push unless all three run and pass. Like CI, which tests one
commit per pull request, it does not test the commits between a ref's old and
new tip. A skip or an expected failure counts as a failure, so the hook
refuses while zizmor is missing and there is a commit to test.
`git push --no-verify` is the visible override. The same core.hooksPath that
enables the pre-commit gitleaks gate activates it.

Scratch and cleanup:
- The scratch base defaults to /var/tmp. The checkout is about 140 MB, and
  file-hierarchy(7) keeps /tmp, usually a tmpfs, for small files.
- The inside-a-repository guard asks git's own discovery, after dropping the
  GIT_DIR that git exports to hooks. An empty .git mount point, as a sandbox
  leaves in /tmp on the WSL host, is not a repository, so it is not refused.
- HUP, INT, QUIT, PIPE and TERM exit through the EXIT trap, as Autoconf's
  configure traps 1, 2, 13 and 15. A reader that closes early
  (`git push 2>&1 | head -n 1`) no longer leaves a worktree behind.
- The runner puts the checked-out tree first on sys.path, so PYTHONSAFEPATH
  plus a PYTHONPATH naming another checkout cannot supply the tests.
- stdin is parsed before the zizmor check, so a deletion needs no zizmor.

Sources:
- githooks(5) for git 2.43.0: pre-push, and the exported GIT_DIR.
- git's templates/hooks--pre-push.sample: the all-zero oid and the read loop.
- git-rev-parse(1) --absolute-git-dir, and git(1) GIT_DIR and
  GIT_CEILING_DIRECTORIES.
- file-hierarchy(7).
- Autoconf lib/autoconf/general.m4 at 0c777e326bd9: traps 1, 2, 13 and 15.
- python3 -P and PYTHONSAFEPATH.
- The repository's pre-commit gate and its test, as the pattern.

tests/test_pre_push_gate.py has 16 tests, run on a shared clone with
plumbing-built commits. Each run sets GIT_CEILING_DIRECTORIES above its
temporary root, and one test pushes natively from a linked worktree to a
shared bare clone. Each test checks that no worktree or scratch is left behind.
Of twenty named mutations of the hook, the tests catch sixteen; the PR lists
the four defensive lines they miss.

Docs: docs/secret-storage.md step 5c (behaviour, prerequisites, override and
cleanup), docs/harness-defaults.md (an anti-pattern row), and the builder
row's Use cell in examples/claude-native/workflows/README.md.

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
seathatflowsinourveins pushed a commit that referenced this pull request Sep 28, 2026
… 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) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/runtime-worker-openhands-20260927 branch from 45d40f6 to e45c3cd Compare September 28, 2026 20:21
seathatflowsinourveins added a commit that referenced this pull request Sep 28, 2026
…-key directory (#468)

* Secret guard and template: cover the OpenHands runtime-worker session-key directory

The OpenHands runtime worker (PR #425) has its host driver write each
attempt's agent-server session key to two 0600 files in a 0700 directory,
~/.local/state/native-agent-stack/runtime-workers/openhands/secrets/:
<run-id>-<arm>.server.env (Docker env-file syntax) and <run-id>-<arm>.headers
(a curl -H @file header line), deleted after the attempt. ENV_FILE_WORD
already caught readers of *.server.env; nothing covered *.headers, the
directory itself or a glob in it.

This follows the guard's own design for home and tool credential stores
(docs/secret-storage.md, "Home and tool credential stores (2026-09-27)"):
HOME_CREDENTIAL_STORE plus matching template Read denies, with
tests/test_secret_path_guard.py deriving a reader from every template Read
deny.

- scripts/hooks/secret_path_guard.py: HOME_CREDENTIAL_STORE adds
  runtime-workers/openhands/secrets as a directory (the directory, anything
  in it and a glob in it); comment and docstring extended.
- adoption/templates/claude.settings.template.json: adds
  Read(~/.local/state/native-agent-stack/runtime-workers/openhands/secrets/**)
  and its Read(**/...) Context Mode twin after the other credential-store
  denies. Claude Code permissions
  (https://code.claude.com/docs/en/permissions, read 2026-09-28): Read rules
  use gitignore syntax, `~/path` is relative to the home directory, and a
  user-settings rule that should apply in every project needs a `//` or
  `~/` anchor.
- tests/test_secret_path_guard.py blocks: cat, head, tail, cp, cp -r,
  grep -r, rg and curl -H @ of the header and env files, the directory and
  a glob in it, also behind rtk proxy and a keyring exec.
- It passes: ls, stat, chmod, test ! -e, docker run --env-file, a read of
  the sibling runs/ state and code searches of OH_SESSION_API_KEYS_0.
- Paper-lane non-regression, all passing on base and after:
  paperkeys.sh run 1 -- echo ok, kernel_keyring.py status and
  kernel_keyring.py exec --help. The documented exec form was already
  covered.
- Recorded pass-throughs: docker exec printenv, a shell in the container,
  a full docker inspect, a search of the state directory above secrets/, and
  a relative read after cd.
- A new test checks that the template keeps the deny and its twin.
- docs/secret-storage.md: guard coverage row, hand-merge block, and in the
  credential-store section a new store bullet, the guard paragraph, clients
  and recorded gaps. The gaps name docker exec into the agent-server and a
  full docker inspect of it (Config.Env holds the key), which the guard
  does not model.
- adoption/hooks/claude/SHA256SUMS and manifests/evidence.json re-pinned
  with host_receipts.register_file.

OH_SESSION_API_KEYS_0 is deliberately not added to SECRET_NAMES (scope
change). It is set only inside the agent-server container, so a host
$OH_SESSION_API_KEYS_0 expands to nothing, and docker exec printenv is not
a reader. Listing it would refuse plain code searches of the name.

Red and green runs (TMPDIR under /var/tmp,
python3 -m unittest tests.test_secret_path_guard):
- Red 1, tests only: FAILED (failures=49): test_blocked_commands 12,
  test_every_rule_applies_behind_rtk_proxy 24,
  test_every_rule_applies_to_a_keyring_exec 12, the template-deny test 1.
- Red 2, plus the template deny: FAILED (failures=52). The derived-reader
  test adds 4 without any change to it: cat, head -n, tail -n and rtk read
  of .../secrets/sample.txt.
- Green, plus the guard: 16 tests, 1 failure,
  test_host_profile_copy_is_verbatim. The host's installed guard copy is
  still the previous f2ae894e until install_claude_profile.py --only guard
  is rerun (docs/secret-storage.md, "User-level guards"). With CI=true:
  OK (skipped=1).

Non-regression replay of the base bdf25d2 guard against this guard:
5917 fenced lines from 616 Markdown files (this repository plus #425's 7
at e45c3cd) and #425's 81 recorded commands, 0 verdict changes.
tests.test_workflow_hardening OK (74 tests). test_install_claude_profile,
test_apply_claude_settings and test_render_config OK (110 tests).
scripts/validate.py passed. git diff --check is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Secret-storage docs: record the review's inherited guard gaps and narrow the variable-name rationale

The GPT-6 cross-family review of 3a4fdba (approve, low/info amends) found:
- path matching is literal: a `./` or interior `//`, a path relative to a
  parent, and pipeline-fed readers (`find -print0 | xargs -0 cat`,
  `find -exec rtk read`) pass, as they do for older stores on the base guard
  (`cat ~/.config/./omniroute/gateway.sqlite`);
- a pattern given via `-e` or an unmodelled long option (`rg --fixed-strings`)
  that names the directory is now read as a search of it;
- "never on the host" overstated: the driver never exports the name; listing
  it would also stop explicit lookups inside `docker exec`.
Each case was replayed in-process against base and head guards. Docs only;
no guard, template or test change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scout and others added 14 commits September 28, 2026 18:36
…nned 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) <noreply@anthropic.com>
…nchmarks, 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) <noreply@anthropic.com>
…way 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…r 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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
…entity

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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
…er-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 = <run-id>-<arm>:
- 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:<port>: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) <noreply@anthropic.com>
…, method pairing

- e2e/netprobe.py: locally composed stdlib probe (plan section 1), run in
  the pinned agent image on <stem>-gw (P0) and <stem>-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) <noreply@anthropic.com>
…e 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) <noreply@anthropic.com>
…vidence 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Scout and others added 8 commits September 28, 2026 18:36
…sults

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) <noreply@anthropic.com>
…rader 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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
…, 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) <state>/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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
… 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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
…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 (11a23f1) covers it,
with docker exec/inspect recorded as the remaining gap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/runtime-worker-openhands-20260927 branch from e45c3cd to ec229a7 Compare September 28, 2026 22:38
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Independent reviews of e45c3cd1 (bounded loop: one review round, one repair round)

Two reviews ran on the same head, both read-only, with no containers started and no requests to gateway listeners.

1. Headless Claude security review: claude -p --model opus, tools limited to Read, Grep, Glob and Bash; Edit, Write, Agent and Workflow disallowed. Overall: changes-needed

id verdict severity location finding
R1 refute blocker dispatch.py:213, e2e/docker_grader.py:31-38 Automatic grading runs the model's patch inside SWE-bench 4.1.0 containers. They are created with no network argument (docker_build.py@v4.1.0:516-523), and owned_container_options refuses network options, so they land on the default bridge. From there, model-written code reaches every host-loopback listener at 10.0.2.2, which bypasses the proxy and the P0–P2 gate.
R2 refute medium host.py:805, host.py:888-892 Per moby docker-v29.8.1 source, gateway_mode_ipv4=isolated makes IPAM skip the gateway, so the agent-server receives <subnet>.1. The probe targets .1 and then refuses probe_target_is_attempt_container, so every live P0–P2 run would fail closed. The unit fixtures assume a gateway that Docker does not allocate.
R3 amend high decision record :152-153, :33-34 The body-selected model (plan gate G5) is not enforced. OmniRoute has /v1 executors that spawn host processes, and the decision record's "no host access" does not mention this.
R4 amend low e2e/netprobe.py:119-129,190,204 P2 counts a connect timeout as containment and tests TCP only. It has no route-table assertion and no positive connect control.
R5 amend low receipt.py:228 Pass correlation_ids={run_id} so usage joins on the X-Correlation-Id that the proxy pins.
R6 amend low host.py:1284-1292 The P3, G2 and G5 holds exist only in documentation. There should be a host-owned gate file.
C1–C8 confirm info — nginx release-1.30.5 location normalisation and fixed URIs; header allowlist and TE/CL rejection; gw:8080 reaches only the agent's own server; embedded DNS has no host forwarding; session-key handling; the probe-receipt binding; F16/F17 exit-0; image identity on both stores.
C9 amend medium decision record :33-34 "Holds by construction" and "no host access" overclaim, given R1 and R3.

The reviewer's checks: python3 -m unittest tests.test_runtime_worker_openhands ran 107 tests, OK, exit 0. python3 scripts/validate.py exited 0. Both are offline, with Docker mocked.

2. GPT-6 cross-family review: Codex CLI codex-cli 0.157.1, model gpt-6-astra, effort max, read-only sandbox, via tools/sota-convergence/landscape-sweep/codex_call.sh. Overall: changes-needed

id verdict severity location finding
containment-grader refute high e2e/docker_grader.py:36 Same defect as R1. Sources: docker_build.py#L516, run_evaluation.py#L164, docker-py 7.1.0 default network.
gw-8080 amend info config/proxy-nginx.conf:67 "Only the three routes are reachable" is literally false: gw:8080 reaches the agent's own server. That is not an OmniRoute escape.
nginx-headers-methods-arguments amend info config/proxy-nginx.conf:45 nginx generates Content-Length or Transfer-Encoding itself. At 1.30.5 its default Connection value is empty. HEAD is permitted under limit_except GET.
session-key-lifecycle confirm (wording) info host.py:639 "Values stay unread" is inaccurate, because read_bounded reads the whole file.
session-key-logging amend low README.md:236 "Never in output or logs" is too strong. The model could put the key in a gw:8080 URI, and nginx's combined log would record it.
probe-receipt-trust amend low host.py:953 The gate trusts passed:true booleans. It accepted a contradictory receipt: 0 observations, connected=99, exit 127.
agent-network-construction, nginx-routing, F16-F17, image-identity, evidence-honesty confirm info — Source-level support for the agent-server topology, the routing, the exit guard, image identity and the evidence classes.

The reviewer's own run of python3 scripts/validate.py passed. Its unittest run failed before discovery, because its read-only sandbox could not create TMPDIR, so the full suite was not validated in that run.

Disposition. Both reviews' non-confirm findings went to one repair round, now rebased onto 83229e24. The PR's "Independent review and repair" section has the per-commit mapping.

  • R1 / containment-grader: 153de40a.
  • R2, R4 and the DNS errno class: 03480d3f.
  • R6/G7 and probe-receipt trust: 5f29b5fb.
  • R3/G5: 30d2cd71.
  • gw-8080 logging, session-key wording and R5: 086b7ec1.
  • C9, the nginx header wording and P0–P2 as bounded observations: 62f6a22a.
  • R5 was not applied as written. At both pinned gateway builds, /v1/responses stores a gateway-generated randomUUID() in call_logs.correlation_id instead of the caller's X-Correlation-Id. So correlation_ids={run_id} would drop every row; the negative control failed with 'empty_window' != 'observed'. Receipts keep time-window attribution, with the reason stated.
  • Live containment (P0–P5 on this host) is still unrun and moves to the resolver PR. Head after rebase and registration: ec229a78.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins changed the title Runtime worker: OpenHands on GPT-6 via OmniRoute (recipe, round 3 and lock relock) Runtime worker: OpenHands on GPT-6 via OmniRoute (recipe, O1 isolation, reviewed repair round) Sep 28, 2026
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review September 28, 2026 22:39
@seathatflowsinourveins
seathatflowsinourveins merged commit f6e5a03 into main Sep 28, 2026
29 of 32 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/runtime-worker-openhands-20260927 branch September 28, 2026 22:57
seathatflowsinourveins pushed a commit that referenced this pull request Sep 29, 2026
… to /private/var)

The resolver stage-2 tests created their temporary roots as
Path(tempfile.mkdtemp(...)) without resolving them. On macOS mkdtemp returns
a /var/folders path, and the recipe refuses an owned path that follows a
symlink (preflight owned_path_must_not_follow_symlinks; read_bounded opens
each component with O_NOFOLLOW), so validate-macos failed 13 tests in
ResolverHostTests, ResolverResultTests and ResolverRunTests. The earlier #425
tests already resolve their roots for the same reason.

Reproduced on Linux with TMPDIR pointing at a symlink: 127 resolver tests ran
with failures=13 errors=12 before this change and all 127 pass after it; the
recipe's own 121 tests pass under the same symlinked TMPDIR. With the normal
TMPDIR, 248 tests pass. Test-only change, 12 lines; manifests/evidence.json
hash row refreshed with host_receipts.register_file.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
seathatflowsinourveins pushed a commit that referenced this pull request Oct 3, 2026
Main contradicts the three "first published" claims. PR #425
(f6e5a03, on main since 2026-09-28T22:57:18Z) already labels
045aa81f3 as 20128's build, and PR #531's stack row (0a41bf8) states
the patch on 20128 about eleven minutes before the rebuild record
(b4056a3).
The record, the routing README addendum and the roadmap update now say
only that the rebuild record has the patch running on 20128 in
ae5539a56.

Cite the PR #425 labels as a coordinator's labels on a source read
(the repair round made no gateway request). Note that, read as the
running build, they conflict with the rebuild record's account
(:30-31, :135-136). Keep 2026-09-29 00:42Z and 2026-09-30 00:03:00Z as
upper bounds only: the record neither asserts nor rules out an earlier
date. Add 045aa81f3 and its git log -S hits to the [^rebuild] search
statement. Narrow the 81c9b6da sentence to the basis, citing the
roadmap :51 report of the gateway owner's rebuild in progress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 4, 2026
#656)

* docs: retire the gateway A/B R02 preregistration draft

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore: register the R02 retirement record and refresh reports

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs: repair the R02 retirement record after review

Date the end of the R02 freeze from the rebuild record's own account: 20128
ran 5fc47d970 at 81c9b6da since 2026-09-29 00:42Z, so it had left dd6e9607e
by then. The 2026-09-30 rebuild is the first published record of the affinity
patch, and part (ii) of the approved patch stays open.

Cite the #445 PR description, an author report, for the review rounds, the
ninth defect and the test runs, in place of a private file. Restate the owner
line from the #445 timeline. Limit the no-run claim to the records, quote the
plan's cost-independent adopt-max rule, and name the rebuild file in the
roadmap update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: re-register the repaired R02 retirement documents

Re-register the retirement record, the routing README addendum and the
roadmap update after the review repair. Both generators left the four
reports byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: drop the first-published claims from the R02 retirement

Main contradicts the three "first published" claims. PR #425
(f6e5a03, on main since 2026-09-28T22:57:18Z) already labels
045aa81f3 as 20128's build, and PR #531's stack row (0a41bf8) states
the patch on 20128 about eleven minutes before the rebuild record
(b4056a3).
The record, the routing README addendum and the roadmap update now say
only that the rebuild record has the patch running on 20128 in
ae5539a56.

Cite the PR #425 labels as a coordinator's labels on a source read
(the repair round made no gateway request). Note that, read as the
running build, they conflict with the rebuild record's account
(:30-31, :135-136). Keep 2026-09-29 00:42Z and 2026-09-30 00:03:00Z as
upper bounds only: the record neither asserts nor rules out an earlier
date. Add 045aa81f3 and its git log -S hits to the [^rebuild] search
statement. Narrow the 81c9b6da sentence to the basis, citing the
roadmap :51 report of the gateway owner's rebuild in progress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: re-register the R02 retirement documents after the re-check

Re-register the retirement record, the routing README addendum and the
roadmap update after the re-check fixes. Only those three rows changed.
Both generators left the four reports byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: record the overtaken R02 freeze as a failed condition with its rule

Review thread on #656 (P1): the record documented the freeze conflict but
named no failed condition or prevention rule. The roadmap's F-WK-3 record
"goes in the rebuild's receipt PR" and lists "the user's R02 freeze release"
(roadmap :185-188 at 9b0b8d6); the rebuild record, the published account
that replaced the 81c9b6da package (:30-32), carries no release; the freeze
held "with no end date" (decisions.json:33).

Add a "Failed condition" paragraph: it assigns no fault, states the rule
(search a host's recorded freezes and ordering conditions before a restart
or build switch, and record each release in the switch's own record) and
the recovery (record an overtaken freeze in fact, supply no release, carry
open items forward). The anti-pattern log row (AGENTS.md:5,
docs/harness-defaults.md:85) is outside this retirement's paths and is left
to a foundation follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: re-register the R02 retirement record after the failed-condition paragraph

Registry only: the record's files[] row takes its new sha256 and bytes via
host_receipts.register_file. The generated reports were re-run with
--write and did not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: qualify the after-R02 trigger as dormant while the retirement stands (root finding at 447f70b)

Line 145 said the trigger can no longer occur, but the reopen conditions allow R02 to reopen, and a completed
reopened run would satisfy the original after-scored-run predicate (decisions.json:38). Part (ii) of the occupancy
patch stays open with its own acceptance gates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-register the R02 retirement record (hot-file protocol, last commit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restore main's registry before the last-commit re-registration (hot-file protocol)

manifests/evidence.json returns to main 1a64e8f's copy so that the next
commit, the branch's last, carries every owned registry row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Re-register the R02 retirement record (hot-file protocol, last commit)

register_file on main 1a64e8f's registry for the three owned files: the
omniroute-routing-20260928 README, the resolved ecosystem roadmap and the R02
retirement record. component_matrix and new_host_grand_list --write changed
nothing. The tree equals the merge commit's tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Roadmap: follow the file's same-date heading convention ("<date>, later, main <short sha>")

The 2026-10-03 update merged after main's own 2026-10-03 section, so its heading takes the ", later,"
form the roadmap already uses for a second same-date update (2026-09-29, later, main 16f3c7f), with the
short SHA. No anchor link in the repository targets the old heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hot-file protocol: reset manifests/evidence.json to the merge base before the final registry commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Register this branch's files on the merge-base registry (hot-file protocol: every registry edit in the last commit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hot-file protocol: reset manifests/evidence.json to the merge base before the final registry commit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Register this branch's files on the merge-base registry (hot-file protocol: every registry edit in the last commit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Scout <scout@local>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
seathatflowsinourveins added a commit that referenced this pull request Oct 4, 2026
…ges 1-2, offline-tested) (#489)

### Scope

This PR adds the host driver for the OpenHands issue-to-PR resolver on top of the merged #425 recipe. One explicitly scoped, owner-authored issue can produce one validated patch, one draft PR, one COMMENT review and one repair round. The model's patch is not executed on the host. A draft PR can execute it in GitHub CI under the resolver-mode amendment decided on 2026-10-04: option 1, with a trusted pre-push gate that checks every agent commit before any push (see "Pre-push gate" below).

- **Base commit:** `d2fc3803e01178b4687771d0bf91faf453bb6933`, merged in `db24156b9` without rebasing or force-pushing. The original PR head `501bcc9e50bf3ffa3acc82b6ecf20aa4e23c593b` remains in history. Head: `7c1d24cc5600bed8b56435aef37ec8d267f889fe`. On top of `0f7b27578` it corrects this round's dates to 2026-10-04, as `date -u` gives them, in `b763cb294` (text only; the commit also resets the registry to main's copy), and re-registers the rows in `7c1d24cc5` (the last commit). On top of `4eb6b4cc9`, `0f7b27578` added the merge `4f963c9b2` of main `6af8e55bd` (#681, CI least privilege, with #672, #626 and #679). It also adds the gate fix that merge needed: the derivation follows code that gate code runs, not code it only reads (`GateReads.executed`), in `57d0dc065`. Main's #679 made `tools/adoption/install_claude_profile.py` read three workflow scripts, and following them had protected all of `blueprints/`. The decision record, `RESOLVER.md` and evidence part 8 are in `85262b1f8`, and the registry rows in `0f7b27578` (the last commit). The main-merge list below predates this merge. On top of `21b24dede`, `4eb6b4cc9` added the repair round for the cross-family read of `40f12ba5` (GPT-6.1 Sol, findings P1 and P2): the merge `db287ea72` of main `3bdacabd5`, the gate-data reader, the instruction fix and their tests in `86688e1e1`, the decision record, `RESOLVER.md` and evidence part 7 in `0092ae213`, the merge `4a03dd796` of main `ba0e8c48f`, the figures on that merge in `1ec9d04c3`, a receipt test and the final control runs in `e1b4f5c68`, and the registry rows in `4eb6b4cc9` (the last commit). Before that, on top of `050bca5d5`: the zizmor pin read from `.github/requirements-ci.txt` in `a5194090b` (registry `4009a96ec` and `40f12ba51`), then the CI-blocker round for CodeQL alert 90 and validate-macos (the merge `d1bb9cf15` of main `f474f6d22`, `36440d64a`, `9602c2274`, `8be0c1d39`, registry `21b24dede`). On top of `456f8fee6`, `050bca5d5` added the decided amendment's trusted pre-push gate: code and tests in `00905292d` and `48831bc65`, the decision record, `RESOLVER.md` and the evidence log in `868f02501`, and the registry rows in `050bca5d5`. Before that, on top of `b0c11c324`, came wording-only fixes for the [independent re-check](#issuecomment-5973091307) and the coordinator's follow-up: content in `f4e7aa2a2` and `54438ce7f`, registry rows in `d89ad3b44` and `456f8fee6`. This description was written for `b0c11c324`. Parts were updated for `050bca5d5`: the "Pre-push gate" bullet, the SOTA "Pre-push gate" line, the evidence rows and commands, the "Decision record" section and the decision item under "What is not done". For `4eb6b4cc9`, this line, the main merges, the owned paths, the "Pre-push gate" bullet and a SOTA "Gate data" line are updated; the evidence table and the commands still describe `050bca5d5`, and the later rounds' evidence is in `evidence/push-gate-fail-first.txt` parts 6 and 7 and the PR comments. The SOTA "Step 6 repair round" line was updated for `456f8fee6`.
- **Main merges during custody.** Each followed the hot-file protocol: main's `manifests/evidence.json`, then this PR's owned rows re-registered.
  - `9b0b8d6d2` in `37cb51899`;
  - `4ced29230`, which carried main's `AGENTS.md` update, in `85830e815`;
  - `59f8a1e36` in `d35633fad`;
  - `d2fc3803e`, which contains `ecea28654`, in `db24156b9`;
  - `f474f6d22`, which carried main's macOS CI scope step (`e0c329ae9`), in `d1bb9cf15`;
  - `3bdacabd5` in `db287ea72`;
  - `ba0e8c48f`, which carried the OSV and SARIF hardening port, in `4a03dd796`.

  Main has since advanced to `b5b9c9ddb` (#672), which is not merged. Its 13 paths are jCodeMunch carrier files, documents, one test and evidence; none is a workflow or a gate script. The coordination merge-tree landing check of `4eb6b4cc9` against it reports LANDABLE: a clean three-way merge, 21 merged-vs-main paths and none outside the PR-owned set, no overlap with main's 13 drifted paths, the registry's foreign rows equal and in order, 20 PR-owned rows, and a sorted `files[]` without duplicates (9823 rows).
- **Lane:** `lane:foundation`.
- **Owned content paths:** `blueprints/runtime-workers/openhands/RESOLVER.md`, `resolver.py`, `resolver/{patch_policy,gh_harness,outgoing_guard,push_gate,gate_reads}.py`, `skills/resolver/SKILL.md`, `host.py`, `dispatch.py`, `worker.py`, `receipt.py`, `e2e/task.py`, both `evidence/stage2-*fail-first.txt` logs and `evidence/push-gate-fail-first.txt`, `docs/decisions/2026-09-28-openhands-resolver-isolation.md`, and the three OpenHands test modules (`tests/test_runtime_worker_openhands_push_gate.py` is new). The registry commits re-register these files over main's `manifests/evidence.json` and regenerate the four foundation reports through their supported commands. The last full pass is in the merge `db24156b9`. `b0c11c324` refreshes the rows of the three files that the step 6 repair round changed. `050bca5d5` refreshes the six rows the gate change touched and adds three new files' rows. `4eb6b4cc9` registers the 20 PR-owned files over main `ba0e8c48f`'s registry: 7 rows updated and 13 added, `resolver/gate_reads.py` among them. Both generators' `--check` passed each time, so no report was rewritten.
- **Existing main defect disclosed.** Commits `0c9b7acf6` and `3e3877979` fix the SWE-bench skill contract and instruction. At the merged base, `host.py:382` requires `verification-before-completion` and `e2e/task.py:52` tells the worker to invoke it, while the runtime manifest lists that skill under `excluded`. The fix keeps the excluded skill out of both contracts and preserves the instruction to run and report the reproducing tests before finishing.
- **Custody repair.** The alias-refusal fixture now builds all seven entries directly in a scratch Git index. It preserves case and decomposed Unicode names on filesystems that fold them, disables Git's macOS argument precomposition for those insertion commands, and exports the cached patch without restaging the worktree. All existing refusal assertions remain, with an added check that every intended name reached the validator.
- **Review-round repair** (`2ede7b871`, tests only; its registry row in `b2d556c95`). The outgoing-guard fixtures no longer depend on `TMPDIR`. Two `host_path` assertions now use a synthetic absolute host root. The symlink case needs a real temporary root, so it probes that prerequisite. It skips with an explicit message when the root matches a `scripts/validate.py` `PRIVATE_CONTENT` pattern, such as a personal home path; the reason was reworded in `cf42c6d08`. No assertion or reason label changed.
- **Step 6 repair round** (`cf42c6d08`: documentation and one test's comment and skip text; its registry rows in `b0c11c324`).
  - The decision record's option 2 and `RESOLVER.md` now state that the resolver pushes to and opens PRs only in this repository. A fork therefore needs a separately reviewed harness change before any run.
  - Both options now address condition 3 and CI egress.
  - The G4 residual gives the actual refusal order.
  - No behaviour changed.
- **Pre-push gate** (the amendment's decision of 2026-10-04; content in `00905292d`, `48831bc65` and `868f02501`, registry in `050bca5d5`; the cross-family repair in `86688e1e1`, `0092ae213`, `1ec9d04c3` and `e1b4f5c68`, registry in `4eb6b4cc9`).
  - **Push path.** `GhHarness.push` runs `resolver/push_gate.py` on the exact agent commit before any push and pushes that commit by name (`<sha>:refs/heads/<branch>`, never `HEAD`). `GhHarness.run` refuses any push of a commit the gate did not pass, and any push without a gate.
  - **Refusals.** The gate diffs the commit against its base. It refuses, with no push, changes to `.github/**`, a `CODEOWNERS` anywhere, the resolver's gate and harness code, the workflow-policy tests (`tests/**.py` naming `.github`), and every file that a workflow reachable from the push or the PR executes or reads as a gate. That last list is derived from the workflow files by a text-level reader that reuses `patch_policy.names_in_text`: the files `run:` steps name, the import closure of the gate scripts, unittest discovery and local actions. It also refuses a step that interpolates untrusted event text.
  - **Gate data** (cross-family P1). The gate also refuses changes to the data those gate scripts read (`ci_read`, `resolver/gate_reads.py`), such as `blueprints/convergence-practice/contract.schema.json`, which `scripts/validate_convergence.py` reads. Each gate script's expressions are evaluated to the repository paths they spell. An exact path protects that file, and a computed one protects the directory or glob it resolves under. Code that gate code runs or reads is followed to a fixpoint. A read the reader cannot resolve refuses every commit (`gate_input_unresolved`) rather than protecting the whole tree; none is unresolved on the merged tree. The cost is breadth. The merged tree `4a03dd796` has 10,528 tracked files, and the gate protects 7,559 of them (441 before). Outside `evidence/`, `tests/` and `.github/` it protects 633 of 3,444 (122 before). Most of it comes from `tools/sota-convergence/gap_wave_ledger.py`'s computed `evidence/artifacts` and `docs/*.md` paths. The decision record's residual risks name the two deferred narrowings, and a repository test bounds the breadth.
  - **zizmor.** The gate runs the zizmor CI pins (1.30.1) with its own flags (no config, no ignores, offline). The excessive-permissions, dangerous-triggers, cache-poisoning, artipacked and template-injection audits refuse, and a missing or wrong zizmor fails closed.
  - **Trusted copy.** The gate runs only from the checkout it sits in, never from inside an agent tree. Its four files (`push_gate.py`, `patch_policy.py`, `gate_reads.py`, `gh_harness.py`) must equal the trusted commit's and the base's blobs, the trusted commit must be on main at the base, and the trusted commit is recorded. Each check leaves one record per commit in `resolver-outcome.json` (`push_gate`) and a code-and-count projection in the receipt.
  - **CLI.** `run` takes `--zizmor`, and the plan reports `push_gate: trusted_copy_checked`.
  - **Agent instructions** (cross-family P2). The resolver skill and the generated instruction name every category the gate refuses. When a fix would need such a change, including a new or changed test (`tests/**` is protected here), they tell the agent to stop and report instead of editing. A test keeps both in step with the gate's rules and the receipt's.

The resolver's Stage 1 logic covers owner/edit provenance, a patch policy derived from the base's host-executed files, fixed `gh` templates, guarded outgoing text and a bounded PR loop. Stage 2 adds:
- the same O1 containment topology;
- fresh P0-P2 receipts and `stage-gates.json`;
- G5 checks before container creation and at dispatch;
- a pinned-main checkout with no MCP servers, and a fixed skill set;
- a validated patch whose committed diff must match byte for byte.

Its receipt does not infer successful completion from model-writable data.

### Independent review history

1. Stage 1 received a read-only GPT cross-family review (`gpt-6-astra`, max). Four findings were repaired, and a Claude closure review confirmed them closed: import shadowing, edited issue provenance, missing required contexts, and a review bound to a different commit.
2. Stage 2 received three independent Claude reviews. The verifier accepted with low notes; the security and design reviewers requested changes. The retained repair round addressed:
   - patch-content checking before `git apply`;
   - binding the G4 reviewer argv to a recorded hash;
   - retaining a PR record when GitHub holds the PR;
   - the residuals comment;
   - the receipt after a dispatch failure;
   - the excluded-skill instruction;
   - containment evidence;
   - fourteen smaller items.

   Both fail-first logs are unchanged: their blob SHAs at this head (`ed55377f`, `3f41c01b`) equal those at `501bcc9e`.
3. Stage 2's requested separate read-only GPT-6.1 Astra/max review through the packaged lane (custody contract step 6(a)) was pending at `b2d556c95`. It ran at `db24156b9` (job `rev-489-astra`) and returned **repair** with one should-fix finding, which `cf42c6d08` repairs (see "Step 6 reviews" and "Step 6 repair round"). The builder's diagnosis and tests do not count as that review.
4. The headless Opus 5.5 closure review of the whole repaired head (step 6(b)) was also pending at `b2d556c95`. It returned **repair** at `db24156b9`, with two should-fix and seven minor findings; "Step 6 repair round" gives each disposition. Before the PR leaves draft, the coordinator must still:
   - have the reviewers re-read this delta (contract step 6);
   - resolve every review thread;
   - confirm the required contexts on the final head.
5. A read-only Opus 5.5 custody review of `d35633fad` returned **repair**, with two should-fix and six minor findings. Their dispositions are under "Review round" below; reviews 3 and 4 cover the resulting head.

### Step 6 reviews (2026-10-03, head db24156b92ac)

Items 3 and 4 of "Independent review history" and row 7 of "Review round" call these two reviews pending at `b2d556c95`; this section records their returned verdicts at `db24156b92ac`. Findings are condensed to one line each (severity, location, problem) without the reviews' fix proposals. Host paths are replaced by repository-relative paths or `<private path>`. Line numbers cited for the PR body are those of the body as read at 2026-10-03T19:20Z, before this section was added.

**(a) GPT-6 Astra/max cross-family review**, run through the packaged lane, job `rev-489-astra`. Verdict: **repair**. One finding.

- **should-fix** | `docs/decisions/2026-09-28-openhands-resolver-isolation.md:228-236`; `blueprints/runtime-workers/openhands/RESOLVER.md:535-537` | The fork option needs explicit implementation and safety prerequisites. The runbook permits proceeding after either choice, but `gh_harness.py:44-47,257-259,704-708` still targets the upstream repository and rejects a fork push URL, so following it after choosing a fork would use the same-repository path. Forking also leaves networked CI and final-message publication unresolved, while the unconditional token/secrets guarantee is explicitly unverified.

**(b) Opus closure review.** Verdict: **repair**. Nine findings: two should-fix and seven minor.

- **should-fix** | `docs/decisions/2026-09-28-openhands-resolver-isolation.md:227-236`; `blueprints/runtime-workers/openhands/RESOLVER.md:519-521` (also `:420-422`) | The fork option (option 2) is described incompletely, and `RESOLVER.md` files it under the wrong kind of change, so the owner cannot see what it costs or leaves open. (i) `RESOLVER.md:519-521` lists "pushing to an owner fork" among the "workflow changes outside this PR", but it changes this PR's own harness, which only targets this repository (`gh_harness.py:44`, `:241-254`, `:257-259`, `:448`, `:627-628`, `:684-689`, `:704-709`; the record's ruleset section, `:131-178`, binds only this repository's `openhands/*` refs), so option 2 needs harness changes, a ruleset on the fork and its own review before any run, while option 1 needs no code change. (ii) Option 2 (`:230-232`) says nothing about condition 3: `build_pr_body` (`resolver.py:547-550`, `:590-592`) publishes up to 6000 characters of the agent's final message wherever the branch lives, so option 2 leaves that deviation open while option 1 explicitly accepts it, and the record's closing sentence (`:235-236`) implies that accepting either option lets the first live run go ahead. (iii) The fork's owner is not named (the repository is public and owned by a User account; isFork false, forkCount 0), and the record already says GitHub's fork-PR limits were not re-read.
- **should-fix** | PR #489 body (`gh pr view 489 --json body`, read 2026-10-03T19:20Z): lines 5, 6, 8, 46, 52, 149 ("CI on this head"), 151 | The body still describes `b2d556c95`, not the reviewed head `db24156b9`: line 5 (base and head); line 6 (main `ecea2865` "not merged", and the merge list omits `db24156b9`, which merges main `d2fc3803e`, already containing `ecea28654`); line 8 (last full registry pass `d35633fad`); lines 46 and 151 (`d35633fad` as the Linux full-suite evidence, "differs from this head only in the repaired test module"); line 52 and the "CI on this head" paragraph, line 149 (`validate` failed on main's unsorted registry pair). At `db24156b9` the registry has 0 unsorted pairs and `validate` passed: job `111264954786` (head_sha `db24156b9`, CI merge `69650a33` onto `d2fc3803e`) shows `validate.py` `{"hashed_files": 9550, "receipts": 193, "status": "passed"}`, `component_matrix.py --check` `{"rows": 32, "status": "checked"}`, the new-host grand-list check passed, and `python3 -m unittest` "Ran 10078 tests ... OK (skipped=968)". Contract step 8 requires the merged main SHA, the local commands with exit codes and an evidence table for the head that lands.
- **minor** | `tests/test_runtime_worker_openhands_resolver.py:1582-1589` | The probe and the assertions are correct; only the skip message is too narrow. The probe string `f"{self.tmp}/"` (`:1584`) fires exactly when the guard (`outgoing_guard.py:165-170`) would refuse as `private_content` before `host_path`, and no assertion is weakened (`host_path` is still asserted at `:1565` on the synthetic root, the ValueError cases moved unchanged to `:1570-1573` ahead of the skip, and the symlink and realpath assertions at `:1590-1592` are unchanged). The skip message (`:1588-1589`) and the comment (`:1582`) blame "TMPDIR is under a personal home path", but the probe fires on any `scripts/validate.py` `PRIVATE_CONTENT` pattern (`validate.py:25-37`), such as a session-UUID, task-handle or Windows-user-path `TMPDIR`, and then the skip names the wrong cause.
- **minor** | PR #489 body line 175; `blueprints/runtime-workers/openhands/RESOLVER.md:514-516`; order at `blueprints/runtime-workers/openhands/resolver.py:1589-1591` and `host.py:1359-1361` | The body says "G4 remains unrecorded, so a real run refuses with `stage_gate_g4_not_recorded`", but that reason code applies only once the other gates are recorded. Today there is no `<state>/stage-gates.json`: `plan_run` calls `host.verify_stage_gates` first (`resolver.py:1589`), and `read_stage_gates` raises `stage_gates_not_recorded` (`host.py:1360-1361`) before `verify_reviewer_gate` (`resolver.py:1591`) can raise `stage_gate_g4_not_recorded`. The gates themselves are intact: no bypass flag or environment override exists, `_cmd_run` requires `--reviewer-command` (`resolver.py:1663-1664`), and at dispatch start `verify_isolation` (`dispatch.py:319`) re-checks P0-P2 freshness (`host.py:1445-1447`), the stage gates and G5 (`host.py:1463-1464`).
- **minor** | Required context `validate-macos` on `db24156b9` (run `37144290967`, job `111264957003`) | Verification gap, not a defect: `validate-macos` was still pending (queued) at 2026-10-03T19:20:37Z; the other seven required contexts passed on `db24156b9`. The native-macOS evidence the review read is from the earlier head `d35633fad` (artifact `11278304065` of run `37129286224`, `full-suite-macos.log`: "Ran 10070 tests ... OK (skipped=1329)", `test_case_unicode_and_filesystem_aliases_are_refused ... ok`, no FAIL or ERROR in `tests.test_runtime_worker_openhands_resolver`). That confirms the git-plumbing alias fixture on APFS but predates the `TMPDIR` repair `2ede7b871`; the `db24156b9` run is the first native-macOS run of that repair.
- **minor** | Contract step 6(a) (`<private path>:71`); PR #489 review state | Verification gap, not a defect: the separate read-only GPT-6.1 Astra/max cross-family review was still pending when this review ran; GraphQL at 19:20Z showed 0 review threads and 0 reviews on the PR.
- **minor** | `origin/main` `1f5a791b02a230aced670c88bab3d3d0ebcf401a` versus the merged base `d2fc3803e01178b4687771d0bf91faf453bb6933`; `manifests/evidence.json` | Verification gap, not a defect: main moved after the custody merge (#640, #648). Three registry rows changed: `docs/harness-defaults.md`, `observability/grand-dashboard/state.json` and `docs/token-session-handbook.md`. Their hunks do not overlap this PR's `evidence.json` hunks, but the landing head no longer merges current main.
- **minor** | Contract step 5 commands at `db24156b9` | Verification gap, not a defect: the review ran no acceptance command (it had no Bash; those are the coordinator's commands). Not re-run at `db24156b9`: the unit-test pair under a neutral `TMPDIR` and a home-path `TMPDIR`, the planted-defect mutations, `resolver.py --help` and `run --help`, `git diff --check` and the pre-push gitleaks scan. The body records them only at `b2d556c95`. CI on `db24156b9` covers the Linux full suite, `validate.py`, the generator checks and secret-scan.
- **minor** | Items 3-4 and the safety boundary, verified at `db24156b9` | No defect (verification record). Scope: `d2fc3803e..db24156b9` is exactly the 17 allowed paths (`README.md`, `.github/` and `blueprints/us-equities/` untouched; owned files equal `b2d556c95`). Hot-file merge: all 9541 rows of main's `evidence.json` kept in order, 9 new and 7 updated owned rows added, all 16 sha256/bytes values match HEAD, `receipts[]` and `convergence_records[]` equal main's, `files[]` sorted with no duplicates. Preserved: both fail-first blobs (`ed55377f`, `3f41c01b`), decision-record lines 1-15 (match `501bcc9e` and main), the amendment heading at `:208`, the skill-contract fix (`host.py:408-413`, `e2e/task.py:46-56`) and the A7 env-name read in teardown (`host.py:1095`). At the merged base the 11 `pull_request` workflows still declare `contents: read` and use no secrets, and the SARIF upload jobs still skip `pull_request`. `fold()`, `HFS_IGNORABLE` and the alias rules (`patch_policy.py:116-129`, `:893-897`, `:953-958`) decide from git data, and the outgoing guard's check order and 0600 `O_EXCL|O_NOFOLLOW` body files are intact.

### SOTA sources

- [git/git `v2.43.0`](https://github.com/git/git/tree/v2.43.0), matching installed Git 2.43.0: [the native index-fixture reference](https://github.com/git/git/blob/v2.43.0/t/t2107-update-index-basic.sh#L59), [git-hash-object(1)](https://github.com/git/git/blob/v2.43.0/Documentation/git-hash-object.txt#L18), [git-update-index(1)](https://github.com/git/git/blob/v2.43.0/Documentation/git-update-index.txt#L75), and [index insertion implementation](https://github.com/git/git/blob/v2.43.0/builtin/update-index.c#L421). The additional macOS requirement follows [git.c's argument conversion](https://github.com/git/git/blob/v2.43.0/git.c#L449), [precompose_utf8.c](https://github.com/git/git/blob/v2.43.0/compat/precompose_utf8.c#L67), and [core.precomposeUnicode](https://github.com/git/git/blob/v2.43.0/Documentation/config/core.txt#L44). Installed help, versioned release notes and these primary sources were read on 2026-10-03. Earlier resolver patch handling also follows `git-apply(1)`, `git-diff(1)` and `git-merge-base(1)` at this revision.
- [Gitleaks `v8.30.1`](https://github.com/gitleaks/gitleaks/tree/v8.30.1): [release notes](https://github.com/gitleaks/gitleaks/releases/tag/v8.30.1), [native Git-range and directory commands](https://github.com/gitleaks/gitleaks/blob/v8.30.1/README.md#L196), and installed CLI help. The sandbox tests select the same installed upstream binary directly because the host wrapper's lock directory is outside the writable sandbox.
- Existing resolver implementation references: [OpenHands/extensions `bea7a20`](https://github.com/OpenHands/extensions/tree/bea7a20), `skills/github-issue-to-pr/scripts/main.py` (branch naming and loop) and `skills/github-pr-reviewer/scripts/worker.py`; [OpenHands/software-agent-sdk `fcc102a`](https://github.com/OpenHands/software-agent-sdk/tree/fcc102a), v1.49.6; [OpenHands/OpenHands `7bc33009`](https://github.com/OpenHands/OpenHands/tree/7bc33009), the retired resolver comparison. These are the original implementation's historical reviewed pins; the custody change adds no runtime or orchestration alternative.
- [cli/cli `v2.101.0`](https://github.com/cli/cli/tree/v2.101.0) (`0cf10924`), including `pkg/cmd/pr/checks/aggregate.go`, credential-helper behavior and fixed `api`/PR operations; [GitHub create-review documentation](https://docs.github.com/en/rest/pulls/reviews#create-a-review-for-a-pull-request) (`commit_id`) and [GraphQL issue/edit provenance](https://docs.github.com/en/graphql/reference/objects#issue), read in the original 2026-09-28/29 implementation review; CPython `v3.12.3`, `importlib._bootstrap_external.FileFinder`, for package-before-module resolution.
- Repository: `docs/decisions/2026-09-28-openhands-resolver-isolation.md`, `docs/lanes.md`, `.github/pull_request_template.md`, the merged #425 recipe, #429's runtime skill exclusion and #465's agent-branch ruleset. The registry follows the hot-file protocol: main's copy at each merge, then `scripts/host_receipts.py:register_file` for the owned rows and the supported report generators.
- Review-round fixture repair: [git/git `v2.43.0` `t/test-lib-functions.sh`](https://github.com/git/git/blob/v2.43.0/t/test-lib-functions.sh#L750) (`test_lazy_prereq`, a probed prerequisite; filesystem examples such as `SYMLINKS` and `CASE_INSENSITIVE_FS` in [`t/test-lib.sh`](https://github.com/git/git/blob/v2.43.0/t/test-lib.sh#L1773)), and [CPython `v3.13.15` `Lib/unittest/case.py`](https://github.com/python/cpython/blob/v3.13.15/Lib/unittest/case.py#L54) (`_Outcome.testPartExecutor` records a `SkipTest` raised inside `subTest` as that subtest's skip; `subTest`, `:538-565`, resumes after the block and stops the method under failfast). Read on 2026-10-03; the installed interpreter's `case.py` is byte-identical to that tag.
- Step 6 repair round: repository source read at `b0c11c324`, covering `resolver/gh_harness.py` (`REPO`, `op_push`, `push`, `op_pr_create` and its allowlist entry, `check_repository` and `branch_rules`), `resolver.py` `build_pr_body` and `plan_run`, and `host.py` `read_stage_gates`. The repository's owner type, visibility and fork count were read with `gh api` on 2026-10-03. GitHub's fork-PR token and secret limits come from [Events that trigger workflows, "Workflows in forked repositories"](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows), for `pull_request` runs from a fork. [Forks, "Which repositories can be forked?"](https://docs.github.com/en/pull-requests/reference/forks) says nothing on forking one's own repository, so the decision record marks the fork's holder undetermined. Both pages were read 2026-10-03 and re-read 2026-10-04T00:23Z.
- Pre-push gate (2026-10-04). The GPT-family job 004's six sources, which also cover the command center's proposal, were each fetched on 2026-10-04 (HTTP 200) and quoted in the decision record:
  - [GitHub Secure use reference](https://docs.github.com/en/actions/reference/security/secure-use): "Any user with write access to your repository has read access to all secrets configured in your repository", plus untrusted checkout and hosted runners;
  - [Workflow syntax](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax): unspecified permissions are set to none, `cache-mode`, `steps[*].run`, `working-directory` and local `uses: ./`;
  - [Events that trigger workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows): `push` "includes workflows that are not merged into the default branch", and the fork limits;
  - [Dependency caching reference](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching): PR runs restore base and default-branch caches, and their own caches are scoped to the merge ref;
  - [OpenSSF Scorecard checks](https://github.com/ossf/scorecard/blob/main/docs/checks.md): Token-Permissions and Dangerous-Workflow;
  - [zizmor audits](https://docs.zizmor.sh/audits/): the five audits the gate fails on, each working offline.
  The gate's mechanisms add three more: [GitHub Script injections](https://docs.github.com/en/actions/concepts/security/script-injections) (the untrusted-context endings), [Python unittest, "Test Discovery"](https://docs.python.org/3/library/unittest.html#test-discovery) with the installed CPython 3.13.15 `unittest/loader.py`, and the installed zizmor 1.30.1 `--help`. In-repository references: `patch_policy.py`'s reviewed derivation, `tests/test_workflow_hardening.py`'s text-level workflow reading, and `.github/requirements-ci.txt` and `validate.yml` for CI's zizmor pin and flags.
- Gate data (cross-family repair, 2026-10-04). The Python behaviour the reader models, from docs.python.org/3.13 (read 2026-10-04, HTTP 200) and checked on the installed CPython 3.13.15: [pathlib](https://docs.python.org/3.13/library/pathlib.html) ("If a segment is an absolute path, all previous segments are ignored (like os.path.join())"; `Path.rglob`), [fnmatch](https://docs.python.org/3.13/library/fnmatch.html) ("the filename separator ('/' on Unix) is not special to this module"), [tomllib](https://docs.python.org/3.13/library/tomllib.html) and [csv](https://docs.python.org/3.13/library/csv.html) (read through a file object), and the comprehension scopes of the [Language Reference 6.2.4](https://docs.python.org/3.13/reference/expressions.html#displays-for-lists-sets-and-dictionaries) and [PEP 572](https://peps.python.org/pep-0572/). In-repository: `scripts/validate_convergence.py` (P1's example) and `tools/sota-convergence/gap_wave_ledger.py` (the main source of breadth).

### Evidence-class table

| Claim | Evidence class | Command / receipt |
| --- | --- | --- |
| Resolver templates, patch policy, outgoing guard, PR loop, host mode and end-to-end fake scenarios | `local_integration` | Required OpenHands test pair at `b0c11c324` with a neutral `TMPDIR`: 248 tests, exit 0. It uses fake Docker, GitHub and agent-server operations and real local Git |
| The pair no longer depends on `TMPDIR` | `local_integration` | The same pair at `b0c11c324` with a throwaway `TMPDIR` under the host's home path: exit 0, `OK (skipped=2)`. The two symlink subtests are skipped by the probed prerequisite with the reworded reason. At `d35633fad` the same run exited 1 with failures=2 (`'private_content' != 'host_path'`) |
| The repaired guard assertions still catch planted defects | `synthetic`, at `2ede7b871` | Scratch worktree at `2ede7b871`. With the `host_path` refusal disabled, both tests fail under both `TMPDIR`s (exit 1). With the realpath form dropped, the symlink subtest fails under a neutral `TMPDIR` (exit 1) and is skipped under a home-path `TMPDIR`. Not re-run at `b0c11c324`: `cf42c6d08` changes only that test's comment and skip text, not an assertion |
| The previous alias fixture fails when its three names collapse | `synthetic` | Existing unittest with only the three alias writes remapped: three matching failed assertions, exit 1 before repair; exit 0 after repair |
| All seven alias paths reach the unchanged validator and retain every refusal check | `local_integration` | All 11 `PatchValidatorTests` inside the pair run at `b0c11c324`, exit 0 on Linux with real Git 2.43.0; no platform skip |
| The repaired alias fixture and the `TMPDIR` repair pass on native macOS | `source_review` of retained CI execution output | `db24156b9`: `validate-macos` job `111264957003` (run `37144290967`), artifact `full-suite-macos.log`: `Ran 10078 tests`, `OK (skipped=1329)`, no FAIL or ERROR block. `test_case_unicode_and_filesystem_aliases_are_refused`, `test_host_paths_and_the_user_name_are_refused` and `test_pr_body_follows_the_template_and_renders_model_text_inert` are each `ok`. This is the first native macOS run of `2ede7b871`. Earlier, `d35633fad`'s job `111220987307` passed the alias test. `b0c11c324`: pending |
| The old native macOS suite failed at exactly the three alias subtests | `source_review` of retained historical execution output | Run `36524134513` (head `501bcc9e`), job `109263298833`, artifact `11015337319`, `full-suite-macos.log`: 7339 tests, failures=3, skipped=959. Its three FAIL blocks are the subtests `docs/A.md`, `Docs/z.md` and `docs/café.md` at test line 725, each `('case_or_unicode_alias', path) not found`. Re-downloaded read-only in the custody review round |
| The original tests catch planted defects | `synthetic`, historical | 9 of 9 repair-round mutations detected; unchanged `evidence/stage2-*fail-first.txt` logs |
| Git's macOS argument precomposition needs an explicit fixture override | `source_review` | git/git `v2.43.0` `git.c:449`, `compat/precompose_utf8.c:67-105`, `Documentation/config/core.txt:44-51` |
| The resolver as built pushes to and opens PRs only in this repository, so the fork option needs a harness change | `source_review` | At `b0c11c324`: `resolver/gh_harness.py:44`, `:241-254`, `:257-259`, `:448`, `:627-628`, `:684-689`, `:704-709`; the final message reaches the PR body through `resolver.py:547-550`, `:590-592` |
| A real run refuses at the gates stage before G4 today | `source_review` | At `b0c11c324`: `plan_run` checks the stage gates, G5 and then G4 (`resolver.py:1589-1591`). `read_stage_gates` raises `stage_gates_not_recorded` when `stage-gates.json` is absent (`host.py:1357-1361`) |
| The original workflow/token bounds | `source_review`, historical scope | Stage 2 security review of 20 workflows at `94894f54`; this is not current-base CI or fork acceptance |
| Linux full suite in CI | `source_review` of retained CI execution output | `db24156b9`: `validate` job `111264954786`, CI merge `69650a33` of `db24156b9` into `d2fc3803e`, `python3 -m unittest`: `Ran 10078 tests in 1664.615s`, `OK (skipped=968)`. `b0c11c324`: pending |
| Linux full suite on the host | `local_integration`, before the step 6 round | Registry commit `96b96c681` (only `.github/workflows/validate.yml` and `manifests/evidence.json` differ at `d35633fad`), home-path `TMPDIR`: exit 1; 10,070 tests, failures=12, errors=1, skipped=850. Its 13 failing IDs are the 2 fixture cases repaired in `2ede7b871` and the 11 compared in the next row. Not re-run at `b0c11c324`: it takes about 43 minutes on this host, which would overrun a scheduled measurement window. The builder's sandbox run (10,046 tests, 485 failures) is superseded |
| None of the 11 remaining IDs is specific to this PR | `local_integration`, at `b2d556c95` | Same command and `TMPDIR` at `b2d556c95`, in a fresh detached worktree at origin/main `ecea2865`, and at the then-merged base `59f8a1e36`. With a neutral `TMPDIR`, the same 1 failure on every tree; with a home-path `TMPDIR`, the same 9 failures on every tree. Supersedes the builder's 607-method archive comparison and the earlier `4ced2923` clone control |
| Publication validation | `local_integration` | `scripts/validate.py` with a neutral `TMPDIR` at `b0c11c324`: exit 0, `"status": "passed"` (9,550 hashed files, 193 receipts). CI's `validate.py` passed on `db24156b9` (job `111264954786`). The builder's exit 1 (the `AGENTS.md` mismatch before the `4ced2923` merge) is superseded |
| Registry rebuild and generated reports | `local_integration` | At `db24156b9`: all 16 owned rows over main `d2fc3803e`'s registry. At `b0c11c324`: the rows of the record, `RESOLVER.md` and the resolver test module are refreshed. Both generators exit 0 and change no file, and `scripts/evidence_manifest.py --check` passes (9,550 files) |
| Configured Gitleaks scan of history | `local_integration` | Gitleaks 8.30.1 over `origin/main..HEAD` at `b0c11c324`: 36 commits, exit 0, no leaks found. The pre-commit scans of both step 6 commits found no leaks |
| Required contexts on the pushed head | `source_review` of CI status | `db24156b9`: all eight passed (`validate` job `111264954786`, `validate-macos` job `111264957003`). A later `validate` re-run there (job `111278594671`) was cancelled at 19:56:39Z, after `b0c11c324` was pushed. `b0c11c324` at 19:58:50Z: dependency-review, osv-scanner, sota-sources, token-report and verdict-review-gate passed; validate and secret-scan were in progress; validate-macos was queued |
| The pre-push gate refuses planted protected changes before any push, passes a benign commit, refuses from inside the agent tree and fails closed without zizmor | `local_integration` | `tests.test_runtime_worker_openhands_push_gate` at `050bca5d5`: 31 tests, exit 0, `OK (skipped=1)`, the PyYAML cross-check, which this interpreter cannot run; its real-zizmor 1.30.1 test ran |
| The gate's workflow reader matches PyYAML on all 21 workflows | `local_integration` | `/usr/bin/python3` (PyYAML 6.0.1) `-m unittest ...push_gate.RepositoryWorkflowTests`: 3 tests, exit 0 |
| The new and updated tests fail without the gate | `synthetic`, failing-first | Base `456f8fee6` with the new tests copied in: gate module exit 1 (errors=7), resolver module exit 1 (failures=1, errors=56), each traced to the missing gate API (`evidence/push-gate-fail-first.txt`, part 1) |
| The gate's tests catch planted defects | `synthetic` | 17 mutations of `push_gate.py` and `gh_harness.py`, each failing its named tests (exit 1); the unmutated copy passes (part 2) |
| The gate on this repository's own trees, with real zizmor | `local_integration`, rehearsal | Local clones of `48831bc65` with one planted commit each, real zizmor 1.30.1, no resolver, container or GitHub call. The benign edit passes. Workflow, CODEOWNERS, gate-script, helper, policy-test, new-test, `.gitleaks.toml` and gate-code edits are refused with their rules, and a planted template injection is refused by zizmor. About 2 s per check (part 4) |
| Live resolver containment, model/gateway behavior, GitHub writes | not run / not accepted | The CI posture is decided (option 1 with the trusted pre-push gate). The first live run waits until the gate lands on main, its negative controls pass there, and G2/P3/G5 and G4 are recorded with fresh P0-P2. No live resolver run occurred |

### Local commands run

```text
# Head 050bca5d5 (pre-push gate), 2026-10-04 04:00-04:02Z. TMPDIR=/var/tmp/489-gate, nice -n 19, after
# git fetch origin main (origin/main aecfaaaa6, merge base d2fc3803e). The tree was clean at this head.
$ python3 -m unittest tests.test_runtime_worker_openhands_resolver tests.test_runtime_worker_openhands
exit 0; Ran 249 tests in 20.319s; OK
$ python3 -m unittest -v tests.test_runtime_worker_openhands_push_gate
exit 0; Ran 31 tests in 8.958s; OK (skipped=1: the PyYAML cross-check; the real-zizmor test ran)
$ /usr/bin/python3 -m unittest tests.test_runtime_worker_openhands_push_gate.RepositoryWorkflowTests
exit 0; Ran 3 tests; OK (PyYAML 6.0.1 cross-check of the workflow reader)
$ python3 scripts/validate.py
exit 0; {"components": 69, "hashed_files": 9553, "profiles": 4, "receipts": 193, "status": "passed"}
$ python3 scripts/evidence_manifest.py --check
exit 0; {"files": 9553, "status": "passed"}
$ python3 scripts/component_matrix.py --check; python3 scripts/new_host_grand_list.py --check   # before the registry commit
exit 0 {"rows": 32, "status": "checked"}; exit 0 {"status": "passed", "layers": 32, "winners": 66}; no --write needed
$ git diff --check origin/main...HEAD
exit 0
$ python3 blueprints/runtime-workers/openhands/resolver.py --help; ... run --help
exit 0; exit 0 (run --help lists --zizmor)
$ gitleaks git . --config .gitleaks.toml --log-opts=origin/main..HEAD --redact --timeout 600
exit 0; 44 commits scanned; no leaks found (the four commits' pre-commit scans: no leaks)
$ python3 <the coordinator's merge-tree landing check> aecfaaaa6 050bca5d5
exit 0; clean three-way merge; merged-vs-main paths 20, outside PR-owned 0; main drift 304 paths, overlap 0;
registry foreign rows equal, order preserved, PR-owned rows 19; merged files[] sorted, no duplicates (9735 rows); LANDABLE
$ git push origin HEAD:claude/openhands-resolver-20260928
exit 0; pre-push registry tests: Ran 3 tests, OK; 456f8fee6..050bca5d5, no force
Failing-first, planted-defect and rehearsal runs: blueprints/runtime-workers/openhands/evidence/push-gate-fail-first.txt.
Not run: the host full suite (about 43 minutes here); CI runs it on the pushed head.

# Head b0c11c324 (step 6 repair round), 2026-10-03 19:53-19:56Z. TMPDIR is a neutral
# directory outside the home directory and every repository, unless the line says
# home-path TMPDIR. The commands ran on the working tree, which was then committed
# unchanged as cf42c6d08 and b0c11c324.
$ git diff --check
exit 0
$ nice -n 19 python3 -c '<host_receipts.register_file(Path("."), p) for each path>' <the record> <RESOLVER.md> <the resolver test module>
exit 0
$ nice -n 19 python3 scripts/component_matrix.py --write
exit 0; {"flip_rule_violations": 0, "rows": 32, "status": "written"}; no file changed
$ nice -n 19 python3 scripts/new_host_grand_list.py --write
exit 0; {"status": "written", "layers": 32, "winners": 66}; no file changed
$ nice -n 19 python3 scripts/evidence_manifest.py --check
exit 0; {"files": 9550, "status": "passed"}
$ nice -n 19 python3 scripts/validate.py
exit 0; {"components": 69, "hashed_files": 9550, "profiles": 4, "receipts": 193, "status": "passed"}
$ nice -n 19 python3 -m unittest tests.test_runtime_worker_openhands_resolver tests.test_runtime_worker_openhands
exit 0; Ran 248 tests in 22.014s; OK
$ (home-path TMPDIR, a throwaway directory removed afterwards) nice -n 19 python3 -m unittest -v <the same pair>
exit 0; Ran 248 tests in 24.993s; OK (skipped=2); both skips give the reworded reason
$ nice -n 19 python3 blueprints/runtime-workers/openhands/resolver.py --help
exit 0
$ nice -n 19 python3 blueprints/runtime-workers/openhands/resolver.py run --help
exit 0
$ nice -n 19 git diff --check origin/main...HEAD
exit 0 (origin/main 463a57b98, merge base d2fc3803e); git diff --name-only origin/main...HEAD: the 17 contract paths
$ cmp <(git show 501bcc9e:<record> | sed -n 1,15p) <(sed -n 1,15p <record>)    # and the same against origin/main
exit 0; exit 0
$ git rev-parse HEAD:<log> 501bcc9e:<log>    # both evidence/stage2-*fail-first.txt logs
ed55377f232aa64f46f1b7dce004fce1be5cc7a7 and 3f41c01b05feff7bf199c16266ea96c73ffa14c5, equal at both commits
$ nice -n 19 gitleaks git . --config .gitleaks.toml --log-opts=origin/main..HEAD --redact --timeout 600
exit 0; 36 commits scanned; no leaks found (the pre-commit scans of cf42c6d08 and b0c11c324: no leaks)
$ nice -n 19 python3 <the coordinator's merge-tree landing check> 463a57b98 b0c11c324
exit 0; clean three-way merge; merged-vs-main paths 17, outside PR-owned 0; main drift 30 paths, overlap 0;
registry foreign rows equal, order preserved, PR-owned rows 16; merged files[] sorted, no duplicates (9571 rows); LANDABLE
$ git push origin HEAD:claude/openhands-resolver-20260928
exit 0; pre-push registry tests: Ran 3 tests, OK; db24156b9..b0c11c324, no force
Not run in this round: the host full suite (about 43 minutes here, which would overrun a scheduled
measurement window; the CI full suites on db24156b9 are below) and the planted-defect mutations
(recorded at 2ede7b871; no assertion has changed since).

# Head b2d556c95, 2026-10-03 17:08-17:10Z (historical). Same TMPDIR convention.
$ nice -n 19 python3 scripts/validate.py
exit 0; {"components": 69, "hashed_files": 9429, "profiles": 4, "receipts": 187, "status": "passed"}
$ nice -n 19 python3 -m unittest tests.test_runtime_worker_openhands_resolver tests.test_runtime_worker_openhands
exit 0; Ran 248 tests in 19.862s; OK
$ (home-path TMPDIR) nice -n 19 python3 -m unittest tests.test_runtime_worker_openhands_resolver tests.test_runtime_worker_openhands
exit 0; Ran 248 tests in 20.310s; OK (skipped=2)
$ nice -n 19 python3 blueprints/runtime-workers/openhands/resolver.py --help
exit 0
$ nice -n 19 python3 blueprints/runtime-workers/openhands/resolver.py run --help
exit 0
$ nice -n 19 git diff --check origin/main...HEAD
exit 0 (origin/main ecea2865, merge base 59f8a1e36)
$ git diff --name-only origin/main...HEAD
exit 0; exactly the 17 contract paths
$ nice -n 19 gitleaks git . --config .gitleaks.toml --log-opts=origin/main..HEAD --redact --timeout 600
exit 0; 34 commits scanned; no leaks found
$ nice -n 19 python3 scripts/component_matrix.py --write
exit 0; {"flip_rule_violations": 0, "rows": 32, "status": "written"}; no file changed
$ nice -n 19 python3 scripts/new_host_grand_list.py --write
exit 0; {"status": "written", "layers": 32, "winners": 66}; no file changed
$ git push origin HEAD:claude/openhands-resolver-20260928
exit 0; pre-push registry tests: Ran 3 tests, OK; d35633fad..b2d556c95, no force

# Head d35633fad, before the fixture repair, 16:55-16:57Z
$ nice -n 19 python3 scripts/validate.py
exit 0; "status": "passed"
$ nice -n 19 python3 -m unittest tests.test_runtime_worker_openhands_resolver tests.test_runtime_worker_openhands
exit 0; Ran 248 tests in 20.274s; OK
$ (home-path TMPDIR) the same pair
exit 1; Ran 248 tests in 20.300s; FAILED (failures=2)
OutgoingGuardTests.test_host_paths_and_the_user_name_are_refused and
PullRequestLoopTests.test_pr_body_follows_the_template_and_renders_model_text_inert: 'private_content' != 'host_path'

# Clean-main comparison of the 11 non-OpenHands IDs that failed in the host full suite below
$ git worktree add --detach <scratch> origin/main      # ecea28654a835fff2cc3651bab77ca0e46b9bec5, clean
$ git -C <scratch> checkout --detach 59f8a1e36          # the then-merged base, clean
$ nice -n 19 python3 -m unittest <the 11 IDs>          # same command in every tree and TMPDIR
tree                     neutral TMPDIR          home-path TMPDIR
b2d556c95                exit 1; failures=1      exit 1; failures=9
d35633fad                exit 1; failures=1      exit 1; failures=9
origin/main ecea2865     exit 1; failures=1      exit 1; failures=9
merged base 59f8a1e36    exit 1; failures=1      exit 1; failures=9
Within each TMPDIR, the failing IDs are identical on every tree:
- under both: tests.test_windows_terminal_defaults.OverlayTests.test_the_installed_client_knows_no_notification_type_without_a_decision
  (the installed client knows a notification type, auth_storage_failure, that has no decision)
- under the home-path TMPDIR only, in tests.test_token_e2e_grader:
  F19c_Stage3Mutants.test_M17c_manifest_canary_off, F19d_RepairRoundMutants.test_M56c_the_export_backstop_is_off,
  F29_Export (3 tests), F29b_CheckHtml (2 tests), F29c_ArgvSanitizer.test_a_grade_run_that_spells_its_flags_with_equals_records_no_path
- passed on every tree under both:
  tests.test_gpt6_family_tiering_20260926.RunnerTests.test_sigterm_records_the_running_call_as_interrupted_without_counting_it,
  tests.test_order_throughput.CapacityRunTests.test_cli_refuses_live_base_url_from_env_file_without_network
$ git worktree remove <scratch>; git worktree prune
exit 0

# Host full suite, registry commit 96b96c681, home-path TMPDIR (coordinator run before the custody review round)
$ nice -n 19 python3 -m unittest
exit 1; Ran 10070 tests in 2568.426s; FAILED (failures=12, errors=1, skipped=850)
13 failing IDs: the 2 OpenHands fixture cases (repaired in 2ede7b871) and the 11 IDs compared above
earlier control, superseded by the comparison above: the 11 IDs in a clean 4ced2923 clone, FAILED (failures=10, errors=1)

# CI, read-only, 2026-10-03T19:57-19:59Z (gh api jobs and check-runs, gh run view --log, gh run download)
db24156b9 validate job 111264954786: success. CI merge 69650a33 (db24156b9 into d2fc3803e);
validate.py {"components": 69, "hashed_files": 9550, "profiles": 4, "receipts": 193, "status": "passed"};
component matrix {"rows": 32, "status": "checked"}; new-host grand list {"status": "passed", "layers": 32, "winners": 66};
python3 -m unittest: Ran 10078 tests in 1664.615s; OK (skipped=968)
db24156b9 validate-macos job 111264957003: success at 19:29:18Z. full-suite-macos.log: Ran 10078 tests in 1794.871s;
OK (skipped=1329); no FAIL or ERROR block; the alias test and both TMPDIR-repair tests ok
db24156b9 required contexts: all eight success; a later validate re-run (job 111278594671) was cancelled at
19:56:39Z, after b0c11c324 was pushed
b0c11c324 at 19:58:50Z: dependency-review, osv-scanner, sota-sources, token-report and verdict-review-gate success;
validate and secret-scan in progress; validate-macos queued

# CI, read-only, historical
$ gh pr checks 489 --required        # 2026-10-03T17:16:15Z
b2d556c95: dependency-review, osv-scanner, secret-scan, sota-sources, token-report and verdict-review-gate pass;
validate fail, job 111250877246: "files[2392]: files[] must be sorted by path (found
'docs/decisions/2026-10-03-omniroute-sdk-worker-0160.md' after
'docs/decisions/2026-10-03-retire-pr320-loki-denominator-host-receipts.md')", the pair main's job 111240392112 reports;
validate-macos pending
d35633fad: all eight passed: dependency-review, osv-scanner, secret-scan, sota-sources, token-report,
validate (29m39s), validate-macos (36m58s), verdict-review-gate
d35633fad validate job 111220987213: Ran 10070 tests in 1679.211s; OK (skipped=968)
d35633fad validate-macos job 111220987307, full-suite-macos.log: Ran 10070 tests in 1849.584s; OK (skipped=1329)

# Builder's sandbox record, before the 4ced2923 and 59f8a1e36 merges (superseded where marked).
# Its first pair run, with the host Gitleaks wrapper, exited 1 on the wrapper's unavailable lock;
# that failed attempt is retained separately, and PATH then selected native Gitleaks 8.30.1.
$ nice -n 19 python3 -m unittest tests.test_runtime_worker_openhands_resolver.PatchValidatorTests
exit 0; Ran 11 tests in 1.185s; OK
$ nice -n 19 python3 -m unittest        # superseded by the host and CI runs above
exit 1; Ran 10046 tests; FAILED (failures=485, errors=195, skipped=863); errors=168 with native Gitleaks on PATH
$ nice -n 19 gitleaks git . --config .gitleaks.toml --pre-commit --redact --timeout 600
exit 0; actual working diff scanned; no leaks found
$ nice -n 19 python3 scripts/validate.py   # superseded: the 4ced2923 merge brought main's AGENTS.md
exit 1; only the AGENTS.md SHA-256 and byte-count mismatch
```

These are repository integration checks, not unchanged upstream acceptance suites.

**CI on this head.** The workflows test GitHub's merge of the head with main.

On the reviewed head `db24156b9`, all eight required contexts passed:
- `validate` job `111264954786` tested merge `69650a33` (`db24156b9` into `d2fc3803e`). `validate.py` reported `"status": "passed"` with 9,550 hashed files and 193 receipts. The component matrix and new-host grand list checks passed. `python3 -m unittest` ran 10,078 tests: `OK (skipped=968)`.
- `validate-macos` job `111264957003` passed. Its `full-suite-macos.log` shows 10,078 tests, `OK (skipped=1329)` and no FAIL or ERROR block.

The merged registry is sorted. The `b2d556c95` `validate` failure (job `111250877246`) on main's unsorted registry pair no longer applies.

The new head `b0c11c324` changes only the decision record, `RESOLVER.md`, one test's comment and skip text, and three registry rows. Its required contexts were still running when this description was written. The head that lands must show all eight SUCCESS, and a macOS full-suite artifact with no failure in `tests.test_runtime_worker_openhands_resolver`.

**Linux full-suite acceptance item.**
- CI's Linux and macOS full suites passed on `db24156b9`, which differs from `b0c11c324` only in the files listed above.
- On the host, the PR's two test modules pass at `b0c11c324` under both `TMPDIR`s.
- The host full suite was not re-run in this round. At `b2d556c95`, its 11 other failing IDs failed identically on clean main and on the then-merged base, environment by environment.

### Review round

The custody review of `d35633fad` (independent Opus 5.5, read-only) returned **repair**. This is its one repair round. Numbers are the findings' indices in the review record, counted from 0, as in commit `2ede7b871`'s message.

| # | Severity | Finding | Disposition |
| --- | --- | --- | --- |
| 0 | should-fix | The description still described the state before the custody merge (`4ced2923` unmerged, `validate.py` exit 1) | Fixed. Scope, the SOTA "Repository" bullet, the evidence table and the commands now state the merged state: base `59f8a1e36` (merged in `d35633fad`), with `4ced2923` merged in `85830e815`. `scripts/validate.py` with a neutral `TMPDIR`: exit 0, `"status": "passed"` at `d35633fad` and `b2d556c95`. The builder's exit 1 remains only as superseded history |
| 1 | should-fix | The Linux full-suite state was contradictory, with no recorded clean-main comparison | Fixed. "Local commands run" records the comparison: a fresh detached worktree, at origin/main `ecea2865` and then at the merged base `59f8a1e36`, the 11 IDs, the command and every exit code under two `TMPDIR`s. The failing sets match this head's. The builder's sandbox numbers are marked superseded, and the "remains an acceptance blocker" paragraph is replaced by the evidence summary |
| 2 | minor | "Including unittest's trailing spaces" is false for the fail-first logs | Fixed. The clause is removed. The blob SHAs `ed55377f` and `3f41c01b` are unchanged from `501bcc9e`. The same wording in the custody contract, which lives outside the repository, is reported to the coordinator |
| 3 | minor | The home-path `TMPDIR` failure was called an environment artifact, but the fixtures and the check order come from this PR | Fixed in `2ede7b871` (tests only), with its registry row in `b2d556c95`. A synthetic absolute host root serves the two `host_path` assertions. The symlink case probes its prerequisite, following git's `test_lazy_prereq` pattern, and skips its two subtests with an explicit message. No assertion or reason label changed. Fail-first at `d35633fad`: exit 1, failures=2. After the fix: exit 0 under both `TMPDIR`s. Two planted defects are caught (see the evidence table) |
| 4 | minor | `validate-macos` had not run on `d35633fad` | Closed for `d35633fad`: job `111220987307` passed, and its log shows `OK (skipped=1329)` with the alias test `ok`. On `b2d556c95`, `validate-macos` is pending and `validate` failed on main's registry order; see "CI on this head" |
| 5 | minor | The reviewer did not re-run the local acceptance commands | Closed: the final-head runs and their exit codes are recorded above |
| 6 | minor | The reviewer did not download the historical macOS artifact | No change needed. Artifact `11015337319` was re-downloaded read-only in this round, and its three FAIL blocks are cited in the evidence table |
| 7 | minor | Neither contract review has a recorded verdict | Open and listed as pending: 6(a), GPT-6.1 Astra/max, after 19:03Z when the GPT-free slot ends; 6(b), the Opus closure review of `b2d556c95`. The PR had no review threads at 17:16Z |

### Step 6 repair round

This is the one repair round for the two step 6 reviews of `db24156b9`. A is the GPT-6 Astra/max review, and O1-O9 are the Opus closure review's findings in the order listed under "Step 6 reviews". The fixes are in `cf42c6d08`, with their registry rows in `b0c11c324`.

| # | Severity | Finding | Disposition |
| --- | --- | --- | --- |
| A | should-fix | Fork option prerequisites; the runbook permits a run after either choice; CI egress and final message; the fork guarantee is unverified | **Fixed.** The record's option 2 now states that the harness pushes to and opens PRs only in this repository, and lists what option 2 needs before a first run: a fork remote and push-URL check, `--head <owner>:<branch>`, the rules lookup and a `non_fast_forward` ruleset on the fork, and its own review. CI egress and condition 3 are addressed under both options, and the "not re-read" caveat is kept. The `RESOLVER.md` runbook precondition now also stops for option 2 until that change lands. The amendment heading, record lines 1-15 and the live-run wait are unchanged |
| O1 | should-fix | Option 2 incomplete; filed under the wrong kind of change; condition 3; the fork's owner | **Fixed** with A. `RESOLVER.md` Residuals separate the egress block (a workflow change outside this PR) from the fork option (a change to this PR's harness). The record names the fork's owner: the repository's owning User account, which is also the admin login the resolver acts through. The amendment summary in `RESOLVER.md` is updated to match |
| O2 | should-fix | The PR body describes `b2d556c95` | **Fixed** in this description: base and head, the merge list, "CI on this head", the evidence rows, and the local commands at `b0c11c324` with exit codes |
| O3 | minor | The skip message is too narrow | **Fixed.** The comment and skip reason name any `scripts/validate.py` `PRIVATE_CONTENT` pattern, such as a personal home path. No assertion changed, and the reworded reason appears in the home-path `TMPDIR` run |
| O4 | minor | The G4 reason-code order is wrong | **Fixed** in the `RESOLVER.md` G4 residual and under "What is not done" below |
| O5 | minor | `validate-macos` on `db24156b9` | **Closed for `db24156b9`:** job `111264957003` passed, and its artifact is cited in the evidence table. **Open:** `validate-macos` on `b0c11c324` |
| O6 | minor | The 6(a) review is pending | **Closed:** 6(a) returned (A above). **Open:** the reviewers' re-read of this delta |
| O7 | minor | Main moved after the custody merge | **Open, not merged in this round.** The landing check against `463a57b98` reports LANDABLE. Contract step 7's "repeat steps 2 and 7" remains the coordinator's call before landing |
| O8 | minor | The step-5 commands were not re-run | **Closed at `b0c11c324`**, except the host full suite and the planted-defect mutations (see "Local commands run") |
| O9 | minor | Verification record | No change needed |

### Decision record

`docs/decisions/2026-09-28-openhands-resolver-isolation.md` records the resolver-mode narrowing and its amendment, **proposed 2026-09-28 and decided 2026-10-04: option 1 with trusted pre-push enforcement**.

The decision section records the following:
- the owner's delegation, in their words: "max quality sota convergenced resolution automation workflow at highest quality";
- the command center's proposal (option 1 with an in-CI tripwire);
- the GPT-family job 004 vote: disagree on sufficiency, because a check inside PR CI cannot protect against the commit under test. Each reason is re-checked against the GitHub, OpenSSF Scorecard and zizmor sources;
- the gate and its trusted-copy invariant;
- the separate defence-in-depth PR for workflow hardening;
- the residuals, the evidence, and the overturn to option 2 if the gate cannot be kept immutable to the agent.

Option 2's text stays as the alternative; only its line citations moved with the harness change.

The history below describes the amendment before the decision. After `cf42c6d08` and the wording fixes `f4e7aa2a2` and `54438ce7f`, the amendment's option 2:
- states that the resolver as built pushes to and opens PRs only in this repository;
- lists the separately reviewed harness change and fork ruleset it needs before a first run;
- marks the fork's holder undetermined: an organization the owner creates, or a second account. The choice changes the push identity, the fork's ruleset and the `--head <holder>:<branch>` value;
- sources the token and secret guarantee, for `pull_request` runs from a fork, in GitHub's "Workflows in forked repositories".

Both options now address condition 3 and CI egress. The 2026-10-03 fixture-repair section records native Git sources, the skipped-check alternative, the argument-normalization completeness finding, evidence limits and the native macOS condition that would overturn the repair. Lines 1-15 remain unchanged.

### What is not done, and what needs the owner

- **Owner decision: decided 2026-10-04.** It chose option 1 with trusted pre-push enforcement; the owner delegated the choice to converged practice. Still open:
  - workflow hardening, in a separate defence-in-depth PR: `permissions: {}` defaults, `persist-credentials: false`, cache, runner and timeout policy, a protected zizmor configuration, and a strict tripwire test on main. Until then, code under test runs with network, and that residual is accepted;
  - the first live run, which waits until this gate lands on main, its negative controls pass there and the stage gates are recorded;
  - the protected list, which is broad by design: all of `tests/**`, and every file a reachable step names. A resolver task that needs those files fails at the gate with no push;
  - option 2, an owner fork, which stays the overturn target and would need its own harness change.

  Resolver-PR check results are still not evidence of model-code safety.
- G4 remains unrecorded. Because no `stage-gates.json` exists, a real run today refuses earlier, at the gates stage, with `stage_gates_not_recorded`. Once the recorded stage gates and G5 pass, it refuses with `stage_gate_g4_not_recorded` until the coordinator records the isolated reviewer argv hash.
- G5 and `stage-gates.json` remain required. Both gateways' provider settings and the confinement design still need their own acceptance.
- These remain separate required steps:
  - fresh P0-P2 receipts, P3-P5 and G2 image qualification;
  - the reviewers' re-read of the step 6 delta;
  - the required contexts on the final head;
  - the first live draft-PR attempt.

  The native macOS check of the alias repair and the `TMPDIR` repair passed on `db24156b9`. A7's environment-name read at teardown stays in place.

### Host evidence

Not applicable: no file under `evidence/hosts/` changes. The historical macOS artifact is distinguished from a new native run, and no live resolver acceptance is claimed.

### Checklist

- [x] No GitHub Actions or workflow files changed; workflow hardening remains a separate defence-in-depth PR.
- [x] New Actions permissions or pins are not introduced by this change.
- [x] No credential value was read or published. Gitleaks scans of the history (36 commits at `b0c11c324`) and of both step 6 commits report no leaks, and the pre-push registry tests passed.
- [x] No new paid hosting, subscription or billing surface.
- [x] Peer-owned files and worktrees preserved.
- [x] Separate Stage 2 GPT review and whole-head Opus closure verdicts recorded, including residuals.
- [ ] The reviewers' re-read of the step 6 delta recorded.
- [ ] Every review thread resolved and all eight required contexts SUCCESS on the pushed head.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant