fix(virtual-view): validate bounded plain data before broker calls

This commit is contained in:
ruv committed 2026-10-10 13:29:51 -04:00
1 parent 88daeb8cf2
commit d779961a55
4 files changed
+226 -9

No files matched your search

+3
View File
@@ -206,6 +206,9 @@ jobs:
- name: Run UI unit tests
run: node --test ui/sw.test.mjs ui/services/ws-ticket.test.mjs ui/services/websocket.service.test.mjs ui/services/sensing.service.test.mjs ui/components/DashboardTab.test.mjs v2/crates/wifi-densepose-desktop/ui/build-config.test.mjs
- name: Run virtual-view adapter boundary tests
run: node --test integrations/virtual-view/adapter.test.mjs
# Unit and Integration Tests
# Python pytest matrix — runs against the archived v1 Python tree.
# `continue-on-error: true` for the same reason as code-quality above:
+15
View File
@@ -4,6 +4,11 @@ Opt-in integration with Dream Machine's `scripts/virtual-view-broker.mjs`.
Disabled by default. No cameras, model calls, network, hardware commands or
automatic promotion. It is NOT part of the Rust production perception pipeline.
The reviewed companion is Dream Machine commit
[`5707c825b2c065de44abed02594e1abf5eab95ea`](https://github.com/ruvnet/dream-machine/tree/5707c825b2c065de44abed02594e1abf5eab95ea),
merged in [Dream Machine PR #162](https://github.com/ruvnet/dream-machine/pull/162).
Use a checkout at that revision when reproducing the integration.
```js
import { generateKeyPairSync } from 'node:crypto';
import { broker } from '../../../dream-machine/scripts/virtual-view-broker.mjs';
@@ -21,6 +26,16 @@ threshold, trust boundary, receipt and acceptance rule. The adapter does not
transform raw `spatial.evidence.v1` RF Gaussians or `ruview.lidar.depth.v1` packets:
neither supplies the independently tracked manipulation scene this method needs.
The injected broker is trusted executable code, not sandboxed by this adapter.
The no-network/no-actuation behavior applies to this adapter and the reviewed
companion broker. Before reading scene markers or calling the broker, the adapter
checks the entire snapshot as bounded plain JSON using property descriptors.
It rejects proxies, getters, inherited/custom prototypes, nonfinite values,
sparse arrays and non-JSON data, including nested values. Structural limits match
the reviewed companion: depth 12, 20,000 values, 1,024 array elements, 64 object
properties, 128-character keys and 4,096-character strings. Full geometry,
provenance-label and signing-key validation remain the companion broker's responsibility.
```sh
node --test integrations/virtual-view/adapter.test.mjs
# From the sibling Dream Machine checkout:
+45 -2
View File
@@ -1,12 +1,55 @@
// Opt-in cross-repository prototype. Imports no hardware, network or model client.
import { types } from 'node:util';
const snapshotError = () => new TypeError(
'calibrated metric scene snapshot required; raw RF/LiDAR packets and non-JSON data are not supported',
);
// Structural preflight only, matching the reviewed companion's JSON budgets.
// Never invoke input methods, getters or proxy traps while inspecting data.
function validateSnapshotData(value, depth = 0, budget = { nodes: 0 }) {
if (depth > 12 || ++budget.nodes > 20000) throw snapshotError();
if (value === null || typeof value === 'boolean') return;
if (typeof value === 'string' && value.length <= 4096) return;
if (typeof value === 'number' && Number.isFinite(value)) return;
if (typeof value !== 'object' || types.isProxy(value)) throw snapshotError();
const array = Array.isArray(value);
if (Object.getPrototypeOf(value) !== (array ? Array.prototype : Object.prototype)) {
throw snapshotError();
}
if (array) {
const length = Object.getOwnPropertyDescriptor(value, 'length').value;
if (length > 1024) throw snapshotError();
if (Reflect.ownKeys(value).length !== length + 1) throw snapshotError();
for (let index = 0; index < length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) throw snapshotError();
validateSnapshotData(descriptor.value, depth + 1, budget);
}
return;
}
const keys = Reflect.ownKeys(value);
if (keys.length > 64) throw snapshotError();
for (const key of keys) {
if (typeof key !== 'string' || key.length > 128) throw snapshotError();
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) throw snapshotError();
validateSnapshotData(descriptor.value, depth + 1, budget);
}
}
export function createVirtualViewAdapter({ broker, privateKey, enabled = false }) {
if (typeof broker !== 'function' || typeof enabled !== 'boolean') throw new TypeError('adapter configuration');
return Object.freeze({
observe(snapshot) {
if (!enabled) return { decision: 'BLOCK', reason: 'disabled', authority: 'none', svg: null };
validateSnapshotData(snapshot);
// Explicit metric scene contract only. RF Gaussians are not manipulation geometry.
if (snapshot?.schema !== 'virtual-view.v1' || snapshot?.frame !== 'room_enu') {
throw new TypeError('calibrated metric scene snapshot required; raw RF/LiDAR packets are not supported');
const schema = snapshot && Object.getOwnPropertyDescriptor(snapshot, 'schema');
const frame = snapshot && Object.getOwnPropertyDescriptor(snapshot, 'frame');
if (schema?.value !== 'virtual-view.v1' || frame?.value !== 'room_enu') {
throw snapshotError();
}
return broker(snapshot, { privateKey });
},
+163 -7
View File
@@ -3,12 +3,168 @@ import assert from 'node:assert/strict';
import { createVirtualViewAdapter } from './adapter.mjs';
test('disabled by default and never actuates', () => {
const adapter = createVirtualViewAdapter({ broker: () => { throw Error('not called'); } });
assert.equal(adapter.observe({}).reason, 'disabled'); assert.equal(adapter.hardwareActuation, false);
let reads = 0;
const snapshot = { get schema() { reads++; throw Error('not read'); } };
assert.deepEqual(adapter.observe(snapshot), {
decision: 'BLOCK', reason: 'disabled', authority: 'none', svg: null,
});
assert.equal(reads, 0);
assert.equal(adapter.hardwareActuation, false);
assert.equal(Object.isFrozen(adapter), true);
});
test('raw RF is rejected; explicit scene is forwarded to validating broker', () => {
let received;
const adapter = createVirtualViewAdapter({ enabled: true, broker: s => { received = s; return { authority: 'none' }; } });
assert.throws(() => adapter.observe({ schema: 'spatial.evidence.v1' }));
const s = { schema: 'virtual-view.v1', frame: 'room_enu' };
assert.equal(adapter.observe(s).authority, 'none'); assert.equal(received, s);
test('raw RF, LiDAR and malformed scene markers never reach the broker', () => {
let calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
for (const snapshot of [
null, undefined, 'virtual-view.v1', [], {},
{ schema: 'spatial.evidence.v1', frame: 'room_enu' },
{ schema: 'ruview.lidar.depth.v1', frame: 'room_enu' },
{ schema: 'virtual-view.v1' },
{ schema: 'virtual-view.v1', frame: 'camera' },
Object.create({ schema: 'virtual-view.v1', frame: 'room_enu' }),
Object.defineProperty({ frame: 'room_enu' }, 'schema', { value: 'virtual-view.v1' }),
]) {
assert.throws(() => adapter.observe(snapshot), /calibrated metric scene snapshot required/);
}
assert.equal(calls, 0);
});
test('scene marker getters are rejected without executing input code', () => {
let reads = 0, calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
for (const key of ['schema', 'frame']) {
const snapshot = { schema: 'virtual-view.v1', frame: 'room_enu' };
Object.defineProperty(snapshot, key, {
enumerable: true,
get() { reads++; return key === 'schema' ? 'virtual-view.v1' : 'room_enu'; },
});
assert.throws(() => adapter.observe(snapshot), /calibrated metric scene snapshot required/);
}
assert.equal(reads, 0);
assert.equal(calls, 0);
});
test('proxy snapshots are rejected without running inspection traps', () => {
let traps = 0, calls = 0;
const snapshot = new Proxy({ schema: 'virtual-view.v1', frame: 'room_enu' }, {
get() { traps++; throw Error('get trap'); },
getPrototypeOf() { traps++; throw Error('prototype trap'); },
getOwnPropertyDescriptor() { traps++; throw Error('descriptor trap'); },
});
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
assert.throws(() => adapter.observe(snapshot), /calibrated metric scene snapshot required/);
assert.equal(traps, 0);
assert.equal(calls, 0);
});
test('nested object and array getters are rejected without reads or broker calls', () => {
let reads = 0, calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
const getter = { enumerable: true, get() { reads++; return 0; } };
const object = Object.defineProperty({}, 'uncertaintyM', getter);
const array = Object.defineProperty([0], '0', getter);
for (const extra of [object, [object], { nested: array }]) {
assert.throws(() => adapter.observe({ schema: 'virtual-view.v1', frame: 'room_enu', extra }),
/calibrated metric scene snapshot required/);
}
assert.equal(reads, 0);
assert.equal(calls, 0);
});
test('nested proxies and custom array prototypes never execute input code', () => {
let traps = 0, calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
const handler = {
get() { traps++; throw Error('get trap'); },
getPrototypeOf() { traps++; throw Error('prototype trap'); },
ownKeys() { traps++; throw Error('keys trap'); },
getOwnPropertyDescriptor() { traps++; throw Error('descriptor trap'); },
};
const custom = Object.setPrototypeOf([], {
get [Symbol.iterator]() { traps++; throw Error('iterator getter'); },
});
class SceneArray extends Array {}
for (const extra of [
{ nested: new Proxy({}, handler) },
[new Proxy([], handler)],
{ nested: custom },
new SceneArray(),
]) {
assert.throws(() => adapter.observe({ schema: 'virtual-view.v1', frame: 'room_enu', extra }),
/calibrated metric scene snapshot required/);
}
assert.equal(traps, 0);
assert.equal(calls, 0);
});
test('cyclic, non-JSON and oversized structures are rejected before the broker', () => {
let calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { calls++; } });
const cyclic = {}; cyclic.self = cyclic;
let deep = 0;
for (let index = 0; index < 13; index++) deep = { next: deep };
const sparse = Array(2); sparse[1] = 0;
const extraProperty = [0]; extraProperty.extra = 0;
for (const extra of [
cyclic, deep, sparse, extraProperty, NaN, Infinity, undefined, () => {}, new Date(),
'x'.repeat(4097), Array(1025).fill(0),
{ ['x'.repeat(129)]: 0 },
Object.fromEntries(Array.from({ length: 65 }, (_, index) => [`k${index}`, 0])),
Array.from({ length: 20 }, () => Array(1024).fill(0)),
]) {
assert.throws(() => adapter.observe({ schema: 'virtual-view.v1', frame: 'room_enu', extra }),
/calibrated metric scene snapshot required/);
}
assert.equal(calls, 0);
});
test('a complete 256-object synthetic scene stays within the structural budget', () => {
const scene = {
schema: 'virtual-view.v1', sceneId: 'fixture', requestId: 'request:1', policyId: 'fixed',
revision: 1, frame: 'room_enu', proof: 'SYNTHETIC', nowMs: 1000, observedMs: 990,
contact: 'static', target: 'o0', reference: 'o1', physicalAxis: 'x',
objects: Array.from({ length: 256 }, (_, index) => ({
id: `o${index}`, observed: [index / 100, index % 3, 0.1],
predicted: [index / 100, index % 3, 0.1], size: [0.01, 0.01, 0.01],
uncertaintyM: 0.015, sourceReceipt: `fixture:${index}`,
})),
};
let calls = 0;
const result = { authority: 'none' };
const adapter = createVirtualViewAdapter({ enabled: true, broker: received => {
calls++;
assert.equal(received, scene);
return result;
} });
assert.equal(adapter.observe(scene), result);
assert.equal(calls, 1);
});
test('explicit immutable scene and signer are forwarded unchanged to trusted broker', () => {
const snapshot = Object.freeze({ schema: 'virtual-view.v1', frame: 'room_enu' });
const privateKey = Object.freeze({ researchSigner: true });
const result = Object.freeze({ decision: 'SKIP', authority: 'none', svg: null });
let calls = 0;
const adapter = createVirtualViewAdapter({ enabled: true, privateKey, broker: (received, options) => {
calls++;
assert.equal(received, snapshot);
assert.equal(options.privateKey, privateKey);
return result;
} });
assert.equal(adapter.observe(snapshot), result);
assert.equal(calls, 1);
});
test('full scene validation remains delegated and broker rejection propagates', () => {
const rejection = new TypeError('missing tracked scene geometry');
const adapter = createVirtualViewAdapter({ enabled: true, broker: () => { throw rejection; } });
assert.throws(() => adapter.observe({ schema: 'virtual-view.v1', frame: 'room_enu' }),
error => error === rejection);
});
test('enablement must be explicit and configuration types are checked', () => {
for (const configuration of [
{ broker: null }, { broker: () => {}, enabled: 'true' }, { broker: () => {}, enabled: 1 },
]) assert.throws(() => createVirtualViewAdapter(configuration), /adapter configuration/);
});