Auditor drill: OpenRouter lane sweep + warren Gitea repoint (post-PR11 polish) #14

Open
profit wants to merge 65 commits from demo/post-pr11-polish-2026-04-28 into main
2 changed files with 478 additions and 0 deletions
Showing only changes of commit 7f0f500050 - Show all commits
+467
View File
@@ -0,0 +1,467 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lakehouse — Biometric Consent Intake</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',-apple-system,system-ui,sans-serif;background:#090c10;color:#b0b8c4;font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}
a{color:#58a6ff;text-decoration:none}
a:hover{color:#79c0ff}
.bar{background:#0d1117;padding:0 24px;height:56px;border-bottom:1px solid #171d27;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:10}
.bar h1{font-size:14px;font-weight:600;color:#e6edf3;letter-spacing:-0.2px}
.bar .cid{font-size:12px;color:#545d68;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
.bar .step{font-size:11px;color:#7d8590;text-transform:uppercase;letter-spacing:1px}
.wrap{max-width:760px;margin:0 auto;padding:28px 20px 60px}
.screen{display:none}
.screen.active{display:block}
h2{font-size:20px;color:#e6edf3;font-weight:600;margin-bottom:8px;letter-spacing:-0.3px}
h3{font-size:14px;color:#e6edf3;font-weight:600;margin:16px 0 8px}
.lede{color:#7d8590;font-size:13px;margin-bottom:24px}
.card{background:#0d1117;border:1px solid #171d27;padding:20px;margin-bottom:16px}
.card.warn{border-left:3px solid #d29922}
.card.bad{border-left:3px solid #f85149}
.card.good{border-left:3px solid #3fb950}
label{display:block;margin-bottom:12px;color:#7d8590;font-size:12px;text-transform:uppercase;letter-spacing:0.5px}
input[type=text],input[type=password]{width:100%;background:#0d1117;border:1px solid #2d333b;color:#e6edf3;padding:10px 12px;font-family:inherit;font-size:14px;border-radius:0;margin-top:6px;transition:border-color .15s}
input[type=text]:focus,input[type=password]:focus{outline:none;border-color:#58a6ff}
input[type=file]{color:#7d8590;font-size:12px;margin-top:8px}
input[type=checkbox]{margin-right:8px;transform:scale(1.2)}
.checkbox-row{display:flex;align-items:flex-start;color:#e6edf3;font-size:14px;margin:16px 0;cursor:pointer;text-transform:none;letter-spacing:0}
.checkbox-row input{margin-top:3px}
button{background:#1f6feb;color:#fff;border:none;padding:10px 20px;font-family:inherit;font-size:14px;font-weight:500;cursor:pointer;border-radius:0;transition:background .15s;margin-right:8px}
button:hover:not(:disabled){background:#388bfd}
button:disabled{background:#21262d;color:#545d68;cursor:not-allowed}
button.secondary{background:#21262d;color:#c9d1d9}
button.secondary:hover:not(:disabled){background:#30363d}
button.danger{background:#da3633}
button.danger:hover:not(:disabled){background:#f85149}
.consent-text{background:#0a0c10;border:1px solid #1f242c;padding:20px;max-height:340px;overflow-y:auto;font-size:13px;line-height:1.7;margin-bottom:16px}
.consent-text h4{color:#e6edf3;font-size:14px;font-weight:600;margin:14px 0 6px}
.consent-text h4:first-child{margin-top:0}
.consent-text p{margin-bottom:8px;color:#b0b8c4}
.consent-text ul{margin:6px 0 8px 20px;color:#b0b8c4}
.row{display:flex;gap:12px;align-items:center}
.row > *{flex:1}
.row > button{flex:0 0 auto}
.preview{max-width:280px;max-height:280px;display:block;margin:12px 0;border:2px solid #30363d}
video{max-width:280px;max-height:280px;display:block;margin:12px 0;border:2px solid #30363d;background:#000}
canvas{display:none}
.hash{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:11px;color:#7ee787;word-break:break-all;background:#0a0c10;padding:8px 12px;border:1px solid #1f242c;margin:6px 0}
.kv{display:grid;grid-template-columns:140px 1fr;gap:8px;margin:6px 0;font-size:13px}
.kv .k{color:#7d8590;text-transform:uppercase;font-size:11px;letter-spacing:0.5px;padding-top:2px}
.kv .v{color:#e6edf3;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:12px}
.error{background:#3a0f0f;border:1px solid #f85149;color:#ffa198;padding:12px 16px;margin:12px 0;font-size:13px;display:none}
.error.show{display:block}
.error code{color:#fff}
.foot{margin-top:32px;padding-top:16px;border-top:1px solid #171d27;font-size:11px;color:#545d68;text-transform:uppercase;letter-spacing:0.5px}
.spec-link{font-size:11px;color:#545d68;letter-spacing:0.4px;text-transform:uppercase}
.spec-link a{color:#58a6ff}
</style>
</head>
<body>
<div class="bar">
<h1>⚡ Biometric Consent Intake</h1>
<span class="step" id="step-label">step 1 of 4</span>
<span class="cid" id="cid-display">…</span>
</div>
<div class="wrap">
<!-- Step 1: operator auth -->
<div id="screen-token" class="screen active">
<h2>Operator authentication</h2>
<p class="lede">Paste the legal-tier audit token. Stored in this tab's session only; cleared on close. Never persists to disk.</p>
<div class="card">
<label for="token-input">Legal audit token</label>
<input type="password" id="token-input" placeholder="44-char token from /etc/lakehouse/legal_audit.token" autocomplete="off">
<div style="margin-top:14px">
<button id="token-submit">Continue →</button>
</div>
</div>
<div class="error" id="token-error"></div>
</div>
<!-- Step 2: consent -->
<div id="screen-consent" class="screen">
<h2>Biometric Information Consent — v1</h2>
<p class="lede">Please read the disclosures below. Sign by checking the box and entering your printed name. The candidate-facing surface required by BIPA §15(b)(1)-(3).</p>
<div class="consent-text">
<h4>Disclosure 1 — Notice of collection (§15(b)(1))</h4>
<p>Lakehouse will collect and store your <strong>biometric identifier</strong> (a photograph of you from which facial geometry is implicit). The photograph itself is the data we keep — we do <strong>NOT</strong> run automated facial-classification (gender / race / age inference) against it in v1. If at a later date we add automated classification, we will re-collect consent under a superseding template before doing so.</p>
<h4>Disclosure 2 — Specific purpose and length of term (§15(b)(2))</h4>
<p>The biometric data will be used for:</p>
<ul>
<li>Identity verification at staffing job sites</li>
<li>Internal record-keeping so coordinators can recognize you across placements</li>
</ul>
<p>Retained for a maximum of <strong>18 months</strong> from your most recent interaction, then permanently destroyed per the <a href="#" onclick="alert('See docs/policies/consent/biometric_retention_schedule_v1.md')">Biometric Retention Schedule v1</a>.</p>
<p>You may withdraw at any time by contacting the operator. Withdrawal triggers permanent destruction.</p>
<h4>Disclosure 3 — Written release (§15(b)(3))</h4>
<p>By checking the box below and providing your printed name, you provide a written release authorizing Lakehouse to collect, store, and use your biometric identifier and biometric information for the purposes and term stated above.</p>
<h4>What you're agreeing to (plain language)</h4>
<p>If you upload a photo of yourself, we'll keep that photo so your staffing coordinator can recognize you when you arrive at job sites. We don't run automated guesses about your age, gender, or race against the photo. We don't sell it. We don't share it outside the staffing operation unless legally compelled. You can withdraw consent at any time.</p>
</div>
<div class="card">
<label for="cand-name">Candidate printed name</label>
<input type="text" id="cand-name" placeholder="First Last" autocomplete="off">
<label class="checkbox-row" style="margin-top:16px">
<input type="checkbox" id="cand-agree">
<span>I have read and understood the disclosures above. I am providing this consent voluntarily and free of coercion.</span>
</label>
<div class="kv" style="margin-top:14px">
<span class="k">Method</span><span class="v">click_acceptance</span>
<span class="k">Version hash</span><span class="v" id="version-hash-display">computing…</span>
<span class="k">Timestamp</span><span class="v" id="timestamp-display">on submit</span>
</div>
<div style="margin-top:16px">
<button id="grant-btn" disabled>Grant consent + continue →</button>
</div>
</div>
<div class="error" id="consent-error"></div>
</div>
<!-- Step 3: photo -->
<div id="screen-photo" class="screen">
<h2>Photo capture</h2>
<p class="lede">Take or upload a clear photo. Stored quarantined under <code>data/biometric/uploads/</code> with mode 0700/0600 + SHA-256 integrity hash. Audit chain records the upload event.</p>
<div class="card">
<h3>Option A — File upload</h3>
<input type="file" id="photo-file" accept="image/jpeg,image/png">
<h3 style="margin-top:24px">Option B — Camera capture</h3>
<button id="cam-start" class="secondary">Start camera</button>
<button id="cam-capture" class="secondary" style="display:none">📸 Capture</button>
<button id="cam-stop" class="secondary" style="display:none">Stop</button>
<video id="cam" autoplay playsinline muted></video>
<canvas id="cam-canvas"></canvas>
<h3 style="margin-top:24px">Preview</h3>
<img id="preview" class="preview" style="display:none">
<p id="no-preview" class="lede" style="margin:12px 0">No photo selected yet.</p>
<div style="margin-top:16px">
<button id="upload-btn" disabled>Upload photo + finish →</button>
<button id="skip-btn" class="secondary">Skip photo (consent only)</button>
</div>
</div>
<div class="error" id="photo-error"></div>
</div>
<!-- Step 4: done -->
<div id="screen-done" class="screen">
<div class="card good">
<h2>✓ Intake complete</h2>
<p class="lede" style="margin:8px 0 0">Audit chain rows (HMAC-SHA256, persisted to <code>data/_catalog/subjects/&lt;id&gt;.audit.jsonl</code>):</p>
</div>
<div class="card">
<h3>Consent grant</h3>
<div class="kv">
<span class="k">Status</span><span class="v" id="done-status">Given</span>
<span class="k">Given at</span><span class="v" id="done-given-at">…</span>
<span class="k">Retention until</span><span class="v" id="done-retention">…</span>
</div>
<h3>Audit hmac</h3>
<div class="hash" id="done-consent-hmac">…</div>
</div>
<div class="card" id="photo-card" style="display:none">
<h3>Photo upload</h3>
<div class="kv">
<span class="k">Data path</span><span class="v" id="done-data-path">…</span>
<span class="k">Template SHA-256</span><span class="v" id="done-template-hash">…</span>
</div>
<h3>Audit hmac</h3>
<div class="hash" id="done-photo-hmac">…</div>
</div>
<div class="card warn">
<h3>Verification</h3>
<p>Operator: confirm the audit chain by running:</p>
<div class="hash" id="verify-cmd">./scripts/staffing/verify_biometric_erasure.sh &lt;candidate_id&gt;</div>
<p style="margin-top:8px">Or hit <code>GET /audit/subject/&lt;id&gt;</code> with legal-tier auth to read the full chain.</p>
</div>
<div style="margin-top:16px">
<button onclick="location.reload()" class="secondary">Start another intake</button>
</div>
</div>
</div>
<div class="foot">
<span class="spec-link">
<a href="https://git.agentview.dev/profit/lakehouse/src/branch/main/docs/PHASE_1_6_BIPA_GATES.md">Phase 1.6 BIPA Gates</a>
· <a href="https://git.agentview.dev/profit/lakehouse/src/branch/main/docs/policies/consent/biometric_consent_template_v1.md">Consent template v1</a>
· <a href="https://git.agentview.dev/profit/lakehouse/src/branch/main/docs/runbooks/BIPA_DESTRUCTION_RUNBOOK.md">Destruction runbook</a>
</span>
</div>
<script>
// ── Config ────────────────────────────────────────────────────────
// Gateway base URL. Same-origin if the page is served behind a reverse
// proxy; otherwise the operator's local gateway.
const GATEWAY = (function(){
const p = new URLSearchParams(location.search);
return p.get('gw') || 'http://localhost:3100';
})();
// Candidate ID from the URL. Required.
const CANDIDATE_ID = (function(){
const p = new URLSearchParams(location.search);
return p.get('candidate_id') || p.get('cid') || '';
})();
// ── State ─────────────────────────────────────────────────────────
const state = {
token: null, // sessionStorage-backed
consentVersionHash: null, // SHA-256 of the rendered consent template
consentResp: null, // server response from /consent
photoBlob: null, // captured/selected photo
photoResp: null, // server response from /photo
};
// ── Helpers ───────────────────────────────────────────────────────
function show(screen) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(screen).classList.add('active');
const stepMap = {'screen-token':'step 1 of 4', 'screen-consent':'step 2 of 4', 'screen-photo':'step 3 of 4', 'screen-done':'step 4 of 4'};
document.getElementById('step-label').textContent = stepMap[screen] || '';
}
function err(elId, msg) {
const e = document.getElementById(elId);
e.textContent = msg;
e.classList.add('show');
}
function clearErr(elId) {
document.getElementById(elId).classList.remove('show');
}
async function sha256Hex(text) {
const enc = new TextEncoder().encode(text);
const buf = await crypto.subtle.digest('SHA-256', enc);
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2,'0')).join('');
}
// ── Init ──────────────────────────────────────────────────────────
(async function init(){
if (!CANDIDATE_ID) {
err('token-error', 'Missing candidate_id in URL. Append ?candidate_id=WORKER-XXX');
document.getElementById('token-submit').disabled = true;
return;
}
document.getElementById('cid-display').textContent = CANDIDATE_ID;
// Restore token from sessionStorage if previously set in this tab.
const saved = sessionStorage.getItem('lh_legal_token');
if (saved) {
state.token = saved;
document.getElementById('token-input').value = '••••••••';
}
// Compute consent template hash (SHA-256 of the rendered consent block).
// This is what we'll send as consent_version_hash. Counsel later
// attests against the canonical signed template hash; operator
// discipline keeps these aligned.
const consentText = document.querySelector('.consent-text').innerText;
state.consentVersionHash = await sha256Hex(consentText);
document.getElementById('version-hash-display').textContent = state.consentVersionHash.substring(0,16) + '…';
})();
// ── Step 1: token ──────────────────────────────────────────────────
document.getElementById('token-submit').addEventListener('click', () => {
clearErr('token-error');
const v = document.getElementById('token-input').value.trim();
// If user kept the masked placeholder, use the saved token.
const tok = v === '••••••••' ? state.token : v;
if (!tok || tok.length < 32) {
err('token-error', 'Token must be ≥32 characters.');
return;
}
state.token = tok;
sessionStorage.setItem('lh_legal_token', tok);
show('screen-consent');
});
// ── Step 2: consent ────────────────────────────────────────────────
function refreshGrantBtn() {
const ok = document.getElementById('cand-agree').checked
&& document.getElementById('cand-name').value.trim().length >= 2;
document.getElementById('grant-btn').disabled = !ok;
}
document.getElementById('cand-agree').addEventListener('change', refreshGrantBtn);
document.getElementById('cand-name').addEventListener('input', refreshGrantBtn);
document.getElementById('grant-btn').addEventListener('click', async () => {
clearErr('consent-error');
document.getElementById('grant-btn').disabled = true;
try {
const candName = document.getElementById('cand-name').value.trim();
const ts = new Date().toISOString();
document.getElementById('timestamp-display').textContent = ts;
const evidence = JSON.stringify({
method: 'click_acceptance',
candidate_printed_name: candName,
acceptance_ts: ts,
user_agent: navigator.userAgent,
page_origin: location.origin,
});
const evidenceHash = await sha256Hex(evidence);
const body = {
consent_version_hash: state.consentVersionHash,
consent_collection_method: 'click_acceptance',
consent_collection_evidence_path: 'inline:sha256=' + evidenceHash,
operator_of_record: 'intake_ui_operator',
};
const r = await fetch(`${GATEWAY}/biometric/subject/${encodeURIComponent(CANDIDATE_ID)}/consent`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Lakehouse-Legal-Token': state.token,
'X-Lakehouse-Trace-Id': 'intake-' + Date.now(),
},
body: JSON.stringify(body),
});
const respBody = await r.json();
if (!r.ok) {
err('consent-error', `${r.status} ${respBody.error || 'unknown'} — ${respBody.detail || ''}`);
document.getElementById('grant-btn').disabled = false;
return;
}
state.consentResp = respBody;
show('screen-photo');
} catch (e) {
err('consent-error', `Network error: ${e.message}`);
document.getElementById('grant-btn').disabled = false;
}
});
// ── Step 3: photo ──────────────────────────────────────────────────
const previewEl = document.getElementById('preview');
const noPreviewEl = document.getElementById('no-preview');
const fileInput = document.getElementById('photo-file');
fileInput.addEventListener('change', () => {
const f = fileInput.files[0];
if (!f) return;
state.photoBlob = f;
previewEl.src = URL.createObjectURL(f);
previewEl.style.display = 'block';
noPreviewEl.style.display = 'none';
document.getElementById('upload-btn').disabled = false;
});
let camStream = null;
document.getElementById('cam-start').addEventListener('click', async () => {
clearErr('photo-error');
try {
camStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480, facingMode: 'user' }, audio: false });
document.getElementById('cam').srcObject = camStream;
document.getElementById('cam').style.display = 'block';
document.getElementById('cam-start').style.display = 'none';
document.getElementById('cam-capture').style.display = 'inline-block';
document.getElementById('cam-stop').style.display = 'inline-block';
} catch (e) {
err('photo-error', `Camera unavailable: ${e.message}`);
}
});
document.getElementById('cam-capture').addEventListener('click', () => {
const v = document.getElementById('cam');
const c = document.getElementById('cam-canvas');
c.width = v.videoWidth;
c.height = v.videoHeight;
c.getContext('2d').drawImage(v, 0, 0);
c.toBlob(b => {
state.photoBlob = b;
previewEl.src = URL.createObjectURL(b);
previewEl.style.display = 'block';
noPreviewEl.style.display = 'none';
document.getElementById('upload-btn').disabled = false;
}, 'image/jpeg', 0.92);
});
document.getElementById('cam-stop').addEventListener('click', () => {
if (camStream) { camStream.getTracks().forEach(t => t.stop()); camStream = null; }
document.getElementById('cam').style.display = 'none';
document.getElementById('cam-start').style.display = 'inline-block';
document.getElementById('cam-capture').style.display = 'none';
document.getElementById('cam-stop').style.display = 'none';
});
document.getElementById('upload-btn').addEventListener('click', async () => {
clearErr('photo-error');
document.getElementById('upload-btn').disabled = true;
try {
const ct = state.photoBlob.type || 'image/jpeg';
const r = await fetch(`${GATEWAY}/biometric/subject/${encodeURIComponent(CANDIDATE_ID)}/photo`, {
method: 'POST',
headers: {
'Content-Type': ct,
'X-Lakehouse-Legal-Token': state.token,
'X-Lakehouse-Consent-Version-Hash': state.consentVersionHash,
'X-Lakehouse-Trace-Id': 'intake-' + Date.now(),
},
body: state.photoBlob,
});
const respBody = await r.json();
if (!r.ok) {
err('photo-error', `${r.status} ${respBody.error || 'unknown'} — ${respBody.detail || ''}`);
document.getElementById('upload-btn').disabled = false;
return;
}
state.photoResp = respBody;
if (camStream) camStream.getTracks().forEach(t => t.stop());
showDone();
} catch (e) {
err('photo-error', `Network error: ${e.message}`);
document.getElementById('upload-btn').disabled = false;
}
});
document.getElementById('skip-btn').addEventListener('click', () => {
if (camStream) camStream.getTracks().forEach(t => t.stop());
showDone();
});
// ── Step 4: done ───────────────────────────────────────────────────
function showDone() {
document.getElementById('done-given-at').textContent = state.consentResp.given_at;
document.getElementById('done-retention').textContent = state.consentResp.retention_until;
document.getElementById('done-consent-hmac').textContent = state.consentResp.audit_row_hmac;
if (state.photoResp) {
document.getElementById('photo-card').style.display = 'block';
document.getElementById('done-data-path').textContent = state.photoResp.data_path;
document.getElementById('done-template-hash').textContent = state.photoResp.template_hash;
document.getElementById('done-photo-hmac').textContent = state.photoResp.audit_row_hmac;
}
document.getElementById('verify-cmd').textContent =
`./scripts/staffing/verify_biometric_erasure.sh ${CANDIDATE_ID}`;
show('screen-done');
}
</script>
</body>
</html>
+11
View File
@@ -767,6 +767,17 @@ async function main() {
});
}
// Biometric intake — Phase 1.6 Gate 2 frontend. Operator-driven
// candidate consent + photo capture flow. POSTs to gateway's
// /biometric/subject/{id}/consent + /photo. URL must include
// ?candidate_id=WORKER-XXX. Operator's legal-tier audit token
// is captured into sessionStorage (cleared on tab close).
if (url.pathname === "/biometric/intake") {
return new Response(Bun.file(import.meta.dir + "/biometric_intake.html"), {
headers: { ...cors, "Content-Type": "text/html" },
});
}
// Workspaces — per-contract state (Phase 8.5). UI layer over the
// gateway's /workspaces/* routes: list, create, detail, handoff,
// save-search, shortlist, log-activity. All persisted on the