|
| 1 | +#!/usr/bin/env node |
| 2 | +// AWF Runtime Authorization — Sprint 3 demo CLI. |
| 3 | +// |
| 4 | +// Simulates the Trust-Based Runtime Authorization algorithm from |
| 5 | +// AWF Sprint Plan v4.4.2. Hard-codes a small library of task risk |
| 6 | +// profiles and per-runtime trust capability profiles, then prints |
| 7 | +// the AUTHORIZED | SUPERVISED | BLOCKED decision with the same |
| 8 | +// fields the production decision record carries: |
| 9 | +// |
| 10 | +// - task risk profile (5 dimensions, composite, risk lane) |
| 11 | +// - trust capability profile (tier, sessions, evidence strength) |
| 12 | +// - decision + required vs current tier |
| 13 | +// - reason text and D1-D4 evidence summary |
| 14 | +// - required controls |
| 15 | +// - recommended alternative runtime (when blocked) |
| 16 | +// - audit event type the auth service would emit |
| 17 | +// |
| 18 | +// Usage: |
| 19 | +// node tools/authorize-task/authorize-task.js \ |
| 20 | +// --task-class db_migration \ |
| 21 | +// --risk-lane high \ |
| 22 | +// --runtime codex \ |
| 23 | +// --workspace awf-demo |
| 24 | + |
| 25 | +const TIER_ORDER = ['PROVISIONAL', 'RESTRICTED', 'STANDARD', 'HIGH']; |
| 26 | + |
| 27 | +// Per-task-class risk dimensions (each 1-5). Composite = sum. |
| 28 | +const TASK_RISK_PROFILES = { |
| 29 | + ui_refactor: { complexity: 1, blast: 2, security: 1, concurrency: 1, business: 2 }, |
| 30 | + api_change: { complexity: 4, blast: 4, security: 3, concurrency: 3, business: 3 }, |
| 31 | + db_migration: { complexity: 4, blast: 5, security: 3, concurrency: 3, business: 5 }, |
| 32 | + security_fix: { complexity: 3, blast: 3, security: 5, concurrency: 2, business: 4 } |
| 33 | +}; |
| 34 | + |
| 35 | +// Per-runtime trust capability, keyed by task_class. |
| 36 | +// Profiles with no D1-D4 fields render as "no history" (E0, n=0). |
| 37 | +const TRUST_PROFILES = { |
| 38 | + cursor: { |
| 39 | + ui_refactor: { |
| 40 | + tier: 'HIGH', sessions: 47, evidence: 'E3', |
| 41 | + d1: '92 (n=47)', d2: '88', d3: '100 (zero violations)', d4: '100 (no recurrence)' |
| 42 | + }, |
| 43 | + api_change: { |
| 44 | + tier: 'STANDARD', sessions: 12, evidence: 'E2', |
| 45 | + d1: '78 (n=12)', d2: '70', d3: '95', d4: '90' |
| 46 | + }, |
| 47 | + db_migration: { tier: 'PROVISIONAL', sessions: 0, evidence: 'E0' }, |
| 48 | + security_fix: { tier: 'PROVISIONAL', sessions: 0, evidence: 'E0' } |
| 49 | + }, |
| 50 | + codex: { |
| 51 | + ui_refactor: { |
| 52 | + tier: 'STANDARD', sessions: 18, evidence: 'E2', |
| 53 | + d1: '81 (n=18)', d2: '72', d3: '98', d4: '94' |
| 54 | + }, |
| 55 | + api_change: { |
| 56 | + tier: 'RESTRICTED', sessions: 4, evidence: 'E1', |
| 57 | + d1: '74 (n=4)', d2: '60', d3: '95', d4: 'insufficient (n<5)' |
| 58 | + }, |
| 59 | + db_migration: { tier: 'PROVISIONAL', sessions: 0, evidence: 'E0' }, |
| 60 | + security_fix: { tier: 'PROVISIONAL', sessions: 0, evidence: 'E0' } |
| 61 | + }, |
| 62 | + claude_code: { |
| 63 | + ui_refactor: { |
| 64 | + tier: 'HIGH', sessions: 62, evidence: 'E3', |
| 65 | + d1: '94 (n=62)', d2: '90', d3: '100', d4: '100' |
| 66 | + }, |
| 67 | + api_change: { |
| 68 | + tier: 'STANDARD', sessions: 22, evidence: 'E3', |
| 69 | + d1: '86 (n=22)', d2: '81', d3: '98', d4: '95', |
| 70 | + recent_failure: 'breaking_change_without_version_bump' |
| 71 | + }, |
| 72 | + db_migration: { |
| 73 | + tier: 'STANDARD', sessions: 14, evidence: 'E3', |
| 74 | + d1: '82 (n=14)', d2: '78', d3: '100', d4: '95' |
| 75 | + }, |
| 76 | + security_fix: { |
| 77 | + tier: 'STANDARD', sessions: 15, evidence: 'E3', |
| 78 | + d1: '85 (n=15)', d2: '80', d3: '100', d4: '95' |
| 79 | + } |
| 80 | + } |
| 81 | +}; |
| 82 | + |
| 83 | +// (task_class, lane) → controls that must accompany a SUPERVISED |
| 84 | +// decision, or that would have been required for a BLOCKED one. |
| 85 | +const CONTROLS = { |
| 86 | + db_migration: { |
| 87 | + critical: [ |
| 88 | + 'Human pre-approval required', |
| 89 | + 'Migration plan review', |
| 90 | + 'Schema validation', |
| 91 | + 'Rollback plan attached' |
| 92 | + ] |
| 93 | + }, |
| 94 | + api_change: { |
| 95 | + high: [ |
| 96 | + 'pre-tool-call hook on schema files', |
| 97 | + 'tech-lead review before PR', |
| 98 | + 'contract tests must pass' |
| 99 | + ] |
| 100 | + } |
| 101 | +}; |
| 102 | + |
| 103 | +// Display-only names for runtime (sentence-start in reason text) |
| 104 | +// and task-class plural (for the BLOCKED critical reason sentence). |
| 105 | +const RUNTIME_DISPLAY = { |
| 106 | + cursor: 'Cursor', |
| 107 | + codex: 'Codex', |
| 108 | + claude_code: 'Claude Code' |
| 109 | +}; |
| 110 | +const TASK_DISPLAY_PLURAL = { |
| 111 | + ui_refactor: 'UI refactors', |
| 112 | + api_change: 'API changes', |
| 113 | + db_migration: 'database migrations', |
| 114 | + security_fix: 'security fixes' |
| 115 | +}; |
| 116 | + |
| 117 | +function parseArgs(argv) { |
| 118 | + const args = {}; |
| 119 | + for (let i = 0; i < argv.length; i++) { |
| 120 | + if (argv[i].startsWith('--')) { |
| 121 | + args[argv[i].slice(2)] = argv[i + 1]; |
| 122 | + i++; |
| 123 | + } |
| 124 | + } |
| 125 | + return args; |
| 126 | +} |
| 127 | + |
| 128 | +function laneFor(composite) { |
| 129 | + if (composite <= 9) return 'low'; |
| 130 | + if (composite <= 14) return 'medium'; |
| 131 | + if (composite <= 19) return 'high'; |
| 132 | + return 'critical'; |
| 133 | +} |
| 134 | + |
| 135 | +function laneLabel(lane) { |
| 136 | + return { low: 'Low', medium: 'Medium', high: 'High', critical: 'Critical' }[lane]; |
| 137 | +} |
| 138 | + |
| 139 | +function requiredTierFor(lane) { |
| 140 | + return { low: 'RESTRICTED', medium: 'STANDARD', high: 'STANDARD', critical: 'HIGH' }[lane]; |
| 141 | +} |
| 142 | + |
| 143 | +const tierIdx = t => TIER_ORDER.indexOf(t); |
| 144 | + |
| 145 | +function decide(lane, currentTier, requiredTier) { |
| 146 | + // Critical lane requires human approval, which runtime auth alone cannot grant. |
| 147 | + if (lane === 'critical') return 'BLOCKED'; |
| 148 | + if (tierIdx(currentTier) < tierIdx(requiredTier)) return 'BLOCKED'; |
| 149 | + if (lane === 'high' && tierIdx(currentTier) < tierIdx('HIGH')) return 'SUPERVISED'; |
| 150 | + return 'AUTHORIZED'; |
| 151 | +} |
| 152 | + |
| 153 | +function recommendRuntime(taskClass, excluding) { |
| 154 | + let best = null, bestIdx = -1; |
| 155 | + for (const [runtime, profiles] of Object.entries(TRUST_PROFILES)) { |
| 156 | + if (runtime === excluding) continue; |
| 157 | + const p = profiles[taskClass]; |
| 158 | + if (!p) continue; |
| 159 | + const idx = tierIdx(p.tier); |
| 160 | + if (idx > bestIdx) { bestIdx = idx; best = { runtime, tier: p.tier }; } |
| 161 | + } |
| 162 | + return best; |
| 163 | +} |
| 164 | + |
| 165 | +function auditEvent(decision) { |
| 166 | + return { |
| 167 | + AUTHORIZED: 'RUNTIME_AUTHORIZATION_GRANTED', |
| 168 | + SUPERVISED: 'RUNTIME_AUTHORIZATION_GRANTED_WITH_CONTROLS', |
| 169 | + BLOCKED: 'RUNTIME_AUTHORIZATION_BLOCKED' |
| 170 | + }[decision]; |
| 171 | +} |
| 172 | + |
| 173 | +function reasonLines({ decision, runtime, taskClass, lane, currentTier, requiredTier }) { |
| 174 | + const name = RUNTIME_DISPLAY[runtime] ?? runtime; |
| 175 | + if (decision === 'BLOCKED') { |
| 176 | + if (lane === 'critical') { |
| 177 | + return [ |
| 178 | + `${name} has not earned sufficient trust for ${laneLabel(lane)} lane`, |
| 179 | + `${TASK_DISPLAY_PLURAL[taskClass] ?? taskClass} in this workspace.` |
| 180 | + ]; |
| 181 | + } |
| 182 | + return [ |
| 183 | + `${name} holds ${currentTier} tier for ${taskClass},`, |
| 184 | + `below the ${requiredTier} requirement for ${laneLabel(lane)} lane execution.` |
| 185 | + ]; |
| 186 | + } |
| 187 | + if (decision === 'SUPERVISED') { |
| 188 | + return [ |
| 189 | + `${name} holds ${currentTier} tier for ${taskClass}, sufficient`, |
| 190 | + `for ${laneLabel(lane)} lane execution under control wrappers.` |
| 191 | + ]; |
| 192 | + } |
| 193 | + return [ |
| 194 | + `${name} holds ${currentTier} tier for ${taskClass}, exceeding`, |
| 195 | + `the ${requiredTier} requirement for ${laneLabel(lane)} lane execution.` |
| 196 | + ]; |
| 197 | +} |
| 198 | + |
| 199 | +// Column alignment helpers. Values land at column 28 in all sections. |
| 200 | +const head = (label, value) => `${label.padEnd(16)}${value}`; |
| 201 | +const row = (label, value) => ` ${label.padEnd(26)}${value}`; |
| 202 | +const ev = (label, value) => ` ${label.padEnd(24)}${value}`; |
| 203 | +const DIV = '─────────────────────────────────────────────'; |
| 204 | + |
| 205 | +function main() { |
| 206 | + const args = parseArgs(process.argv.slice(2)); |
| 207 | + const taskClass = args['task-class']; |
| 208 | + const riskLaneArg = args['risk-lane']; |
| 209 | + const runtime = args.runtime; |
| 210 | + const workspace = args.workspace; |
| 211 | + |
| 212 | + const risk = TASK_RISK_PROFILES[taskClass]; |
| 213 | + if (!risk) { console.error(`unknown task class: ${taskClass}`); process.exit(1); } |
| 214 | + const trust = TRUST_PROFILES[runtime]?.[taskClass]; |
| 215 | + if (!trust) { console.error(`unknown runtime/task pair: ${runtime}/${taskClass}`); process.exit(1); } |
| 216 | + |
| 217 | + const composite = risk.complexity + risk.blast + risk.security |
| 218 | + + risk.concurrency + risk.business; |
| 219 | + const lane = laneFor(composite); |
| 220 | + const requiredTier = requiredTierFor(lane); |
| 221 | + const decision = decide(lane, trust.tier, requiredTier); |
| 222 | + |
| 223 | + const out = []; |
| 224 | + out.push('AWF Runtime Authorization'); |
| 225 | + out.push(DIV); |
| 226 | + out.push(head('Task class:', taskClass)); |
| 227 | + out.push(head('Risk lane:', riskLaneArg)); |
| 228 | + out.push(head('Candidate:', runtime)); |
| 229 | + out.push(head('Workspace:', workspace)); |
| 230 | + out.push(''); |
| 231 | + out.push('Task Risk Profile:'); |
| 232 | + out.push(row('Code complexity:', risk.complexity)); |
| 233 | + out.push(row('Blast radius:', risk.blast)); |
| 234 | + out.push(row('Security sensitivity:', risk.security)); |
| 235 | + out.push(row('Concurrency risk:', risk.concurrency)); |
| 236 | + out.push(row('Business criticality:', risk.business)); |
| 237 | + out.push(row('Composite score:', `${composite} → ${laneLabel(lane)} lane`)); |
| 238 | + out.push(''); |
| 239 | + out.push('Trust Capability Profile:'); |
| 240 | + out.push(row('Runtime:', runtime)); |
| 241 | + out.push(row('Task class:', taskClass)); |
| 242 | + out.push(row('Current trust tier:', trust.tier)); |
| 243 | + out.push(row('Sessions:', trust.sessions)); |
| 244 | + out.push(row('Evidence strength:', trust.evidence)); |
| 245 | + out.push(''); |
| 246 | + out.push('Authorization Decision:'); |
| 247 | + out.push(row('Decision:', decision)); |
| 248 | + out.push(row('Required trust tier:', |
| 249 | + decision === 'SUPERVISED' ? `${requiredTier} + controls` : requiredTier)); |
| 250 | + out.push(row('Current trust tier:', trust.tier)); |
| 251 | + out.push(' '); |
| 252 | + out.push(' Reason:'); |
| 253 | + reasonLines({ decision, runtime, taskClass, lane, currentTier: trust.tier, requiredTier }) |
| 254 | + .forEach(l => out.push(` ${l}`)); |
| 255 | + out.push(' '); |
| 256 | + out.push(' Evidence:'); |
| 257 | + out.push(ev('D1 Correctness:', trust.d1 ?? 'no history')); |
| 258 | + out.push(ev('D2 Observability:', trust.d2 ?? 'no history')); |
| 259 | + out.push(ev('D3 Policy:', trust.d3 ?? 'no history')); |
| 260 | + out.push(ev('D4 Recurrence:', trust.d4 ?? 'no history')); |
| 261 | + if (trust.recent_failure) { |
| 262 | + out.push(''); |
| 263 | + out.push(row('Recent failure pattern:', trust.recent_failure)); |
| 264 | + } |
| 265 | + out.push(''); |
| 266 | + |
| 267 | + const controls = CONTROLS[taskClass]?.[lane]; |
| 268 | + if (decision === 'BLOCKED') { |
| 269 | + if (controls) { |
| 270 | + out.push(' Required controls:'); |
| 271 | + controls.forEach(c => out.push(` - ${c}`)); |
| 272 | + out.push(''); |
| 273 | + } |
| 274 | + const rec = recommendRuntime(taskClass, runtime); |
| 275 | + if (rec) { |
| 276 | + out.push(row('Recommended runtime:', rec.runtime)); |
| 277 | + out.push(` Note: ${rec.runtime} holds ${rec.tier} tier for ${taskClass}`); |
| 278 | + out.push(` but ${laneLabel(lane)} lane requires HIGH. No runtime is authorized`); |
| 279 | + out.push(` for ${laneLabel(lane)} lane without human approval regardless of tier.`); |
| 280 | + out.push(''); |
| 281 | + } |
| 282 | + } else if (decision === 'SUPERVISED' && controls) { |
| 283 | + out.push(' Required controls:'); |
| 284 | + controls.forEach(c => out.push(` - ${c}`)); |
| 285 | + out.push(''); |
| 286 | + } else if (decision === 'AUTHORIZED') { |
| 287 | + out.push(' Required controls:'); |
| 288 | + out.push(' None beyond standard merge approval'); |
| 289 | + out.push(''); |
| 290 | + } |
| 291 | + |
| 292 | + out.push(row('Audit event:', auditEvent(decision))); |
| 293 | + out.push(DIV); |
| 294 | + console.log(out.join('\n')); |
| 295 | +} |
| 296 | + |
| 297 | +main(); |
0 commit comments