Disciplines · Audits

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

private method a public one delegates to, tracing from the MCP tool surface down to the process/socket boundary.

6sections8 minread

On this page

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 simulationstransport:'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.