I read this as read-only failure adjudication - I’ll inspect the sealed evidence and relevant test/runtime sources without executing anything, then distinguish harness-induced failures from genuine defects and propose append-only recertification.


I read this as read-only failure adjudication - I inspected the harness, runtime, and test contracts without executing or modifying anything.

## Adjudication

### H1: supported in principle, but the sealed 21-node set could not be independently recovered

The repository’s media tests intentionally replace the HTTP transport while leaving URL validation active. Any outer isolation layer that disables DNS/socket primitives before those mocks run changes the test contract and can cause false failures.

Evidence:

- `tools/url_safety.py:319-376` — `is_safe_url()` performs real `socket.getaddrinfo()` and fails closed on DNS failure or unexpected isolation exceptions.
- `tests/gateway/test_discord_document_handling.py:126-140` — `_mock_aiohttp_download()` mocks `aiohttp.ClientSession`, not DNS.
- `tests/gateway/test_discord_document_handling.py:146-354` — document tests expect mocked Discord CDN downloads to be cached and inspected.
- `tests/gateway/test_matrix.py:3288-3418` — Matrix external-media tests similarly mock `aiohttp.ClientSession`, while calling `_download_external_media_with_cap()` with `https://example.com/...`.
- `tests/gateway/test_matrix.py:4259-4307` — session-construction tests intentionally instantiate local `aiohttp.ClientSession` objects and close them without external traffic.
- `tests/conftest.py:1-330` — repository hermetic setup isolates credentials/home/runtime state but does not impose global network denial.
- `scripts/run_tests.sh:38-65` — the canonical runner imports an optional external `$HOME/.hermes/pytest_live_guard.py` through `PYTEST_PLUGINS`; this is the only documented path by which an ambient global harness can enter every per-file pytest process.
- `scripts/run_tests_parallel.py:230-258` — each file inherits the runner environment, so an injected plugin affects every test in that file.

The resulting failure path is:

```text
Discord/Matrix media method
 -> tools.url_safety.is_safe_url()
 -> socket.getaddrinfo()
 -> outer network-isolation denial
 -> is_safe_url() returns False
 -> media path rejects/skips the mocked fetch
 -> assertion expecting cached bytes/path or a later validation error fails
```

That is harness-induced because the contract is “mock transport, exercise adapter behavior,” not “perform external network I/O.”

I cannot lawfully provide the exact sealed 21 node IDs: the supplied sealed-root name contains an ellipsis and no readable exact artifact path, and command/search execution was expressly forbidden. Inventing those IDs would be unsound. Source-confirmed candidate groups are:

- `tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling`
- `tests/gateway/test_discord_document_handling.py::TestAllowAnyAttachment`
- `tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization`
- `tests/gateway/test_matrix.py::TestCreateMatrixSession`

The sealed failure output remains authoritative for the exact membership.

### H3: rejected for normal ambient `TERMINAL_CWD`

- `tests/conftest.py:160-239` includes `TERMINAL_CWD` in `_HERMES_BEHAVIORAL_VARS` and deletes it before every test.
- `scripts/run_tests.sh:52-65` starts from `env -i` and does not forward `TERMINAL_CWD`.
- `tests/gateway/test_runtime_footer.py:222-236` only asserts an empty footer when `TERMINAL_CWD` is absent; if ambient setup had supplied it, the conditional skips the assertion rather than failing.
- `gateway/runtime_footer.py:75-97` consults `TERMINAL_CWD` only when explicit `cwd` is empty.

Therefore ordinary ambient environment inheritance cannot explain the runtime-footer failure under the canonical runner. H3 would require a noncanonical post-fixture mutation, such as an injected plugin setting `TERMINAL_CWD` after `_hermetic_environment`; that must be demonstrated by the sealed stack/setup trace, not presumed.

## Lawful append-only recertification

Do not alter, delete, relabel, or rerun the sealed failed full run.

Append a separate recertification record containing:

1. Immutable reference to the original Gate20 run ID, artifact digest, commit/tree digest, command, environment manifest, and complete failed-node list.
2. A written exception scope containing only the sealed Discord/Matrix harness-induced node IDs.
3. A corrected isolation policy:
   - deny external egress at the connect boundary;
   - allow Unix sockets, event-loop socketpairs, loopback listeners/connections, and mocked transports;
   - do not replace DNS globally for tests whose SSRF contract intentionally validates public hostnames;
   - alternatively patch `socket.getaddrinfo` deterministically for those tests.
4. A fresh targeted run of exactly the exception node IDs under the corrected harness.
5. A fresh runtime-footer node run under canonical `env -i`, recording `TERMINAL_CWD` absence and plugin list.
6. Append-only result mapping:
   - original failure: preserved;
   - recertification disposition: `HARNESS_INVALIDATED` only when stack signatures match the isolation denial and targeted rerun passes;
   - runtime-footer: independently pass/fail, never bundled into the 21-node waiver.
7. Sign/digest the appended record and link it as a successor, not a replacement, to Gate20.

This supports recertification without pretending the original full run passed.


I read this as a precise recertification-schema ruling - I’ll trace the existing Gate18/19/20 receipt and release-plan contracts, then state exactly what may be rebound and how the successor verdict must preserve Gate20 FAIL.


message: WebSocket error

stop_reason: error


I read this as a precise append-only recertification ruling - I’ll define the binding, evidence, verdict, and node-selection rules without treating the sealed Gate20 failure as passed.

## Required receipt layout

Create a new sibling record; never write beneath or replace the sealed Gate20 artifact:

```text
<gate20-parent>/
  <sealed-gate20-root>/                         # unchanged, remains FAIL
  recertifications/
    <gate20-root-sha256>-harness-adjudication-v1/
      adjudication.json
      targeted-nodeids.txt
      targeted-result.json
      targeted-output.log
      harness-manifest.json
      environment-manifest.json
      artifact-index.json
```

`artifact-index.json` must bind every file by relative path, byte length, and SHA-256.

### `adjudication.json`

Required fields:

```json
{
  "schema": "dualcoach-gate20-harness-adjudication-v1",
  "receipt_kind": "append_only_successor",
  "candidate_eligible": false,
  "verdict": "PASS_NON_CANDIDATE_HARNESS_INVALIDATED",
  "original_gate20": {
    "verdict": "FAIL",
    "preserved": true,
    "root_path": "<exact sealed root path>",
    "root_manifest_sha256": "<64 hex>",
    "artifact_index_sha256": "<64 hex>",
    "full_run_command_sha256": "<64 hex>",
    "full_run_output_sha256": "<64 hex>",
    "failed_nodeids_sha256": "<64 hex>",
    "candidate_digest": "<64 hex>",
    "repository_status_sha256": "<64 hex>"
  },
  "prior_receipts": {
    "gate18": {
      "path": "<original immutable path>",
      "file_sha256": "<64 hex>",
      "payload_receipt_sha256": "<64 hex or null>",
      "candidate_digest": "<64 hex>",
      "rebound": true
    },
    "gate19": {
      "path": "<original immutable path>",
      "file_sha256": "<64 hex>",
      "payload_receipt_sha256": "<64 hex or null>",
      "candidate_digest": "<64 hex>",
      "rebound": true
    }
  },
  "harness_finding": {
    "classification": "INVALID_TEST_HARNESS",
    "mechanism": "global_network_isolation_preempted_mocked_media_contract",
    "affected_nodeids_sha256": "<sha256(targeted-nodeids.txt bytes)>",
    "failure_signature_sha256": "<canonical stack-signature digest>",
    "unaffected_failures": []
  },
  "recertification": {
    "scope": "targeted_only",
    "full_suite_rerun": false,
    "sealed_run_rerun": false,
    "targeted_result": "PASS",
    "targeted_result_sha256": "<64 hex>",
    "targeted_output_sha256": "<64 hex>",
    "harness_manifest_sha256": "<64 hex>",
    "environment_manifest_sha256": "<64 hex>"
  },
  "effects": {
    "sealed_artifacts_modified": 0,
    "service_actions": 0,
    "network_actions": 0,
    "git_actions": 0,
    "profile_mutations": 0
  },
  "issued_at_utc": "<RFC3339 UTC>",
  "receipt_sha256": "<canonical payload digest>"
}
```

`receipt_sha256` should follow the existing receipt convention in `gateway/platforms/dualcoach_admin.py:615-663`: hash canonical JSON with sorted keys and compact separators before adding `receipt_sha256`.

## Required bindings

Record these hashes independently; a directory hash alone is insufficient:

- Exact Git commit/tree digest.
- `git status --porcelain=v1 -z --untracked-files=all` SHA-256.
- Candidate manifest SHA-256 and `candidate_digest`.
- Wheel SHA-256, if Gate18 sealed a wheel.
- Python executable and lockfile SHA-256.
- `pyproject.toml`, `uv.lock`, `tests/conftest.py`, `scripts/run_tests.sh`, and `scripts/run_tests_parallel.py` SHA-256.
- Every affected production and test source file SHA-256.
- Original Gate20 command, environment, node list, output, summary, and artifact-index SHA-256.
- Corrected harness source/config SHA-256.
- Targeted command, ordered node list, output, JUnit/JSON result, and exit-code evidence SHA-256.
- Gate18 and Gate19 original receipt-file hashes.
- Hash of the adjudication rationale itself.

The environment manifest must explicitly record:

```json
{
  "TERMINAL_CWD": null,
  "PYTEST_PLUGINS": [],
  "network_policy": {
    "external_egress": "denied",
    "loopback": "allowed",
    "unix_sockets": "allowed",
    "event_loop_socketpair": "allowed",
    "mocked_transports": "allowed",
    "dns_fixture": "deterministic"
  }
}
```

## Gate18/19 rebinding

The existing controller establishes the lawful rebinding pattern:

- `gateway/platforms/dualcoach_tasks21_25_controller.py:292-325` binds historical workspace receipt, workspace index, synthetic-identity receipt, repository status, and candidate digest.
- `gateway/platforms/dualcoach_tasks21_25_controller.py:267-289` validates prior synthetic-identity evidence by both path hash and required payload facts.
- `gateway/platforms/dualcoach_tasks21_25_controller.py:327-365` validates the provider receipt against the same candidate digest and exact side-effect/request contract.
- `gateway/platforms/dualcoach_tasks21_25_controller.py:390-406` records prior receipt hashes rather than regenerating them.

Therefore Gate18/19 receipts may be rebound only when:

1. Their original files remain byte-identical.
2. Their recorded candidate digest equals Gate20’s candidate digest.
3. Every source/config/profile surface they attest remains hash-identical.
4. Their claims are independent of the invalid Gate20 harness.
5. Any time-sensitive readiness claim is revalidated separately; the historical receipt itself is not rewritten.

May be rebound:

- Immutable candidate/workspace manifest and index.
- Wheel/package hash evidence.
- Repository-status evidence, if unchanged.
- Synthetic-identity evidence, if its bound profile facts remain unchanged.
- Provider-auth receipt only if its candidate binding and readiness validity remain acceptable under its own policy.

May not be rebound:

- Gate20’s full-suite PASS claim—none exists.
- Gate20’s failed-node disposition.
- Any Gate18/19 receipt whose candidate, repository status, source file, config, wheel, or profile snapshot differs.
- A prior provider-readiness receipt if the plan requires freshness rather than merely historical proof.

## Verdict semantics

Keep both facts:

```text
Gate20 original verdict: FAIL
Gate20 candidate status: NOT CERTIFIED
Successor targeted adjudication: PASS_NON_CANDIDATE_HARNESS_INVALIDATED
Candidate eligible: false
```

Do not call the successor `Gate20 PASS`, `full-suite PASS`, or `candidate PASS`.

The successor means only:

> The listed failures are invalid evidence against the candidate because the imposed harness contradicted their mocked-network contracts, and the exact listed nodes passed under a corrected hermetic harness.

Promotion still requires a new candidate/full-suite gate if the release plan requires a clean full run.

## Targeted node artifact

The authoritative `targeted-nodeids.txt` must be generated by exact set intersection:

```text
sealed Gate20 failed node IDs
∩
nodes whose sealed stack reaches the network-isolation primitive
∩
nodes whose test contract mocks transport or intentionally uses local networking
```

It must be newline-delimited, UTF-8, sorted bytewise, with no `-k`, class wildcard, or file-level selector.

Source-confirmed DNS-sensitive candidates are:

```text
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_pdf_document_cached
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_txt_content_injected
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_md_content_injected
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_log_content_injected
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_mid_sized_zip_under_32mb_is_cached
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_zip_document_cached
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_large_txt_cached_not_injected
tests/gateway/test_discord_document_handling.py::TestIncomingDocumentHandling::test_multiple_text_files_both_injected
tests/gateway/test_discord_document_handling.py::TestAllowAnyAttachment::test_unknown_type_cached_when_flag_on
tests/gateway/test_discord_document_handling.py::TestAllowAnyAttachment::test_unknown_type_no_content_type_becomes_octet_stream
tests/gateway/test_discord_document_handling.py::TestAllowAnyAttachment::test_max_attachment_bytes_zero_means_unlimited
tests/gateway/test_discord_document_handling.py::TestAllowAnyAttachment::test_allowlisted_doc_unchanged_when_flag_on
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_external_media_download_rejects_oversized_content_length
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_external_media_download_rejects_oversized_stream
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_external_media_download_rejects_unsafe_redirect
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_external_media_download_rejects_non_image_content_type
```

These 16 are source-confirmed candidates, not a lawful substitute for the sealed 21-node list. The remaining five must come from the sealed Gate20 failed-node artifact and matching stacks. Since the supplied sealed-root identifier was abbreviated with `...`, no exact readable artifact path was available under the no-command constraint; claiming five additional IDs would fabricate evidence.

The runtime-footer node must be separate:

```text
tests/gateway/test_runtime_footer.py::test_build_footer_no_data_returns_empty_even_when_enabled
```

It must not be included in the harness-invalidated set unless its sealed stack independently proves post-fixture environment injection.

## Does plan language forbid this?

No inspected code or task language forbids an append-only, non-candidate successor adjudication. The user’s original constraint expressly allowed such a design by requiring that the sealed failed full run not be erased or rerun.

What is forbidden by the existing fail-closed contracts is using this successor as a candidate/full-suite PASS:

- `dualcoach_tasks21_25_controller.py:380-406` requires exact candidate and historical-evidence binding.
- `dualcoach_tasks21_25_controller.py:463-518` revalidates those bindings before launch.
- The controller has no mechanism for converting a failed gate into a successful one.

Thus append-only `PASS_NON_CANDIDATE_HARNESS_INVALIDATED` is lawful; rebadging Gate20 or promoting the candidate solely from it is not.