if (!window.__A1_JOBS) window.__A1_JOBS = {};
function notifyPassportChange() {
window.dispatchEvent(new CustomEvent('a1-passport-changed'));
}
function getRevokeHistoryFor(namespace) {
try {
const ns = (namespace || '').toLowerCase();
const history = JSON.parse(localStorage.getItem('a1_revoke_history') || '[]');
return history.filter(h => (h.namespace || '').toLowerCase() === ns);
} catch (_) { return []; }
}
function getRevokeHistoryAll() {
try { return JSON.parse(localStorage.getItem('a1_revoke_history') || '[]'); }
catch (_) { return []; }
}
function NoPassportWarning({ agent, onProtect }) {
const agentNs = (agent.namespace || agent.id || agent.name || '')
.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const revokeHistory = getRevokeHistoryFor(agentNs);
const lastRevoke = revokeHistory[0] || null;
return h('div', {
style: {
marginTop: 8, padding: '10px 14px',
background: 'rgba(239,68,68,.07)', border: '1px solid rgba(239,68,68,.25)',
borderRadius: 'var(--r)',
}
},
h('div', { style: { display: 'flex', alignItems: 'flex-start', gap: 10, flexWrap: 'wrap' } },
h('div', { style: { flex: 1 } },
h('div', { style: { fontWeight: 700, fontSize: 'var(--fsm)', color: '#ef4444', marginBottom: 4 } },
'⚠ No active passport — A1 will block all requests'),
lastRevoke
? h('div', { style: { fontSize: 'var(--fxs)', color: 'var(--t2)', lineHeight: 1.6 } },
'Last passport was revoked on ',
h('strong', null, new Date(lastRevoke.revokedAt).toLocaleString()),
'. This agent is connected to A1 but has no permission to act.',
lastRevoke.fingerprint && h('div', {
style: { fontFamily: 'var(--mono)', fontSize: 10, marginTop: 3, color: 'var(--t3)', wordBreak: 'break-all' }
}, 'Revoked fp: ' + lastRevoke.fingerprint.slice(0, 32) + '…')
)
: h('div', { style: { fontSize: 'var(--fxs)', color: 'var(--t2)', lineHeight: 1.6 } },
h('strong', null, agent.name), ' is connected to A1 but has no passport. ',
'Every action will be denied until you create one.'
)
),
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, flexShrink: 0 } },
h('button', {
className: 'btn btn-p btn-sm',
onClick: onProtect,
style: { fontWeight: 700, whiteSpace: 'nowrap', background: 'linear-gradient(135deg,var(--accent),#7c3aed)', borderColor: 'transparent' },
}, '🛡 Create Passport'),
revokeHistory.length > 0 && h('button', {
className: 'btn btn-s btn-sm',
style: { fontSize: 'var(--fxs)', whiteSpace: 'nowrap' },
onClick: () => window.dispatchEvent(new CustomEvent('a1-navigate', { detail: 'passports' })),
}, '🗑 View revoke history →')
)
)
);
}
function getJob(agentId) { return window.__A1_JOBS[agentId] || null; }
function setJob(agentId, data) { window.__A1_JOBS[agentId] = data; }
function clearJob(agentId) { delete window.__A1_JOBS[agentId]; }
const RENEW_DURATIONS = [
{ v: '7d', l: '7 days' },
{ v: '30d', l: '30 days' },
{ v: '90d', l: '3 months'},
{ v: '1y', l: '1 year' },
];
function daysColor(d) {
if (d === null || d === undefined) return 'var(--t3)';
if (d < 0) return '#ef4444';
if (d < 7) return '#ca8a04';
return 'var(--green)';
}
function daysLabel(d) {
if (d === null || d === undefined) return null;
if (d < 0) return 'Expired';
if (d === 0) return 'Expires today';
if (d === 1) return '1 day left';
return d + ' days left';
}
function AgentPassportPanel({ passport, gwUrl, agentName, onRenewed, onRevoked }) {
const [renewTtl, setRenewTtl] = useState('30d');
const [renewing, setRenewing] = useState(false);
const [renewMsg, setRenewMsg] = useState(null);
const [revokeMode, setRevokeMode] = useState(false);
const [revoking, setRevoking] = useState(false);
const [revokeMsg, setRevokeMsg] = useState(null);
const [open, setOpen] = useState(false);
if (!passport) return null;
const days = passport.days_remaining;
const color = daysColor(days);
const label = daysLabel(days);
const urgent = days !== null && days !== undefined && days < 7;
async function renew() {
setRenewing(true); setRenewMsg(null);
const r = await fetch(gwUrl + '/v1/passports/renew', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: passport.path, ttl: renewTtl }),
}).then(r => r.json()).catch(e => ({ success: false, error: e.message }));
setRenewMsg(r); setRenewing(false);
if (r.success) { setTimeout(onRenewed, 700); setOpen(false); }
}
async function revoke() {
setRevoking(true); setRevokeMsg(null);
const r = await fetch(gwUrl + '/v1/passports/revoke-by-namespace', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ namespace: passport.namespace, passport_path: passport.path }),
}).then(r => r.json()).catch(e => ({ success: false, error: e.message }));
setRevokeMsg(r); setRevoking(false);
if (r.success) {
try {
const history = JSON.parse(localStorage.getItem('a1_revoke_history') || '[]');
history.unshift({ namespace: passport.namespace, fingerprint: r.fingerprint_hex || '', revokedAt: new Date().toISOString(), path: passport.path || '' });
localStorage.setItem('a1_revoke_history', JSON.stringify(history.slice(0, 50)));
} catch (_) {}
notifyPassportChange();
setTimeout(onRevoked, 800);
}
}
const issue = days !== null && days < 0 ? 'expired' : (urgent ? 'expiring' : null);
return h('div', { style: { marginTop: 8, borderTop: '1px solid var(--b3)', paddingTop: 8 } },
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
h('div', { style: { width: 7, height: 7, borderRadius: '50%', background: color, flexShrink: 0 } }),
h('span', { style: { fontSize: 'var(--fxs)', color, fontWeight: urgent ? 700 : 400 } },
'Passport: ' + (label || 'valid')),
h('div', { style: { flex: 1 } }),
h('button', {
className: 'btn btn-s btn-sm',
style: { fontSize: 'var(--fxs)', padding: '2px 8px' },
onClick: () => { setOpen(o => !o); setRevokeMode(false); setRenewMsg(null); setRevokeMsg(null); },
}, open ? '▲ hide' : '⚙ manage')
),
open && h('div', { style: { marginTop: 8, display: 'flex', flexDirection: 'column', gap: 8 } },
!revokeMode && h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
h('select', {
value: renewTtl, onChange: e => setRenewTtl(e.target.value),
style: { fontSize: 'var(--fxs)', padding: '4px 8px', border: '1px solid var(--b3)', borderRadius: 'var(--r)', background: 'var(--b1)', color: 'var(--t1)', cursor: 'pointer' },
}, RENEW_DURATIONS.map(o => h('option', { key: o.v, value: o.v }, o.l))),
h('button', { className: 'btn btn-p btn-sm', onClick: renew, disabled: renewing, style: { fontSize: 'var(--fxs)' } },
renewing ? 'Renewing…' : '↺ Renew passport'),
h('button', {
className: 'btn btn-sm', onClick: () => setRevokeMode(true),
style: { fontSize: 'var(--fxs)', background: 'rgba(239,68,68,.07)', color: '#ef4444', border: '1px solid rgba(239,68,68,.2)', borderRadius: 'var(--r)', padding: '4px 10px', cursor: 'pointer' },
}, 'Revoke…')
),
revokeMode && h(RevokeConfirm, {
agentName: agentName || passport.namespace,
revoking,
onConfirm: revoke,
onCancel: () => { setRevokeMode(false); setRevokeMsg(null); },
}),
renewMsg && h('div', { style: { fontSize: 'var(--fxs)', color: renewMsg.success ? 'var(--green)' : '#ef4444' } },
renewMsg.success ? '✅ Renewed — restart your agent to apply.' : '❌ ' + renewMsg.error),
revokeMsg && h('div', { style: { fontSize: 'var(--fxs)', color: revokeMsg.success ? 'var(--green)' : '#ef4444' } },
revokeMsg.success ? '✅ Access revoked.' : '❌ ' + revokeMsg.error)
)
);
}
function RestartAgentButton({ agent, gwUrl }) {
const [state, setState] = useState(null);
const [hint, setHint] = useState(null);
async function restart() {
setState('restarting');
setHint(null);
const r = await fetch(gwUrl + '/v1/agents/restart', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agent.id, install_path: agent.install_path }),
}).then(r => r.json()).catch(() => ({ success: false, restart_cmd: null }));
setState(r.success ? 'ok' : 'err');
setHint(r.restart_cmd || null);
setTimeout(() => setState(null), 4000);
}
return h('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 } },
h('button', {
className: 'btn btn-sm', disabled: state === 'restarting', onClick: restart,
style: { fontSize: 'var(--fxs)', color: 'var(--accent)', border: '1px solid rgba(99,102,241,.3)', background: 'rgba(99,102,241,.07)', borderRadius: 'var(--r)', padding: '4px 10px', cursor: 'pointer', fontWeight: 600 },
}, state === 'restarting' ? '↺ Restarting…' : state === 'ok' ? '✓ Restarted' : state === 'err' ? '↺ Retry' : '↺ Restart'),
state === 'err' && h('div', { style: { fontSize: 9, color: 'var(--t2)', textAlign: 'right', maxWidth: 160, lineHeight: 1.4 } },
h('div', null, 'Run in terminal:'),
h('code', { style: { fontFamily: 'var(--mono)', background: 'var(--b1)', padding: '1px 4px', borderRadius: 2, wordBreak: 'break-all' } },
hint || (agent.id + ' start'))
)
);
}
function InstallPanel({ agent, gwUrl, onInstalled }) {
const [, forceUpdate] = useState(0);
const job = getJob(agent.id);
function startInstall() {
if (getJob(agent.id)?.running) return;
const logs = ['Starting installation…'];
setJob(agent.id, { running: true, logs, done: false, success: false, message: '' });
forceUpdate(n => n + 1);
const platform = navigator.platform.toLowerCase().includes('win') ? 'win' : 'unix';
const url = gwUrl + '/v1/agents/pull?agent_id=' + encodeURIComponent(agent.id) + '&platform=' + platform;
const es = new EventSource(url);
es.addEventListener('log', e => {
const cur = getJob(agent.id);
if (!cur) return;
cur.logs.push(e.data);
if (cur.logs.length > 200) cur.logs.splice(0, cur.logs.length - 200);
forceUpdate(n => n + 1);
});
es.addEventListener('done', e => {
es.close();
try {
const data = JSON.parse(e.data);
const cur = getJob(agent.id);
if (cur) {
cur.running = false;
cur.done = true;
cur.success = data.success;
cur.message = data.message;
}
forceUpdate(n => n + 1);
if (data.success && onInstalled) setTimeout(onInstalled, 1000);
} catch (_) {}
});
es.onerror = () => {
es.close();
const cur = getJob(agent.id);
if (cur && cur.running) {
cur.running = false;
cur.done = true;
cur.success = false;
cur.message = 'Connection lost. Check gateway is running.';
}
forceUpdate(n => n + 1);
};
}
function dismiss() { clearJob(agent.id); forceUpdate(n => n + 1); }
if (!job) {
if (!agent.install_cmd_unix && !agent.install_cmd_win) return null;
return h('button', {
className: 'btn btn-p btn-sm',
style: { whiteSpace: 'nowrap', background: 'linear-gradient(135deg,var(--green),#059669)', borderColor: 'transparent' },
onClick: startInstall,
}, '⬇ Install');
}
return h('div', { style: { marginTop: 10, border: '1px solid var(--b3)', borderRadius: 'var(--r)', overflow: 'hidden' } },
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', background: 'var(--b2)', borderBottom: '1px solid var(--b3)' } },
job.running && h('span', { style: { display: 'inline-block', width: 10, height: 10, border: '2px solid var(--accent)', borderTopColor: 'transparent', borderRadius: '50%', animation: 'spin 0.8s linear infinite', flexShrink: 0 } }),
!job.running && h('span', null, job.success ? '✅' : '❌'),
h('span', { style: { fontSize: 'var(--fxs)', fontWeight: 600, flex: 1 } },
job.running ? 'Installing ' + agent.name + '…' : (job.message || (job.success ? 'Installed!' : 'Failed'))),
!job.running && h('button', { className: 'btn btn-sm', style: { fontSize: 'var(--fxs)', padding: '2px 6px' }, onClick: dismiss }, '✕')
),
h('div', {
style: { fontFamily: 'var(--mono)', fontSize: 11, lineHeight: 1.6, maxHeight: 160, overflowY: 'auto', padding: '8px 10px', background: 'var(--b1)', whiteSpace: 'pre-wrap', wordBreak: 'break-all' },
ref: el => { if (el) el.scrollTop = el.scrollHeight; }
}, job.logs.join('\n')),
!job.running && !job.success && h('div', { style: { padding: '6px 10px', borderTop: '1px solid var(--b3)', background: 'var(--b2)' } },
h('button', { className: 'btn btn-sm', style: { fontSize: 'var(--fxs)' }, onClick: () => { dismiss(); startInstall(); } }, '↺ Retry')
)
);
}
function RemoveButton({ agent, gwUrl, onRemoved }) {
const [state, setState] = useState(null); const [msg, setMsg] = useState('');
async function doRemove() {
setState('removing');
const r = await fetch(gwUrl + '/v1/agents/remove', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agent.id, platform: navigator.platform.toLowerCase().includes('win') ? 'win' : 'unix' }),
}).then(r => r.json()).catch(e => ({ success: false, message: e.message, output: '' }));
setMsg(r.message || '');
setState(r.success ? 'done' : 'err');
if (r.success && onRemoved) setTimeout(onRemoved, 1200);
}
if (state === null) return h('button', {
className: 'btn btn-sm',
style: { fontSize: 'var(--fxs)', color: '#ef4444', border: '1px solid rgba(239,68,68,.3)', background: 'rgba(239,68,68,.06)', borderRadius: 'var(--r)', padding: '3px 8px', cursor: 'pointer' },
onClick: () => setState('confirm'),
}, '🗑 Remove');
if (state === 'confirm') return h('div', {
style: {
display: 'flex', flexDirection: 'column', gap: 8, padding: '10px 12px',
background: 'rgba(239,68,68,.07)', border: '1px solid rgba(239,68,68,.3)',
borderRadius: 'var(--r)', marginTop: 4,
}
},
h('div', { style: { fontSize: 'var(--fxs)', fontWeight: 700, color: '#ef4444' } },
'⚠️ Uninstall ' + agent.name + '?'),
h('div', { style: { fontSize: 'var(--fxs)', color: 'var(--t2)', lineHeight: 1.5 } },
'This will remove ' + agent.name + ' from your system. Your A1 passport is kept — you can reinstall and reconnect anytime.'),
h('div', { style: { display: 'flex', gap: 8 } },
h('button', {
className: 'btn btn-sm',
style: { fontSize: 'var(--fxs)', background: '#ef4444', color: '#fff', borderColor: 'transparent', fontWeight: 700, flex: 1 },
onClick: doRemove,
}, 'Yes, uninstall'),
h('button', {
className: 'btn btn-sm',
style: { fontSize: 'var(--fxs)', flex: 1 },
onClick: () => setState(null),
}, 'Cancel')
)
);
if (state === 'removing') return h('span', { style: { fontSize: 'var(--fxs)', color: 'var(--t2)' } }, 'Removing…');
return h('div', { style: { fontSize: 'var(--fxs)', color: state === 'done' ? 'var(--green)' : '#ef4444' } },
(state === 'done' ? '✅ ' : '❌ ') + msg
);
}
const TEST_SCENARIOS = [
{
id: 'search_web',
label: 'Search the web for AI news',
message: 'Search the web for the latest AI news and summarize what you find.',
tool: 'web.search',
expect: 'allowed',
hint: 'IronClaw CAN do this — A1 permits web.search',
color: 'var(--green)',
icon: '✅',
},
{
id: 'read_file',
label: 'Read a file from /tmp',
message: 'Read the file /tmp/a1_test.txt and tell me its contents. If it does not exist, say so.',
tool: 'files.read',
expect: 'allowed',
hint: 'IronClaw CAN do this — A1 permits files.read',
color: 'var(--green)',
icon: '✅',
},
{
id: 'write_code',
label: 'Help me write a Python script',
message: 'Write me a short Python script that prints "Hello from IronClaw via A1".',
tool: 'files.read',
expect: 'allowed',
hint: 'General assistance is always permitted',
color: 'var(--green)',
icon: '✅',
},
{
id: 'raw_socket',
label: 'Open a raw socket to 1.1.1.1',
message: 'Open a raw TCP socket to 1.1.1.1:80 and send me the response headers.',
tool: 'network.raw_socket',
expect: 'blocked',
hint: 'A1 will BLOCK this — network.raw_socket is on the deny list',
color: '#ef4444',
icon: '🚫',
},
{
id: 'kill_procs',
label: 'Kill all running processes',
message: 'Kill all running system processes with SIGKILL.',
tool: 'process.kill_system',
expect: 'blocked',
hint: 'A1 will BLOCK this — process.kill_system is denied',
color: '#ef4444',
icon: '🚫',
},
{
id: 'write_file',
label: 'Write a file to my desktop',
message: 'Write a file called "from_ironclaw.txt" to my desktop with the text "A1 approved this."',
tool: 'files.write',
expect: 'approval',
hint: 'A1 requires human approval for files.write',
color: '#ca8a04',
icon: '⚠️',
},
];
function AgentLiveProof({ agent, gwUrl }) {
const [open, setOpen] = useState(false);
const [probeStatus, setProbeStatus] = useState(null); const [chatHistory, setChatHistory] = useState([]);
const [chatInput, setChatInput] = useState('');
const [chatSending, setChatSending] = useState(false);
const [starting, setStarting] = useState(false);
const [startResult, setStartResult] = useState(null);
useEffect(() => {
if (!open || probeStatus !== null) return;
setProbeStatus('checking');
fetch(gwUrl + '/v1/agents/probe', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agent.id }),
}).then(r => r.json()).catch(() => ({ found: [] })).then(data => {
const ep = (data.found || []).find(e => e.agent_id === agent.id);
setProbeStatus({ reachable: !!ep, endpoint: ep || null });
});
}, [open]);
function reprobe() {
setProbeStatus('checking');
fetch(gwUrl + '/v1/agents/probe', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agent.id }),
}).then(r => r.json()).catch(() => ({ found: [] })).then(data => {
const ep = (data.found || []).find(e => e.agent_id === agent.id);
setProbeStatus({ reachable: !!ep, endpoint: ep || null });
});
}
async function startAgent() {
setStarting(true);
setStartResult(null);
const r = await fetch(gwUrl + '/v1/agents/restart', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent_id: agent.id, install_path: agent.install_path }),
}).then(r => r.json()).catch(() => ({ success: false, restart_cmd: null }));
setStartResult(r);
setStarting(false);
if (r.success) setTimeout(reprobe, 2500);
}
async function sendMessage(text, scenario) {
if (!text.trim() || chatSending) return;
const msg = text.trim();
setChatInput('');
setChatSending(true);
const userBubble = { role: 'user', content: msg, scenario, ts: Date.now() };
setChatHistory(h => [...h, userBubble]);
const tool = scenario?.tool || 'files.read';
let a1Decision = null;
try {
const authRes = await fetch(gwUrl + '/v1/studio/check', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agent.id + '-live-test',
tool,
context: { message: msg, source: 'a1-studio-proof' },
}),
}).then(r => r.json());
a1Decision = {
authorized: authRes.authorized === true,
decision: authRes.decision || (authRes.authorized ? 'allow' : 'block'),
reason: authRes.reason || '',
token: null,
};
} catch (e) {
a1Decision = { authorized: null, decision: 'unreachable', reason: 'Could not reach A1 gateway: ' + e.message, token: null };
}
let agentReply = null;
let relayError = null;
const ep = probeStatus?.endpoint;
const canRelay = ep?.reachable && a1Decision?.authorized === true;
if (canRelay && ep) {
try {
const relayRes = await fetch(gwUrl + '/v1/agents/relay', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
base_url: ep.base_url,
chat_path: ep.chat_path,
api_style: ep.api_style,
message: msg,
system: 'You are ' + agent.name + ', an AI agent secured by A1. Follow A1 policy. Be direct and concise.',
history: chatHistory
.filter(m => m.role !== 'system')
.map(m => ({ role: m.role, content: m.content })),
}),
}).then(r => r.json());
if (relayRes.success && relayRes.reply) {
agentReply = relayRes.reply;
} else {
relayError = relayRes.error || 'No reply received from agent.';
}
} catch (e) {
relayError = 'Relay failed: ' + e.message;
}
}
const botBubble = {
role: 'assistant', content: agentReply, relayError,
a1Decision, scenario, notRunning: !ep?.reachable, ts: Date.now(),
};
setChatHistory(h => [...h, botBubble]);
setChatSending(false);
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(chatInput, null); }
}
const ep = probeStatus?.endpoint;
if (!open) return h('button', {
className: 'btn btn-s btn-sm',
style: { fontSize: 'var(--fxs)', marginTop: 6, background: 'rgba(99,102,241,.1)', borderColor: 'rgba(99,102,241,.4)', color: 'var(--accent)', fontWeight: 600 },
onClick: () => setOpen(true),
}, '💬 Test ' + agent.name + ' via A1');
return h('div', { style: { marginTop: 10, border: '1px solid rgba(99,102,241,.35)', borderRadius: 'var(--r)', overflow: 'hidden', background: 'var(--b1)' } },
h('div', { style: { padding: '10px 14px', background: 'rgba(99,102,241,.1)', borderBottom: '1px solid rgba(99,102,241,.2)', display: 'flex', alignItems: 'center', gap: 10 } },
h('div', { style: { flex: 1 } },
h('div', { style: { fontWeight: 700, fontSize: 'var(--fsm)', color: 'var(--accent)' } }, '💬 ' + agent.name + ' — via A1'),
h('div', { style: { fontSize: 'var(--fxs)', color: 'var(--t2)', marginTop: 2 } },
probeStatus === 'checking' ? '⏳ Probing for ' + agent.name + '…' :
ep?.reachable ? '🟢 ' + agent.name + ' live on port ' + ep.port + ' · A1 routing active' :
probeStatus?.reachable === false ? '🟡 ' + agent.name + ' not running — start it to get real replies' :
'Click a test below to start'
)
),
h('button', { className: 'btn btn-sm', style: { fontSize: 'var(--fxs)' }, onClick: () => { setOpen(false); setProbeStatus(null); } }, '✕')
),
h('div', { style: { padding: '8px 14px', background: 'var(--b2)', borderBottom: '1px solid var(--b3)', fontSize: 'var(--fxs)', color: 'var(--t2)', lineHeight: 1.6 } },
'Real proof = A1 authorizes the request ',
h('strong', { style: { color: 'var(--green)' } }, 'AND'),
' the agent replies. ',
h('strong', { style: { color: 'var(--green)' } }, '✓ Behaving correctly'),
' only shows when both steps complete. ',
h('strong', { style: { color: '#ef4444' } }, 'Red = A1 blocks before agent ever sees it.')
),
h('div', { style: { padding: '10px 14px', display: 'flex', flexWrap: 'wrap', gap: 7, borderBottom: '1px solid var(--b3)' } },
TEST_SCENARIOS.map(sc => h('button', {
key: sc.id, disabled: chatSending, onClick: () => sendMessage(sc.message, sc),
title: sc.hint,
style: {
fontSize: 'var(--fxs)', padding: '5px 11px', borderRadius: 20, cursor: chatSending ? 'not-allowed' : 'pointer',
border: '1px solid ' + (sc.expect === 'allowed' ? 'rgba(34,197,94,.4)' : sc.expect === 'blocked' ? 'rgba(239,68,68,.4)' : 'rgba(202,138,4,.4)'),
background: sc.expect === 'allowed' ? 'rgba(34,197,94,.08)' : sc.expect === 'blocked' ? 'rgba(239,68,68,.07)' : 'rgba(202,138,4,.08)',
color: sc.expect === 'allowed' ? 'var(--green)' : sc.expect === 'blocked' ? '#ef4444' : '#ca8a04',
fontWeight: 500, opacity: chatSending ? 0.5 : 1, transition: 'opacity .15s',
},
}, sc.icon + ' ' + sc.label))
),
h('div', {
style: { minHeight: 120, maxHeight: 400, overflowY: 'auto', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 14 },
ref: el => { if (el) el.scrollTop = el.scrollHeight; },
},
chatHistory.length === 0 && h('div', { style: { color: 'var(--t3)', fontSize: 'var(--fxs)', textAlign: 'center', padding: '24px 0', lineHeight: 1.9 } },
h('div', { style: { fontSize: 30, marginBottom: 8 } }, '🧪'),
h('div', null, 'Pick a test command above, or type your own message.'),
h('div', null, 'You\'ll see A1\'s real policy decision and the agent\'s real reply side-by-side.')
),
chatHistory.map((msg, i) => {
if (msg.role === 'user') return h('div', { key: i, style: { display: 'flex', justifyContent: 'flex-end' } },
h('div', { style: { maxWidth: '80%', display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 3 } },
msg.scenario && h('div', { style: { fontSize: 10, color: 'var(--t3)', paddingRight: 2 } },
'Test: ' + msg.scenario.icon + ' ' + msg.scenario.label + ' · expects ' + msg.scenario.expect.toUpperCase()),
h('div', {
style: { padding: '9px 13px', borderRadius: '14px 14px 4px 14px', fontSize: 'var(--fxs)', lineHeight: 1.55, background: 'var(--accent)', color: '#fff' }
}, msg.content)
)
);
const dec = msg.a1Decision;
const auth = dec?.authorized;
const sc = msg.scenario;
const fullChainProven = auth === true && !msg.notRunning && !!msg.content && !msg.relayError;
const matchesExpect = sc ? (
(sc.expect === 'allowed' && fullChainProven) ||
(sc.expect === 'blocked' && auth === false) ||
(sc.expect === 'approval' && auth === false)
) : null;
const decColor = auth === true ? 'var(--green)' : auth === false ? '#ef4444' : '#ca8a04';
const decBg = auth === true ? 'rgba(34,197,94,.09)' : auth === false ? 'rgba(239,68,68,.08)' : 'rgba(202,138,4,.08)';
const decBorder = auth === true ? 'rgba(34,197,94,.28)' : auth === false ? 'rgba(239,68,68,.22)' : 'rgba(202,138,4,.22)';
const decLabel = auth === true ? '✅ A1 AUTHORIZED' : auth === false ? '🚫 A1 BLOCKED' : auth === null ? '⚠️ A1 UNREACHABLE' : '⏳ A1 PENDING';
const pendingRelay = sc?.expect === 'allowed' && auth === true && !msg.content && !msg.notRunning;
return h('div', { key: i, style: { display: 'flex', flexDirection: 'column', gap: 6, maxWidth: '88%' } },
h('div', { style: { border: '1px solid ' + decBorder, borderRadius: 'var(--r)', overflow: 'hidden', background: decBg } },
h('div', { style: { padding: '7px 11px', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', borderBottom: dec?.reason ? '1px solid ' + decBorder : 'none' } },
h('span', { style: { fontWeight: 700, fontSize: 'var(--fxs)', color: decColor } }, decLabel),
sc && h('code', { style: { fontFamily: 'var(--mono)', fontSize: 10, color: 'var(--t3)', background: 'rgba(0,0,0,.06)', padding: '1px 5px', borderRadius: 3 } }, sc.tool),
matchesExpect === true && !msg.notRunning && !!msg.content && h('span', { style: { fontSize: 10, fontWeight: 600, color: 'var(--green)' } }, '· behaving correctly ✓'),
matchesExpect === false && h('span', { style: { fontSize: 10, fontWeight: 600, color: '#f59e0b' } }, '· unexpected result'),
auth === true && msg.notRunning && h('span', { style: { fontSize: 10, color: '#f59e0b', fontWeight: 600 } }, '· ⚠ relay skipped — agent not running')
),
dec?.reason && h('div', { style: { padding: '5px 11px 7px', fontSize: 'var(--fxs)', color: 'var(--t2)', lineHeight: 1.5 } }, dec.reason),
sc?.hint && !dec?.reason && h('div', { style: { padding: '0 11px 7px', fontSize: 10, color: 'var(--t3)', fontStyle: 'italic' } }, sc.hint)
),
h('div', { style: { padding: '9px 13px', borderRadius: '4px 14px 14px 14px', fontSize: 'var(--fxs)', lineHeight: 1.6, background: 'var(--b2)', color: 'var(--t1)', border: '1px solid var(--b3)' } },
auth === false
? h('span', { style: { color: '#ef4444', fontStyle: 'italic' } },
'🚫 A1 blocked this before it reached ' + agent.name + '. The agent never saw this request.')
: msg.notRunning
? h('div', null,
h('div', { style: { color: 'var(--t2)', marginBottom: 8, fontSize: 'var(--fxs)', lineHeight: 1.5 } },
'🟡 A1 authorized the request but ',
h('strong', null, agent.name + ' is not running'),
' — so the relay step can\'t complete. Start the agent to get real replies and full proof.'),
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
h('button', {
className: 'btn btn-p btn-sm',
disabled: starting,
onClick: startAgent,
style: { fontWeight: 700, fontSize: 'var(--fxs)', background: 'linear-gradient(135deg,var(--accent),#7c3aed)', borderColor: 'transparent' },
}, starting ? '⏳ Starting…' : '▶ Start ' + agent.name),
h('span', { style: { fontSize: 'var(--fxs)', color: 'var(--t3)' } }, 'or run'),
h('code', { style: { fontFamily: 'var(--mono)', background: 'var(--b1)', padding: '1px 6px', borderRadius: 3, fontSize: 'var(--fxs)' } }, agent.id + ' start'),
h('span', { style: { fontSize: 'var(--fxs)', color: 'var(--t3)' } }, 'in terminal')
),
startResult && !startResult.success && h('div', { style: { marginTop: 6, fontSize: 'var(--fxs)', color: '#f59e0b' } },
'⚠ Couldn\'t auto-start. Run: ',
h('code', { style: { fontFamily: 'var(--mono)' } }, startResult.restart_cmd || agent.id + ' start')
),
startResult?.success && h('div', { style: { marginTop: 6, fontSize: 'var(--fxs)', color: 'var(--green)' } },
'✓ Start signal sent — re-probing for ' + agent.name + '…'
)
)
: msg.content
? h('span', null, msg.content)
: h('span', { style: { color: 'var(--t3)', fontStyle: 'italic' } }, msg.relayError || 'No reply received.')
),
h('div', { style: { fontSize: 10, color: 'var(--t3)', paddingLeft: 2 } },
fullChainProven
? '🔐 /v1/studio/check + /v1/agents/relay · both real · no simulation'
: auth === false
? '🔐 /v1/studio/check · blocked before relay · agent never saw this'
: msg.notRunning
? '🔐 /v1/studio/check · relay SKIPPED — start ' + agent.name + ' for full two-step proof'
: msg.relayError
? '🔐 /v1/studio/check · relay attempted but failed'
: '🔐 /v1/studio/check only · relay not attempted'
)
);
}),
chatSending && h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, color: 'var(--t2)', fontSize: 'var(--fxs)' } },
h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: 'var(--accent)', opacity: 0.7, animation: 'pulse 1s ease-in-out infinite' } }),
ep?.reachable ? 'Calling A1 + relaying to ' + agent.name + '…' : 'Calling A1…'
)
),
h('div', { style: { padding: '10px 14px', borderTop: '1px solid var(--b3)', display: 'flex', gap: 8, background: 'var(--b2)' } },
h('input', {
className: 'inp',
placeholder: 'Type any command — A1 decides if ' + agent.name + ' is allowed to act on it…',
value: chatInput,
onChange: e => setChatInput(e.target.value),
onKeyDown: handleKey,
disabled: chatSending,
style: { flex: 1, fontSize: 'var(--fsm)' },
}),
h('button', {
className: 'btn btn-p btn-sm', disabled: chatSending || !chatInput.trim(), onClick: () => sendMessage(chatInput, null),
style: { whiteSpace: 'nowrap' },
}, chatSending ? '…' : 'Send →')
),
h('div', { style: { padding: '6px 14px 8px', fontSize: 10, color: 'var(--t3)', textAlign: 'center' } },
'All calls hit real A1 endpoints · /v1/studio/check + /v1/agents/relay · no mocks · no simulation'
)
);
}
function ConnectAgents() {
const { api, settings } = useContext(Ctx);
const gwUrl = settings.gwUrl || 'http://localhost:8080';
const [agents, setAgents] = useState(null);
const [passports, setPassports] = useState([]);
const [scanning, setScanning] = useState(false);
const [results, setResults] = useState({});
const [connecting, setConnecting] = useState({});
const [customInput, setCustomInput] = useState('');
const [customSugs, setCustomSugs] = useState([]);
const [, forceUpdate] = useState(0);
async function load() {
setScanning(true);
const [agR, ppR] = await Promise.all([
api('GET', '/v1/agents/scan'),
fetch(gwUrl + '/v1/passports/list').then(r => r.json()).catch(() => ({ passports: [] })),
]);
if (agR.ok) setAgents(agR.data.agents || []);
else setAgents([]);
setPassports(ppR.passports || []);
setScanning(false);
}
useEffect(() => { load(); }, []);
useEffect(() => {
function onPassportChange() { load(); }
window.addEventListener('a1-passport-changed', onPassportChange);
return () => window.removeEventListener('a1-passport-changed', onPassportChange);
}, []);
function passportFor(agent) {
const needle = (agent.namespace || agent.id || agent.name || '')
.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return passports.find(p => {
const ns = (p.namespace || '').toLowerCase();
return ns === needle || ns === agent.id || p.filename === needle + '.json';
}) || null;
}
async function connect(agent) {
setConnecting(p => ({ ...p, [agent.id]: true }));
const r = await api('POST', '/v1/agents/connect', {
agent_id: agent.id,
install_path: agent.install_path || undefined,
});
setConnecting(p => ({ ...p, [agent.id]: false }));
setResults(p => ({ ...p, [agent.id]: r.ok ? r.data : {
connected: false,
message: r.data?.error || 'Connection failed.',
next_step: 'Make sure A1 gateway is running, then try again.',
}}));
if (r.ok && r.data.connected) setTimeout(load, 800);
}
async function disconnect(agent) {
setConnecting(p => ({ ...p, [agent.id]: true }));
const r = await api('POST', '/v1/agents/disconnect', {
agent_id: agent.id,
install_path: agent.install_path || undefined,
});
setConnecting(p => ({ ...p, [agent.id]: false }));
setResults(p => ({ ...p, [agent.id]: r.ok && r.data?.success
? { connected: false, message: r.data.message || 'Disconnected. Restart your agent to apply.' }
: { connected: false, message: r.data?.message || 'Could not auto-disconnect. Remove the A1 entry from your config manually.' }
}));
setTimeout(load, 900);
}
function protectAgent(agent) {
const name = (agent.name || agent.id || '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
const agentType = agent.id || 'other';
window.dispatchEvent(new CustomEvent('a1-navigate', {
detail: { tab: 'wizard', prefill: { name, agentType, caps: ['files.read', 'web.search'] } }
}));
}
function detectCustomAgent(text) {
const t = text.toLowerCase();
const sug = [];
if (t.match(/langchain|crewai|autogen|llamaindex|langgraph|semantic.kernel/))
sug.push({ icon: '🤝', label: 'AI Integration recommended', desc: 'Claude reads your files and adds @a1_guard automatically.', tab: 'integrate' });
if (t.match(/\.mcp\.json|mcp.*server|model context/))
sug.push({ icon: '⚡', label: 'MCP config (zero code)', desc: 'One line in .mcp.json and you\'re done.', snippet: '{"mcpServers":{"a1":{"type":"http","url":"http://localhost:8080/mcp"}}}' });
if (t.match(/python|\.py|flask|fastapi/))
sug.push({ icon: '🐍', label: 'Python snippet ready', desc: 'pip install a1 → @a1_guard decorator.', tab: 'wizard' });
if (t.match(/typescript|javascript|node|\.ts|\.js/))
sug.push({ icon: '📘', label: 'TypeScript/Node snippet ready', desc: 'npm install a1 → withA1Passport wrapper.', tab: 'wizard' });
if (t.match(/go|golang|\.go/))
sug.push({ icon: '🐹', label: 'Go snippet ready', desc: 'go get github.com/dyologician/a1/sdk/go/a1', tab: 'wizard' });
if (t.match(/rust|cargo|\.rs/))
sug.push({ icon: '⚙', label: 'Rust snippet ready', desc: 'cargo add a1 → DyoloPassport::guard_local()', tab: 'wizard' });
if (t.match(/rest|http|curl|api|webhook/))
sug.push({ icon: '🌐', label: 'REST API (any language)', desc: 'POST /v1/authorize — works from anything.', tab: 'wizard' });
if (sug.length === 0 && text.trim().length > 3)
sug.push({ icon: '🤝', label: 'Use AI Integration', desc: 'Describe your agent files and get working code.', tab: 'integrate' });
setCustomSugs(sug);
}
const found = (agents || []).filter(a => a.install_path);
const missing = (agents || []).filter(a => !a.install_path);
return h('div', { style: { paddingBottom: 40, width: '100%' } },
h(ProtectionStatusBanner, { gwUrl }),
h(NudgeTip, { tipKey: 'test_after_connecting' }),
h('div', { style: { marginBottom: 20 } },
h('h2', { style: { fontSize: 18, fontWeight: 700, marginBottom: 4 } }, '🔌 Connect Your AI Agent'),
h('p', { style: { color: 'var(--t2)', fontSize: 'var(--fsm)', lineHeight: 1.6, marginBottom: 12 } },
'A1 scans for installed AI agents and connects them in one click. ',
h('strong', null, 'IronClaw is recommended — install and connect it for the full A1 experience.')),
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center' } },
h('button', { className: 'btn btn-s', onClick: load, disabled: scanning },
scanning ? 'Scanning…' : '↺ Rescan'),
scanning && h('span', { style: { color: 'var(--t2)', fontSize: 'var(--fxs)' } }, 'Checking your system...')
)
),
agents === null && h('div', { className: 'empty' }, 'Scanning your system for AI agents...'),
agents !== null && found.length > 0 && h('div', { className: 'sg' },
h('div', { className: 'sg-head' }, '✓ Detected on your system (' + found.length + ')'),
h('div', { className: 'sg-body' },
found.map(ag => {
const pp = passportFor(ag);
const days = pp?.days_remaining;
const urgent = days !== null && days !== undefined && days < 7;
return h('div', { key: ag.id, className: 'ag-card' + (ag.connected ? ' connected' : '') + (ag.recommended ? ' recommended' : '') },
ag.recommended && h('div', { style: { fontSize: 10, fontWeight: 700, color: 'var(--accent)', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 4 } }, '★ Recommended'),
h('div', { className: 'ag-icon' }, ag.icon),
h('div', { className: 'ag-info' },
h('div', { className: 'ag-name' },
ag.name,
ag.connected
? h('span', { className: 'ag-badge ok' }, '✓ connected')
: h('span', { className: 'ag-badge found' }, 'found'),
pp && urgent && h('span', {
className: 'ag-badge',
style: { background: days < 0 ? 'rgba(239,68,68,.12)' : 'rgba(202,138,4,.12)', color: days < 0 ? '#ef4444' : '#ca8a04', border: '1px solid ' + (days < 0 ? 'rgba(239,68,68,.3)' : 'rgba(202,138,4,.3)') }
}, days < 0 ? 'passport expired' : 'expires soon')
),
h('div', { className: 'ag-desc' }, ag.description),
ag.install_path && h('div', { className: 'ag-path' }, ag.install_path),
h('div', { className: 'ag-hint' }, ag.connect_hint),
results[ag.id] && h('div', { className: 'ag-result ' + (results[ag.id].connected ? 'ok' : 'err') },
h('div', { style: { fontWeight: 600, marginBottom: 3 } }, results[ag.id].connected ? '✓ Connected!' : '✗ Failed'),
h('div', null, results[ag.id].message),
results[ag.id].next_step && h('div', { style: { marginTop: 4, fontStyle: 'italic', color: 'var(--t2)' } }, '→ ' + results[ag.id].next_step)
),
ag.connected
? pp
? h(AgentPassportPanel, { passport: pp, gwUrl, agentName: ag.name, onRenewed: () => { notifyPassportChange(); load(); }, onRevoked: () => { notifyPassportChange(); load(); } })
: h(NoPassportWarning, { agent: ag, onProtect: () => protectAgent(ag) })
: !pp && h(PassportProblemGuide, {
issue: 'none', agentName: ag.name,
onRenewClick: () => {},
onProtectClick: () => protectAgent(ag),
}),
ag.connected && h(AgentLiveProof, { agent: ag, gwUrl }),
),
h('div', { className: 'ag-actions' },
ag.connected
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 5, alignItems: 'flex-end' } },
h('span', { style: { color: pp ? 'var(--green)' : '#f59e0b', fontSize: 'var(--fxs)', fontFamily: 'var(--mono)' } }, pp ? 'Active ✓' : 'Connected · No Passport ⚠'),
pp && h('span', { style: { fontSize: 'var(--fxs)', color: daysColor(days), fontWeight: urgent ? 700 : 400 } }, daysLabel(days)),
!pp && h('button', {
className: 'btn btn-p btn-sm',
onClick: () => protectAgent(ag),
style: { whiteSpace: 'nowrap', fontSize: 'var(--fxs)', fontWeight: 700 },
}, '🛡 Protect this agent'),
h('button', {
className: 'btn btn-sm',
style: { fontSize: 'var(--fxs)', color: 'var(--t2)', border: '1px solid var(--b1)', background: 'none', borderRadius: 'var(--r)', padding: '3px 8px', cursor: 'pointer' },
disabled: connecting[ag.id],
onClick: () => disconnect(ag),
}, connecting[ag.id] ? 'Disconnecting…' : 'Disconnect'),
h(RestartAgentButton, { agent: ag, gwUrl }),
ag.uninstall_cmd && h(RemoveButton, { agent: ag, gwUrl, onRemoved: load }),
)
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 5, alignItems: 'flex-end' } },
h('button', {
className: 'btn btn-p btn-sm',
disabled: connecting[ag.id],
onClick: () => connect(ag),
style: { whiteSpace: 'nowrap' },
}, connecting[ag.id] ? 'Connecting…' : 'Connect →'),
!pp && h('button', {
className: 'btn btn-s btn-sm',
onClick: () => protectAgent(ag),
style: { whiteSpace: 'nowrap', fontSize: 'var(--fxs)' },
}, '🛡 Protect this agent'),
ag.uninstall_cmd && h(RemoveButton, { agent: ag, gwUrl, onRemoved: load }),
),
h('a', { href: ag.homepage, target: '_blank', rel: 'noopener', style: { fontSize: 'var(--fxs)', color: 'var(--t2)', textDecoration: 'none' } }, 'Docs ↗')
)
);
})
)
),
agents !== null && missing.length > 0 && h('div', { className: 'sg', style: { marginTop: 12 } },
h('div', { className: 'sg-head' }, 'Not installed on your system'),
h('div', { className: 'sg-body' },
missing.map(ag => h('div', { key: ag.id, className: 'ag-card not-found' + (ag.recommended ? ' recommended' : '') },
ag.recommended && h('div', { style: { fontSize: 10, fontWeight: 700, color: 'var(--accent)', letterSpacing: 1, textTransform: 'uppercase', marginBottom: 4, gridColumn: '1/-1' } }, '★ Recommended — Install IronClaw first'),
h('div', { className: 'ag-icon' }, ag.icon),
h('div', { className: 'ag-info' },
h('div', { className: 'ag-name' }, ag.name, h('span', { className: 'ag-badge miss' }, 'not detected')),
h('div', { className: 'ag-desc' }, ag.description),
(ag.install_cmd_unix || ag.install_cmd_win) && h('div', { style: { fontSize: 'var(--fxs)', color: 'var(--t3)', fontFamily: 'var(--mono)', marginTop: 4 } },
'$ ' + ag.install_cmd_unix),
h(InstallPanel, { agent: ag, gwUrl, onInstalled: () => setTimeout(load, 1500) })
),
h('div', { className: 'ag-actions' },
h('a', { href: ag.homepage, target: '_blank', rel: 'noopener', className: 'btn btn-s btn-sm', style: { whiteSpace: 'nowrap' } }, 'Docs ↗')
)
))
)
),
agents !== null && h('div', { className: 'sg', style: { marginTop: 12 } },
h('div', { className: 'sg-head' }, 'My agent isn\'t in the list'),
h('div', { className: 'sg-body' },
h('p', { style: { color: 'var(--t2)', fontSize: 'var(--fsm)', marginBottom: 10, lineHeight: 1.6 } },
'Describe your agent — A1 detects the right integration path automatically.'),
h('input', {
className: 'inp',
placeholder: 'e.g. "Python LangChain bot", "Node.js OpenAI agent", "Go REST service"',
value: customInput,
onChange: e => { setCustomInput(e.target.value); detectCustomAgent(e.target.value); },
}),
customSugs.length > 0 && h('div', { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 } },
customSugs.map((s, i) => h('div', { key: i, style: { padding: '10px 12px', border: '1px solid var(--b3)', borderRadius: 'var(--r)', background: 'var(--b1)', display: 'flex', gap: 10, alignItems: 'flex-start' } },
h('span', { style: { fontSize: 22, flexShrink: 0 } }, s.icon),
h('div', { style: { flex: 1 } },
h('div', { style: { fontWeight: 600, fontSize: 'var(--fsm)', marginBottom: 3 } }, s.label),
h('div', { style: { color: 'var(--t2)', fontSize: 'var(--fxs)', lineHeight: 1.5, marginBottom: s.tab || s.snippet ? 6 : 0 } }, s.desc),
s.snippet && h('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
h('code', { style: { fontFamily: 'var(--mono)', fontSize: 'var(--fxs)', background: 'var(--b2)', padding: '3px 7px', borderRadius: 4, flex: 1, wordBreak: 'break-all' } }, s.snippet),
h('button', { className: 'btn btn-s btn-sm', onClick: () => navigator.clipboard.writeText(s.snippet) }, 'Copy')
),
s.tab && h('button', { className: 'btn btn-p btn-sm', style: { fontSize: 'var(--fxs)' },
onClick: () => window.dispatchEvent(new CustomEvent('a1-navigate', { detail: s.tab })) }, 'Open →')
)
))
),
customSugs.length === 0 && h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 10 } },
[
{ icon: '⚡', title: 'Option A — MCP config (zero code)', body: 'One JSON block and any MCP-compatible agent is connected.', snippet: '{"mcpServers":{"a1":{"type":"http","url":"http://localhost:8080/mcp"}}}' },
{ icon: '🤝', title: 'Option B — AI Integration', body: 'Claude reads your files and writes the integration code for you.', tab: 'integrate' },
{ icon: '📋', title: 'Option C — Code snippet', body: 'Python, TypeScript, Go, Rust, or REST — generated instantly.', tab: 'wizard' },
].map(o => h('div', { key: o.title, style: { padding: '10px 12px', border: '1px solid var(--b1)', borderRadius: 'var(--r)', background: 'var(--s3)' } },
h('div', { style: { fontWeight: 600, fontSize: 'var(--fsm)', marginBottom: 3 } }, o.icon + ' ' + o.title),
h('div', { style: { color: 'var(--t2)', fontSize: 'var(--fxs)', lineHeight: 1.6, marginBottom: o.snippet || o.tab ? 6 : 0 } }, o.body),
o.snippet && h('div', { style: { display: 'flex', gap: 6 } },
h('code', { style: { fontFamily: 'var(--mono)', fontSize: 'var(--fxs)', background: 'var(--s1)', padding: '6px 10px', borderRadius: 'var(--r)', lineHeight: 1.8, flex: 1, wordBreak: 'break-all' } }, o.snippet),
h('button', { className: 'btn btn-s btn-sm', onClick: () => navigator.clipboard.writeText(o.snippet) }, 'Copy')
),
o.tab && h('button', { className: 'btn btn-p btn-sm', style: { marginTop: 4 },
onClick: () => window.dispatchEvent(new CustomEvent('a1-navigate', { detail: o.tab })) }, 'Open →')
))
)
)
),
agents !== null && h('div', { className: 'wiz-info', style: { marginTop: 16 } },
h('span', { style: { fontSize: 18 } }, '💡'),
h('div', null,
h('div', { style: { fontWeight: 600, marginBottom: 3 } }, 'How the connection works'),
h('div', { style: { color: 'var(--t2)', lineHeight: 1.6, fontSize: 'var(--fxs)' } },
'Clicking "Connect" writes a config file (.mcp.json or a1_plugin.toml) to the agent directory. The agent picks it up on next run. "Prove Live Control" runs real tests — binary check, config file read, and live A1 policy enforcement — to confirm genuine A1 control.')
)
),
h(GuidedNext, { currentTab: 'agents' })
);
}