diff --git a/package.json b/package.json index 0ea5957..c33a778 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,11 @@ "demo:setup": "node scripts/demo-setup.js", "demo": "node examples/awf-demo/src/run-demo.js", "demo:cross-runtime": "cat examples/cross-runtime/terminal-demo.md", + "//authorize": "Live trust data: export AWF_DATABASE_URL=postgres://localhost/awf (or pass --db postgres://... to the CLI). Without it the scripts below render hard-coded example profiles.", "authorize:blocked": "node tools/authorize-task/authorize-task.js --task-class db_migration --risk-lane high --runtime codex --workspace awf-demo", "authorize:authorized": "node tools/authorize-task/authorize-task.js --task-class ui_refactor --risk-lane low --runtime cursor --workspace awf-demo", "authorize:supervised": "node tools/authorize-task/authorize-task.js --task-class api_change --risk-lane high --runtime claude_code --workspace awf-demo", + "authorize:authorized:live": "node tools/authorize-task/authorize-task.js --task-class ui_refactor --risk-lane low --runtime cursor --workspace awf-demo --db $AWF_DATABASE_URL", "test": "npm run test --workspaces --if-present" }, "dependencies": { diff --git a/tools/authorize-task/authorize-task.js b/tools/authorize-task/authorize-task.js index 6d923b0..4281ec7 100644 --- a/tools/authorize-task/authorize-task.js +++ b/tools/authorize-task/authorize-task.js @@ -21,6 +21,15 @@ // --risk-lane high \ // --runtime codex \ // --workspace awf-demo +// +// Live data: +// Pass --db postgres://... (or set AWF_DATABASE_URL) to read the +// most-recently-scored row from trust_capability_profiles for the +// (runtime, task_class) pair. On miss or DB error the CLI falls back +// to the hard-coded example profiles silently (error path warns to +// stderr). + +import pg from 'pg'; const TIER_ORDER = ['PROVISIONAL', 'RESTRICTED', 'STANDARD', 'HIGH']; @@ -202,16 +211,78 @@ const row = (label, value) => ` ${label.padEnd(26)}${value}`; const ev = (label, value) => ` ${label.padEnd(24)}${value}`; const DIV = '─────────────────────────────────────────────'; -function main() { +// SQL is intentionally narrow: pick the most-recently-scored profile +// for the (runtime_provider, task_class) pair across all workspaces. +// A demo-grade lookup — production callers use the writer's +// getTrustCapabilityProfile() keyed by (workspace, runtime, task_class, +// subject_key). +const LIVE_PROFILE_SQL = ` + SELECT trust_level, avg_score, avg_d1, avg_d2, + avg_d3, avg_d4, session_count, + evidence_strength, confidence_band + FROM trust_capability_profiles tcp + JOIN trust_subjects ts ON ts.id = tcp.trust_subject_id + WHERE ts.runtime_provider = $1 + AND tcp.task_class = $2 + ORDER BY tcp.last_score_at DESC + LIMIT 1 +`; + +// Format a NUMERIC dimension (pg returns NUMERIC as string). Returns +// undefined for null/missing so the existing `?? 'no history'` render +// path applies. +function fmtDim(v) { + if (v === null || v === undefined) return undefined; + return String(Math.round(Number(v))); +} + +// Map a live trust_capability_profiles row into the shape the renderer +// expects (tier/sessions/evidence/d1..d4). Zero-session rows yield no +// d1..d4 so they render as "no history". +function liveRowToTrust(r) { + const hasHistory = r.session_count > 0; + const d1 = hasHistory ? `${fmtDim(r.avg_d1)} (n=${r.session_count})` : undefined; + return { + tier: r.trust_level, + sessions: r.session_count, + evidence: r.evidence_strength, + d1, + d2: hasHistory ? fmtDim(r.avg_d2) : undefined, + d3: hasHistory ? fmtDim(r.avg_d3) : undefined, + d4: hasHistory ? fmtDim(r.avg_d4) : undefined, + }; +} + +// Returns { trust, source } where source is 'live' | 'fallback'. +// dbUrl null/undefined means: skip DB entirely, use fallback silently. +async function getTrustProfile(runtime, taskClass, dbUrl) { + const fallback = TRUST_PROFILES[runtime]?.[taskClass]; + if (!dbUrl) return { trust: fallback, source: 'fallback' }; + + const pool = new pg.Pool({ connectionString: dbUrl }); + try { + const r = await pool.query(LIVE_PROFILE_SQL, [runtime, taskClass]); + if (r.rows.length === 0) return { trust: fallback, source: 'fallback' }; + return { trust: liveRowToTrust(r.rows[0]), source: 'live' }; + } catch (e) { + console.error(`warning: trust DB query failed (${e.message}); using example data`); + return { trust: fallback, source: 'fallback' }; + } finally { + await pool.end().catch(() => {}); + } +} + +async function main() { const args = parseArgs(process.argv.slice(2)); const taskClass = args['task-class']; const riskLaneArg = args['risk-lane']; const runtime = args.runtime; const workspace = args.workspace; + const dbUrl = args.db ?? process.env.AWF_DATABASE_URL ?? null; const risk = TASK_RISK_PROFILES[taskClass]; if (!risk) { console.error(`unknown task class: ${taskClass}`); process.exit(1); } - const trust = TRUST_PROFILES[runtime]?.[taskClass]; + const { trust, source } = await getTrustProfile(runtime, taskClass, dbUrl); if (!trust) { console.error(`unknown runtime/task pair: ${runtime}/${taskClass}`); process.exit(1); } const composite = risk.complexity + risk.blast + risk.security @@ -242,6 +313,9 @@ function main() { out.push(row('Current trust tier:', trust.tier)); out.push(row('Sessions:', trust.sessions)); out.push(row('Evidence strength:', trust.evidence)); + out.push(row('Source:', source === 'live' + ? 'live trust database' + : 'example data (set AWF_DATABASE_URL for live data)')); out.push(''); out.push('Authorization Decision:'); out.push(row('Decision:', decision)); @@ -294,4 +368,4 @@ function main() { console.log(out.join('\n')); } -main(); +main().catch(e => { console.error(e); process.exit(1); });