# LAB E12 — Bounded shadow transport to the external perception worker

Date: 2026-07-22
State: near-live transport qualification accepted; physical K1 live run pending
Ground truth: no
Navigation/safety acceptance: no
Publication scope: diagnostic shadow transport only

## Purpose and answer

LAB E12 asks whether Mission Core can carry the already proven camera,
LiDAR and pose inputs to the external RTX worker at source rate without giving
that worker a second connection to K1, without allowing it to back-pressure the
raw recording, and without granting it any command authority.

The answer is **yes for a 15-second source-paced RAVNOVES00 near-live run**:

- Mission Core admitted and the Windows worker verified 446/446 ordered events;
- the stream contained 151 camera frames, one camera init, 142 LiDAR frames,
  150 poses and two lifecycle records;
- ingress sequence gaps, payload-integrity failures, queue overflow and oversize
  rejection were all zero in the nominal run;
- the worker completed in 15.073 seconds for a 15-second source interval;
- the worker connected only to an authenticated Mission Core endpoint through
  an SSH reverse tunnel and had no K1 address or K1 transport;
- both sides declared `commands_enabled=false` and
  `navigation_or_safety_accepted=false`.

The physical K1 run was not attempted because the unit was not present in the
BLE scan and the only unclassified LAN neighbour rejected both MQTT and RTSP.
No subnet scan or guessed device address was used. The exact same ingress is
already attached to the physical acquisition path and will receive live events
when K1 is next awake.

## Architecture under test

```text
K1 camera RTSP ──> Mission Core FFmpeg ──> durable fMP4 segment commit
                                              │
                                              └─> camera latest-wins queue (2)

K1 MQTT ──> durable raw K1MQTT commit ──> reviewed live callback
                                              ├─> LiDAR latest-wins queue (2)
                                              └─> pose bounded queue (16)

separate modality queues ──> one exclusive localhost WebSocket consumer
  ──> bearer authentication ──> SSH reverse tunnel
  ──> read-only Docker probe on RTX worker D:
```

The camera observer is invoked only after `CameraArchiveWriter.append()` has
atomically published and fsynced the fragment, its index entry and its crash
checkpoint. The MQTT observer is invoked only by `capture_mqtt`'s
`on_message_recorded` callback after the raw frame has been committed. Derived
perception therefore remains expendable: it cannot become the source of record.

The ingress is process-local and bounded by modality:

| Queue | Capacity | Maximum payload |
|---|---:|---:|
| control | 4 | 16 KiB |
| camera init | 1 | 1 MiB |
| camera frame | 2 | 1 MiB |
| LiDAR | 2 | 2 MiB |
| pose | 16 | 2 MiB |

Camera bursts cannot evict pose or LiDAR observations. A full queue drops its
oldest derived item and retains the latest work; it never waits on the worker.
Only one authenticated worker consumer may hold the stream lease.

## Nominal qualification input

- canonical session: `20260720T065719Z_viewer_live` / `RAVNOVES00`;
- camera source: `sensor.camera.right`, codec epoch 1, 800x600 H.264 fMP4;
- source interval: the first 15 seconds of the canonical camera epoch;
- MQTT input: the matching canonical raw K1MQTT arrival interval;
- source pacing: 1.0x recorded host-arrival time;
- camera index SHA-256:
  `e029815a60ad9fbfedb6169142c7449df2b119a51d1ce001f08806e04eb0be14`;
- raw MQTT SHA-256:
  `70da0edad5cbf0e89b6e2355c0aac294f8c36a67249cd33b8918353fd88c83af`.

This replay is a transport qualification, not a synthetic message generator.
Every camera segment was rechecked against its durable length and SHA-256 index
record. K1MQTT framing and aligned timing metadata were read through the
existing reviewed replay parser.

## Nominal result

### Event accounting

| Modality | Selected | Edge admitted | Worker verified | Payload bytes |
|---|---:|---:|---:|---:|
| camera init | 1 | 1 | 1 | 763 |
| camera frame | 151 | 151 | 151 | 11,583,362 |
| LiDAR | 142 | 142 | 142 | 3,674,244 |
| pose | 150 | 150 | 150 | 25,346 |
| control | 2 | 2 | 2 | 48 |
| **Total** | **446** | **446** | **446** | **15,283,763** |

- worker first/last ingress sequence: 1/446;
- sequence gaps: 0;
- per-event payload digest mismatches: 0;
- session-end observed: yes;
- worker wall time: 15.072754 s;
- edge publication wall time including orderly disconnect: 15.698036 s.

### Queue behaviour

| Queue | Maximum depth / capacity | Consumed | Dropped | Oversize rejected | Final depth |
|---|---:|---:|---:|---:|---:|
| control | 1 / 4 | 2 | 0 | 0 | 0 |
| camera init | 1 / 1 | 1 | 0 | 0 | 0 |
| camera frame | 2 / 2 | 151 | 0 | 0 | 0 |
| LiDAR | 2 / 2 | 142 | 0 | 0 | 0 |
| pose | 2 / 16 | 150 | 0 | 0 | 0 |

The camera and LiDAR queues reached their configured maximum capacity but did
not overflow: the tunnel/worker consumer drained them within source rate.

## Worker-disconnect negative control

A separate four-second source-paced run intentionally removed the consumer
after approximately one second. The source still admitted its complete selected
interval:

- camera frames: 41/41 admitted;
- LiDAR: 38/38 admitted;
- pose: 40/40 admitted;
- camera init: 1/1 admitted;
- source run completed in 4.147395 seconds;
- the worker saw 23 ordered events and no session-end, as intended.

After disconnect, the bounded derived queues behaved as designed:

| Queue | Consumed | Overflow drops | Retained final depth / capacity |
|---|---:|---:|---:|
| camera frame | 10 | 29 | 2 / 2 |
| LiDAR | 6 | 30 | 2 / 2 |
| pose | 6 | 18 | 16 / 16 |
| control | 1 | 0 | 1 / 4 |

Exact accounting holds for every modality: consumed + dropped + retained equals
published. The consumer failure did not stop, delay or throw into the source
publisher. This is the required fail-open property for raw capture and the
required fail-closed property for derived perception availability.

## Transport and security boundary

- Mission Core remains bound to `127.0.0.1`.
- A private 0600 bearer token is stored under
  `.runtime/live-perception/shadow-worker.token`; it is not returned in plugin
  state or passed on a remote command line.
- The Mac opens an SSH reverse tunnel using the pinned `mission-gpu` host key.
- The worker receives the token on stdin and connects through
  `host.docker.internal` to that tunnel.
- The server permits one consumer lease and immediately releases it on client
  disconnect.
- Every binary event contains an explicit schema, bounded header, exact payload
  length and SHA-256 digest.
- The worker container was read-only, capability-dropped, `no-new-privileges`,
  PID-limited and given only read-only runner plus D-backed report mounts.
- No GPU access was required for this transport-only probe.
- The worker never received a K1 address and never opened MQTT or RTSP.

## Storage and runtime

All task-controlled Windows files remained under `D:\NDC_MISSIONCORE`:

- runner:
  `D:\NDC_MISSIONCORE\workspace\mission-core-compute\run_e12_shadow_transport_probe.py`;
- report root:
  `D:\NDC_MISSIONCORE\runtime\derived\e12-shadow-transport`.

Windows drive C was not used by this task. D had approximately 375.75 GiB free
before the nominal run and 376.06 GiB at the final read; the report is only a
small JSON document and raw payloads were not retained on the worker.

## Artifact and producer identity

- nominal edge report:
  `.runtime/compute-experiments/e12/20260722T065957Z-e12-source.json`,
  SHA-256 `36018374bc82b720d6ee50dace9ca1621431a5f90f49bfaf824054ecf5e6dd15`;
- nominal D-worker report:
  `D:\NDC_MISSIONCORE\runtime\derived\e12-shadow-transport\20260722T065955Z-e12-shadow-transport.json`,
  SHA-256 `4983b6bcb489892170d7dbc8014dc04e4576202a8590394658259dd8d1b42de6`;
- negative edge report:
  `.runtime/compute-experiments/e12/20260722T070212Z-e12-source.json`,
  SHA-256 `2cc4f45959b4ce58fa2a58e004a6572a26a483b33e458f46a471012bf5abda74`;
- negative worker report:
  `.runtime/compute-experiments/e12/local-negative/20260722T070209Z-e12-shadow-transport.json`,
  SHA-256 `311e34f3e514f77caf0a36355902b15a2fc22cd861ecbf00a91d00f08a695a63`;
- ingress implementation SHA-256:
  `73e9359d926f7004ebe9519e0de4a05e4230922b714058ded9d27eff55ca06c0`;
- authenticated router SHA-256:
  `4c409299d99304a0330256113f188c0bca97aadab6f742a9a626c059a91c0ce6`;
- camera raw-first hook SHA-256:
  `086adf61d8143587025b85f46a59c9af746eee5656213cc1aa1c80974592d0a9`;
- physical acquisition integration SHA-256:
  `a9aaf129f083cfb219767e13765b8575fc62df8e0ed05e6d50cff00c45ef8186`;
- near-live source runner SHA-256:
  `9492804d8ecb629cb4a43b7b2d3220484c7b545b2e53dbad09eb8069f680d17d`;
- D-worker transport probe SHA-256 on both hosts:
  `0ad67a1eb6935dd111c490128645b0d4345dc86d171dd7c4045ba990287da44d`.

## Accepted scope

- raw-first post-commit camera/MQTT fan-out;
- separate bounded modality queues;
- exclusive authenticated worker lease;
- source-rate camera/LiDAR/pose transport through the real Windows/Docker path;
- per-event integrity and ordered lifecycle;
- worker-disconnect isolation from the source;
- D-only worker persistence;
- zero K1 command authority.

## Not accepted

- a physical live K1 run in this experiment;
- live camera decode on the worker;
- live detector, semantic model or LiDAR fusion execution over this transport;
- cross-host latency based on unsynchronised wall clocks;
- long-duration soak, reconnect/recovery or worker-process restart;
- forest/off-road domain accuracy;
- vehicle-body extrinsics, navigation or safety use.

## Next gate

1. Attach the worker's persistent camera decoder to the camera-init/frame branch.
2. Feed decoded frames to the already accepted capacity-2 10 Hz detector queue.
3. Decode LiDAR/pose into their verified K1 schemas and carry the latest matched
   depth epoch into the existing E10 fusion runtime.
4. Run YOLOX/Triton at 10 Hz and EoMT semantics at 2 Hz without writing per-frame
   images; publish only transient diagnostic world state and telemetry.
5. Repeat the disconnect negative control with inference loaded.
6. When K1 is awake, run the same profile first for 15 seconds and then for 60
   seconds against the physical acquisition path before any longer soak.
