a11oy / packages /perception-loop /src /__tests__ /privacy.test.ts
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame Contribute Delete
2.42 kB
import { describe, expect, it } from 'vitest';
import { detect, type DetectorAdapter } from '../pipeline.js';
/**
* Privacy invariant: the perception envelope is the only thing that
* leaves the device-local in-memory pipeline. Raw frame bytes (the
* adapter-opaque `Frame.payload`) must NEVER appear in the serialised
* envelope — not under `consumerArtifact`, not under per-head
* detections, not under liveness reasons.
*
* Re-expressed from the privacy contract in
* docs/research/perception-bio-synthesis-2026.md §1 and reinforced by
* the api-server `/perception/verify` rule that the server only ever
* accepts feature vectors, never frames.
*/
const SECRET_FRAME_BYTES = new Uint8Array(64);
for (let i = 0; i < SECRET_FRAME_BYTES.length; i++) {
SECRET_FRAME_BYTES[i] = 0xab; // distinctive byte so a search hit is unambiguous
}
const SECRET_TAG = 'super-secret-frame-payload-do-not-leak-2026';
describe('perception-loop privacy invariant', () => {
it('never serialises raw frame bytes or payload tokens into the envelope', async () => {
const frame = {
frameHash: 'test-frame-hash',
tMs: 0,
// Payload carries both binary bytes and a unique searchable tag;
// either appearing in the envelope JSON is a privacy regression.
payload: { bytes: SECRET_FRAME_BYTES, tag: SECRET_TAG },
};
const adapter: DetectorAdapter = {
head: 'face',
costMs: 1,
detect: async () => [
{
score: 0.9,
box: [0.1, 0.1, 0.2, 0.2],
modelVersion: 'mock@0',
},
],
livenessSignal: (f) => ({ tMs: f.tMs, irisMotion: 0.02, eyeAperture: 0.9, headPoseDelta: 0.0 }),
};
const envelope = await detect(frame, [adapter], {
budgetMs: 100,
consumerArtifact: 'a11oy-approvals-center',
});
const serialised = JSON.stringify(envelope);
expect(serialised).not.toContain(SECRET_TAG);
// 0xab repeated → if bytes leaked through any toJSON pathway they would
// appear as a long run of `171` decimal integers.
expect(serialised.includes('171,171,171,171,171')).toBe(false);
expect(serialised).not.toMatch(/payload/i);
// The feature-vector summary is what's allowed to leave the device.
expect(envelope.frameHash).toBe('test-frame-hash');
expect(envelope.liveness).toBeDefined();
expect(envelope.detectionsSummary.counts.face).toBe(1);
});
});