1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// 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://<did:key host>/mcp/<name>
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'