Skip to content

Commit 507fd9c

Browse files
feat: awf authorize-task CLI demo — Sprint 3 trust-based runtime authorization (#4)
Adds tools/authorize-task/authorize-task.js, a dependency-free Node CLI that simulates the Sprint 3 Trust-Based Runtime Authorization algorithm from AWF Sprint Plan v4.4.2. Hard-codes risk profiles for four task classes (ui_refactor, api_change, db_migration, security_fix) and trust capability profiles for three runtimes (cursor, codex, claude_code), then prints AUTHORIZED | SUPERVISED | BLOCKED with the same fields a production decision record carries: risk lane, required vs current tier, D1-D4 evidence, required controls, recommended alternative runtime when blocked, and the audit event type the auth service would emit. npm scripts: authorize:blocked → BLOCKED (Codex, db_migration, Critical lane) authorize:authorized → AUTHORIZED (Cursor, ui_refactor, Low lane) authorize:supervised → SUPERVISED (Claude Code, api_change, High lane) All three outputs match v4.4.2 Examples A, B, C. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent afefaf4 commit 507fd9c

2 files changed

Lines changed: 300 additions & 0 deletions

File tree

‎package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"demo:setup": "node scripts/demo-setup.js",
1616
"demo": "node examples/awf-demo/src/run-demo.js",
1717
"demo:cross-runtime": "cat examples/cross-runtime/terminal-demo.md",
18+
"authorize:blocked": "node tools/authorize-task/authorize-task.js --task-class db_migration --risk-lane high --runtime codex --workspace awf-demo",
19+
"authorize:authorized": "node tools/authorize-task/authorize-task.js --task-class ui_refactor --risk-lane low --runtime cursor --workspace awf-demo",
20+
"authorize:supervised": "node tools/authorize-task/authorize-task.js --task-class api_change --risk-lane high --runtime claude_code --workspace awf-demo",
1821
"test": "npm run test --workspaces --if-present"
1922
},
2023
"dependencies": {
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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

Comments
 (0)