// Wiring only (the logic is spa.js): sign-in, the agent form and transcript, the confirm dialog, the // MCP servers and their publishing through the ingress, and a form per lib action. import { BrowserOAuthClient } from '@atproto/oauth-client-browser' import { secp256k1 } from '@noble/curves/secp256k1.js' import * as age from 'age-encryption' import { attestHost } from '@publicdomainrelay/atlibs/attest/index.js' import { createDidKey, ingressHost } from '@publicdomainrelay/atlibs/didkey/index.js' import { argumentsOf } from '@publicdomainrelay/atlibs/mcp/index.js' import { pageClientId, requestScope } from '@publicdomainrelay/atlibs/oauth/index.js' import { createPluginContext } from '@publicdomainrelay/atlibs/plugin-context/index.js' import { WebrtcHost } from '@publicdomainrelay/atlibs-webrtc/index.js' import { recordSignals } from '@publicdomainrelay/atlibs-webrtc/record-signals.js' import { LIBS } from './config.js' import { ANTHROPIC, agentComposition, cocoreModel, cocoreUsable, connectedOf, keptFetchers, loadAll, mcpServers, publicMcp, remoteServers, rows, runAction, runAgent, serveIngress, sessionRepo, systemPrompt, vmSignals, vmsAfter } from './spa.js' const $ = (selector) => document.querySelector(selector) const el = (tag, props = {}, children = []) => { const node = Object.assign(document.createElement(tag), props) node.append(...children) return node } const fail = (error) => ($('#error').textContent = error ? String(error.message ?? error) : '') // Secret settings are sealed in this browser (plugin-context): the API key, the ingress did:key and the // requester's attestation key. managed_records: the records this host saw a module create (release deletes them). const ctx = createPluginContext({ id: 'atlibs-agent', storage: { managed_records: {} }, settingsSchema: { apiKey: { type: 'secret' }, didkeys: { type: 'secret' }, attestKeys: { type: 'secret' } } }) const base = new URL('./', import.meta.url) const { modules, policy } = await loadAll(ctx, base) // One sign-in with every session's scope; on 127.0.0.1 the loopback client, deployed the one `atlibs stage --target spa --name` wrote. const client = await BrowserOAuthClient.load({ clientId: await pageClientId(requestScope(policy, Object.keys(policy.sessions))), handleResolver: 'https://bsky.social' }) const session = (await client.init().catch(fail))?.session $('#login').hidden = Boolean(session) $('#who').hidden = !session if (session) $('#who code').textContent = session.did $('#login').onsubmit = async (e) => { e.preventDefault() await client.signIn(new FormData(e.target).get('handle')).catch((error) => ($('#login-error').textContent = error.message)) } $('#logout').onclick = async (e) => { e.preventDefault() await client.revoke(session.did).catch(() => undefined) location.reload() } // --- confirm: one question at a time (the agent calls tools in parallel) let asking = Promise.resolve() const ask = ({ tool, server, arguments: args, caller }) => { const dialog = $('#confirm') dialog.querySelector('.who').textContent = caller ? `A remote MCP client (${caller})` : 'The agent' dialog.querySelector('.tool').textContent = `${server} ${tool}` dialog.querySelector('.args').textContent = JSON.stringify(args, null, 2) dialog.showModal() return new Promise((resolve) => { for (const button of dialog.querySelectorAll('button')) button.onclick = () => (dialog.close(), resolve(button.value === 'allow')) dialog.oncancel = () => resolve(false) }) } const confirm = (question) => (asking = asking.then(() => ask(question), () => ask(question))) // --- VMs: the requester signs with an attestation key kept in this browser; the webrtc transport signals the // guest through requester runs (as you) and fetches webrtc: URLs (a kept VM's MCP shell) over datachannels. const host = { ctx, policy, session } const requesterRun = async (entry, input) => { const { result, error } = await runAction(host, modules.requester, entry, input) if (error !== undefined) throw new Error(`requester ${entry}: ${error}`) return result } let webrtc if (session) { // a bidder's webrtc endpoint (offering endpoints): offers and answers as computer.socialweb.ingress records const records = recordSignals({ repo: sessionRepo(ctx, session), subhandle: 'requester' }) webrtc = new WebrtcHost({ ...vmSignals(requesterRun), records, makePeer: (config) => new RTCPeerConnection(config), age }) host.vm = { attest: attestHost(ctx, secp256k1, session.did), calls: webrtc.calls, fetchers: keptFetchers(webrtc.fetchers(), () => vms), peers: webrtc.fetchers() } } // the VMs kept: vm_list's (one record in your repo), then each vm tool's run let vms = [] const observe = (server, entry, args, ran) => { const next = vmsAfter(vms, server, entry, args, ran) if (next === vms) return vms = next renderVms() } function renderVms() { $('#vms').replaceChildren( ...vms.map((v) => { const out = el('small') const release = el('button', { className: 'outline', textContent: 'Release' }) release.onclick = async () => { release.setAttribute('aria-busy', 'true') const args = { vm: v.vm } const ran = await runAction(host, modules['vm-mcp'], 'vm_release', args).catch((error) => ({ error: error.message })) release.removeAttribute('aria-busy') out.textContent = ran.error ?? `released: ${JSON.stringify(ran.result)}` observe('vm', 'vm_release', args, ran) } return el('li', {}, [el('strong', { textContent: `${v.vm} ` }), `${v.cpus} vCPUs, ${v.mem} GiB from `, el('code', { textContent: v.bidder ?? '' }), ` since ${v.created}${v.expired ? ', expired' : ` until ${v.expires}`} `, release, ' ', out]) }), ) $('#vms-none').hidden = vms.length > 0 } renderVms() if (session) runAction(host, modules['vm-mcp'], 'vm_list', {}).then((ran) => observe('vm', 'vm_list', {}, ran), () => {}) const showConnected = (servers) => $('#connected').replaceChildren(...servers.map((s) => el('li', {}, [`${s.name} `, el('code', { textContent: s.url ?? s.hook ?? '' }), ` · ${s.phase ?? 'connected'}${s.error ? ` (${s.error})` : ''} · ${(s.tools ?? []).length} tools${s.dynamic ? ' · connected by the agent' : ''}`]))) const servers = mcpServers({ ctx, policy, modules, session, confirm, vm: host.vm, observe }) const composition = await agentComposition(ctx, modules.asyncai, Object.keys(servers)) // --- the agent const form = $('#agent') const SAVED = ['provider', 'endpoint', 'format', 'model', 'maxSteps', 'system', 'remote'] for (const name of SAVED) form[name].value = (await ctx.settings.get(name)) ?? form[name].value ?? '' // The switch: co/core (no key) or a custom endpoint (base URL, format, key). A saved Anthropic choice is that custom endpoint. if (form.provider.value === 'anthropic') (form.provider.value = 'custom'), (form.endpoint.value ||= ANTHROPIC) // co/core from localhost only (its CORS): elsewhere the switch stays on custom, greyed out, and says why if (!cocoreUsable(location.hostname)) { if (form.provider.value === 'cocore') form.provider.value = 'custom' $('#use-custom').disabled = true $('#cocore-cors').hidden = false } const providerShown = () => (($('#use-custom').checked = form.provider.value !== 'cocore'), ($('#custom').hidden = $('#key').hidden = form.provider.value === 'cocore')) $('#use-custom').onchange = (e) => ((form.provider.value = e.target.checked ? 'custom' : 'cocore'), providerShown()) providerShown() const keyState = async () => { const saved = Boolean(await ctx.settings.get('apiKey')) $('#key-state').textContent = saved ? 'a key is saved in this browser (type to replace it)' : '' $('#forget-key').hidden = !saved } $('#forget-key').onclick = async (e) => (e.preventDefault(), await ctx.settings.delete('apiKey'), keyState()) await keyState() let memory = [] let stop function show(list, note = '') { $('#transcript').replaceChildren( ...list.map(({ role, kind, text, error }) => el('li', { className: `${kind}${error ? ' error' : ''}` }, [el('small', { textContent: `${role} · ${kind}${error ? ' · error' : ''}` }), el('pre', { textContent: text })])), ) $('#status').textContent = note } form.onsubmit = async (e) => { e.preventDefault() let remote try { remote = remoteServers(form.remote.value, Object.keys(servers)) } catch (error) { return show(rows(memory), error.message) } for (const name of SAVED) await ctx.settings.set(name, form[name].value) if (form.apiKey.value) await ctx.settings.set('apiKey', form.apiKey.value) form.apiKey.value = '' await keyState() stop = new AbortController() $('#run').setAttribute('aria-busy', 'true') $('#stop').disabled = false try { const options = { prompt: form.prompt.value, system: form.system.value || systemPrompt(session?.did), provider: form.provider.value, endpoint: form.endpoint.value, format: form.format.value, model: form.provider.value === 'cocore' && !form.model.value ? await cocoreModel(host, modules) : form.model.value, apiKey: form.provider.value === 'cocore' ? undefined : await ctx.settings.get('apiKey'), maxSteps: Number(form.maxSteps.value) || undefined, remote, messages: memory } const ran = await runAgent(ctx, composition, servers, options, { signal: stop.signal, fetchers: host.vm?.fetchers, host, progress: (list) => (show(list, 'running...'), showConnected(connectedOf(list))) }) const r = ran.result if (r?.messages) memory = r.messages if (r?.servers) showConnected(r.servers.filter((s) => s.dynamic)) const mcp = (r?.servers ?? []).map((s) => `${s.name}: ${s.phase}${s.error ? ` (${s.error})` : ''}`).join(', ') show(rows(memory), [ran.error ?? r?.error, r && `stop: ${r.stop}, steps: ${r.steps}, usage: ${JSON.stringify(r.usage)}`, mcp && `MCP: ${mcp}`].filter(Boolean).join(' | ')) form.prompt.value = '' } catch (error) { show(rows(memory), error.message) } finally { $('#run').removeAttribute('aria-busy') $('#stop').disabled = true } } $('#stop').onclick = () => stop?.abort() $('#forget').onclick = () => show((memory = [])) // --- the servers, each tool public or not; publishing serves them all at https:///mcp/ const publicTools = new Set() const tools = async (name) => (await servers[name].message({ jsonrpc: '2.0', id: 'ui', method: 'tools/list' })).result.tools let published = '' const renderServers = async () => { const sections = [] for (const name of Object.keys(servers)) { const items = (await tools(name)).map((tool) => { const key = `${name} ${tool.name}` const box = el('input', { type: 'checkbox', checked: publicTools.has(key), onchange: (e) => (e.target.checked ? publicTools.add(key) : publicTools.delete(key)) }) const write = !servers[name].readOnly(tool.name) return el('li', {}, [el('label', {}, [box, ` ${tool.name} `, el('small', { className: write ? 'write' : '', textContent: write ? 'changes your data' : 'read-only', title: tool.description })])]) }) const url = published && `https://${published}/mcp/${name}` sections.push(el('details', {}, [el('summary', {}, [`${name} (${items.length} tools)`, ...(url ? [' ', el('code', { textContent: url })] : [])]), el('ul', {}, items)])) } $('#servers').replaceChildren(...sections) } for (const name of Object.keys(servers)) for (const tool of await tools(name)) if (servers[name].readOnly(tool.name)) publicTools.add(`${name} ${tool.name}`) await renderServers() const forward = publicMcp(servers, (server, tool) => publicTools.has(`${server} ${tool}`)) const logged = async (request) => { const started = performance.now() const response = await forward(request) const method = request.body && typeof request.body === 'object' ? request.body.method : /"method"\s*:\s*"([^"]+)"/.exec(request.body ?? '')?.[1] $('#log').prepend(el('li', { textContent: `${new Date().toISOString().slice(11, 19)} ${request.method} ${request.path} ${method ?? ''} -> ${response.status} (${Math.round(performance.now() - started)} ms)${request.callerDid ? ` ${request.callerDid}` : ''}` })) while ($('#log').childElementCount > 200) $('#log').lastChild.remove() return response } let serving $('#publish').onchange = async (e) => { if (!e.target.checked) return serving?.abort() // The did:key lives in this browser, never in the wasm. const keys = (await ctx.settings.get('didkeys')) ?? {} const key = await createDidKey(keys.ingress) if (!keys.ingress) await ctx.settings.set('didkeys', { ...keys, ingress: key.jwk }) published = ingressHost(key.did, modules.ingress.didkey.services[0]) await renderServers() serving = new AbortController() const ran = await serveIngress(ctx, modules.ingress, key, logged, { signal: serving.signal }) e.target.checked = false published = '' await renderServers() if (!serving.signal.aborted) fail(`the ingress stopped: ${ran.error ?? 'done'}`) } // --- a form per action, from module.json for (const lib of LIBS) { const section = el('details', {}, [el('summary', { textContent: lib.name })]) for (const mod of lib.modules.map((m) => modules[m])) { section.append(el('h4', { textContent: `${mod.name}: ${mod.description ?? ''}` })) for (const action of mod.actions ?? []) { const out = el('pre', { className: 'out' }) const fields = (action.fields ?? []).map(({ key, label }) => el('label', { textContent: label }, [['changes', 'prompt', 'bio', 'user_data'].includes(key) ? el('textarea', { name: key, rows: 3 }) : el('input', { name: key })])) const actionForm = el('form', {}, [el('h5', { textContent: action.label }), el('small', { textContent: action.description ?? '' }), ...fields, el('button', { textContent: action.label }), out]) actionForm.onsubmit = async (e) => { e.preventDefault() const button = actionForm.querySelector('button') button.setAttribute('aria-busy', 'true') out.textContent = '' try { // typed as module.json says (a list or an object as JSON text) const { args, problems } = argumentsOf(action.fields, { ...lib.defaults?.[action.entry], ...Object.fromEntries([...new FormData(actionForm)].filter(([, v]) => String(v).trim() !== '')) }) if (problems) throw new Error(problems.join('; ')) // and the lib's defaults the fields do not declare (vm_create's via) const ran = await runAction(host, mod, action.entry, { ...lib.defaults?.[action.entry], ...args }) out.textContent = ran.error ?? JSON.stringify(ran.result, null, 2) observe(lib.name, action.entry, args, ran) } catch (error) { out.textContent = error.message } finally { button.removeAttribute('aria-busy') } } section.append(actionForm) } } $('#actions').append(section) } document.body.dataset.ready = 'true'