Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437// How the browser writes a record: by running the CLI.//// Not a browser reimplementation of it — the actual `runCli()` from `@radial/sidecar`, with the same// argument vector `radial goal create` takes at a shell. That layer is already pure (its Node// dependencies live in `cli.ts` and `socket.ts`, never in `commands.ts`), so the record this tab// writes is constructed by the same code the daemon and the terminal construct theirs with. It is// the write half of the claim §1 makes about the fold: the UI is a second implementation of Radial,// not a client of one.//// What the browser supplies is the four seams `CliDependencies` declares://// - `writer` the human's own OAuth session (`auth.svelte.ts`), never a shared credential// - `resolver` the space's read transport, so a `--project` argument is pinned to the CID that// was actually read rather than to whatever the URI names later// - `resolveActor` handle → DID, asked of the actor's own PDS// - `warn` soft policy (design §7) arriving as a toast rather than as stderr//// There is deliberately no `readText`: a browser has no filesystem, so `--body-file` and friends// simply do not exist here and the inline spellings are what the compose cards produce.//// And deliberately no `hasApprovingReview`. The CLI supplies it because a shell has no index and has// to go and materialize the space to answer; it can only warn *after* the record is written. This tab// is already holding the fold, so `requests.ts` answers the same question before the write — on the// button and on the card, where a warning can still change what a human does. Wiring it here would// only make the app say the same sentence a third time, to somebody who has already read it twice.//// 6.7 adds two more, and they go the other way, because they are not warnings://// - `resolveMembership` the grant a `member remove` revokes. A removal that named a stale grant is// one the fold drops, so this must be the grant the index holds.// - `findArtifactType` the duplicate-name guard `type create` refuses on. Unlike soft policy this// is a REFUSAL, and a refusal that lives only in a form is a rule the form// owns rather than one the command does. The browser is meant to be a second// implementation of Radial, so the command's own guard is armed here too —// the registry form says it earlier and better, and this is what makes it// true rather than merely displayed.//// Both are answered out of the fold this tab is already holding rather than off the network: the// CLI's versions of them go and look because a shell has nothing to look in.//// A PRIVATE space swaps two of those seams and adds no third path (design §18, ADR §11). `writer`// becomes the `EnvelopeWriter` this tab's replica was opened with — it seals, admits through gate 1// and commits into the local store, because there is no PDS to be the authority — and `resolver`// becomes `privateResolver`, since a strongref argument names a record that exists only here. Every// command above them is the same code: `runCli(['goal','create',…])` does not know which bus it is// on, which is the property the seam exists for.
import { FetchRepoTransport, ForeignStrongRefResolver, StrongRefResolver, XrpcError, parseAtUri, resolveHandleDid, type RepoReadTransport,} from '@radial/atproto'import { privateBlobRef, type Collection, type RecordByCollection, type StoredRecord, type StrongRef,} from '@radial/core'import { privateResolver, runCli, type CliDependencies, type CommandResult, type RecordWriter,} from '@radial/sidecar'import { signedIn, type Credentials } from './auth.svelte.js'import { Identities } from './identity.js'import { altFromName, imageMarkdown, readImageFile, type PickedFile } from './images.js'import type { PrivateReplica } from './private-space.js'import { privateReplica, publishPrivateWrite, publishWritten, session, spaceTransport,} from './session.svelte.js'import { toast } from './ui.svelte.js'
/** * The seams every command runs through, whichever transport is behind them. Split out because * `createSpace` is the one write with no space open, and so with no space transport to borrow. */function dependencies( credentials: Credentials, transport: RepoReadTransport, writer: RecordWriter = credentials.writer,): CliDependencies { return { writer, resolver: new StrongRefResolver(transport), foreignResolver: new ForeignStrongRefResolver(transport), // A handle is rebindable and a DID is not, so every actor argument is resolved at write time. // The directory asked is the writer's own PDS: a browser has no DNS, and the identity we are // most certain of is the one that just signed the request. resolveActor: async (identifier) => resolveHandleDid(identifier, { directory: await transport.resolvePds(credentials.did) }), resolveMembership: async (spaceUri, did) => { const index = session.space?.index const grant = index?.space.uri === spaceUri ? index.members.find((member) => member.active && member.did === did)?.membership : undefined if (!grant) throw new Error(`${did} holds no active membership grant in this space`) return grant }, findArtifactType: async (name, spaceUri) => { const index = session.space?.index if (index?.space.uri !== spaceUri) return undefined return index.artifactTypes.find((record) => record.value.name === name)?.uri }, warn: (text) => toast(text), }}
/** * The same seams, pointed at this tab's private replica. * * Three of them change and the rest are shared on purpose: `resolveActor` still asks a PDS, because * a handle is a public fact about an identity whatever space it is used in, and `resolveMembership` * and `findArtifactType` still read `session.space.index` — which IS the private fold when a private * space is open, so they need no private version at all. * * `privateSpace` is the deliberate opening of `runCli`'s guard: it names the one space this writer * may write into, and every command checks the space it is about to touch against it. Without it a * private space is refused; with it, a public one is. That is what makes "an envelope written into * a public space would reach nobody" a refusal rather than a silent loss. */function privateDependencies( credentials: Credentials, transport: RepoReadTransport, replica: PrivateReplica,): CliDependencies { if (!replica.writer) { throw new Error( `This browser holds no signing key for ${credentials.did} in ${replica.spaceUri}, so it cannot ` + 'write into it. Accept the space ticket in this browser to publish one.', ) } return { ...dependencies(credentials, transport, replica.writer), resolver: privateResolver(replica.stores.records, replica.spaceUri), privateSpace: { uri: replica.spaceUri, blobs: replica.stores.blobs }, }}
const OPTIMISTIC_REV = '\uffff-ui-write'
/** * The one write failure a reader cannot act on as reported: their session predates the permission it * needs. * * The scope is derived from `RADIAL_WRITES`, so shipping a new collection — `closeGoal`, most * recently — narrows what an *already issued* token authorizes relative to what the app now writes. * The PDS is right to refuse, and it refuses in its own words ("Bad token scope"), which name nothing * a person can do about it. Signing out and back in re-authorizes with the current scope, so that is * what the message says. * * Detection is by the PDS's own error codes rather than by the prose: `InvalidToken` and * `InsufficientScope` are what a granular-scope refusal arrives as, and the two are checked with the * status so an unrelated 400 keeps its own message. Anything else passes through untouched — a * message the app invented over a failure it did not understand is worse than the server's. */export function writeFailure(error: unknown): unknown { const scopeRefusal = error instanceof XrpcError && (error.status === 401 || error.status === 403) && ['InvalidToken', 'InsufficientScope', 'BadTokenScope'].includes(error.error) if (!scopeRefusal) return error return new Error( 'This sign-in was authorized before Radial asked for this permission — ' + 'sign out and sign in again to grant it. Nothing was written.', )}
/** * Keep the validated values sent by runCli beside the durable refs returned by the PDS. That pair * is everything the fold needs except the repo revision, which is observer-local and reconciled by * the next poll. */function recordingWriter(writer: RecordWriter, did: string, records: StoredRecord[]): RecordWriter { const remember = <K extends Collection>( collection: K, value: RecordByCollection[K], ref: StrongRef, ): StrongRef => { const parts = parseAtUri(ref.uri) records.push({ did, collection, rkey: parts.rkey, uri: ref.uri, cid: ref.cid, rev: OPTIMISTIC_REV, firstSeenAt: new Date().toISOString(), value: structuredClone(value), } as StoredRecord) return ref } return { create: async (collection, value, options) => remember(collection, value, await writer.create(collection, value, options)), ...(writer.put ? { put: async <K extends Collection>( collection: K, uri: string, value: RecordByCollection[K], options?: { swapRecord?: string }, ) => remember(collection, value, await writer.put!(collection, uri, value, options)), } : {}), }}
/** * Run one command and leave the screen showing its result. Once the PDS acknowledges each write, * fold that exact URI, CID and value locally instead of waiting for a second network round trip to * re-read it. The normal poll remains the authority for the repo revision and reconciles it later. */export async function write(args: string[]): Promise<CommandResult> { const credentials = signedIn() if (!credentials) throw new Error('Sign in to write.') const transport = spaceTransport() if (!transport) { throw new Error('The demo is read-only. Open a live space to make changes.') } const replica = privateReplica() if (replica) return writePrivate(args, credentials, transport, replica)
const records: StoredRecord[] = [] const writer = recordingWriter(credentials.writer, credentials.did, records) let result: CommandResult try { result = await runCli(args, dependencies(credentials, transport, writer)) } catch (failure) { // A command can write several records; whatever landed before the refusal is still folded, since // the PDS acknowledged it and the poll would find it anyway. publishWritten(records) throw writeFailure(failure) } publishWritten(records) return result}
/** * One command against a private space. * * There is no `recordingWriter` here, and its absence is the point: the envelope writer already put * the record in this replica's store before it returned the strongref, because the store IS the * commit (`writer.ts`). Replaying an optimistic copy over it would invent a provenance — a repo * revision, and a version with no signing device — for a record that has neither. * * A command that fails halfway leaves whatever it already committed, exactly as the public path * does, so the refold happens either way. */async function writePrivate( args: string[], credentials: Credentials, transport: RepoReadTransport, replica: PrivateReplica,): Promise<CommandResult> { let result: CommandResult try { result = await runCli(args, privateDependencies(credentials, transport, replica)) } catch (failure) { await publishPrivateWrite().catch(() => {}) throw failure } // Awaited, unlike the public path's: this is where a browser that could not make the write durable // says so, and a private record that is not durable is a record that exists nowhere else (§17). await publishPrivateWrite() return result}
export interface UploadedImage { /** The `at://` URI of the image record — the durable name a body refers to. */ uri: string /** ``, ready to go in at the caret. */ markdown: string /** The alt text that was derived, so a composer can select it for editing. */ alt: string}
/** * A file, uploaded and made durable, in the order that makes it durable. * * Validation first, and locally: a file that is the wrong type or over the ceiling is refused before * anything crosses the network, because the alternative is asking somebody's PDS to reject 40 MB of * video on our behalf. Then the blob, then the record — and the record is the point, since atproto * reclaims a blob nothing references (`images.ts`). * * The record goes through `runCli` like every other write in this app, so the value is built by the * same code the CLI builds it with and validated against the same lexicon. What does NOT go through * `runCli` is the upload: a `File` is a browser object, and `CliDependencies` is deliberately the * short list of seams a shell and a tab can both supply. * * A failure after the upload but before the record is reported as a failure, full stop. The bytes * may sit on the PDS for a while, but they are not referenced by anything, so presenting them as an * image somebody now has would be presenting something the PDS is entitled to delete. */export async function uploadImage(file: PickedFile): Promise<UploadedImage> { const credentials = signedIn() if (!credentials) throw new Error('Sign in to add an image — it is stored in your own account.') const transport = spaceTransport() if (!transport) { throw new Error('The demo is read-only. Open a live space to make changes.') } const candidate = await readImageFile(file) const alt = altFromName(candidate.name) const replica = privateReplica() if (replica) return uploadPrivateImage(candidate, alt, credentials, transport, replica)
let blob try { blob = await credentials.uploadBlob(candidate.bytes, candidate.mimeType) } catch (failure) { throw writeFailure(failure) }
const records: StoredRecord[] = [] const writer = recordingWriter(credentials.writer, credentials.did, records) let result: CommandResult try { result = await runCli( ['image', 'create', '--blob', JSON.stringify(blob), '--alt', alt, '--name', candidate.name], dependencies(credentials, transport, writer), ) } catch (failure) { // The blob landed and nothing points at it. Say that plainly: an unreferenced upload is not // storage, and offering a retry is the only honest thing to do with it. throw new Error( `${message(writeFailure(failure))} The image was uploaded but not saved, so nothing was inserted — try again.`, ) } publishWritten(records) return { uri: result.primary.uri, markdown: imageMarkdown(result.primary.uri, alt), alt }}
const message = (error: unknown): string => (error instanceof Error ? error.message : String(error))
/** * The same upload against a private space: the bytes go into this replica, never to a PDS (§18). * * The order is the public path's for the public path's reason, arrived at from the other end. There * a record is written second because atproto reclaims a blob nothing references; here it is written * second because a private `image` record is refused unless the bytes it names are ones this replica * actually holds — a private blob has no PDS to be fetched from later, so a record naming bytes * nobody has is a hole in the corpus rather than a slow load. * * The blob's name is the CID of its bytes, computed here and recomputed by the store: content * addressing is the whole authentication, and it is atproto's own rule, so this record is * byte-identical to the one a public upload would have produced (ADR §15). */async function uploadPrivateImage( candidate: { bytes: Uint8Array; mimeType: string; name: string }, alt: string, credentials: Credentials, transport: RepoReadTransport, replica: PrivateReplica,): Promise<UploadedImage> { const blob = await privateBlobRef(candidate.bytes, candidate.mimeType) await replica.stores.blobs.put(candidate.bytes, blob.ref.$link) let result: CommandResult try { result = await runCli( ['image', 'create', '--blob', JSON.stringify(blob), '--alt', alt, '--name', candidate.name], privateDependencies(credentials, transport, replica), ) } catch (failure) { // The bytes are in this replica and nothing points at them. Unlike an orphaned PDS upload, which // the server eventually reclaims, these stay until the space is forgotten — which is why the // sentence offers the retry rather than implying the picture is somewhere it can be seen. throw new Error( `${message(failure)} The image was stored in this replica but no record names it, so nothing was inserted — try again.`, ) } await publishPrivateWrite() return { uri: result.primary.uri, markdown: imageMarkdown(result.primary.uri, alt), alt }}
/** * The one write that happens outside a space, because it is the write that makes one. * * Everything else in this app targets a record inside a space it has already read, and borrows that * space's transport to pin its arguments. `space create` resolves nothing and pins nothing — it * writes a space record and seeds the registry into the author's own repo — so it brings its own * transport rather than waiting for one to exist. * * There is no refresh afterwards either: there is nothing yet to refresh. The caller opens the space * it just created, which reads the record over the wire like any other, and a space that could not * be re-read is still a space that exists. */export async function createSpace(args: string[]): Promise<CommandResult> { const credentials = signedIn() if (!credentials) throw new Error('Sign in to create a space — it is stored in your own account.') const transport = spaceTransport() ?? new FetchRepoTransport(new Identities().pdsResolver) try { return await runCli(args, dependencies(credentials, transport)) } catch (failure) { throw writeFailure(failure) }}
/** * The same write, into a private space's replica: the genesis records of a space that does not exist * yet (design §18, ADR §23). * * It takes a replica rather than deriving one because on this path the store comes FIRST. A browser's * replica is a database named after the space URI, so the URI has to be minted and the store opened * before the space record can be sealed into it — which is why `space create --private` writes at the * rkey that URI names rather than at a fresh TID (`commands.ts`). `private-mode.ts` does that half; * this is the one line of it that is a write, kept here beside `createSpace` because both are the * same `runCli` invocation over the same argument vector and differ only in which writer refuses. * * The settle is the private path's, for §17's reason and not for tidiness: a genesis record that did * not reach durable storage is the only copy of a space, and a founder told "created" over a store * that dropped it would go on to publish a bookmark naming nothing. */export async function writeGenesis( replica: PrivateReplica, args: string[], /** The transport the founding flow already resolved; a second one would read the same repos. */ transport?: RepoReadTransport,): Promise<CommandResult> { const credentials = signedIn() if (!credentials) throw new Error('Sign in to create a space — its device key is stored in your own account.') const reads = transport ?? spaceTransport() ?? new FetchRepoTransport(new Identities().pdsResolver) const result = await runCli(args, privateDependencies(credentials, reads, replica)) await replica.settle() return result}