Intent_Control_Aware / index.html
eaglelandsonce's picture
Update index.html
cd097b3 verified
Raw History Blame Contribute Delete
24.1 kB
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Permission Isn't Enough — Policy Fabric Lab</title><style>
:root{--blue:#2563eb;--green:#16825b;--red:#c9334c;--amber:#ad7000;--text:#172b48;--muted:#5f7188;--line:#dce5ef}*{box-sizing:border-box}body{margin:0;background:#f1f5fa;color:var(--text);font:15px system-ui,sans-serif}main{max-width:1450px;margin:auto;padding:28px}header{display:flex;justify-content:space-between;align-items:center;gap:18px}.eyebrow{color:var(--blue);font-size:11px;font-weight:800;letter-spacing:2px}h1{font-size:clamp(28px,3.5vw,44px);letter-spacing:-1.5px;margin:8px 0}p{color:var(--muted);line-height:1.5;margin:6px 0}.buttons{display:flex;gap:8px;flex-wrap:wrap}button,select{font:inherit;border:1px solid #bfccdc;background:white;color:var(--text);border-radius:9px;padding:10px 13px;cursor:pointer}button:hover{border-color:var(--blue);background:#eff5ff}button:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid #91b5ff;outline-offset:3px}button:disabled{opacity:.45;cursor:default}.primary{background:var(--blue);color:white;border-color:var(--blue)}.primary:hover{background:#1d4ed8;color:white}.scenes{display:grid;grid-template-columns:repeat(5,1fr);gap:9px;margin:22px 0 16px}.scenes button{text-align:left;font-size:13px;font-weight:700}.scenes small{display:block;font-size:11px;color:var(--muted);font-weight:400;margin-top:5px}.active{box-shadow:inset 0 -3px var(--blue);border-color:var(--blue)}.workspace{display:grid;grid-template-columns:300px 1fr;gap:18px}.card{background:white;border:1px solid var(--line);border-radius:14px;padding:18px}.controls{display:flex;flex-direction:column;gap:18px}h2{font-size:19px;margin:0 0 6px}h3{font-size:14px;margin:0 0 12px}label{display:block;font-size:13px;font-weight:650}select{width:100%;margin-top:7px;font-size:13px}input[type=range]{width:100%;accent-color:var(--blue);margin:10px 0 0}output{float:right;color:var(--blue)}.hint{font-size:11px;color:var(--muted);line-height:1.5}.toggle{display:flex;align-items:center;gap:8px;margin:10px 0}input[type=checkbox]{accent-color:var(--blue);width:17px;height:17px}.intent{border-left:4px solid #eabd36;background:#fffbed;padding:12px 15px;margin-bottom:14px}.intent b{display:block;margin:5px 0}.intent p{font-size:13px}.lane{padding:18px 18px 14px;margin-bottom:14px;background:white;border:1px solid var(--line);border-radius:14px}.lanehead{display:flex;justify-content:space-between;align-items:center;gap:8px}.chip{font-size:11px;background:#eef3fa;padding:5px 9px;border-radius:20px;white-space:nowrap}.path{position:relative;display:grid;grid-template-columns:repeat(5,1fr);gap:13px;padding:38px 0 18px;isolation:isolate}.path:before{content:'';position:absolute;top:81px;left:9%;right:9%;height:3px;background:#dce5ef;z-index:-1}.gate{border:1px solid #cbd8e8;background:#f8faff;border-radius:10px;min-height:90px;text-align:center;padding:12px 3px;position:relative;transition:background .25s,border-color .25s}.gate b{display:block;font-size:12px}.gate span{display:block;font-size:10px;margin-top:7px;color:var(--muted)}.gate i{font-style:normal;font-size:20px;display:block;margin-bottom:3px}.gate.off{background:#f5f5f5;border-style:dashed}.gate.ok{border-color:var(--green);background:#ebf9f1}.gate.bad{border-color:var(--red);background:#fff0f2}.gate.hold{border-color:#d09a25;background:#fff6dc}.packet{position:absolute;top:18px;left:10%;transform:translateX(-50%);transition:left .65s ease-in-out;color:white;background:var(--blue);padding:5px 10px;border-radius:20px;font-size:11px;box-shadow:0 3px 12px #2563eb33;z-index:2;white-space:nowrap}.status{min-height:46px;font-size:13px;line-height:1.5;padding:9px 12px;background:#f4f7fb;border-radius:8px}.status.ok{background:#ecf8f2;color:#146746}.status.bad{background:#fff0f2;color:#a5203a}.status.hold{background:#fff7df;color:#8c5b00}.metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:12px}.metrics b{font-size:22px;display:block}.metrics span{font-size:10px;color:var(--muted)}.bottom{display:grid;grid-template-columns:1fr 1fr;gap:14px}.approval{border-color:#e0b75a;background:#fffcf3}.approval p{font-size:13px;margin:10px 0}.audit{max-height:185px;overflow:auto;font:11px/1.8 ui-monospace,monospace}.audit div{border-bottom:1px solid #edf0f6;padding:3px 0}.key{display:flex;gap:15px;flex-wrap:wrap;font-size:11px;color:var(--muted);margin:8px 0 16px}.key b{margin-right:4px}details{margin-top:18px}summary{cursor:pointer;font-weight:650;font-size:13px}details p{font-size:12px}.footer{font-size:11px;color:var(--muted);margin-top:15px}#notice{font-size:12px;color:var(--blue);min-height:18px;margin-top:8px}@media(max-width:1050px){.workspace{grid-template-columns:1fr}.controls{display:grid;grid-template-columns:1fr 1fr}.scenes{grid-template-columns:repeat(3,1fr)}header{align-items:flex-start;flex-direction:column}}@media(max-width:650px){main{padding:14px}.controls,.bottom{grid-template-columns:1fr}.scenes{grid-template-columns:1fr 1fr}.lane{padding:12px 8px}.path{gap:5px}.gate b{font-size:10px}.gate span{font-size:9px}.chip{white-space:normal}.packet{font-size:10px;padding:4px 6px}.lanehead h2{font-size:16px}.buttons button{font-size:13px}}@media(prefers-reduced-motion:reduce){*{transition:none!important}}
</style></head><body><main>
<header><div><div class="eyebrow">THE POLICY FABRIC · INTERACTIVE LAB</div><h1>Permission isn't enough.</h1><p>An approved agent. An inappropriate action. Which control stops it?</p></div><div class="buttons"><button id="guided" class="primary">▶ Guided demo</button><button id="pause">Pause</button><button id="reset">Reset</button><button id="full">Full screen</button></div></header>
<nav class="scenes" aria-label="Teaching scenarios"><button data-scene="0">01 · Legitimate work<small>Read invoices for a summary</small></button><button data-scene="1">02 · Unknown agent<small>Identity boundary</small></button><button data-scene="2">03 · Intent drift<small>Approved agent exports records</small></button><button data-scene="3">04 · Custom approval<small>A valid refund needs review</small></button><button data-scene="4">05 · Context changes<small>Same action, sensitive data</small></button></nav>
<div class="workspace"><aside class="card controls"><section><h3>Compose a request</h3><label for="intent">User's authorized task</label><select id="intent"><option value="summary">Summarize customer invoices</option><option value="refund">Process a customer refund</option><option value="export">Export customer records</option></select><label for="action" style="margin-top:12px">Agent's attempted action</label><select id="action"><option value="read">Read invoices</option><option value="export">Export customer records</option><option value="refund">Issue refund</option></select><label class="toggle"><input id="registered" type="checkbox" checked>Registered / approved agent</label></section>
<section><label for="amount">Refund amount <output id="amountOut"></output></label><input id="amount" type="range" min="100" max="10000" step="100" value="2500"><div class="hint">Custom approval threshold: greater than $1,000.</div></section>
<section><label for="sensitivity">Data sensitivity <output id="sensitivityOut"></output></label><input id="sensitivity" type="range" min="0" max="2" step="1" value="1"><div class="hint">Public → Internal → Restricted</div></section>
<section><label for="strictness">Dynamic policy <output id="strictnessOut"></output></label><input id="strictness" type="range" min="1" max="3" step="1" value="2"><div class="hint" id="strictHelp"></div></section>
<section><h3>Right-lane controls</h3><label class="toggle"><input id="iam" type="checkbox" checked>IAM access policy</label><label class="toggle"><input id="dynamic" type="checkbox" checked>Dynamic intent + context</label><label class="toggle"><input id="extension" type="checkbox" checked>Custom service extension</label><div class="hint">Left lane always uses only the demo's coarse IAM access check.</div></section>
<section><label for="speed">Animation speed <output id="speedOut"></output></label><input id="speed" type="range" min="0.5" max="3" step="0.5" value="1"><button id="send" class="primary" style="width:100%;margin-top:14px">Send request →</button><div id="notice" aria-live="polite"></div></section></aside>
<article><div class="intent"><div class="eyebrow">CURRENT SCENARIO</div><b id="sceneTitle"></b><p id="sceneText"></p><p id="requestText"></p></div>
<div id="lanes"></div><div class="key"><span><b style="color:var(--green)">●</b>Allowed / passed</span><span><b style="color:var(--red)">●</b>Blocked / harmful execution</span><span><b style="color:var(--amber)">●</b>Awaiting approval</span><span>Dashed box = control off</span></div>
<div class="bottom"><section class="card approval"><h3>Human approval desk</h3><p id="approvalText">No request awaiting approval. Try scenario 4 to route a valid refund here.</p><div class="buttons"><button id="approve" disabled>Approve once</button><button id="deny" disabled>Reject</button></div><p class="hint">A held request cannot execute until you decide. Approval applies only to this request.</p></section><section class="card"><h3>Decision audit trail</h3><div id="audit" class="audit" role="log" aria-label="Decision audit trail"></div></section></div>
</article></div>
<details class="card"><summary>Teaching notes and exact rules</summary><p>This conceptual lab is inspired by the three control categories in the supplied slide: IAM policy, a dynamic policy engine, and service extensions. The serial checkpoints visualize separate decisions; they do not assert a particular vendor's deployment architecture. All requests and approvals are simulated locally.</p><p>The IAM-only lane deliberately gives approved agents coarse access to all three demo tools. Real IAM can also enforce granular permissions and conditions. The point is that access permission alone does not establish that every permitted action matches the user's current task.</p><p>Dynamic policy levels: <b>1 — Intent:</b> only task-aligned actions pass (summary → read; refund → read or refund; export → read or export). <b>2 — Context:</b> also blocks export of restricted data. <b>3 — Strict:</b> also blocks refunds above $5,000. The service extension holds every refund over $1,000 for approval, independently of dynamic policy. These are invented teaching rules, not product defaults or an actual natural-language policy engine.</p><p>A harmful execution is an action by an unapproved agent, an action outside the user's task, or a restricted-data export. Missing business approval is counted separately in the audit. Counters are cumulative until Reset. The two lanes receive the same request snapshot. Changes to controls affect the next request. Scenario selection cancels in-flight requests and pending approvals; Reset clears counters and returns to scenario 1.</p><p>Use Guided demo for all five scenarios. It pauses at the approval desk until you approve or reject. Send request repeats the current experiment. Animation speed changes presentation timing only; it does not change the policy outcomes. Keyboard: Space pauses/resumes when focus is outside a control.</p></details><div class="footer">PERMISSION ISN'T ENOUGH · Standalone HTML / JavaScript · No external dependencies, network traffic, or real transactions.</div>
</main><script>
'use strict';const $=id=>document.getElementById(id);const tasks={summary:'Summarize customer invoices',refund:'Process a customer refund',export:'Export customer records'},actions={read:'Read invoices',export:'Export customer records',refund:'Issue refund'},levels=['Public','Internal','Restricted'];
const scenes=[{title:'Legitimate work passes every layer.',text:'The agent reads invoices to produce the requested summary. Security should preserve useful work.',intent:'summary',action:'read',registered:true,sensitivity:1,amount:2500},{title:'An unknown agent hits the identity boundary.',text:'Both lanes stop an unapproved agent at IAM. Turn IAM off on the right to demonstrate why identity still matters.',intent:'summary',action:'read',registered:false,sensitivity:1,amount:2500},{title:'Approved identity does not imply approved intent.',text:'The user requested a summary. The agent attempts an export instead. Watch IAM allow access while the intent check stops the action.',intent:'summary',action:'export',registered:true,sensitivity:1,amount:2500},{title:'A valid task can still require a business approval.',text:'The user requested a refund. Identity and intent pass, but the custom extension holds refunds over $1,000 for your decision.',intent:'refund',action:'refund',registered:true,sensitivity:1,amount:2500},{title:'Live context changes the decision.',text:'Export is the requested task, but the records are now Restricted. Context-aware policy prevents the export. Lower sensitivity and send again.',intent:'export',action:'export',registered:true,sensitivity:2,amount:2500}];
let current=0,run=null,totals=[{executed:0,blocked:0,harm:0},{executed:0,blocked:0,harm:0}],paused=false,guided=false,timer=0,phase='idle',seq=0,audit=[];
const money=n=>'$'+Number(n).toLocaleString('en-US');
function aligned(r){return r.action==='read'||(r.intent==='refund'&&r.action==='refund')||(r.intent==='export'&&r.action==='export')}
function harmful(r){return !r.registered||!aligned(r)||(r.action==='export'&&r.sensitivity===2)}
function decision(r,gate,right){if(!right&&gate!==1)return{kind:'skip',reason:'Not installed in this lane.'};if(gate===1){if(right&&!r.iam)return{kind:'skip',reason:'IAM disabled.'};return r.registered?{kind:'pass',reason:'Approved agent has access to this demo tool.'}:{kind:'block',reason:'Unregistered agent: server access denied.'}}
if(gate===2){if(!r.dynamic)return{kind:'skip',reason:'Dynamic policy disabled.'};if(!aligned(r))return{kind:'block',reason:'Intent mismatch: attempted action is outside the user’s task.'};if(r.strictness>=2&&r.action==='export'&&r.sensitivity===2)return{kind:'block',reason:'Context rule: Restricted records cannot be exported.'};if(r.strictness>=3&&r.action==='refund'&&r.amount>5000)return{kind:'block',reason:'Strict policy: refunds above $5,000 denied.'};return{kind:'pass',reason:'Action matches the task and active context rules.'}}
if(gate===3){if(!r.extension)return{kind:'skip',reason:'Custom extension disabled.'};if(r.action==='refund'&&r.amount>1000)return{kind:'hold',reason:'Refund exceeds $1,000. Human approval required.'};return{kind:'pass',reason:'Custom business rule satisfied; no approval needed.'}}return{kind:'pass',reason:'Ready to execute.'}}
function snapshot(){return{intent:$('intent').value,action:$('action').value,registered:$('registered').checked,amount:+$('amount').value,sensitivity:+$('sensitivity').value,strictness:+$('strictness').value,iam:$('iam').checked,dynamic:$('dynamic').checked,extension:$('extension').checked}}
function log(s){audit.unshift(s);audit=audit.slice(0,50);$('audit').replaceChildren(...audit.map(v=>{const d=document.createElement('div');d.textContent=v;return d}))}
function status(i,text,cls=''){$('status'+i).textContent=text;$('status'+i).className='status '+cls}
function metrics(){totals.forEach((t,i)=>{$('metrics'+i).innerHTML='<div><b>'+t.executed+'</b><span>EXECUTED</span></div><div><b>'+t.blocked+'</b><span>BLOCKED / REJECTED</span></div><div><b style="color:var(--red)">'+t.harm+'</b><span>HARMFUL EXECUTIONS</span></div>'})}
function build(){const titles=['Agent','IAM policy','Dynamic policy','Extension','System'],icons=['◈','▣','◎','⌘','▤'];$('lanes').innerHTML=[0,1].map(i=>'<section class="lane"><div class="lanehead"><h2>'+(i?'Layered policy fabric':'IAM access alone')+'</h2><span class="chip">'+(i?'Identity + intent + business rules':'Identity / access boundary')+'</span></div><div class="path"><div class="packet" id="packet'+i+'">READY</div>'+titles.map((t,g)=>'<div id="gate'+i+g+'" class="gate"><i>'+icons[g]+'</i><b>'+t+'</b><span id="label'+i+g+'">'+['Request source','Access check','Intent + context','Custom rule','Tool execution'][g]+'</span></div>').join('')+'</div><div id="status'+i+'" class="status" aria-live="polite">Ready to compare the same request.</div><div id="metrics'+i+'" class="metrics"></div></section>').join('');metrics();paintEnabled()}
function paintEnabled(){const r=snapshot();[0,1].forEach(i=>{for(let g=1;g<=3;g++){const enabled=i?[r.iam,r.dynamic,r.extension][g-1]:g===1;$('gate'+i+g).classList.toggle('off',!enabled);$('label'+i+g).textContent=enabled?['','Access check','Intent + context','Custom rule'][g]:'OFF / BYPASS'}})}
function outputs(){$('amountOut').textContent=money($('amount').value);$('sensitivityOut').textContent=levels[+$('sensitivity').value];$('strictnessOut').textContent=['','1 · Intent','2 · Context','3 · Strict'][+$('strictness').value];$('strictHelp').textContent=['','Require action to match the user’s task.','Also block Restricted-data exports.','Also deny refunds above $5,000.'][+$('strictness').value];$('speedOut').textContent=$('speed').value+'×';const r=snapshot();$('requestText').textContent='Task: '+tasks[r.intent]+' | Action: '+actions[r.action]+(r.action==='refund'?' '+money(r.amount):'')+' | Data: '+levels[r.sensitivity];if(!run)paintEnabled()}
function approvalClear(){$('approve').disabled=$('deny').disabled=true;$('approvalText').textContent='No request awaiting approval. Try scenario 4 to route a valid refund here.'}
function selectScene(i,keep=false){if(run)log('Request #'+seq+' cancelled by scenario change.');run=null;phase='idle';timer=0;paused=false;$('pause').textContent='Pause';if(!keep){guided=false;$('guided').textContent='▶ Guided demo'}current=i;const s=scenes[i];['intent','action','amount','sensitivity'].forEach(k=>$(k).value=s[k]);$('registered').checked=s.registered;['iam','dynamic','extension'].forEach(k=>$(k).checked=true);$('strictness').value=2;$('sceneTitle').textContent=s.title;$('sceneText').textContent=s.text;document.querySelectorAll('[data-scene]').forEach(b=>b.classList.toggle('active',+b.dataset.scene===i));build();outputs();approvalClear();$('send').disabled=false;$('notice').textContent='Click Send request to run this scenario.'}
function start(){if(run)return;seq++;run={r:snapshot(),lanes:[{done:false,approved:false},{done:false,approved:false}],gate:0};build();phase='travel';timer=0;paused=false;$('pause').textContent='Pause';$('send').disabled=true;$('notice').textContent='Request #'+seq+' in progress. Controls apply to the next request.';approvalClear();[0,1].forEach(i=>{status(i,'Request #'+seq+': '+actions[run.r.action]);$('packet'+i).textContent='#'+seq;$('gate'+i+'0').classList.add('ok')});log('#'+seq+' · '+actions[run.r.action]+' | '+(run.r.registered?'approved':'unknown')+' agent');travel()}
function travel(){if(!run)return;run.gate++;phase='travel';timer=0;run.lanes.forEach((l,i)=>{if(!l.done){$('packet'+i).style.left=(10+20*run.gate)+'%';$('packet'+i).textContent='#'+seq+' →'}})}
function execute(i){const l=run.lanes[i],r=run.r;if(l.done)return;l.done=true;totals[i].executed++;const bad=harmful(r);if(bad)totals[i].harm++;let noApproval=r.action==='refund'&&r.amount>1000&&!l.approved;let text=bad?'HARMFUL EXECUTION: '+(!r.registered?'unapproved agent reached the tool.':!aligned(r)?'action exceeded the user’s task.':'Restricted records exported.'):noApproval?'EXECUTED without the required business approval.':'EXECUTED: '+actions[r.action]+(r.action==='refund'?' '+money(r.amount):'')+'.';status(i,text,bad?'bad':noApproval?'hold':'ok');$('gate'+i+'4').classList.add(bad?'bad':noApproval?'hold':'ok');$('packet'+i).textContent=bad?'HARM':noApproval?'NO REVIEW':'DONE';log('#'+seq+' · '+(i?'Fabric':'IAM only')+' · '+text);metrics()}
function evaluate(){if(!run)return;const g=run.gate;run.lanes.forEach((l,i)=>{if(l.done)return;if(g===4){execute(i);return}const d=decision(run.r,g,!!i);const box=$('gate'+i+g);box.classList.add(d.kind==='block'?'bad':d.kind==='hold'?'hold':d.kind==='pass'?'ok':'off');$('label'+i+g).textContent=d.kind.toUpperCase();status(i,d.reason,d.kind==='block'?'bad':d.kind==='hold'?'hold':'');log('#'+seq+' · '+(i?'Fabric':'IAM only')+' · '+['','IAM','Dynamic','Extension'][g]+': '+d.reason);if(d.kind==='block'){l.done=true;totals[i].blocked++;$('packet'+i).textContent='BLOCKED'}if(d.kind==='hold'){l.hold=true;$('packet'+i).textContent='HELD';$('approvalText').textContent='Request #'+seq+': approve a '+money(run.r.amount)+' refund? Identity and any enabled dynamic checks have passed.';$('approve').disabled=$('deny').disabled=false}});metrics();phase='dwell';timer=0}
function settle(){if(!run)return;if(run.lanes.every(l=>l.done)){run=null;phase='complete';timer=0;$('send').disabled=false;$('notice').textContent='Comparison complete. Change settings and send again.';return}if(run.lanes.some(l=>l.hold)){phase='approval';run.lanes.forEach((l,i)=>{if(!l.done&&!l.hold){$('packet'+i).style.left='90%';execute(i)}});return}travel()}
function resolve(approved){if(!run||!run.lanes[1].hold)return;const l=run.lanes[1];l.hold=false;l.approved=approved;approvalClear();$('approvalText').textContent=approved?'Approved once. This specific refund may proceed.':'Rejected. No refund executes in the layered lane.';log('#'+seq+' · Human '+(approved?'APPROVED':'REJECTED')+' '+money(run.r.amount));if(!approved){l.done=true;totals[1].blocked++;status(1,'REJECTED by the human reviewer. Transaction stopped.','bad');$('packet1').textContent='REJECTED';$('gate13').classList.remove('hold');$('gate13').classList.add('bad');metrics();settle()}else{$('gate13').classList.remove('hold');$('gate13').classList.add('ok');$('label13').textContent='APPROVED';status(1,'Approved for this request only. Proceeding to execution.','ok');run.gate=3;travel()}}
function tick(dt){if(paused)return;timer+=dt*+$('speed').value;if(run){if(phase==='travel'&&timer>=1)evaluate();else if(phase==='dwell'&&timer>=1.3)settle()}else if(guided&&phase==='complete'&&timer>=3){if(current<4){selectScene(current+1,true);start()}else{guided=false;$('guided').textContent='▶ Guided demo';$('notice').textContent='Guided demo complete. Explore the controls.'}}}
$('send').onclick=()=>{guided=false;$('guided').textContent='▶ Guided demo';start()};$('guided').onclick=()=>{if(guided){guided=false;$('guided').textContent='▶ Guided demo';return}selectScene(0,true);guided=true;$('guided').textContent='■ Stop guide';start()};$('pause').onclick=()=>{paused=!paused;$('pause').textContent=paused?'Resume':'Pause';document.querySelectorAll('.packet').forEach(p=>p.style.transitionDuration=paused?'0s':'.65s')};$('reset').onclick=()=>{totals=[{executed:0,blocked:0,harm:0},{executed:0,blocked:0,harm:0}];selectScene(0);seq=0;audit=[];$('audit').replaceChildren()};$('approve').onclick=()=>resolve(true);$('deny').onclick=()=>resolve(false);$('full').onclick=()=>{if(document.fullscreenElement)document.exitFullscreen?.();else document.documentElement.requestFullscreen?.().catch(()=>{})};document.querySelectorAll('[data-scene]').forEach(b=>b.onclick=()=>selectScene(+b.dataset.scene));['intent','action','registered','amount','sensitivity','strictness','iam','dynamic','extension','speed'].forEach(id=>$(id).addEventListener('input',()=>{outputs();if(id!=='speed'){guided=false;$('guided').textContent='▶ Guided demo';$('notice').textContent=run?'Current request keeps its original settings. Changes apply to the next request.':'Settings updated. Send a request to compare.'}}));document.addEventListener('keydown',e=>{if(e.code==='Space'&&!['INPUT','SELECT','BUTTON','SUMMARY'].includes(e.target.tagName)){e.preventDefault();$('pause').click()}});let last=0;function frame(t){tick(Math.min(.1,(t-last)/1000||0));last=t;requestAnimationFrame(frame)}selectScene(0);requestAnimationFrame(frame);window.policyLab={decision,aligned,harmful,snapshot,start,selectScene,resolve,tick,state:()=>({run,totals,phase,current,guided})};
</script></body></html>