johnandersen777.bsky.social/atlibs-socialweb-computer-app
app.socialweb.computer: socialweb.computer in one page, one atlibs composition
0
0
// 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
let run = runner(ctx, app, session)
if (session) {
const vm = { attest: attestHost(ctx, secp256k1, me) }
run = runner(ctx, app, session, vm)
const records = recordSignals({ repo: sessionRepo(ctx, session), subhandle: 'requester' })
const webrtc = new WebrtcHost({ ...vmSignals(must(run)), records, makePeer: (config) => new RTCPeerConnection(config), age })
Object.assign(vm, { 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. <a href="#signin">Sign in</a>' }))
// --- 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()