// The page's logic, DOM-free: load the staged composition, run an instance's entry as the user (each // instance held to its own module's OAuth scope), the webrtc transport's signals, and what the views show. import { bindParameters, loadComposition, runCompose, staged } from '@publicdomainrelay/atlibs/compose/index.js' import { policyCall } from '@publicdomainrelay/atlibs/oauth/index.js' import { hostCall, sessionXrpc } from '@publicdomainrelay/atlibs/run-module/host-call.js' // a run ends when it is done: vm_create waits minutes for the bid, the receipt, the guest const BUDGET = { calls: Infinity, events: Infinity, wallMs: 1_800_000 } /** stage.json (sessions, scopes) and the composition `atlibs stage --target spa` staged beside the page. */ export async function loadApp(ctx, base, fetchImpl = globalThis.fetch) { const json = async (name) => (await fetchImpl(new URL(name, base))).json() const [stage, manifest] = await Promise.all([json('stage.json'), json('composition.json')]) const bound = bindParameters(manifest) const composition = await loadComposition(ctx, bound, { load: staged(base, fetchImpl) }) return { stage, manifest: bound, composition } } /** A composition parameter's bound value (bindParameters wrote defaults into instance inputs). */ export const paramDefault = (manifest, key) => manifest.parameters?.find((p) => p.key === key)?.values?.default /** * run(instance, entry, input) -> { result, error }: runCompose as the user. Each instance's calls go through * policyCall (its module's session); signed out, public reads only. vm: { attest, calls, fetchers }: the * attestation key and the webrtc transport (webrtc.connect, and webrtc: URLs for process-mcp's http.fetch and the * bidders' market endpoints). */ export function runner(ctx, { stage, composition }, session, vm = {}) { const host = hostCall({ ctx, canHeader: false, fetchers: vm.fetchers }) const calls = session && Object.fromEntries(Object.keys(stage.sessions).map((name) => [name, hostCall({ ctx, xrpc: sessionXrpc(session), canHeader: false, attest: vm.attest, calls: vm.calls, fetchers: vm.fetchers })])) const callFor = (inst) => (session ? policyCall(stage, inst.module, calls) : host) return (instance, entry, input = {}, { signal, onEvent } = {}) => runCompose(ctx, composition, instance, entry, { ...input, ...(session && { did: session.did }) }, { callFor, signal, onEvent, budget: BUDGET, http: { 'webrtc://*': {} }, fetchers: vm.fetchers }) } /** As run, but the result or a thrown error. */ export const must = (run) => async (instance, entry, input, options) => { const ran = await run(instance, entry, input, options) if (ran.error !== undefined) throw new Error(`${instance} ${entry}: ${ran.error}`) return ran.result } /** * atlibs-webrtc recordSignals' repo for a session: reads from each repo's own PDS; the offer (a fresh TID * rkey) and its delete as the user. */ export function sessionRepo(ctx, session) { const read = hostCall({ ctx, canHeader: false }) const xrpc = sessionXrpc(session) return { did: session.did, getRecord: ({ repo, collection, rkey }) => read('com.atproto.repo.getRecord', { params: { repo, collection, rkey }, service: repo }).catch((e) => (/RecordNotFound|not found|404/i.test(e.message) ? null : Promise.reject(e))), putRecord: ({ collection, rkey, record }) => xrpc('com.atproto.repo.createRecord', { input: { repo: session.did, collection, rkey, record } }), deleteRecord: ({ collection, rkey }) => xrpc('com.atproto.repo.deleteRecord', { input: { repo: session.did, collection, rkey } }), } } /** WebrtcHost's events and signal as requester (instance machine) runs: the guest's reports, the sealed offer. */ export const vmSignals = (run) => ({ events: async ({ receipt, tag }) => { const bidder = receipt.bidder ?? receipt.uri.split('/')[2] return (await run('machine', 'watch', { bidder, receipt: receipt.uri, tags: [tag], max_polls: 1 })).events }, signal: (input) => run('machine', 'signal', input), }) /** atirc's channel for a handle or subhandle. */ export const channelOf = (name) => `#${name.slice(0, 63).replace(/[.,\s]/g, '_').toLowerCase()}` /** * atlibs-atirc's guest for a VM you keep: its contract receipt (from the shell URL's ref, base64url JSON * {receipt: {uri, cid}, bidder}) and the requester's watch and signal (vmSignals) for its "atirc" tunnel. */ export function guestOf(shell, signals) { const ref = shell.split('/')[3] ?? '' const { receipt, bidder } = JSON.parse(atob(ref.replace(/-/g, '+').replace(/_/g, '/'))) if (!receipt?.uri || !receipt?.cid) throw new Error(`no receipt in ${shell.slice(0, 40)}`) return { receipt: { ...receipt, ...(bidder && { bidder }) }, ...signals } } /** A heartbeat's age: live within 5 minutes. */ export function liveness(beatAt, now = Date.now()) { const t = Date.parse(beatAt ?? '') if (Number.isNaN(t)) return { live: false, age: 'no heartbeat' } const s = Math.max(0, Math.round((now - t) / 1000)) const age = s < 120 ? `${s}s ago` : s < 7200 ? `${Math.round(s / 60)}m ago` : s < 172800 ? `${Math.round(s / 3600)}h ago` : `${Math.round(s / 86400)}d ago` return { live: s < 300, age } } /** * The Running view's bidder groups from market-status rows: yours (you, or an operator confirmed to be you) * and those of accounts you vouch for (the bidder, or its confirmed operator, vouched). */ export function groupBidders(rows, me, vouched) { const who = (r) => r.operator ?? r.did const has = (r) => r.offering || r.heartbeat return { mine: rows.filter((r) => has(r) && who(r) === me), vouched: rows.filter((r) => has(r) && who(r) !== me && vouched.has(who(r))), } } /** The DIDs whose bidders the Running view reads: the composition's, yours followed, you, and those you vouch for. */ export const bidderDids = (defaults, known, me, vouched) => [...new Set([...(defaults ?? []), ...known, me, ...vouched].filter((d) => d?.startsWith('did:')))].slice(0, 50) /** Quick shell commands (the Shell view's buttons). */ export const QUICK = [ ['uname', 'uname -a'], ['uptime', 'uptime'], ['disk', 'df -h /'], ['memory', 'free -m'], ['agents', 'systemctl --no-pager --type=service list-units "atirc-*"'], ['agent log', 'journalctl --no-pager -n 40 -u "atirc-agent-*"'], ]