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-coresource (excluding tests) — 6 hits, all legitimate physics-simulation domain vocabulary ("simulated strands", "simulate empty"), zero stub indicators. Silent-stub scan forMath.randomin 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 ahandshakeevent),createConnectionId,createMessageId(non-secret id suffix viaMath.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 ofcreateHandshakeCommand.handlerwith 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 aretypeof 'function'and nothing about behavior — it would pass even ifsendCommandwere gutted. This audit addedsrc/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.tsDccBridgeGatewayis the real path: it routes eachRemoteCommandEnvelopeto a registeredDccCommandTransport(any@bellona/adaptersBaseBridge) over the genuine command protocol and returns the bridge's real response. Fail-loudDccBridgeNotConfiguredErrorwhen no binding/transport is registered; honeststatus:'failed'with anadapter.offline/adapter.unavailableerror when the transport is disconnected or throws — never a fabricated scene. StructuralDccCommandTransportseam so it takes no compile dependency on any engine bridge.remote-control-tools.tsregisters real MCP tools that delegate to the injected gateway (gateway.listDevices/startSession/stopSession/dispatchCommand, 10 call sites). No fabricated success.stdio-server.tsbuilds a real@modelcontextprotocol/sdkMcpServer+StdioServerTransport; it registers remote-control tools/resources only when a gateway is injected.cloud-agent-mvp-smoke.tsandremote-mac-no-inbound-smoke.tsare 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:
BlenderStdinRpcTransportspawns real Blender (--background --factory-startup --python <bootstrap>) and speaks a JSON-line protocol over stdin/stdout; the generated bootstrap (createBlenderStdinRpcBootstrapScript) runs realbpy(execute_python,execute_python_file,invoke_operator,handshake,health,shutdown).BlenderWebSocketRpcTransportconnects overwsto the addon protocol (@bellona/blenderbellona_addon/server.py, whoseexecute_pythonrunsexec(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_operatorcould not serialize a normal operator result.bpy.ops.*return a Pythonsetsuch as{'FINISHED'}; the bootstrap'srespond()didjson.dumps(message)with nodefault=, which raisedTypeError: Object of type set is not JSON serializable. The operator executed (state mutated) but the RPC reportedsuccess:false— a fabrication of failure for a real mutation. Fixed: a_json_defaultcoerces sets to sorted lists (and passes it tojson.dumps), andinvoke_operatorreturns 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).handleStdoutLinecalledcleanupPending()— rejecting every pending request — on any line that failedJSON.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
FakeStdioProcessthat 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: aFakeStdioProcessvariant 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 viaBELLONA_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.randomin deterministic logic, catch-return-success, fixed-literal returns): none on the path. The twoMath.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.pyexecute_pythonruns arbitraryexec(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_responsejson.dumpsshares 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 runinlibs/bellona/blender-agent(197 passed, 1 skipped) andlibs/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 --noEmitonblender-agentandadapters(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.