An interview-integrity platform that turns behavioural telemetry into an explainable score and a list of flags, never a verdict.
Show the evidence. Leave the decision to a person.
Get started · Evidence fusion · Architecture · Key results · Limitations · Roadmap
Remote interviews are easy to game: a phone held below the frame, a second face leaning into view, a tab switch, an answer pasted from another window. Hiring Guard watches for those signals in the candidate's browser during a live interview and reports what it saw. It produces an integrity score and a list of flags. It does not decide anything. A flag is a reason for a reviewer to look, and the score is built from the same evidence.
The system has three parts. A Next.js frontend serves the interviewer, the reviewer and the candidate. An Express and Prisma backend runs the live session, the evidence pipeline and the reports. A Python package, vtml, is the lab where the fusion model is specified, tested and calibrated. The lab and the live backend implement the same model twice, and this README lists where they differ.
Every fusion result below comes from synthetic data, and no number here is an accuracy figure (see Key results). Three things the system has slots for do not exist: audio analysis, identity matching and recording. Limitations covers them first.
The interface has no red token. The palette is six colours defined in frontend/tailwind.config.ts: sand, clay, sage, amber, terra and slate. The danger colour is terra, a burnt clay. The integrity gauge reads sage at 85 and above, amber from 70 to 84, terra below 70, and sand while a session is still calibrating. Those thresholds are hard-coded in the gauge component, so the band is a display choice and does not come from the engine.
The interface draws a period with no usable signal as sand with a hatch, never as a low score. Flag severity uses a dot shape (solid, centred or hollow) plus a text label, so colour is never the only way to tell a high flag from a low one.
- Live sessions. Candidates join over Socket.IO. In the candidate's browser, MediaPipe runs a face landmarker (face count, absence, gaze) and an object detector (phone, book, laptop or remote in view). A monitor checks that the shared screen stays a whole monitor. The page also reports focus, paste, pointer, typing-rhythm, display and network events. The camera and screen analysis runs in the browser, and only its results reach the server.
- Interview call. Interviewer and candidate talk over WebRTC. Signalling runs over Socket.IO, and the media flows between the two browsers, through a TURN relay when one is configured. The API server does not receive it.
- Speech-to-text. Each browser transcribes its own microphone with the Web Speech API (Chrome and Edge, English only). In those browsers the vendor's cloud recogniser receives the audio. The server stores the text, labels the speaker by the socket it arrived on, and pairs interviewer questions with candidate answers for grading.
- Explainable flags. Every flag lists the observations behind it.
- Reviewer adjudication. Reviewers can dismiss or downgrade a flag. In the lab, dismissing a flag removes only that flag's evidence and the score recomputes from the rest. In the backend, the rescore step at report time drops a dismissed flag's evidence and halves a downgraded flag's.
- Neutral candidate warnings. Candidates see templated, non-accusatory warnings. They never see a score or the detail of a flag.
- Interview kit. Coding tasks and a question bank. Candidate code runs in a per-run Docker container with no network, a read-only root filesystem, 256 MB of memory, one CPU and 128 processes (Python and JavaScript only). That sandbox is opt-in: the default provider is
mock, which runs no code and passes every test on a non-empty submission. - LLM assist. Job-description parsing, question suggestions and answer grading go through an LLM provider. The default is
mock, a keyword and length heuristic. Thegroqprovider is real and needsGROQ_API_KEY, and the deploy example setsLLM_PROVIDER=groq. - Reports. A signed evidence report per interview (Ed25519). HTML is always produced and PDF is an option.
- Calibration lab. Golden fixtures, a prior-sensitivity sweep and a fitted weights artifact that the backend can adopt.
Each observation carries a log-likelihood ratio (LLR). LLRs accumulate per channel, and a signal that a second channel confirms inside a short window counts for more. The score falls as accumulated evidence rises.
The model exists twice. The lab in ml/src/vtml is the specification: it has the clamping, duration scaling and damping, and it computes exact leave-one-out flag deltas. The live scorer in backend/src/live is the running system and is simpler in several places.
These rows come from the demo replay (python -m vtml.replay --speed 0), the engine config and a minimal two-observation session. They do not come from the design documents.
| Situation | Lab behaviour | Result |
|---|---|---|
| First 60 seconds | Observe-only window. Background noise feeds the baseline and nothing is scored. | Score reads 97.13, clear, when calibration ends |
| A single gaze glance | Accumulates, raises no flag. | 96.52, clear, no flag |
| Gaze and scene agree within 6 s | The boost fires and a flag is emitted. | scene.multiple_faces, medium, "corroborated by gaze", 34.40, delta -56.43 |
| Repeats on one channel | Same-channel damping of 0.6. | Repeats add less than the first hit |
| Reviewer dismisses the flag | Only that flag's evidence is removed. | 90.83, not back to 96.52 |
| A channel goes dark | An unscored window opens. Absence is never evidence. | 93.25 before and after the gap |
| A dropped connection | A network.telemetry_gap 2 s before a 4 s scene.face_absent cannot corroborate it. |
93.5889 with and without the gap, cost 0.0 points, no flag |
Before the corroboration fix, the same two observations scored 90.9148 and raised a flag narrated "corroborated by network". An honest candidate's dropped connection cost 2.674 points and manufactured a flag. A channel with zero weight, which network has by design, can no longer corroborate another channel. test_zero_weight_channel_cannot_corroborate in ml/tests/test_corroboration.py pins the rule, and test_dropped_connection_costs_the_honest_session_nothing in ml/tests/test_replay_golden.py checks that removing the noisy fixture's real telemetry gaps leaves its score unchanged. This is a lab result. The live scorer does not have the rule (see Limitations).
Lab (ml/src/vtml) |
Live (backend/src/live) |
|
|---|---|---|
| Channels | 6: gaze, audio, scene, focus, input, network | 11, of which audio and identity never receive an observation |
| Score | 100 / (1 + exp(1.6 (S - m))), m = 3.2 / 2.2 / 1.5 |
min(100, 200 / (1 + exp(S / sigma))), sigma = 6 / 4 / 3 |
| Sensitivity tiers | LENIENT / STANDARD / STRICT | LOW / STANDARD / HIGH |
| LLR handling | Clamped to [-1, 4], log-saturating duration scaling, same-channel damping 0.6 | No clamp, no scaling, no damping |
| Clean-behaviour evidence | None | 3 of 19 LLR rows (focus resume -0.4, pointer return -0.2, rhythm normal -0.3), accumulator floor -2 |
| Baselines | Gaze and rhythm | Rhythm only (KS test) |
| Corroboration | A zero-weight channel cannot corroborate | Any channel with a positive observation inside the window corroborates, including a network anomaly |
| Flag trigger | One boosted LLR of 0.8 or more | A channel accumulator crosses its threshold upward (3 for STANDARD; 2.5 for paste and identity; 4 for pointer and environment) |
| Flag delta | Exact leave-one-out | Single-observation before and after; merged flags sum |
| Calibration window | Observe-only, nothing accumulates | Evidence accumulates; only flags and warnings are held back |
Both share a 6 s corroboration window, a boost of 0.45 per extra channel, a cap of 2.35 and a 15 s flag merge window.
The calibration-window row is a known divergence. On the honest_seed7 fixture the lab scores 94.28 with the window observe-only and 93.11 when its evidence accumulates, so the leak costs 1.17 points there. The live size is unmeasured.
Seven tests in ml/tests/test_ethics.py hold the model to its promises:
- No boolean field on a result or a flag has a verdict-style name (cheat, verdict, guilty, pass, fail).
- Absence is never evidence: suppressing a channel for a stretch never lowers the score, and the gap appears as an unscored window.
- A flag's score delta can be rebuilt to within 1e-9 by replaying the observations up to that flag and removing its contribution.
- Narrative templates contain no banned word.
- No text enters the engine: an observation with a string feature, such as a transcript, fails validation.
- Every result carries its
weights_version, itscalibrationstatus and itsunscoredwindows. - Dismissing any subset of flags can only raise the score.
The backend schema has no verdict-like field either. In the composite step (composite-score.step.ts), an integrity score below 70 sets the composite to null and marks the interview as needing review. From 85 up the integrity modifier is full marks, and from 70 to 85 it rises in proportion to the distance above 70. The step also returns null with a review flag when an input is missing (see Limitations).
ml/ is the offline lab and backend/src/live/ is the only live scorer. There is no network integration between them. One JSON file crosses at startup: the lab fits LLR magnitudes and writes ml/weights/weights.json, and backend/src/config/calibrated-weights.ts reads it. The switch is CALIBRATED_WEIGHTS_ENABLED, which defaults to false, and the deploy Compose file pins it to false.
The current artifact is phase3-synthetic-1, with dataset.kind set to synthetic: 1,025 rows from 30 synthetic sessions (10 honest, 20 staged), 80 positives. Four detector types are fitted and twelve stay on priors, each with a recorded reason ("not present in the calibration set" for ten, "only 0 positives, needs 15" for two).
At STANDARD sensitivity, the backend adopts four values:
| Backend detector | Hand-set | Adopted | Lab prior for the fitted type |
|---|---|---|---|
focus_loss |
1.20 | 1.042 | 1.10 |
gaze_away |
1.10 | 1.295 | 1.40 |
multiple_faces |
1.70 | 1.207 | 2.00 |
paste_large |
1.60 | 1.509 | 1.80 |
Three rules govern the seam:
- Only per-detector LLR magnitudes cross. The backend takes each fitted curve's value at its operating point, and the artifact's
channel_weightsare parsed away by the backend's zod schema and never used. - A backend type is adopted only when exactly one fitted lab type maps to it in contracts/detector-registry.json. Three lab gaze types map to
gaze_awayand one of them is fitted, sogaze_awayis adopted. - With the flag on, a missing, invalid or newer-schema artifact stops the API at startup with a message that names the resolved path. The API never falls back to the hand-set table in that state. The report builds at boot, so the log line that says the weights are "fitted on SYNTHETIC fixtures" appears then. Nothing surfaces the calibration provenance in
/readyor in reports.
LOW and HIGH keep their hand-set ratios. Adopting the fit raises gaze_away from 1.10 to 1.295, while in the lab the same fit lowered it from a prior of 1.40. The two implementations disagree on direction there, which is the reconciliation gap listed in the Roadmap.
flowchart LR
C[Candidate browser<br/>MediaPipe, screen monitor, focus, paste, pointer, network, speech-to-text] -- Socket.IO --> L
subgraph B[Backend]
L[Live layer<br/>ingest, detectors, fusion, warden]
P[Pipeline and workers<br/>BullMQ, grading, reports, evidence signing]
L --> P
end
L -- neutral warnings --> C
R[Interviewer and reviewer UI<br/>speech-to-text] -- REST and Socket.IO --> B
C -. WebRTC video call .- R
B --- D[(Postgres 17)]
B --- Q[(Redis 7)]
B -. Docker socket .-> S[Sandbox containers<br/>per run, no network]
B -. optional .-> G[Groq LLM API]
ML[vtml lab<br/>goldens, sweep, calibration] -- weights.json --> L
K[[contracts/detector-registry.json]] -.-> L
K -.-> ML
hiring-guard/
├── frontend/ Next.js app: interviewer, reviewer and candidate views, in-browser CV, speech-to-text
├── backend/ Express API, live session runtime, pipeline, workers, Prisma schema
│ └── src/live/ ingest, detectors, fusion, warden, session runtime
├── ml/ vtml: fusion model, fixtures, sweep, calibration, ethics tests
│ └── weights/ fitted artifact read by the backend
├── contracts/ detector-registry.json shared by both sides
├── deploy/ production Compose file: Caddy, coturn, frontend, API, worker, Postgres, Redis
└── docs/ deployment guide, cross-component architecture and audits
| Layer | Tools |
|---|---|
| Frontend | Next.js ^16.3, React ^18.3, Tailwind ^3.4, Zod ^3.23, TypeScript ^5.6, Monaco editor, @mediapipe/tasks-vision |
| Backend | Express ^5.2, Prisma ^7.10 with the pg adapter, Zod ^4.6, Socket.IO ^4.8, BullMQ ^6.3, ioredis ^6, argon2, jose, Eta ^4.6, dockerode, Puppeteer (optional), TypeScript ^7.0, Vitest ^5 |
| Browser APIs | WebRTC, Web Speech API (Chrome and Edge), MediaPipe face landmarker and object detector |
| Evidence | Ed25519 signing of evidence reports |
| Data | Postgres 17, Redis 7, Mailpit for local email |
| Optional services | Groq API for the LLM provider, Docker for the code sandbox |
| Deploy | Docker Compose with Caddy 2.10 and coturn 4.6 |
| Lab | Python 3.11 or newer with pydantic 2.5 or newer and numpy 1.26 or newer at runtime. Development adds pytest, pytest-benchmark, scikit-learn, pandas, matplotlib, mediapipe, opencv-headless and mypy. |
You need Node 22 or newer, Python 3.11 or newer (developed on 3.12), and Docker with Compose.
git clone https://github.com/YashGupta2116/hiring-guard.git
cd hiring-guardcd backend
docker compose up -d
npm install
cp .env.example .env
npm run keys:generatekeys:generate prints six lines: EVIDENCE_SIGNING_PRIVATE_KEY, EVIDENCE_SIGNING_KEY_ID, HASH_PEPPER, JWT_ACCESS_SECRET, JOIN_TOKEN_SECRET and CANDIDATE_TOKEN_SECRET. Paste them into .env. It does not print INTERNAL_SERVICE_TOKEN, and .env.example leaves it empty. The API exits with "Invalid environment variables" until you set it to a value of at least 32 characters:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Then generate the client, apply the migrations and seed the database:
npm run db:generate
npm run db:deploy
npm run db:seedStart the API on port 9000 and, in a second terminal, the worker:
npm run devnpm run workerCheck that it is up:
curl http://localhost:9000/api/v1/readyThe response is {"data":{"db":"ok","redis":"ok"}}. The seed creates a demo organisation with two logins, owner@demo.veritrust.local and interviewer@demo.veritrust.local, both with the password demo-password-1234. It also creates three coding tasks and 20 question-bank items. The coding tasks carry placeholder tests and the questions are numbered demo text.
Compose starts Postgres, Redis and Mailpit (SMTP on 1025, web UI on 8025). The api, worker and migrate services only start under --profile app.
The LLM provider, the sandbox provider and the calibrated weights are off by default. To switch them on, set these in backend/.env:
LLM_PROVIDER=groqwith aGROQ_API_KEY.SANDBOX_PROVIDER=docker. The API host needs a Docker daemon, and the first run pullspython:3.12-alpineandnode:20-alpine.CALIBRATED_WEIGHTS_ENABLED=trueto adopt the fitted artifact (see The artifact seam).
cd frontend
npm install
npm run devThe app runs on port 3000 and calls http://localhost:9000/api/v1 by default. Set NEXT_PUBLIC_API_URL to change that.
cd ml
python -m venv .venv
source .venv/bin/activate # Windows Git Bash: source .venv/Scripts/activate
pip install -e ".[dev]"
pytest
mypy
python -m vtml.replay --speed 0The last command replays the demo session shown in Evidence fusion.
scikit-learn's _pairwise_fast DLL is blocked by Windows Application Control on some machines, which makes test_calibrate.py uncollectable there.
cd backend && npm run typecheck
cd frontend && npm run typecheck && npm run build && npm run lintnpm run lint exits 1 today because of nine open errors (see Limitations).
The backend tests need Postgres and Redis running and a migrated test database. Compose creates an empty veritrust_test database when it first initialises the Postgres volume, and nothing migrates it, so point Prisma at it first:
cd backend
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/veritrust_test" npm run db:deploy
npm testdeploy/docker-compose.yml runs Caddy, the frontend, coturn, the API, the worker, Postgres and Redis on one host, and docs/AWS_LIGHTSAIL_DEPLOYMENT.md walks through a Lightsail server. Copy deploy/.env.production.example to deploy/.env.production and fill it in. Three host prerequisites are easy to miss:
-
Sandbox temp directory. The API container runs as uid 1001 and hands this directory to the host Docker daemon as a bind-mount source, so it must exist at the same path on the host and be writable by that uid. Compose mounts it whether or not
SANDBOX_PROVIDERisdocker.sudo mkdir -p /var/lib/veritrust-sandbox-tmp sudo chown 1001:1001 /var/lib/veritrust-sandbox-tmp
-
Firewall. coturn needs UDP 3478 and UDP 49160 to 49200 in addition to TCP 80 and 443. The Lightsail guide opens only TCP 80 and 443.
-
Docker group. Compose requires
DOCKER_GID(getent group docker | cut -d: -f3) so the API container can reach the host Docker socket.
All counts below were measured on 2026-09-20 at commit 8103746, by running the commands in Getting started. All lab data is synthetic.
| Check | Result |
|---|---|
ml: pytest |
167 passed |
ml: mypy (strict) |
No issues in 34 source files |
backend: typecheck |
Clean |
frontend: typecheck |
Clean |
frontend: production build |
Succeeds |
frontend: ESLint |
9 errors, 12 warnings (see Limitations) |
The backend test suite needs Postgres and Redis and has no count here.
| Fixture | Score | Band | Flags |
|---|---|---|---|
honest_clean |
94.61 | clear | 0 |
honest_noisy_camera |
89.29 | clear | 0 |
staged_phone_and_glance |
11.99 | suppressed | 3, all medium |
These are scored on the hand-set priors. On the fitted artifact the honest sessions do not change and the staged session moves to 17.01, still suppressed with 3 flags. The fit moved the staged score toward less suspicion.
python -m vtml.evaluate.priors_sweep scales each channel's priors from 0.25x to 4x in nine steps of square root of two, which gives 45 points across five channels. The nine audio points exercise nothing, because no fixture emits an audio observation. Of the other 36 points, 35 keep both verdicts: the honest fixture raises no flags and scores clear, and the staged fixture stays suppressed.
- Gaze, scene and input hold both verdicts over the full range.
- Focus holds from 0.25x to 2.83x. At 2.83x the honest session scores 87.60 and is still clear.
- Focus breaks at 4x. The honest score falls from 94.28 to 80.40, the band changes from clear to review, and no flag fires. Focus is the narrowest stable channel.
Every fixture is synthetic and generated for this project. Agreement between them shows that the model is consistent with itself and that its promises hold. It does not measure how often it flags a real cheater or a real honest candidate. No false-positive rate, no detection rate, no precision, no recall and no accuracy figure exists for this system, and none should be inferred from the tables above.
We declined to manufacture an accuracy number without real recorded sessions. We tested prior robustness instead: within the ranges above, the fixture verdicts do not depend on the hand-set priors, and the one exception is named.
Why a score and flags instead of a pass or fail verdict? A behavioural signal is evidence, not proof. Glances, dropped connections and noisy cameras happen to honest people. A verdict would hide that uncertainty, and a score with traceable flags exposes it.
Why require corroboration across channels? One weak signal is common in honest sessions. Two channels agreeing inside a 6 s window is stronger evidence, so the boost goes to agreement and not to volume.
Why is absence never evidence? A dropped camera says nothing about the candidate. When a channel goes dark, both scorers open an unscored window and add no evidence from that channel while it lasts, and a lab test enforces the rule.
Why exact leave-one-out deltas in the lab? A reviewer reading "this flag cost 56.43 points" needs that to be true. Dismissing the flag changes the score by that amount. The deltas belong to individual flags and do not sum to the total loss.
Why keep a lab beside the backend? The model is easier to test, sweep and fit in a small pure-Python package than inside a live system with sockets and queues. The cost is two implementations, which the Roadmap is meant to close.
Why ship a fitted artifact from synthetic data? It tests the seam end to end and gives the backend a place to read real numbers once recorded sessions exist. An integration test in backend/tests/integration/calibrated-weights.test.ts drives a real session over the candidate socket and the internal producer API. It asserts that the stored LLRs are the calibrated ones and that four focus_loss events give one flag with a positive score delta.
Each item was checked against the code at commit 8103746.
- Audio. No component emits an audio observation. The engine still carries the slot. The lab weights the audio channel at 1.25, the highest in its table. The backend gives
AUDIOa weight of 1.0, a 300 s decay and asecond_voiceLLR row, and the fitted artifact listsaudio.second_voiceon a prior of 2.2. None of that has ever changed a score. Speech-to-text produces transcript text, which is not an audio observation. The system does not detect a second voice, a whispered prompt or an off-camera helper. - Identity verification. Nothing matches the candidate's face to a reference ID. The backend has an
IDENTITYchannel (weight 1.2, flag threshold 2.5) with no detector type and no LLR row behind it, so it cannot score anything. - Recording. The media provider is a mock (
MEDIA_PROVIDERaccepts onlymock). It cannot record, and its track check returns true for camera, microphone, screen and whole-monitor without looking. No video or audio is stored. When the provider cannot record, noRecordingrow is created and the session pages show "Not available". The seal marks a recordingREADYonly when the provider returns an artifact, and a provider that returns none endsFAILED. - What the consent screen says. Candidates see a bullet for each channel that has a detector. The screen also says that the interviewer can see and hear them live, that no video or audio is stored, that camera and screen checks run in their browser, that speech becomes text (with audio sent to Google or Microsoft in Chrome and Edge) and the transcript is saved instead of the audio, that code runs in a sandbox, and that evidence is kept for up to 90 days. It no longer mentions identity matching, extra-voice detection or recording.
- A restarted API process stops monitoring without saying so. The live runtime is an in-memory object, created only when the interviewer starts a session, and nothing recreates it at boot. After a restart, which includes a normal redeploy, the socket layer accepts telemetry, camera, editor and heartbeat events and discards them. Transcript batches fail with a logged error. The 1 s tick, the duration limit, the candidate-abandon timer, the producer-health check and the score snapshots are gone, so the duration limit no longer ends the session. The session stays
LIVEin Postgres, and nothing writes an unscored window for the gap. - The composite score needs a graded question and answer pair. A composite exists only with at least one graded pair, integrity of at least 70 and a successful rescore. A pair needs a final interviewer segment followed by a final candidate segment. Since
37c8279the browsers post these through the Web Speech API, which means Chrome or Edge on both ends, English only, with segment times estimated as a fixed 3 s window. Without a pair the composite is null and the interview is marked for review. With the defaultmockLLM, grades come from a word-count and phrase heuristic, so the composite then measures answer length. - Two composite sub-formulas were invented during development.
technicalaverages correctness, depth and hands-on per answer and blends that 50/50 with the code pass rate.communicationaverages structure and specificity. The architecture document names the grade dimensions only for communication and gives no blend weight. Nothing confirms either formula against a spec, and the decision log inbackend/docs/Memory.mdrecords them as inventions. llrsits outside the evidence hash. Each observation's hash covers its session, sequence number, source, channel, type, timestamp and payload. It does not coverllrorclientTs. The rescore readsllr, so editing it in Postgres changes the score and chain verification still passes.- The chain head lives only in Redis. The last sequence number and hash sit in
s:<sessionId>:chain. If that key is lost mid-session, the next write restarts at sequence 1, the unique index on(sessionId, seq)rejects it, and every later observation in the session is dropped with a log line. The seal then verifies the rows Postgres still holds and signs that shorter chain as valid. - On the live scorer, a dropped connection still costs an honest candidate about 11.7 points. A
face_absentalone scores 82.68. With anetwork_anomaly2 s earlier it scores 71.02 (-11.66), because the environment channel counts as a corroborator. The frontend reports a network event on the browser'sonlineandofflineevents, and the environment detector turns an offline event intonetwork_anomaly. That a real socket drop takes this path is inferred from the code and untested. A telemetry sequence gap is different: it opens an unscored window and adds no evidence. - Lockdown ends the interview. Leaving full screen or hiding the tab ends it at once, and losing window focus for 1.5 s ends it too, with no warning first. A notification or another app that takes focus can end an honest candidate's session.
- SCENE and SCREEN have no health heartbeat. The browser heartbeat covers FACE and GAZE only, and its status follows the face landmarker. The object detector loads inside a try block that swallows failure, so a silent load failure still shows SCENE as monitored. SCREEN has no heartbeat at all, so nothing reports if its monitor stops running.
- The calibration window diverges between lab and live (see Lab and live compared).
- The noisy-camera golden is fragile. It has 6 sub-second face dropouts in 5 minutes and scores 89.29. On the same seed, 7 dropouts score 86.18 and 8 score 84.18, which moves the session to review with no flags. If a real webcam is noisier than that, the fix belongs in the scene channel.
- The consent screen understates retention. It says evidence is kept for up to 90 days. The defaults keep media for 90 days, observations for 180 and reports for 1,095. The schema also still defaults
recordVideo,recordAudioandrecordScreento true. The scheduler now sends false, and nothing records either way.
- The API container mounts the host Docker socket.
deploy/docker-compose.ymlmounts/var/run/docker.sockinto the API container and adds the host's Docker group, which is root-equivalent on the host if the API is compromised. The sandbox containers set no network, a read-only root filesystem and memory, CPU and process caps. They set no capability drop, nono-new-privilegesand no non-root user. - TURN credentials are in the public JavaScript bundle. The frontend image is built with
NEXT_PUBLIC_TURN_USERNAMEandNEXT_PUBLIC_TURN_CREDENTIAL, so anyone who loads the app can read the long-term coturn credential and relay traffic through the server. - The deployment guide opens the wrong ports for coturn. It allows TCP 80 and 443, and coturn needs UDP (see Deploy).
SANDBOX_PROVIDER=localhas no isolation. It runs candidate code on the API host. The API refuses to start with it whenNODE_ENV=production.
- All calibration data is synthetic. The artifact says so in
dataset.kind. It holds 30 generated sessions and no recorded one. - Only 4 of 16 detector types are fitted. The other 12 stay on priors.
- The calibrated path has never run on recorded behaviour. It is off by default, and one integration test exercises it on synthetic curves.
- No accuracy, precision, recall or false-positive figure exists for this system. See What the numbers do not say.
- Nine frontend lint errors are open. All are
react-hooksrules, sevenset-state-in-effectand tworefs, across seven files. TypeScript is clean. - Other documents have drifted.
backend/README.mdandbackend/docs/REMAINING_WORK.mdstill call the frontend mock-data driven, which is wrong: the frontend calls the API and the sockets.frontend/README.mdis an unrelated leftover.docs/AWS_LIGHTSAIL_DEPLOYMENT.mdsays TURN is not included, and the Compose file now runs coturn.
- Checkpoint the live runtime so a restart resumes monitoring, or at least writes an unscored window for the gap.
- Cover
llrandclientTswith the evidence hash, and rebuild the chain head from Postgres so a lost Redis key cannot truncate a chain. - Port the zero-weight network corroboration rule to the live scorer. It removes the 11.66-point penalty.
- Add SCENE and SCREEN heartbeats, and warn candidates before lockdown ends an interview.
- Build audio, identity and recording, or remove them from the engine, the weights and the schema.
- Harden the sandbox with dropped capabilities,
no-new-privilegesand a non-root user, and stop giving the API container the host Docker socket. Replace the baked-in TURN credential with short-lived credentials. - Reconcile backend evidence strength with lab confidence. This is the largest scoring gap: the backend's per-detector strengths and the lab's fitted values disagree in direction for
gaze_away. - Guard the calibration window in
session-runtime.tsandintegrity-rescore.step.ts. - Confirm the two composite sub-formulas against a written spec, or replace them.
- Record 20 real sessions and refit with
--dataset-kind recorded. - Point
evaluate/at the goldens and the artifact. Its report is still titled for the phase 1 priors. - Settle the asymmetric [-1, 4] LLR clamp.
- Port the lab's clamp, duration scaling and baselines to the live scorer, with approval.
- Add a real media provider that records, and an S3 storage provider.
No license file is included in this repository yet.
Yash Gupta