mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
Merge pull request #2189 from ruvnet/prototype/uncertainty-virtual-view
Add opt-in Dream Machine virtual-view research adapter
This commit is contained in:
4 files changed
+281
No files matched your search
@@ -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:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Virtual-view broker adapter (research prototype)
|
||||
|
||||
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';
|
||||
import { createVirtualViewAdapter } from './adapter.mjs';
|
||||
// Example in-memory research identity only; use a trusted host signer in deployment.
|
||||
const { privateKey } = generateKeyPairSync('ed25519');
|
||||
const adapter = createVirtualViewAdapter({ broker, privateKey, enabled: true });
|
||||
// adapter.observe(calibratedSnapshot) returns RENDER, SKIP or BLOCK plus authority:none.
|
||||
```
|
||||
|
||||
The example assumes sibling RuView and Dream Machine clones; adjust the import
|
||||
from the executing module's location. Pass calibrated `virtual-view.v1` scene
|
||||
snapshots in room-local ENU metres. See Dream Machine ADR-0108 for every field,
|
||||
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:
|
||||
node scripts/bench-virtual-view.mjs ../ruview/integrations/virtual-view/adapter.mjs
|
||||
```
|
||||
|
||||
The cross-repository benchmark exercises this adapter with the real broker,
|
||||
renders 256-object synthetic scenes, signs and replays receipts. It measures local
|
||||
CPU overhead only. Required robot evidence remains missing: +20 points on two of
|
||||
three held-out tasks, no regression beyond five points, added p95 below 150 ms,
|
||||
at least 95% pre-action drift detection, and replayable receipts for all trials.
|
||||
No receipt grants actuator authority. Do not wire this to live robot control.
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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.
|
||||
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 });
|
||||
},
|
||||
hardwareActuation: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import test from 'node:test';
|
||||
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'); } });
|
||||
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, 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/);
|
||||
});
|
||||
Reference in new issue
Block a user