mirror of
https://github.com/ruvnet/RuView.git
synced 2026-09-01 21:15:56 +00:00
feat: implement ADR-292/293/294/295/296 — provenance, UDP hardening, multi-node, model gates, CSI policy
ADR-292 (sensing-server): SourceState enum + pure transition (provenance.rs); auth-error/unknown can never resolve to LiveVerified; synthetic exports watermarked. Pose-fusion simulator starts SYNTHETIC and only shows LIVE on a real decoded frame (#1557); sensing client no longer labels an unauthorized status endpoint as live (#1526). ADR-294 (sensing-server): NodeInference distinct from RoomInference (inference.rs); deterministic freshness-weighted fuse_room; RateLimiter re-keyed to (NodeId,EntityKind) so nodes do not starve each other (#1541); stale nodes go unavailable not frozen-online (#1555). ADR-293 (sensing-server): --udp-bind (default 127.0.0.1) + --udp-allow allowlist + fail-closed refusal of routable bind without allowlist unless --udp-insecure-lan (udp_bind.rs); crate SECURITY.md documents the threat model and the deferred per-device-auth step two. ADR-295 (train): model_gates.rs — constant-output, unreachable-boundary (the issue-1521 degenerate presence head), class-balance, baseline, and metric-name-provenance gates. ADR-296 (ci): scripts/csi-data-policy-check.sh (+ allowlist) and a workflow that fails on tracked CSI-format/oversized-JSONL files; 6/6 self-tests pass. Per-crate suites reported green by the swarm; CSI policy self-test 6/6 and JS syntax verified here. Full workspace re-verification deferred until the concurrent phase-1 spine build frees the target dir (disk pressure). Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_015TcKegTS7QqhWPC2L2SzaS
This commit is contained in:
@@ -55,11 +55,15 @@ export class CsiSimulator {
|
||||
this.ws = new WebSocket(url);
|
||||
this.ws.binaryType = 'arraybuffer';
|
||||
this.ws.onmessage = (evt) => this._handleLiveFrame(evt.data);
|
||||
this.ws.onopen = () => { this.mode = 'live'; resolve(true); };
|
||||
// ADR-292 (issue #1557): a socket that merely *opened* is NOT live —
|
||||
// synthetic demo data keeps flowing until a real frame is decoded. We
|
||||
// stay in demo mode (watermarked) on open; `_handleLiveFrame` flips to
|
||||
// live only once it has parsed a verified frame.
|
||||
this.ws.onopen = () => { this.socketOpen = true; resolve(true); };
|
||||
this.ws.onerror = () => resolve(false);
|
||||
this.ws.onclose = () => { this.mode = 'demo'; };
|
||||
this.ws.onclose = () => { this.mode = 'demo'; this.verifiedFrame = false; this.socketOpen = false; };
|
||||
// Timeout after 3s
|
||||
setTimeout(() => { if (this.mode !== 'live') resolve(false); }, 3000);
|
||||
setTimeout(() => { if (!this.socketOpen) resolve(false); }, 3000);
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
@@ -69,9 +73,12 @@ export class CsiSimulator {
|
||||
disconnect() {
|
||||
if (this.ws) { this.ws.close(); this.ws = null; }
|
||||
this.mode = 'demo';
|
||||
this.verifiedFrame = false;
|
||||
this.socketOpen = false;
|
||||
}
|
||||
|
||||
get isLive() { return this.mode === 'live'; }
|
||||
/** True only once a real frame has been decoded — not merely on socket open. */
|
||||
get isLive() { return this.mode === 'live' && this.verifiedFrame === true; }
|
||||
|
||||
/**
|
||||
* Update person state from video detection (for correlated demo data).
|
||||
@@ -292,6 +299,15 @@ export class CsiSimulator {
|
||||
this._liveAmplitude[i] = Math.sqrt(real * real + imag * imag) / 2048;
|
||||
this._livePhase[i] = Math.atan2(imag, real);
|
||||
}
|
||||
// ADR-292 (issue #1557): a real frame was decoded — only now is this live.
|
||||
this._markVerifiedFrame();
|
||||
}
|
||||
|
||||
/** ADR-292: promote from watermarked demo to live once a real frame lands. */
|
||||
_markVerifiedFrame() {
|
||||
this.verifiedFrame = true;
|
||||
this.mode = 'live';
|
||||
if (typeof this.onVerifiedFrame === 'function') this.onVerifiedFrame();
|
||||
}
|
||||
|
||||
_handleJsonFrame(msg) {
|
||||
@@ -311,6 +327,8 @@ export class CsiSimulator {
|
||||
for (let i = 0; i < n; i++) {
|
||||
this._liveAmplitude[i] = Math.abs(ampArr[i]) * scale;
|
||||
}
|
||||
// ADR-292 (issue #1557): a real frame carrying amplitude was decoded.
|
||||
this._markVerifiedFrame();
|
||||
}
|
||||
|
||||
// Phase from node (if available)
|
||||
|
||||
@@ -151,12 +151,21 @@ function init() {
|
||||
if (wsUrlInput) wsUrlInput.value = defaultWsUrl;
|
||||
// ADR-272: exchange the stored bearer for a single-use ?ticket= before the
|
||||
// upgrade — a browser cannot set an Authorization header on a WebSocket.
|
||||
withWsTicket(defaultWsUrl).then(u => csiSimulator.connectLive(u)).then(ok => {
|
||||
if (ok && connectWsBtn) {
|
||||
// ADR-292 (issue #1557): opening the socket does NOT mean live — the
|
||||
// simulator keeps producing watermarked SYNTHETIC data until a real CSI frame
|
||||
// is decoded. Only the verified-frame callback promotes the label to LIVE.
|
||||
csiSimulator.onVerifiedFrame = () => {
|
||||
if (connectWsBtn) {
|
||||
connectWsBtn.textContent = '✓ Live ESP32';
|
||||
connectWsBtn.classList.add('active');
|
||||
statusLabel.textContent = 'LIVE CSI';
|
||||
statusDot.classList.remove('offline');
|
||||
}
|
||||
statusLabel.textContent = 'LIVE CSI';
|
||||
statusDot.classList.remove('offline');
|
||||
};
|
||||
withWsTicket(defaultWsUrl).then(u => csiSimulator.connectLive(u)).then(ok => {
|
||||
if (ok) {
|
||||
// Socket open, but no verified frame yet — stay honest.
|
||||
statusLabel.textContent = 'SYNTHETIC';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -304,25 +304,41 @@ class SensingService {
|
||||
* hardware or simulation. Called once on WebSocket open.
|
||||
*/
|
||||
async _detectServerSource() {
|
||||
// ADR-292 (issue #1526): an unreachable or unauthorized status endpoint is
|
||||
// an *unknown* state — it must NOT collapse to "live". Prefer the canonical
|
||||
// `source_state` the server now returns; on any error stay conservative
|
||||
// (server-simulated) until a real frame's `source` field promotes us.
|
||||
try {
|
||||
const resp = await fetch('/api/v1/status');
|
||||
if (resp.ok) {
|
||||
const json = await resp.json();
|
||||
this._applyServerSource(json.source);
|
||||
this._applyServerSource(json.source, json.source_state);
|
||||
} else {
|
||||
// Can't reach status endpoint — assume live until first frame tells us
|
||||
this._setDataSource('live');
|
||||
this._setDataSource('server-simulated');
|
||||
}
|
||||
} catch {
|
||||
this._setDataSource('live');
|
||||
this._setDataSource('server-simulated');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a raw server source string to the UI data-source label.
|
||||
* Map a raw server source string (and optional canonical ADR-292
|
||||
* `source_state`) to the UI data-source label.
|
||||
*/
|
||||
_applyServerSource(rawSource) {
|
||||
_applyServerSource(rawSource, sourceState) {
|
||||
this._serverSource = rawSource;
|
||||
// ADR-292: only the verified/unverified live states may show "live"; any
|
||||
// synthetic/stale/disconnected state must not.
|
||||
if (sourceState) {
|
||||
if (sourceState === 'live_verified' || sourceState === 'live_unverified') {
|
||||
this._setDataSource('live');
|
||||
} else if (sourceState === 'synthetic') {
|
||||
this._setDataSource('server-simulated');
|
||||
} else {
|
||||
this._setDataSource('server-simulated');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rawSource === 'esp32' || rawSource === 'wifi' || rawSource === 'live') {
|
||||
this._setDataSource('live');
|
||||
} else if (rawSource === 'simulated' || rawSource === 'simulate') {
|
||||
|
||||
Reference in New Issue
Block a user