I read this as a read-only causal investigation - I’ll reconstruct the 4e9962 delta from sealed evidence and working-tree files, map all 22 Gate20 failures to candidate dependency paths, compare the passing and failing harnesses, then return per-group verdicts and a minimal non-full-suite control matrix.


The sealed artifacts identify the comparison point: the earlier direct-host full suite passed 8,138 tests, while the final candidate used the same nominal pytest surface but produced 22 failures. I’m now separating structural reachability from harness/environment effects and grouping the exact JUnit traces by first causal frame.


I read this as a read-only causal investigation - I compared the sealed candidate delta, all 22 JUnit failures, dependency paths, and prior harness evidence without executing tests or changing anything.

## Overall verdict

**H2 is disproved.** The 4e9962 Telegram onboarding candidate did **not** cause a shared runtime-footer or network regression.

- **21/22 failures are caused by `bwrap --unshare-net`**, which makes `tools.url_safety.is_safe_url()` fail closed when DNS cannot resolve `cdn.discordapp.com` or `example.com`.
- **1/22 is caused by `HOME=/tmp`**, which correctly renders `/tmp/wd` as `~/wd`; the test assumes `/tmp` is not HOME.
- None of the failing production or test modules imports or references any of the four cumulative candidate production modules.
- The same 21 network-sensitive nodes failed under an earlier unrelated candidate in the same network-isolated harness.
- The earlier direct-host Gate20 passed with **8,138 passed, 59 skipped, zero failures**.
- Candidate-focused isolated-wheel validation passed **176/176** under network isolation, showing the candidate itself does not require network access.

## Candidate reachability

The cumulative production set is:

1. `nutrition_onboarding_reconciliation.py`
2. `telegram_nutrition_onboarding_copy.py`
3. `telegram_nutrition_onboarding_runtime.py`
4. `telegram_nutrition_onboarding_runtime_publication.py`

The final incremental 4e9962 delta changed only:

- `telegram_nutrition_onboarding_copy.py`
- `telegram_nutrition_onboarding_runtime.py`
- their two onboarding test files

The changes are limited to:

- parsing a legacy Korean activity answer;
- normalizing activity category/rationale during schedule rewind;
- onboarding-only assertions around recovery, summary, and publication generation.

Their reverse dependency closure stays inside Telegram nutrition onboarding, previews, reconciliation, and associated tests. There is no path to:

- `plugins.platforms.discord.adapter`
- `gateway.platforms.matrix`
- `tools.url_safety`
- `gateway.runtime_footer`
- their failing tests

The changed tests also contain no module-level socket, environment, URL-safety, Discord, Matrix, or footer mutation.

## Strict verdict by failure group

### Discord document handling: 12 failures — **NOT CANDIDATE-CAUSED**

Affected nodes:

- `test_pdf_document_cached`
- `test_txt_content_injected`
- `test_md_content_injected`
- `test_log_content_injected`
- `test_mid_sized_zip_under_32mb_is_cached`
- `test_zip_document_cached`
- `test_large_txt_cached_not_injected`
- `test_multiple_text_files_both_injected`
- `test_unknown_type_cached_when_flag_on`
- `test_unknown_type_no_content_type_becomes_octet_stream`
- `test_max_attachment_bytes_zero_means_unlimited`
- `test_allowlisted_doc_unchanged_when_flag_on`

Causal chain:

```text
test attachment lacks att.read()
-> DiscordAdapter._cache_discord_document()
-> is_safe_url(att.url)
-> socket.getaddrinfo("cdn.discordapp.com")
-> network namespace has no DNS
-> is_safe_url() returns False
-> attachment download is skipped
-> no cached URL/content/MIME reaches MessageEvent
```

The tests mock `aiohttp.ClientSession`, but not the preceding DNS-based SSRF check. Therefore the mock is never reached.

**Verdict:** harness/test-isolation defect, independent of 4e9962.

### Matrix external-media handling: 9 failures — **NOT CANDIDATE-CAUSED**

Affected nodes:

- `test_external_media_download_rejects_oversized_content_length`
- `test_external_media_download_rejects_oversized_stream`
- `test_external_media_download_rejects_unsafe_redirect`
- `test_external_media_download_rejects_non_image_content_type`
- `test_send_image_failure_log_redacts_signed_url`
- `test_send_image_failure_response_does_not_expose_signed_url_query`
- `test_send_image_failure_response_does_not_expose_signed_url_fragment`
- `test_send_image_failure_response_preserves_caption`
- `test_send_image_failure_log_still_redacts_signed_url`

Causal chain:

```text
Matrix media call
-> is_safe_url("https://example.com/...")
-> socket.getaddrinfo("example.com")
-> DNS unavailable under --unshare-net
-> initial URL rejected before mocked HTTP/download behavior
```

Consequences:

- Four cap/redirect/content-type tests receive `"blocked unsafe media URL"` before reaching their intended branch.
- Five `send_image` fallback tests take the superclass unsafe-URL path before the mocked `_download_external_media_with_cap`; expected logging and positional `send()` calls therefore never occur.

**Verdict:** harness/test-isolation defect, independent of 4e9962.

### Runtime footer: 1 failure — **NOT CANDIDATE-CAUSED**

Affected node:

- `test_format_footer_skips_missing_context_length`

Causal chain:

```text
Gate20 sets HOME=/tmp
-> _home_relative_cwd("/tmp/wd")
-> expanduser("~") == "/tmp"
-> valid home-relative rendering == "~/wd"
-> test incorrectly expects literal "/tmp/wd"
```

No candidate module imports `gateway.runtime_footer`, and the candidate does not mutate HOME.

**Verdict:** environment-sensitive test assumption, independent of 4e9962.

## Harness comparison

Passing Gate20:

```text
direct host
normal HOME
normal DNS/network
HERMES_HOME unset
pytest -p no:cacheprovider tests/gateway
```

Final failing Gate20:

```text
bwrap --unshare-net
HOME=/tmp
TMPDIR=/tmp
read-only root
pytest -p no:cacheprovider tests/gateway
```

The pytest surface is nominally the same; the decisive differences are network namespace and HOME.

Historical evidence is particularly strong:

- Earlier isolated Gate20: the **same 12 Discord + 9 Matrix nodes** failed.
- Its 22nd failure was an unrelated PID-namespace test.
- Direct-host retry removed all 21 network failures.
- A later direct-host successor passed the complete suite.
- Final isolation replaced the old PID failure with the HOME-sensitive footer failure.

Thus the observed signature tracks harness isolation, not candidate lineage.

## Smallest targeted causal-control matrix

Use only these exact 22 nodes, not the full suite. Three total cells are sufficient; the first already exists:

| Cell | Source | Environment | Expected result | Purpose |
|---|---|---|---|---|
| A, sealed | 4e9962 | `--unshare-net`, `HOME=/tmp` | 21 network + 1 footer failures | Observed condition |
| B | predecessor overlay for the four candidate files | identical to A | Same 22 failures | Direct source-substitution control; disproves candidate effect |
| C | 4e9962 | deterministic public DNS stubs for `cdn.discordapp.com`/`example.com`, normal non-`/tmp` HOME; HTTP remains mocked | 22 pass | Demonstrates environmental sufficiency |

For Cell C, stub `socket.getaddrinfo` to public non-private addresses before invoking the 21 network-sensitive nodes. This avoids live DNS/network while preserving URL-safety behavior, including the Matrix unsafe-redirect check against `127.0.0.1`.

Decision rule:

- **B matches A and C passes:** candidate causality disproved, harness causality proved.
- **B passes while A fails:** candidate/environment interaction exists.
- **A and B differ only on specific nodes:** inspect only those nodes’ import-time state.
- **C still fails:** the remaining failure is test/code-specific, but the current evidence still excludes the Telegram candidate structurally.

No full-suite rerun is needed.


I read this as an execution contract - I’ll provide only the exact targeted nodes, commands, environments, and verdict rules.

## Exact node list

```bash
NODES=(
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
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_send_image_failure_log_redacts_signed_url
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_send_image_failure_response_does_not_expose_signed_url_query
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_send_image_failure_response_does_not_expose_signed_url_fragment
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_send_image_failure_response_preserves_caption
tests/gateway/test_matrix.py::TestMatrixImageOnlyMediaNormalization::test_send_image_failure_log_still_redacts_signed_url
tests/gateway/test_runtime_footer.py::test_format_footer_skips_missing_context_length
)
```

Run from:

```bash
cd /home/cube/projects/richard/hermes-agent
PY=/home/cube/projects/richard/hermes-agent/.venv/bin/python
E="/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-gates18-20-clarification-normalized-4e9962be49b72951c3b9ed7e1a4fe36d0ca03e757872ae03364c1ab7a790f32b-st_01a0080c"
B="/home/cube/projects/richard/traning coach/.omo/evidence/task26/task26-clarification-normalized-successor-4e9962be49b72951c3b9ed7e1a4fe36d0ca03e757872ae03364c1ab7a790f32b/source-delta/authenticated-fix-evidence/baseline"
```

## A — sealed candidate under failing environment

```bash
bwrap --die-with-parent --unshare-net \
  --ro-bind / / \
  --tmpfs /tmp \
  --tmpfs /home/cube/projects/richard/hermes-agent/.pytest-cache \
  --dev /dev --proc /proc \
  --setenv HOME /tmp \
  --setenv TMPDIR /tmp \
  --setenv PYTHONDONTWRITEBYTECODE 1 \
  --chdir /home/cube/projects/richard/hermes-agent \
  "$PY" -B -m pytest -q -p no:cacheprovider \
  --junitxml=/tmp/control-A.junit.xml \
  "${NODES[@]}"
```

Expected: 22 failures: 12 Discord, 9 Matrix, 1 footer.

## B — predecessor delta under identical failing environment

```bash
bwrap --die-with-parent --unshare-net \
  --ro-bind / / \
  --ro-bind "$B/gateway/platforms/telegram_nutrition_onboarding_copy.py" \
    /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_copy.py \
  --ro-bind "$B/gateway/platforms/telegram_nutrition_onboarding_runtime.py" \
    /home/cube/projects/richard/hermes-agent/gateway/platforms/telegram_nutrition_onboarding_runtime.py \
  --ro-bind "$B/tests/gateway/test_telegram_nutrition_onboarding.py" \
    /home/cube/projects/richard/hermes-agent/tests/gateway/test_telegram_nutrition_onboarding.py \
  --ro-bind "$B/tests/gateway/test_telegram_nutrition_onboarding_copy.py" \
    /home/cube/projects/richard/hermes-agent/tests/gateway/test_telegram_nutrition_onboarding_copy.py \
  --tmpfs /tmp \
  --tmpfs /home/cube/projects/richard/hermes-agent/.pytest-cache \
  --dev /dev --proc /proc \
  --setenv HOME /tmp \
  --setenv TMPDIR /tmp \
  --setenv PYTHONDONTWRITEBYTECODE 1 \
  --chdir /home/cube/projects/richard/hermes-agent \
  "$PY" -B -m pytest -q -p no:cacheprovider \
  --junitxml=/tmp/control-B.junit.xml \
  "${NODES[@]}"
```

Expected: same 22 failures as A.

## C — candidate with deterministic DNS and non-`/tmp` HOME

```bash
bwrap --die-with-parent --unshare-net \
  --ro-bind / / \
  --tmpfs /tmp \
  --tmpfs /home/cube/projects/richard/hermes-agent/.pytest-cache \
  --dev /dev --proc /proc \
  --setenv HOME /home/cube \
  --setenv TMPDIR /tmp \
  --setenv PYTHONDONTWRITEBYTECODE 1 \
  --chdir /home/cube/projects/richard/hermes-agent \
  "$PY" -B -c '
import socket
import sys
import pytest

real_getaddrinfo = socket.getaddrinfo
controlled = {"cdn.discordapp.com", "example.com"}

def getaddrinfo(host, *args, **kwargs):
    if str(host).lower().rstrip(".") in controlled:
        return [
            (
                socket.AF_INET,
                socket.SOCK_STREAM,
                socket.IPPROTO_TCP,
                "",
                ("93.184.216.34", 0),
            )
        ]
    return real_getaddrinfo(host, *args, **kwargs)

socket.getaddrinfo = getaddrinfo
raise SystemExit(pytest.main(sys.argv[1:]))
' \
  -q -p no:cacheprovider \
  --junitxml=/tmp/control-C.junit.xml \
  "${NODES[@]}"
```

Expected: 22 passed; HTTP remains mocked and no live network is available.

## Verdict logic

- **A=22 fail, B=same 22 fail, C=22 pass:** candidate causality disproved; network namespace plus HOME proved sufficient.
- **B passes or differs from A:** candidate/environment interaction remains causal for differing nodes.
- **C retains network failures:** deterministic DNS control is insufficient; inspect only retained nodes.
- **C retains footer failure:** HOME is not the sole footer input.
- **A and B both pass:** sealed failure is not reproducible under the stated harness.

## Sealed failing harness

Artifact:

```text
.omo/evidence/task26/task26-gates18-20-clarification-normalized-4e9962be49b72951c3b9ed7e1a4fe36d0ca03e757872ae03364c1ab7a790f32b-st_01a0080c/artifacts/gate20-full-gateway/command.txt
```

Exact command:

```bash
bwrap --die-with-parent --unshare-net --ro-bind / / \
  --bind "$E" "$E" \
  --tmpfs /tmp \
  --tmpfs /home/cube/projects/richard/hermes-agent/.pytest-cache \
  --dev /dev --proc /proc \
  --setenv HOME /tmp \
  --setenv TMPDIR /tmp \
  --setenv PYTHONDONTWRITEBYTECODE 1 \
  --chdir /home/cube/projects/richard/hermes-agent \
  "$PY" -B -m pytest -q -p no:cacheprovider \
  --junitxml="$E/artifacts/gate20-full-gateway.junit.xml" \
  tests/gateway
```

Environment: read-only root, isolated network, private `/tmp`, `HOME=/tmp`, `TMPDIR=/tmp`, bytecode disabled, pytest cache disabled.