// Wiring only: the session, the transport, the views. The logic is app.js; chat is atlibs-atirc (vendor/). 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 { 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 { QUICK, bidderDids, channelOf, groupBidders, guestOf, liveness, loadApp, must, paramDefault, runner, sessionRepo, vmSignals } from './app.js' import { atirc } from './vendor/atlibs-atirc.js' const $ = (s) => document.querySelector(s) const el = (tag, props = {}, children = []) => { const node = Object.assign(document.createElement(tag), props) node.append(...children.filter((c) => c !== undefined && c !== null)) return node } const status = (text = '') => ($('#status').textContent = text) const fail = (error) => { $('#error').hidden = !error $('#error').textContent = error ? String(error.message ?? error) : '' } const busy = async (button, fn) => { button?.setAttribute('aria-busy', 'true') if (button) button.disabled = true try { fail() return await fn() } catch (error) { fail(error) } finally { button?.removeAttribute('aria-busy') if (button) button.disabled = false } } // --- header: theme and menu, as exec.fedfork.com's const root = document.documentElement const dark = matchMedia('(prefers-color-scheme: dark)') const isDark = () => (root.dataset.theme ? root.dataset.theme === 'dark' : dark.matches) const themed = () => document.querySelectorAll('[data-theme-toggle]').forEach((b) => b.setAttribute('aria-checked', String(isDark()))) themed() dark.addEventListener('change', themed) for (const b of document.querySelectorAll('[data-theme-toggle]')) b.onclick = () => { root.dataset.theme = isDark() ? 'light' : 'dark' try { localStorage.setItem('theme', root.dataset.theme) } catch {} themed() } const header = $('.site-header') const menu = $('[data-menu-toggle]') const openMenu = (on) => (header.classList.toggle('is-open', on), menu.setAttribute('aria-expanded', String(on))) menu.onclick = () => openMenu(menu.getAttribute('aria-expanded') !== 'true') // --- views: one section per hash const views = {} function show() { const id = (location.hash.slice(1) || 'running').split('?')[0] const view = document.getElementById(id)?.hasAttribute('data-view') ? id : 'running' document.querySelectorAll('[data-view]').forEach((s) => s.classList.toggle('is-active', s.id === view)) document.querySelectorAll('.site-nav a[href^="#"]').forEach((a) => (a.getAttribute('href') === `#${view}` ? a.setAttribute('aria-current', 'page') : a.removeAttribute('aria-current'))) openMenu(false) views[view]?.() } addEventListener('hashchange', show) // --- session: OAuth (one sign-in, every module session's scope); a test may set __ATLIBS_SESSION__ first const ctx = createPluginContext({ id: 'app-socialweb-computer', storage: { managed_records: {} }, settingsSchema: { apiKey: { type: 'secret' }, attestKeys: { type: 'secret' } } }) const base = new URL('./', import.meta.url) const app = await loadApp(ctx, base) let session = globalThis.__ATLIBS_SESSION__ let client if (!session) { client = await BrowserOAuthClient.load({ clientId: await pageClientId(requestScope(app.stage, Object.keys(app.stage.sessions))), handleResolver: 'https://bsky.social' }).catch((e) => (fail(e), undefined)) session = (await client?.init().catch(fail))?.session } const me = session?.did $('#sign').textContent = me ? 'Sign out' : 'Sign in' $('#sign').onclick = async (e) => { if (!me) return e.preventDefault() await client?.revoke(me).catch(() => undefined) location.reload() } $('#login').onsubmit = (e) => busy(e.submitter, async () => { e.preventDefault() await client.signIn(new FormData(e.target).get('handle')) }) // --- the transport: the requester signs with an attestation key kept here; WebRTC to the VMs' shells and the // bidders' webrtc endpoints, signalled through requester runs (as you) and computer.socialweb.ingress records // the transport signals through requester runs, and those runs need the transport: the runner comes last let run = runner(ctx, app, session) if (session) { const records = recordSignals({ repo: sessionRepo(ctx, session), subhandle: 'requester' }) const webrtc = new WebrtcHost({ ...vmSignals(must((...a) => run(...a))), records, makePeer: (config) => new RTCPeerConnection(config), age }) run = runner(ctx, app, session, { attest: attestHost(ctx, secp256k1, me), calls: webrtc.calls, fetchers: webrtc.fetchers() }) } const call = must(run) // --- names: DIDs shown as handles (public profiles) const handles = new Map() async function named(dids) { const want = [...new Set(dids)].filter((d) => d?.startsWith('did:') && !handles.has(d)) for (let i = 0; i < want.length; i += 25) { const q = want.slice(i, i + 25).map((d) => `actors=${encodeURIComponent(d)}`).join('&') const r = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfiles?${q}`).then((r) => r.json(), () => ({})) for (const p of r.profiles ?? []) if (p.handle && p.handle !== 'handle.invalid') handles.set(p.did, p.handle) } } const who = (did) => handles.get(did) ?? (did ? `${did.slice(0, 18)}…${did.slice(-4)}` : '') const signedOut = (node) => node.replaceChildren(el('p', { className: 'empty', innerHTML: 'Sign in to see this. Sign in' })) // --- Running let vms = [] let agents = [] const card = (title, meta, children = []) => el('article', { className: 'card' }, [el('h3', { textContent: title }), ...meta.map((m) => el('p', { className: 'meta', textContent: m })), ...children]) const bidderCard = (r) => { const beat = liveness(r.beatAt) const ends = (r.endpoints ?? []).map((e) => e.kind).join(', ') || 'none' return card(who(r.did), [r.did, `operator ${r.operator ? who(r.operator) : r.claimedOperator ? `${who(r.claimedOperator)} (unconfirmed)` : 'none'}`, `endpoints ${ends}`, `bids on ${(r.appliesTo ?? []).join(', ') || 'nothing'}`], [ el('span', { className: `pill ${r.withdrawn ? 'pill--off' : beat.live ? 'pill--on' : 'pill--bad'}`, textContent: r.withdrawn ? 'withdrawn' : `heartbeat ${beat.age}` }), ]) } const vmCard = (v) => card(v.vm, [`${v.cpus} vCPUs, ${v.mem} GiB from ${who(v.bidder)}`, `since ${v.created}`, v.expired ? 'expired' : `until ${v.expires}`], [ el('div', { className: 'toolbar' }, [ el('a', { className: 'btn btn--small btn--ghost', href: `#shell?vm=${v.vm}`, textContent: 'Shell' }), el('button', { className: 'btn btn--small btn--danger', textContent: 'Release', onclick: (e) => busy(e.target, async () => { status(`releasing ${v.vm}…`) await call('vm', 'vm_release', { vm: v.vm }) status(`${v.vm} released`) await loadVms() views.running() }), }), ]), ]) const fill = (node, items, empty) => node.replaceChildren(...(items.length ? items : [el('p', { className: 'empty', textContent: empty })])) async function loadVms() { vms = me ? ((await call('vm', 'vm_list', {})).vms ?? []) : [] return vms } async function loadAgents() { agents = me ? ((await call('agent', 'list', {})).agents ?? []) : [] return agents } let vouched = new Map() async function loadVouches() { const list = me ? ((await call('vouches', 'list', {})).vouches ?? []) : [] vouched = new Map(list.filter((v) => v.kind !== 'denounce').map((v) => [v.subject, v])) return list } views.running = () => busy($('#refresh-running'), async () => { if (!me) return [$('#my-bidders'), $('#my-vms'), $('#vouched-bidders')].forEach(signedOut) status('reading the market…') await Promise.all([loadVouches(), loadVms()]) // the bidders you follow, and those each account you vouch for follows (its computer.socialweb.bidders/self) const known = await Promise.all([me, ...vouched.keys()].slice(0, 50).map((of) => call('status', 'known', { of }).then((r) => r.bidders ?? [], () => []))) const dids = bidderDids(paramDefault(app.manifest, 'bidders'), known.flat(), me, [...vouched.keys()]) const rows = (await call('status', 'bidders', { dids })).bidders ?? [] await named([...rows.flatMap((r) => [r.did, r.operator, r.claimedOperator]), ...vms.map((v) => v.bidder)]) const groups = groupBidders(rows, me, new Set(vouched.keys())) fill($('#my-bidders'), groups.mine.map(bidderCard), 'No bidders of yours. Follow one you run by its DID.') fill($('#my-vms'), vms.map(vmCard), 'No VMs kept: creating an agent rents one.') fill($('#vouched-bidders'), groups.vouched.map(bidderCard), 'No bidders run by accounts you vouch for.') status('') }) $('#refresh-running').onclick = () => views.running() $('#track').onsubmit = (e) => busy(e.submitter, async () => { e.preventDefault() await call('status', 'track', { bidder: new FormData(e.target).get('bidder') }) e.target.reset() views.running() }) // --- Vouches views.vouches = () => busy(null, async () => { if (!me) return signedOut($('#vouch-list')) const list = await loadVouches() await named(list.map((v) => v.subject)) fill( $('#vouch-list'), list.map((v) => card(who(v.subject), [v.kind === 'denounce' ? 'denounced' : 'vouched', v.reason ?? '', v.createdAt ?? ''].filter(Boolean), [ el('div', { className: 'toolbar' }, [ el('button', { className: 'btn btn--small btn--danger', textContent: 'Unvouch', onclick: (e) => busy(e.target, async () => (await call('vouches', 'unvouch', { subject: v.subject }), views.vouches())), }), ]), ]), ), 'You have not vouched for anyone yet.', ) }) $('#vouch').onsubmit = (e) => busy(e.submitter, async () => { e.preventDefault() const f = new FormData(e.target) const r = await call('vouches', 'vouch', { subject: f.get('subject'), reason: f.get('reason') || undefined }) status(`vouched for ${r.subject}`) e.target.reset() views.vouches() }) // --- Agents const steps = (text) => $('#launch-steps').append(el('li', { textContent: text })) const agentCard = (a) => card(a.nick, [a.subhandle, `on ${a.vm ?? 'a VM'}`, a.channels.join(' ')], [ el('div', { className: 'toolbar' }, [ el('a', { className: 'btn btn--small', href: `#chat?c=${encodeURIComponent(a.channels[1])}`, textContent: 'Chat 1:1' }), el('a', { className: 'btn btn--small btn--ghost', href: `#chat?c=${encodeURIComponent(a.channels[0])}`, textContent: 'Your channel' }), el('a', { className: 'btn btn--small btn--ghost', href: `#shell?vm=${a.vm}`, textContent: 'Shell' }), el('button', { className: 'btn btn--small btn--danger', textContent: 'Stop', onclick: (e) => busy(e.target, async () => (await call('agent', 'stop', { nick: a.nick }), views.agents())), }), ]), ]) views.agents = () => busy(null, async () => { if (!me) return signedOut($('#agent-list')) await Promise.all([loadAgents(), loadVms()]) $('#vm').replaceChildren(el('option', { value: '', textContent: 'a new VM' }), ...vms.filter((v) => !v.expired && !agents.some((a) => a.vm === v.vm)).map((v) => el('option', { value: v.vm, textContent: `${v.vm} (${v.cpus} vCPUs)` }))) fill($('#agent-list'), agents.map(agentCard), 'No agents yet.') const form = $('#launch') for (const k of ['base_url', 'model']) form[k].value ||= (await ctx.settings.get(k)) ?? '' form.api_key.placeholder = (await ctx.settings.get('apiKey')) ? 'a key is saved in this browser' : 'your API key' }) $('#launch').onsubmit = (e) => busy(e.submitter, async () => { e.preventDefault() const f = Object.fromEntries(new FormData(e.target)) const api_key = f.api_key || (await ctx.settings.get('apiKey')) if (!api_key) throw new Error('an API key, please') await Promise.all([ctx.settings.set('apiKey', api_key), ctx.settings.set('base_url', f.base_url), ctx.settings.set('model', f.model)]) $('#launch-steps').replaceChildren() let v = vms.find((x) => x.vm === f.vm) if (!v) { steps('renting a VM (a bid, the receipt, the guest: a few minutes)…') v = await call('vm', 'vm_create', {}) steps(`${v.vm} is up: ${v.cpus} vCPUs from ${who(v.bidder)}`) } steps(`installing atirc and starting ${f.nick} on ${v.vm}…`) const a = await call('agent', 'launch', { vm: v.vm, shell: v.shell, expires: v.expires, nick: f.nick, model: f.model, base_url: f.base_url, api_key, harness: f.harness || undefined }) steps(`${a.subhandle} is on the mesh (units: ${a.units}); it says hello in ${a.channels[1]} once its harness is up`) e.target.nick.value = '' await views.agents() location.hash = `#chat?c=${encodeURIComponent(a.channels[1])}` }) // --- Chat: atlibs-atirc to your node on an agent's VM (its contract's "atirc" WebRTC tunnel); your channel through the newest agent's const clients = new Map() // vm -> client let unsubscribe const clientFor = (a) => { if (!clients.has(a.vm)) clients.set(a.vm, atirc({ guests: [guestOf(a.shell, vmSignals(call))], age })) return clients.get(a.vm) } const via = (channel) => [...agents].reverse().find((a) => a.channels.includes(channel)) views.chat = () => busy(null, async () => { if (!me) return signedOut($('#messages')) if (!agents.length) await loadAgents() const channels = [...new Set(agents.flatMap((a) => a.channels))] const want = new URLSearchParams(location.hash.split('?')[1] ?? '').get('c') ?? channels[0] $('#channel').replaceChildren(...channels.map((c) => el('option', { value: c, textContent: c, selected: c === want }))) if (!channels.length) return fill($('#messages'), [], 'No agents yet: create one, then chat here.') await openChannel($('#channel').value) }) async function openChannel(channel) { const a = via(channel) const client = clientFor(a) unsubscribe?.() $('#messages').replaceChildren() const seen = new Set() unsubscribe = client.onMessage((m) => { if (m.error) return status(`chat: ${m.error}`) if (m.channel !== channel || seen.has(`${m.identity} ${m.seq} ${m.ts}`)) return seen.add(`${m.identity} ${m.seq} ${m.ts}`) const mine = m.self || m.identity?.startsWith('you-') const li = el('li', { className: mine ? 'mine' : '' }, [el('span', { className: 'nick', textContent: mine ? 'you' : m.nick ?? m.identity }), m.text, el('time', { textContent: new Date(m.ts).toLocaleTimeString() })]) $('#messages').append(li) li.scrollIntoView({ block: 'end' }) }) status(`joining ${channel} through ${a.vm}…`) await client.join(channel) const peers = await client.peers(channel).catch(() => null) $('#peers').textContent = peers ? `${(peers.peers ?? []).length} peers` : '' status('') } $('#channel').onchange = (e) => busy(null, () => openChannel(e.target.value)) $('#post').onsubmit = (e) => busy(e.submitter, async () => { e.preventDefault() const channel = $('#channel').value await clientFor(via(channel)).post(channel, e.target.text.value) e.target.reset() }) // --- Shell: commands on a VM you keep (vm-mcp vm_exec, over its MCP shell) const term = (cls, text) => { $('#terminal').append(el('span', { className: cls, textContent: text })) $('#terminal').scrollTop = $('#terminal').scrollHeight } $('#quick').replaceChildren(...QUICK.map(([label, command]) => el('button', { className: 'btn btn--small btn--ghost', textContent: label, onclick: (e) => exec(e.target, command) }))) async function exec(button, command) { const vm = $('#shell-vm').value if (!vm) return fail('no VM: create an agent (it rents one)') term('cmd', `${vm}$ ${command}\n`) await busy(button, async () => { const r = await call('vm', 'vm_exec', { vm, command }) if (r.stdout) term('', r.stdout) if (r.stderr) term('err', r.stderr) if (r.exit) term('err', `exit ${r.exit}\n`) }) } $('#exec').onsubmit = (e) => (e.preventDefault(), exec(e.submitter, e.target.command.value), e.target.reset()) views.shell = () => busy(null, async () => { if (!me) return signedOut($('#terminal')) await loadVms() const want = new URLSearchParams(location.hash.split('?')[1] ?? '').get('vm') $('#shell-vm').replaceChildren(...vms.filter((v) => !v.expired).map((v) => el('option', { value: v.vm, textContent: v.vm, selected: v.vm === want }))) }) // --- Tiles: this page as the mothership (web tiles), the running summary as a tile let tiles views.tiles = () => busy(null, async () => { tiles ??= await import('./tiles.js').then((m) => m.mothership()) const open = (url) => busy(null, async () => { const state = { session: me ? 'signed-in' : 'signed-out', handle: who(me), vms, agents, vouches: [...vouched.values()] } $('#tile').replaceChildren(await tiles.show(url, state)) }) fill($('#tile-list'), tiles.list.map((t) => card(t.name, [t.description], [el('div', { className: 'toolbar' }, [el('button', { className: 'btn btn--small', textContent: 'Open', onclick: () => open(t.url) })])])), '') }) // --- atlibs: the composition this page runs views.atlibs = async () => { const lock = await fetch(new URL('atlibs.lock.json', base)).then((r) => r.json(), () => ({ modules: {} })) const edges = app.manifest.edges ?? [] fill( $('#instances'), app.manifest.instances.map((i) => { const m = lock.modules?.[i.module] const session = app.stage.modules[i.module]?.session return card(i.name, [`module ${m?.ref ?? i.module}`, m?.pin ? `pin ${m.pin.slice(0, 16)}…` : 'unpublished (staged from its repo)', session ? `scope ${app.stage.sessions[session].scope}` : 'no session: public reads', ...edges.filter((e) => e.from === i.name).map((e) => `${e.nsid} → ${e.to}`)]) }), '', ) } if (session) status(`signed in as ${me}`) $('#who').textContent = me ? `Signed in as ${me}` : '' document.body.dataset.ready = 'true' show()