# Bellona wiring-path audit — Phase 5.1 (2026-09-06)

Two-pass fabrication/security audit of the Bellona DCC wiring path named by
`EVE_SOTA_GAP_CLOSURE_TODOS_2026-09-01` task 5.1: `@bellona/blender-agent`,
`@bellona/mcp-gateway`, `@bellona/bridge-core`, and the required
`@bellona/adapters`, with every delegated method read for semantic correctness.
This audit records per-library verdicts; it is **not** a first-light, breadth,
or negative-control campaign (those are tasks 5.3, 5.6, 5.7, 5.8) and does not
close them.

Host runtime at audit time: **Blender 4.0.2** on `/usr/bin/blender`, Python
3.12.3 with `websockets` 16.0 available inside Blender's interpreter. This host
Blender was used to ground the audit against the real runtime rather than
reading alone.

## Method

- **Confirmatory pass:** read every public function on the wiring path and every
  private method a public one delegates to, tracing from the MCP tool surface
  down to the process/socket boundary.
- **Adversarial pass:** the checkbox-rule grep for confession vocabulary across
  `blender-agent`, `mcp-gateway`, `bridge-core` source (excluding tests) — 6
  hits, all legitimate physics-simulation domain vocabulary ("simulated
  strands", "simulate empty"), zero stub indicators. Silent-stub scan for
  `Math.random` in deterministic functions, catch-blocks returning fabricated
  success, and fixed-literal returns — none on the path.
- **Test semantic-correctness pass:** read each library's tests and asked
  whether they assert domain correctness or only shape/data-flow.
- **Runtime grounding:** drove the actual `BlenderRpcBridge` (stdin transport)
  against real Blender 4.0.2, and confirmed real state mutation and artifact
  write, not fabricated success.

## Per-library verdicts

### `@bellona/bridge-core` — REAL, thinly tested

`src/index.ts` (27,937 bytes) is genuine WebSocket bridge infrastructure:
`createBridgeServer`, typed message envelopes, request/reply correlation by
`MessageId`, connection lifecycle, handshake. No fabrication.

- **Delegated methods read:** `createHandshakeCommand.handler` (real: updates
  connection identity, merges handshake metadata, emits a `handshake` event),
  `createConnectionId`, `createMessageId` (non-secret id suffix via
  `Math.random().toString(36)` — annotated and carved out by the CSPRNG-id
  audit; legitimate for a correlation id, not a secret).
- **Test verdict:** the single test (`index.test.ts`) is a genuine behavioral
  test of `createHandshakeCommand.handler` with real assertions, but it covers
  only that one command handler. The WebSocket server, `request()` correlation,
  and connection lifecycle are behaviorally untested. **Finding B1 (coverage).**

### `@bellona/adapters` — REAL, was shape-only tested (raised this audit)

`src/bridge/base-bridge.ts` `BaseBridge` is a genuine correlation-id WebSocket
RPC: `sendCommand` encodes a command, registers a pending request under a
generated id with a timeout, sends over an injected socket, and `handleMessage`
resolves/rejects the matching pending request by `correlationId`. Fail-loud
`throw new Error('Not connected')` when the socket is absent; a
`{success:false}` engine payload rejects rather than resolving. The injected
`WebSocketFactory` is a **legitimate dependency-boundary double** (the socket is
the dependency; the protocol is the thing under test), not a stub of the bridge.

- **Delegated methods read:** `connect`, `disconnect`, `sendCommand`, `send`,
  `handleMessage`, `handleClose`, heartbeat and reconnection managers — all
  real.
- **Test verdict (finding B2, remediated):** the only pre-existing test,
  `phase-8-completion.test.ts`, asserted the exported classes are
  `typeof 'function'` and **nothing about behavior** — it would pass even if
  `sendCommand` were gutted. This audit added `src/bridge/base-bridge.spec.ts`
  (5 tests) driving correlation-resolve, timeout, engine-failure propagation,
  the fail-loud "Not connected" guard, and no-cross-response between two
  concurrent commands, through an injected fake socket. Adapters test count 1
  → 6.

### `@bellona/mcp-gateway` — REAL path + honestly-labelled simulations

- `dcc-bridge-gateway.ts` `DccBridgeGateway` is **the real path**: it routes
  each `RemoteCommandEnvelope` to a registered `DccCommandTransport` (any
  `@bellona/adapters` `BaseBridge`) over the genuine command protocol and
  returns the bridge's real response. Fail-loud `DccBridgeNotConfiguredError`
  when no binding/transport is registered; honest `status:'failed'` with an
  `adapter.offline` / `adapter.unavailable` error when the transport is
  disconnected or throws — **never a fabricated scene**. Structural
  `DccCommandTransport` seam so it takes no compile dependency on any engine
  bridge.
- `remote-control-tools.ts` registers real MCP tools that delegate to the
  injected gateway
  (`gateway.listDevices/startSession/stopSession/dispatchCommand`, 10 call
  sites). No fabricated success.
- `stdio-server.ts` builds a real `@modelcontextprotocol/sdk` `McpServer` +
  `StdioServerTransport`; it registers remote-control tools/resources **only
  when a gateway is injected**.
- `cloud-agent-mvp-smoke.ts` and `remote-mac-no-inbound-smoke.ts` are **honest,
  self-labelled in-memory simulations** —
  `transport:'mcp-in-memory-cloud-agent-simulation'`,
  `remoteHostTransport:'outbound-websocket-simulated'`,
  `liveDeploymentExercised:false`, and a module docstring that explicitly says
  the smoke gateway "fabricates a fixed scene ... it is NOT a real path to a
  DCC." These are labelled test doubles, not masquerading stubs.
- **Test verdict:** 129 passing tests, behaviorally meaningful (routing,
  fail-loud, offline handling). Good.

### `@bellona/blender-agent` — REAL, verified live; two defects found and fixed

`blender-rpc-bridge.ts` is the action-execution seam. Two real transports:

- `BlenderStdinRpcTransport` spawns real Blender
  (`--background --factory-startup --python <bootstrap>`) and speaks a JSON-line
  protocol over stdin/stdout; the generated bootstrap
  (`createBlenderStdinRpcBootstrapScript`) runs real `bpy` (`execute_python`,
  `execute_python_file`, `invoke_operator`, `handshake`, `health`, `shutdown`).
- `BlenderWebSocketRpcTransport` connects over `ws` to the addon protocol
  (`@bellona/blender` `bellona_addon/server.py`, whose `execute_python` runs
  `exec(code)` on Blender's main thread).

**Delegated methods read:** `connect`, `disconnect`, `request`, `executePython`,
`executePythonFile`, `invokeOperator`, `executeBatch`, `spawnProcess`,
`attachProcess`, `handleStdoutLine`, `cleanupPending`, plus the WebSocket
transport's `sendHandshake`/`sendRequest`/`sendMessage` and the health probe.

**Runtime grounding (Blender 4.0.2, stdin transport, via the real bridge):**
created a cube (objects 3 → 4, named `AuditCube`), invoked
`mesh.primitive_uv_sphere_add`, read the object list and the cube location
`[1,2,3]` back, saved a real 908,656-byte `.blend` (magic `BLENDER-v400`), and a
Python error surfaced as a rejection — not a fabricated success.

Two defects were **measured** against real Blender during grounding and fixed:

- **Finding A1 — `invoke_operator` could not serialize a normal operator
  result.** `bpy.ops.*` return a Python `set` such as `{'FINISHED'}`; the
  bootstrap's `respond()` did `json.dumps(message)` with no `default=`, which
  raised `TypeError: Object of type set is not JSON serializable`. The operator
  **executed** (state mutated) but the RPC reported `success:false` — a
  fabrication of failure for a real mutation. Fixed: a `_json_default` coerces
  sets to sorted lists (and passes it to `json.dumps`), and `invoke_operator`
  returns a structured `{operatorId, status:[...]}`. Now returns
  `{operatorId:'mesh.primitive_uv_sphere_add', status:['FINISHED']}` live.
- **Finding A2 — a non-JSON stdout line rejected all in-flight requests.** Real
  Blender writes human-readable text to **stdout** (the banner `Blender 4.0.2`,
  `Saved "scene.blend"`, render progress). `handleStdoutLine` called
  `cleanupPending()` — rejecting every pending request — on any line that failed
  `JSON.parse`. Against real Blender this made the stdin transport unreliable
  (the banner raced mid-session and killed live requests). Fixed: a non-JSON
  line is retained in a bounded diagnostic buffer and skipped; pending requests
  are left intact; a later timeout surfaces the recent noise.

- **Test verdict (remediated):** the pre-existing stdin unit test drove a
  `FakeStdioProcess` that only ever emits clean JSON and an idealized operator
  echo, so it passed while the real path was broken on both A1 and A2. This
  audit added: a `FakeStdioProcess` variant that interleaves banner noise (goes
  red without the A2 fix — verified: `"Blender 4.0.2" is not valid JSON`), a
  bootstrap assertion for the A1 coercion, and a gated real-Blender first-light
  spec (`blender-rpc-bridge.integration.spec.ts`, opt-in via
  `BELLONA_AUDIT_BLENDER_PATH`) that runs the full create → operator → readback
  → save → fail-loud sequence against the live executable and skips cleanly when
  no Blender is configured. blender-agent test count 196 → 198 (+1 skipped
  integration by default).

## Structural finding (S1) — the wiring path is real but inert

Every named layer is real and, where load-bearing, now verified. **Nothing in
the runtime constructs and connects them.** `stdio-cli.ts` (the shipped
`bellona-mcp-gateway` binary) calls `connectBellonaMcpGatewayStdioServer()` with
no options, so no `remoteControlGateway` is injected and the MCP server
registers zero DCC tools; the BFF imports only `@bellona/asset-export` and
`@bellona/interchange` (package-builder store), never the gateway, a bridge, or
an adapter. This corroborates the task-4.5 blocker ("the BFF mounts no bridge,
gateway or adapter … there is no DCC execution to isolate"): the components
exist and work in isolation, but no code path wires `DccBridgeGateway` to a live
`BaseBridge`/Blender. It is not fabrication — the empty surface is honest — but
the path does not yet run end-to-end in production. Closing this is tasks 5.5
(register the gateway on a leased surface) and 5.6 (first light).

## Fabrication/security scan summary

- Confession-vocabulary grep over the three core libs' source: 0 stub indicators
  (6 legitimate physics-domain hits).
- Silent-stub scan (`Math.random` in deterministic logic, catch-return-success,
  fixed-literal returns): none on the path. The two `Math.random().toString(36)`
  id suffixes are annotated non-secret correlation ids.
- Simulated runtimes are all explicitly labelled
  (`liveDeploymentExercised:false`, `*-simulation`, `*-simulated`) and never
  presented as live.
- Security note carried to 5.5/5.8: the addon `server.py` `execute_python` runs
  arbitrary `exec(code)` with no origin/token gate on the bare WebSocket path
  (the addon exposes an optional bearer token but the server does not require
  it), and its `_send_response` `json.dumps` shares the A1 set-serialization
  fragility. These belong to the addon library (`@bellona/blender`) and to the
  governed-admission work of 5.4/5.5/5.8, not to a checkbox this audit flips.

## Evidence commands

- Unit + regression: `vitest run` in `libs/bellona/blender-agent` (197 passed, 1
  skipped) and `libs/bellona/adapters` (6 passed).
- Negative control observed red (A2) by reverting the fix:
  `vitest run src/blender-rpc-bridge.test.ts -t "non-JSON stdout noise"` →
  `Invalid stdin RPC response: "Blender 4.0.2" is not valid JSON`; green after
  restore.
- Live first light:
  `BELLONA_AUDIT_BLENDER_PATH=/usr/bin/blender vitest run src/blender-rpc-bridge.integration.spec.ts`
  (1 passed); skips with the env var unset.
- Typecheck: `tsc --noEmit` on `blender-agent` and `adapters` (exit 0). Lint:
  eslint on the touched source (0 errors). Prettier: clean.

## What this audit does NOT establish

First light is one live example, not breadth. This audit proves the delegated
methods are real and, for `blender-agent`, verified against real Blender for a
create/operator/readback/save/fail-loud sequence. It does **not** establish:
governed leased admission (5.5), independent semantic/hash/render verification
of a leased first light (5.6), the breadth and negative-control matrices (5.7,
5.8), the WebSocket-addon path against real Blender, or any non-Blender DCC.
Phase 5 and the tasks that depend on it remain open.
