Skip to content

Runtime worker: crawl4ai with GPT-6 extraction via OmniRoute (recipe, round 2) - #428

Closed
seathatflowsinourveins wants to merge 5 commits into
mainfrom
claude/runtime-worker-crawl4ai-20260927
Closed

seathatflowsinourveins wants to merge 5 commits into
mainfrom
claude/runtime-worker-crawl4ai-20260927

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

Runtime-worker recipe: crawl4ai with GPT-6 extraction through the local OmniRoute gateway, on rootless Docker. Built by GPT-6 (astra, max) in two rounds.

SOTA sources

  • unclecode/crawl4ai v0.9.4 @ 133e1d92e37885dfccc03ea2e3687d06c98b7ceb (wheel and locks pinned by sha256)
  • Grader: promptfoo/promptfoo 0.123.1, with unchanged is-json and equals assertions in standalone mode
  • Gateway rules: OmniRoute 3.8.51 routing verdict of 2026-09-27

Status

Draft, recipe only. Nothing is installed, no container is started and no model has run.

  • Round 3 is done. The Claude review (C-01 to C-07) and the security review (S-02, S-05, S-09) got one GPT-6 repair round (gpt-6-astra, max).
    • Fixed:
      • Explicit max effort survives LiteLLM (allowed_openai_params).
      • The arm selects both the native and the container URLs.
      • Usage is counted at each arm's entry gateway by correlation id.
      • REST /md counts no longer imply an MCP tool call.
      • The promptfoo grader installs with npm ci --ignore-scripts from a SHA512-integrity lock.
      • Container hardening: dropped capabilities, no-new-privileges, a read-only root and tmpfs.
    • Declined, with residuals recorded: evidence-manifest edits, which the coordinator does, and per-port host-loopback isolation (S-02). The verified Docker switch is daemon-wide only.
    • The dispositions and the ten fail-first/pass pairs are in verification-round3.json.
  • Arms:
    • Control: cx/gpt-6-astra-max through 127.0.0.1:20128. The container uses 10.0.2.2:20128.
    • Engines-on: sharedgw/gpt-6-astra-max through 127.0.0.1:20129 (container 10.0.2.2:20129), with the compression header. Exactly one slash.
  • Dispatch for workflow children: dispatch.sh start|wait|result --run-id <id> --arm control|engines-on. Exit codes: 0 pass, 2 setup failure, 3 negative verdict, 4 incomplete evidence. REST and SSE registration are documented as fallbacks.
  • Next: the clean locked install and hardened container start/restart, host-loopback isolation, live routing, header, correlation and effort checks, both arms' E2E with the unchanged promptfoo grader, then receipts.

Checks (head 35b1e7fe, rebased onto e82e6be7)

  • python3 -m unittest tests.test_runtime_worker_crawl4ai: 25 tests OK, no skips.
  • The registry tests tests.test_osv_lockfile_coverage, tests.test_blind_checkout and tests.test_workflow_security_coverage pass, 57 tests OK. They fixed the failing validate check.
    • The OSV inventory now lists requirements.in, requirements.lock and the grader's package-lock.json, with package.json covered by that lock.
    • requirements.lock joins IGNORE_ALLOWED_LOCKS for nltk GHSA-8mgp-746c-j5xp (high, no patched release). crawl4ai 0.9.4 requires nltk, and a grep of its wheel shows it calls only sent_tokenize, word_tokenize, TextTilingTokenizer, stopwords, WordNetLemmatizer and punkt find/download. It calls none of the affected model-artifact APIs. An independent review of this acceptance is running.
    • The review dispositions fixed and declined are classified as blind-checkout labels.
  • python3 scripts/validate.py: passed after re-registration per docs/lanes.md.
  • The leak scan of added lines found no personal paths, user names or emails.

🤖 Generated with Claude Code

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Sep 27, 2026
Scout and others added 5 commits September 27, 2026 21:09
…ned 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>
…ed resources)

GPT-6 (astra, max) round-2 build: Promptfoo 0.123.1 standalone grading with unchanged is-json/equals assertions; check.py validates transport only.
Gateway calls omit temperature, carry a stable x-omniroute-session and a fresh
Idempotency-Key per logical call, and accept only configurable GPT-6 max routes.
Loopback-only ports in 3730-3799, owned rw-crawl4ai- resource names and labels,
literal-name cleanup. Not installed, not run: host E2E and A/B remain open.

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

GPT-6 repair builder (effort max) against the Claude review (C-01..C-07) and
the security review (S-02, S-05, S-09):
- explicit max effort kept through LiteLLM (allowed_openai_params);
- arms: control cx/gpt-6-astra-max on 20128, engines-on
  sharedgw/gpt-6-astra-max (one slash) on 20129 with the compression header;
  usage counted once at each arm's entry gateway by correlation id;
- dispatch.sh start/wait/result with distinct exit codes for workflow
  children; promptfoo grader installed with npm ci --ignore-scripts from a
  SHA512-integrity lock;
- container hardening (capabilities, no-new-privileges, read-only root).
Declined with residuals recorded: evidence-manifest edits (coordinator) and
per-port host-loopback isolation (daemon-wide switch only).

Repository registry fixes: the OSV inventory lists the new requirements and
grader locks; crawl4ai's lock joins IGNORE_ALLOWED_LOCKS for nltk
GHSA-8mgp-746c-j5xp, because crawl4ai 0.9.4 calls none of the affected APIs;
blind_checkout classifies the review dispositions fixed/declined as labels.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t for the runtime lock (static scan, not reached)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…GHSA-8mgp-746c-j5xp only, bound to its sha256 and reachability receipt

The #441 allowlist mapping replaces this branch's earlier set entry, whose comment the review found inaccurate and under-evidenced (the #336 condition, 'PerceptronTagger save/load', a wheel-only grep, no evidence path). The setuptools advisory is not exempted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/runtime-worker-crawl4ai-20260927 branch from 35b1e7f to 3ad8ba2 Compare September 28, 2026 01:10
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Rebased onto #441 (ba1700a), head 3ad8ba20.

  • The runtime lock is now in the scoped IGNORE_ALLOWED_LOCKS mapping. It is exempt only for nltk GHSA-8mgp-746c-j5xp, only at sha256 515633e3…, with evidence blueprints/runtime-workers/crawl4ai/evidence/nltk-reachability.json (static scan over the 95 hash-verified wheels, not reached; independently verified). The setuptools advisory is not exempted.
  • The earlier set entry and its comment are gone, which resolves the review's items 3-5 on that comment: the SPY one_zero engine trials: ml4t-backtest 0.1.12 PASS 29/29, Lumibot 4.6.1 FAIL 23/29 (preregistered, unchanged scorer) #336 condition, "PerceptronTagger save/load", and "crawl4ai pins nltk 3.10.3" with a wheel-only grep and no evidence path.
  • The registry tests (77) are OK and validate.py passed. The push went through the pre-push gate.

🤖 Generated with Claude Code

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 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 added a commit that referenced this pull request Sep 28, 2026
…n, reviewed repair round) (#425)

* Runtime worker recipe draft: openhands on GPT-6 through OmniRoute (pinned install, gateway FW rules, token MCP wiring, skills, frozen E2E)

Written by GPT-6 (gpt-6-astra, effort max) through the Codex stack-worker profile on the OmniRoute lane from the coordinator's brief; tests red then green; no host install or live run yet. Draft pending the Claude cross-family review, the upstream eval-harness mapping and the skills-lifecycle wiring.

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

* Runtime worker openhands: round 2 (SWE-bench grading via OpenHands/benchmarks, native LLM transport)

GPT-6 (astra, max) round-2 build: the verdict comes from official SWE-bench 4.1.0
through OpenHands/benchmarks at 405bae71 (SDK submodule 43376f18); the standalone
worker pins SDK 1.49.6. check.py validates transport only and relays the official
report. Native tool calling on a configurable cx/gpt-6-astra-max route with stable
conversation headers and fresh idempotency keys; exact native LLM objects keep
SDK accounting and condenser registration. No host ports published; owned
rw-openhands- resources. Not installed, not run.

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

* OpenHands runtime worker: round-3 repair, REST dispatch and both gateway arms

GPT-6 repair builder (effort max) against the Claude review (C1-C8), the
security review (S2-S12) and the common round-3 requirements:
- arms: control cx/gpt-6-astra-max on 20128; engines-on
  sharedgw/gpt-6-astra-max (one slash) on 20129; arm URL and model carried
  into the native request and the receipt;
- usage only from each arm's entry gateway (unknown stays unknown),
  compression counted separately;
- native agent-server REST start/wait/result on loopback 127.0.0.1:3730 with
  deterministic paths, early status, a deadline and distinct exits (setup 3,
  official negative 1, incomplete evidence 2);
- SWE-bench checkout via the pinned REPO_BASE_COMMIT_BRANCH; no local grader
  resource limits;
- security: remote MCPs disabled with fail-closed network evidence gates,
  verified upstream lock and hashed build tools, a resolved mount allowlist,
  non-root model containers with dropped capabilities, no-new-privileges and
  read-only roots, bounded no-follow reads of worker files.
Declined with residuals: host firewall enforcement (needs host probes) and
containerized grader builds; the receipt keeps evidence_complete=false.

Coordinator fixes: build-requirements.lock moves setuptools 80.9.0 to 84.0.0
(PyPI wheel sha256 51a52592...), clearing GHSA-h35f-9h28-mq5c (fixed in 83);
the four SDK build systems at 43376f1 require only setuptools>=61, and a
hashed --only-binary install of the lock succeeds. pins.json carries the new
lock digest. The OSV inventory lists requirements.lock and
build-requirements.lock.

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

* test_runtime_worker_openhands: resolve temp roots for macOS

validate-macos (run 36351247715) failed 6 tests with NotADirectoryError: 'var'.
read_bounded refuses every symlink hop from "/" by design, and macOS temp dirs
sit under the /var -> /private/var symlink. The test module now
realpath-resolves all nine temp roots and keeps the production rule unchanged.

Reproduced on Linux with TMPDIR set to a symlinked directory: failures=2,
errors=4 before, the same counts as macOS. After the change: 59 tests OK with a
symlinked or a plain TMPDIR.

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

* OpenHands runtime lock: linux/amd64 environments restriction plus four security upgrades

OSV-Scanner 2.6.0 flagged 14 advisories in five packages of the previous lock
(CI run 36351247754). v1.49.6 is still upstream's latest release, and its main
branch pins the same versions.

- Restrict the resolution to the recipe's only install target with uv's
  documented `environments` setting (pins.json image.platform linux/amd64).
- Upgrade anyio 4.14.2, click 8.5.0, pypdf 6.19.0 and soupsieve 2.9.2
  (version-pinned `uv lock --upgrade-package`).
- cryptography needs no upgrade: the restriction drops the darwin x86_64
  48.0.1 line, and the linux 50.0.0 already fixes all three of its advisories.

The coordinator reproduced the relock from a fresh copy of the unchanged
upstream workspace: uv.lock 30e608b1 and requirements.lock 02d0a7f0, byte for
byte. `uv lock --check` exits 0. The classification reports 0 violations (no
linux x86_64 wheel or sdist hash removed, no marker change that alters linux
x86_64 applicability). The CI osv command exits 0 with "No issues found". The
install-container.sh sequence, `uv pip check` and the offline import check
all exit 0.

research.md and the evidence file (section F) describe the final lock. A
superseded five-package variant (cryptography 50.0.1) stays in sections C to E.

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

* OpenHands worker: phase-1 takeover repairs for #425 (F16-F20, session key, port)

- F16: runs that end stuck or at MaxIterationsReached now export and
  officially grade the partial patch and exit 1, never 0; other native
  errors stay exit 3. The termination comes from the native status plus the
  newest ConversationErrorEvent (SDK@fcc102a state.py:48-79,
  local_conversation.py:727-733,753-755,2021-2043,2339-2360,
  event_router.py:68-139, event_service.py:456-460).
- F17: receipts no longer read /run-output or the server state directory;
  trace, skill, MCP and version fields are not_collected with the reason
  (receipt schema 4).
- F18: README scope corrected (the relock covers only the recipe venv) and a
  grype 0.119.0 digest-scan receipt added for the pinned image.
- F19: recorded as overturned for the reference host; documentation only.
- F20: removed the unreferenced round-1 files.
- Preflight requires the OH_SESSION_API_KEYS_0 name in OPENHANDS_SERVER_ENV,
  checking the name only (docker/cli@v29.8.1 pkg/kvfile/kvfile.go:92-124).
- --port is validated to 3730..3799 as in PR #428 host.py:44-45; default
  3730, resolver 3740; stored in status.json and revalidated by dispatch.

manifests/evidence.json is intentionally not updated; the coordinator
re-registers hashes after rebasing.

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

* OpenHands takeover phase 1 follow-up: bound F16, fix install image identity

Applies the coordinator decisions of 2026-09-28 on top of bac0b278.

- F16: finish_result keeps a termination label only as a refinement of
  the REST execution_status (PERMITTED_TERMINATIONS). Exit 0 requires
  REST "finished"; a planted event can move exit 3 to 1 at most. The
  README states the residual (SDK@fcc102a event_store.py:144-169,320-362,
  event_service.py:420-431, subprocess_terminal.py:157-170) and the
  analysis rule: exit 1 and 3 are both non-success, and reruns re-queue
  only exit 3.
- install(): pinned_image_identity() requires the exact pinned ref in
  RepoDigests. It accepts a plain .Id equal to the index digest
  (containerd store) or the config digest (classic store), and on
  containerd it checks the linux/amd64 platform manifest. Sources: moby
  docker-v29.8.1@464cd50c daemon/containerd/image_inspect.go:28,71-73,
  95,97; daemon/images/image_inspect.go:59;
  daemon/internal/image/store.go:152,160; fs.go:120;
  daemon/internal/distribution/pull_v2.go:431-434.
- The empty OH_SESSION_API_KEYS_0 value is documented as a residual.
- run_worker, record_event and observations() are kept because they are
  still referenced.

Red: 73 tests, failures=29, errors=1. Green: 73 tests OK.
Record: blueprints/runtime-workers/openhands/evidence/phase1-followup-commands.json

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

* OpenHands phase 2 (1/4): proxy base URL, recorded compression combos, unreachable host MCP

Both arms now call the per-attempt proxy alias http://gw:8081/v1; the
arm's gateway (10.0.2.2:20128 control, :20129 engines-on) becomes the
proxy's fixed upstream (gateway_upstream). engines-on selects one of the
seven header combos recorded in the 20129 apply record (default
allow-lossy); control sends none. One helper, environment_selection,
reads OPENHANDS_* for host.run and the container worker, with an empty
OPENHANDS_COMPRESSION meaning unset. The receipt (schema 5) records the
proxy base URL, the upstream and the combo.

ai-memory and socraticode are disabled in the tool policy as unreachable
by design under O1; no proxy route is added for them.

Red: 75 tests, failures=5 errors=7 (all in the changed contracts).
Green: 75 tests OK.

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

* OpenHands phase 2 (2/4): O1 topology, pinned nginx allowlist proxy, per-attempt session key

Replaces the DOCKER-USER receipt contract (model_network, the egress
host-file block) with the per-attempt topology from the phase-2 plan
(section 1, E1). For stem S = <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>

* OpenHands phase 2 (3/4): P0-P2 containment probe, dispatch-start gate, method pairing

- e2e/netprobe.py: locally composed stdlib probe (plan section 1), run in
  the pinned agent image on <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>

* OpenHands phase 2 (4a/4): teardown action and README for O1, the probe and the gate

- host.py teardown: removes a prepared or failed attempt by exact name
  without starting a conversation; refuses with no Docker call while a
  conversation may be live or the serial reservation names the attempt;
  a prepared status becomes torn_down, which start and probe refuse.
- README: session-key files and the container environment (E2), proxy
  ingress and method pairing (E3), the O1 topology, the locally composed
  nginx allowlist with its nginx.org and nginx source citations, the P0-P2
  probe and its receipt, the dispatch gate, P3-P5 as documented steps, and
  the coordinator's live probe sequence. G7's P3 half is not enforced in
  code and is documented as the coordinator's duty.

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

* OpenHands phase 2 (4b/4): E5 decision record, research corrections, evidence record

- docs/decisions/2026-09-28-openhands-resolver-isolation.md (E5): O1
  against split-port, DOCKER-USER, O2, O3/O4, O5, O7 and the daemon-wide
  loopback switch, with overturn conditions; the agent-branch ruleset
  variants; the scoped narrowing of the 2026-09-25 host-request-lane clause
  for the resolver lane only; a pointer to the follow-up PR for the GitHub
  harness; the build's deviations from the plan.
- research.md: takeover phase 2 corrections and open items.
- evidence/phase2-commands.json: red, failed-attempt and green unit runs
  for all four slices, and the read-only host observations behind the
  nginx pin (registry byte hashes, pull by digest, image identity) and the
  selected-field network template; a test ties it to pins.json and scans it
  with the repository's private-content patterns.

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

* OpenHands phase 2: correct $S-int membership and host-side reach wording

The decision record said only the agent-server and the P1/P2 probe join
$S-int; the proxy also joins it as gw. The README said only the host
reaches the proxy's 8080 while the next sentence said the agent reaches
it as gw:8080. Both now state the actual membership and reach.

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

* OpenHands phase 2: record the repository checks and their base control

Adds entries 21-24 to evidence/phase2-commands.json: the post-fix recipe
suite, the validate job's read-only check commands and the full unit
suite, both run in an unprivileged network namespace with only loopback,
and a base-versus-head control. Every failure also occurs at efa73f40:
the stale evidence manifest, credential ancestor checks under the user
namespace, and a worktree under /tmp. The record now names the 4b and
review-fix commits, and the decision record and research note describe
the added evidence part.

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

* OpenHands phase 2: bound the namespace claim and mark masked suite results

The record said no network request could leave the namespace; a network
namespace isolates TCP/IP only, so it now says no TCP or UDP traffic could
leave, that path-based unix sockets stayed reachable, and what bounded
them (no Docker CLI on PATH, zero owned resources afterwards). Entry 24
now says the base control shows no new failure but not a pass at head,
and names the one failing test that reads changed paths.

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

* OpenHands repair R1: grade with network mode none and inspect every grader container

The official grader containers had upstream network settings (the default
bridge), so grading could reach host-loopback listeners through 10.0.2.2.
e2e/docker_grader.py now:
- forces network_mode "none" on every ContainerCollection.create spec and
  refuses any upstream network, network_mode, networking_config or
  network_disabled option (docker-py 7.1.0 models/containers.py:686-694);
- refuses any SDK POST /containers/create body whose HostConfig.NetworkMode
  is not "none" (api/container.py:445-457, types/containers.py:351);
- refuses to grade when the created container's inspect shows another mode
  or any network besides "none", and removes that container (moby
  docker-v29.8.1 daemon/create.go:251, container_operations.go:363-406).

main() installs the transport through install_transport, which the tests
drive with fake docker modules. README and the decision record scope
containment to grading, document the fail-closed offline-verdict trade-off
(SWE-bench v4.1.0 test_spec/python.py:443-444, test_spec.py:55-60) and the
overturn condition: a separately probed internal network for graders.

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

* OpenHands repair R2, R4 and addendum b: probe targets, route evidence and resolver class

R2. probe_targets added each network's subnet .1 and run_probe refused
when an attempt container held a target. moby docker-v29.8.1 allocates no
gateway in isolated mode (bridge_linux.go:700-713, network.go:1594-1602),
so the server always holds .1 on $S-int and every live probe would have
refused. Targets now come from recorded IPAM gateways only; the attempt
containers' own addresses (server_int, proxy_int, proxy_gw) are excluded and
recorded by role in targets.excluded, never a refusal. The unit fixtures
now model the isolated network (no int gateway, server at .1).

R4. P2 now:
- starts with a positive control, a TCP connect to gw:8081;
- requires every pair outside the run subnet to fail with ENETUNREACH or
  EHOSTUNREACH, not a timeout (the host passes --subnets and classifies
  from the expected pairs);
- sends one DNS datagram (RFC 1035 4.1.1-4.1.2) to 10.0.2.3:53, which must
  get no answer and fail for want of a route;
- reads /proc/net/route (linux v6.18 fib_trie.c:2940-3000) and requires no
  default route and no RTF_GATEWAY route.

Addendum (b). The P2 summary and receipt keep dns_errors (EAI_NONAME vs
EAI_AGAIN); the pass condition is still that no address resolved.

README probe section and research.md:436 are corrected to match.

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

* OpenHands repair R6/G7 and probe-receipt trust: re-derive the verdict, require recorded stage gates

Probe-receipt trust (GPT-6 host.py:953). verify_isolation trusted the
receipt's passed booleans. checked_probe_counts now re-derives the verdict
from the recorded counts and refuses contradictions: both exit codes 0; P0
and P1 fully observed and matched; a non-empty target set with pairs equal
to addresses times ports; every P2 pair observed, failed with a named error
and, off-subnet, unreachable; the positive control connected; no DNS answer;
all five names matched; no IPv6 address, default route or gateway route.

R6/G7. verify_stage_gates requires the host-owned (0600, outside the
checkout) <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>

* OpenHands repair R3 (G5): read each reachable gateway store read-only and allowlist its providers

verify_isolation now runs verify_gateway_providers before any Docker read.
For each OmniRoute store the arm reaches (control: 20128; engines-on: 20129
and the 20128 store it forwards to), gateway_surface opens sqlite with
mode=ro and query_only and runs four statements only:
  SELECT provider FROM provider_connections   (no credential column)
  SELECT count(*) FROM combos
  the values of settings keys blockedProviders and
  noAuthFallbackDisabledProviders             (no other settings key)
check_gateway_surface refuses a provider row outside the host file's
per-arm allowlist (gateway_providers: control codex, engines-on
openai-compatible-responses-*; only a trailing * wildcard), any routing
combo, and any no-auth or anonymous-fallback provider that settings do not
disable by id or alias. Extension beyond the brief: OmniRoute 045aa81f3 and
dd6e9607e serve those providers with a synthetic credential and no
provider_connections row (src/sse/services/auth.ts:739-800,1193-1201), and
they include the subprocess-backed devin-cli-agentic, auggie and zcode, so a
row-only check would pass while they stay reachable.

read_host_file is factored out of preflight, which now also validates the
allowlists. Tests use fixture stores only (trace-checked statements, mode=ro,
unchanged bytes, no secret in the output); an audited suite run recorded no
access under ~/.local/share/omniroute*. README (new G5 section), research.md
and the decision record describe the check, its consequence (the live stores
refuse until those providers are disabled) and its limits (codex app-server
opt-in unread, inactive rows count, combos refused not resolved).

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

* OpenHands repair R5 (source-checked), gw:8080 log format and key-file wording

R5 deviation: receipts keep time-window attribution. At OmniRoute@045aa81f3
and @dd6e9607e the /v1/responses route passes a fresh randomUUID to
handleChat (src/app/api/v1/responses/route.ts:193,213; requestId.ts:100-102;
chat.ts:436), which call_logs.correlation_id stores (attemptLogging.ts:611).
The recipe fixes /v1/responses, so filtering on the run id would drop every
row (negative control: 'empty_window' != 'observed'). The attribution string
now says why, and a regression test counts rows with gateway-generated IDs.
The P3 skeleton moves its correlation check to a /v1/chat/completions call,
the route that keeps a caller ID (route.ts:292-322).

gw:8080 access log: an http-level ingress format (time, method, status) so a
URI sent to the host side is not copied whole; 8081 keeps combined for P5
(nginx release-1.30.5 ngx_http_log_module.c:170-171,230-232).

check_server_env docstring: reads the file and compares only the variable
name; the value is never logged or returned (addendum a).

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

* OpenHands repair round docs: scope the containment claims and record red/green

C9: the decision record separates what holds by construction (the
agent-server's containment, grading with no network) from G5's checked
gateway state, and narrowing condition 2 names the proxy routes, the session
key, G5 and offline grading. G5 joins the narrowing's overturn list.

GPT-6 items: every "three routes" claim now says three routes reach OmniRoute
and gw:8080 reaches only the agent's own server (README, decision record,
MCP policy reasons, netprobe docstring). The header bullet lists nginx's own
defaults at release-1.30.5 (generated Content-Length or chunked framing, an
empty and so omitted Connection; ngx_http_proxy_module.c:747-757,4210-4228)
and its framing refusals (ngx_http_request.c:2034-2064). The session key is
"not intentionally logged", with the retained-log residual. P0-P2 are
described as bounded observations with the trusted-producer limit.

R5: README usage accounting and the P3 steps state why receipts cannot match
the run id on /v1/responses at the pinned OmniRoute builds; P3 expects the
routed model the receipt matches.

evidence/repair-round-commands.json records each code item's red run, the
suite on each fixing commit (re-run from git archive), the literal-R5
negative control, the live-store audit and its planted-event control, and
the pinned source reads; a new test checks its pairing and publishability.

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

* Rebase onto main 83229e2: register the PR's files; research notes #468's guard coverage

The rebase dropped the earlier registration commit (conflict with #468 in
manifests/evidence.json, resolved to main's side), so all 55 files this PR
changes are registered again with host_receipts.register_file. research.md no
longer says the secret-path guard misses secrets/: #468 (11a23f1) covers it,
with docker exec/inspect recorded as the remaining gap.

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>
@seathatflowsinourveins

Copy link
Copy Markdown
Owner Author

Closing as superseded by #535, which carries every path this PR changed (byte-identical or with later qualification, routing and test updates), per the 2026-10-04 superseded-work cleanup (GPT-lane classification with per-path evidence). The branch stays available for history.

🤖 Generated with Claude Code

@seathatflowsinourveins
seathatflowsinourveins deleted the claude/runtime-worker-crawl4ai-20260927 branch October 8, 2026 07:57
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