Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221// The private-mode commands of the `radial` CLI (design §18, plan phases 5.3–5.4).//// Everything here is the human side of a private space: creating one, minting and accepting the// ticket that bootstraps a member, and keeping this device's keys. The ordinary write commands are// NOT here — `goal create`, `request create`,// `artifact post` and the rest run through the same `runCli()` they always did, over a writer that// seals envelopes instead of posting to a PDS. That is the whole point of the seam, and the reason// this file is short relative to what it enables.//// Two rules run through all of it://// - **Public repos hold the directory and nothing else.** `device`, `deviceAddress`,// `deviceAddress` and the member's own `join` bookmark are written to the actor's ordinary PDS,// because key material carried in an envelope would vouch for itself and a fresh browser has// nothing else to read. Everything else is an envelope.// - **Rotation is local hygiene.** It publishes a new key and retires the old one locally; the fold// never notices and a stolen old key remains trusted while its DID remains a member.
import { type RepoReadTransport, type ResolvedRecord } from '@radial/atproto'import { COLLECTIONS, IMAGE_LIMITS, assertBootstrapTicket, createTidSource, decodeArchive, decodeTicket, deviceKeyStatus, encodeArchive, encodeTicket, ensurePublishedDevice, isImageMimeType, mintTicket, privateBlobRef, retireDeviceAddress, rotateDeviceKey, ticketDirectoryMismatch, ticketFingerprint, type ArchiveHeader, type Collection, type DeviceKeyRecord, type MaterializedIndex, type PrivateTicket, type RadialRecord, type RecordByCollection, type SpacePin, type SpacePinMismatch, type StrongRef,} from '@radial/core'import { FileDeviceKeyStore } from '@radial/core/node'import { runCli, type CliDependencies, type CommandResult } from './commands.js'import { inspectJoinBookmark, privateResolver, writeCheckedJoinBookmark,} from './private-records.js'import { findPrivateSpace, listPrivateSpaces, openPrivateSpace, spaceDirectory, spacePinOf, writeSecretFile, writeSpaceMeta, type OpenPrivateSpace, type PrivateSpaceMeta,} from './private-space.js'
/** The actor's PUBLIC repo, for the four record types a private space still writes to a PDS. */export interface PublicRepoWriter { create<K extends Collection>( collection: K, value: RecordByCollection[K], options?: { rkey?: string }, ): Promise<StrongRef> /** * The sanctioned-edit path, needed for exactly one record: a `deviceAddress` is rewritten in place * to say where a device is now, and rewritten once more to say it is no longer anywhere (ADR §5). */ put<K extends Collection>( collection: K, uri: string, value: RecordByCollection[K], options?: { swapRecord?: string }, ): Promise<StrongRef> getOwnRecord(collection: Collection, rkey: string): Promise<ResolvedRecord | undefined>}
export interface PrivateCliContext { dataDir: string actorDid: string transport: RepoReadTransport publicWriter: PublicRepoWriter readText?: (path: string) => Promise<string> /** Binary file reads, for blobs — the one thing in a private space that is not text (§18). */ readBytes?: (path: string) => Promise<Uint8Array> writeBytes?: (path: string, bytes: Uint8Array) => Promise<void> resolveActor?: (identifier: string) => Promise<string> now?: () => string tid?: () => string log?: (line: string) => void warn?: (line: string) => void}
const nowOf = (context: PrivateCliContext): string => context.now?.() ?? new Date().toISOString()
function values(args: string[], flag: string): string[] { const found: string[] = [] for (let index = 0; index < args.length; index += 1) { if (args[index] === flag && args[index + 1] !== undefined) found.push(args[index + 1] as string) } return found}
const optional = (args: string[], flag: string): string | undefined => values(args, flag).at(-1)
function required(args: string[], flag: string): string { const value = optional(args, flag) if (value === undefined) throw new Error(`Missing ${flag}`) return value}
/** * This device's signing key, published. * * Idempotent in both halves: the key id is derived from the key, so a device that already has one * keeps it, and the `device` record's rkey is that id, so re-running writes nothing new. The order * matters — the key exists locally before its record is published, never the other way round, or a * directory would name a key nothing on this machine can sign with. */export async function ensureDevice( context: PrivateCliContext, options: { kind?: 'node' | 'browser' } = {},): Promise<DeviceKeyRecord> { const result = await ensurePublishedDevice( new FileDeviceKeyStore(context.dataDir), context.publicWriter, context.actorDid, { kind: options.kind ?? 'node', ...(context.now ? { now: context.now } : {}), }, ) if (result.created) { context.log?.(`published device ${result.key.deviceKeyId} (${result.key.kind}) at ${result.ref.uri}`) } return result.key}
/** Everything a command needs to write into one private space through `runCli`. */export interface PrivateCommandContext { space: OpenPrivateSpace deps: CliDependencies close(): void}
function dependenciesFor( context: PrivateCliContext, space: OpenPrivateSpace,): CliDependencies { if (!space.writer) { throw new Error( `this machine holds no signing key for ${context.actorDid}; run "radial device rotate" to mint one`, ) } const index = (): MaterializedIndex => space.index() return { writer: space.writer, resolver: privateResolver(space.records, space.spaceUri), privateSpace: { uri: space.spaceUri, blobs: space.blobs }, ...(context.now ? { now: context.now } : {}), ...(context.readText ? { readText: context.readText } : {}), ...(context.resolveActor ? { resolveActor: context.resolveActor } : {}), // Every one of these is a fold over the local store rather than a fetch: a private space has no // PDS to ask, and the store IS what this replica knows. // The space is implied: this writer serves exactly one, and its fold is the local store. resolveMembership: async (_spaceUri: string, did: string) => { const folded = index() const target = folded.members.find((member) => member.did === did) if (!target?.active || !target.membership) throw new Error(`Member is not active: ${did}`) const author = folded.members.find((member) => member.did === context.actorDid) if (context.actorDid !== folded.space.did && (!author?.active || author.role !== 'admin')) { throw new Error('The selected actor is not an active space admin') } return target.membership }, findArtifactType: async (name: string) => index().artifactTypes.find((type) => type.value.name === name)?.uri, hasApprovingReview: async (subject: StrongRef) => [...index().goals, ...index().projects].some((view) => view.reviews.some( (review) => review.value.verdict === 'approve' && review.value.subject.uri === subject.uri && review.value.subject.cid === subject.cid, ), ), ...(context.warn ? { warn: context.warn } : {}), }}
/** * The pin, in the shape `openPrivateSpace` takes it — spread, so a replica without one passes * nothing rather than `undefined` (`exactOptionalPropertyTypes`). */function pinOption(meta: PrivateSpaceMeta): { spacePin?: SpacePin } { const pin = spacePinOf(meta) return pin ? { spacePin: pin } : {}}
/** Open one private space replica and build the CLI dependencies that write into it. */export async function openPrivateCommandContext( context: PrivateCliContext, meta: PrivateSpaceMeta,): Promise<PrivateCommandContext> { const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid, ...(meta.peerHints ?? [])], ...pinOption(meta), ...(context.tid ? { tid: context.tid } : {}), ...(context.now ? { now: context.now } : {}), }) return { space, deps: dependenciesFor(context, space), close: () => space.close() }}
/** * Which private space, if any, a command's references belong to. * * A private record's URI is a NAME and not a location, so nothing in it says which replica holds it * — the only way to find out is to look. Every `at://` argument is matched against each replica this * data directory holds: the space's own URI, or any record in its store. Exactly one match selects * that space; none leaves the command on the public path, where it was before private mode existed. * * Ambiguity is an error rather than a guess. Two replicas holding a record with the same URI would * mean the same author wrote the same rkey into two spaces, and picking one for them would write the * next record into a space the operator did not name. */export async function detectPrivateSpace( context: PrivateCliContext, args: string[],): Promise<PrivateSpaceMeta | undefined> { const locators = args .filter((arg) => arg.startsWith('at://')) .map((arg) => (arg.includes('#') ? arg.slice(0, arg.lastIndexOf('#')) : arg)) if (locators.length === 0) return undefined const matches: PrivateSpaceMeta[] = [] for (const meta of await listPrivateSpaces(context.dataDir)) { if (locators.includes(meta.spaceUri)) { matches.push(meta) continue } const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, }) try { const held = space.records.records() if (locators.some((uri) => held.some((record) => record.uri === uri))) matches.push(meta) } finally { space.close() } } if (matches.length > 1) { throw new Error( `${locators.join(', ')} matches more than one private space on this machine: ` + `${matches.map((meta) => meta.spaceUri).join(', ')}`, ) } return matches[0]}
/** `radial space create --private` — a space whose records never reach a PDS. */export async function createPrivateSpace( context: PrivateCliContext, args: string[],): Promise<CommandResult & { spaceUri: string }> { const key = await ensureDevice(context) const tid = context.tid ?? createTidSource() // The rkey is minted before anything is written, because the replica directory on this machine is // keyed by the space URI: the store has to exist before the space record can be sealed into it. const spaceUri = `at://${context.actorDid}/${COLLECTIONS.space}/${tid()}` const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [context.actorDid], tid, ...(context.now ? { now: context.now } : {}), }) try { // The founder's own device record has to be readable from their public repo before their first // envelope can be verified — by this replica as much as by anybody else's. The directory half // only: there is no space record to fold yet, which is the very thing about to be written. await space.pollDirectory() const result = await runCli(args, dependenciesFor(context, space)) const now = nowOf(context) await writeSpaceMeta(spaceDirectory(context.dataDir, spaceUri), { spaceUri, // The founder pins their own genesis too. Their ticket hands this cid to every invitee, so a // replica that later folded a different space record at this name would be minting tickets // for a space nobody else is in — and the founder is the one observer who could never have // been told about it out of band. spaceCid: result.primary.cid, founder: context.actorDid, protocol: 1, peerHints: [context.actorDid], ...(optional(args, '--label') !== undefined ? { label: optional(args, '--label') as string } : {}), createdAt: now, }) // The founder's own bookmark, public like every member's: it makes the space listable in a // fresh browser, but never creates or bootstraps a replica, and confers nothing (§3). await writeCheckedJoinBookmark( context.publicWriter, { version: 1, space: result.primary, founder: context.actorDid, device: { deviceKeyId: key.deviceKeyId, publicKey: key.publicKey, algorithm: 'ed25519' }, issuedAt: now, }, result.primary, { peerHints: [context.actorDid], now }, ) return { ...result, spaceUri } } finally { space.close() }}
/** `radial ticket mint` — the out-of-band invitation an admin hands over. */export async function mintSpaceTicket( context: PrivateCliContext, args: string[],): Promise<{ ticket: string; fingerprint: string }> { const meta = await requireSpace(context, optional(args, '--space')) const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { // Tickets are handed to a replica with no prior directory. Read the current public directory // before selecting routes so a newly published address is not mistaken for an absent one. await space.pollDirectory() const index = space.indexOrNothing() if (!index) { throw new Error( `this replica of ${meta.spaceUri} holds no space record yet, so there is nothing to invite ` + 'anybody to. Catch up from a peer first.', ) } const key = await new FileDeviceKeyStore(context.dataDir).active(context.actorDid) if (!key) throw new Error(`this machine holds no signing key for ${context.actorDid}`) const relays = values(args, '--relay') const endpointId = optional(args, '--endpoint') const ticket = mintTicket({ space: { uri: index.space.uri, cid: index.space.cid }, // The ticket's founder is the space record's author: a ticket minted by anybody else would // name a repo they do not control, and `assertTicket` refuses it. founder: index.space.did, device: await founderDevice(context, index, meta), peerHints: index.members.filter((member) => member.active).map((member) => member.did) .filter((did, position, all) => all.indexOf(did) === position) .filter((did) => index.devices.some((device) => device.did === did && device.endpointId)) .slice(0, 16), ...(endpointId !== undefined ? { endpointId } : {}), ...(relays.length > 0 ? { relays } : {}), issuedAt: nowOf(context), }) const encoded = encodeTicket(ticket) return { ticket: encoded, fingerprint: await ticketFingerprint(encoded) } } finally { space.close() }}
/** * The device key a ticket should name: the founder's, as the public directory currently publishes * it. * * A ticket is a bootstrap for somebody who can read nothing yet, so it has to name the key that * verifies the space's oldest envelopes — the founder's — even when the person minting it is a * different admin. Read from the index's device view, which is the fold's own answer. */async function founderDevice( context: PrivateCliContext, index: MaterializedIndex, meta: PrivateSpaceMeta,): Promise<{ deviceKeyId: string; publicKey: string; algorithm: 'ed25519' }> { const founder = index.space.did const active = index.devices.find((device) => device.did === founder) if (active) { return { deviceKeyId: active.deviceKeyId, publicKey: active.publicKey, algorithm: 'ed25519' } } if (founder === context.actorDid) { const key = await new FileDeviceKeyStore(context.dataDir).active(context.actorDid) if (key) return { deviceKeyId: key.deviceKeyId, publicKey: key.publicKey, algorithm: 'ed25519' } } throw new Error( `no active device is published for the founder ${founder} of ${meta.spaceUri}; ` + 'sync the directory (radial private sync) or ask them to publish one before minting a ticket', )}
/** * `radial ticket accept` — join a private space from the invitation. * * Accepting grants nothing by itself, and the message says so: membership is an admin's `addMember` * envelope (§3), which this replica will only see once it can reach a peer. What accepting DOES is * everything a replica needs to be reachable and verifiable: a device key published where anyone can * check it, a bookmark that makes the space listable, and a local store with the currently * reachable peers recorded for the first directory poll and dial. */export async function acceptTicket( context: PrivateCliContext, args: string[],): Promise<{ meta: PrivateSpaceMeta; fingerprint: string; warnings: string[] }> { const inline = optional(args, '--ticket') const path = optional(args, '--ticket-file') if (Boolean(inline) === Boolean(path)) { throw new Error('Provide exactly one of --ticket or --ticket-file') } const text = inline ?? (await readTicketFile(context, path as string)) const ticket = decodeTicket(text) assertBootstrapTicket(ticket) // A contradictory local pin is a bootstrap refusal, not a condition this accept can repair. // Inspect it before publishing a key or creating the replica so a pasted wrong ticket leaves no // new public directory record and no local state behind. await inspectJoinBookmark(context.publicWriter, ticket.space) const warnings: string[] = [] await ensureDevice(context) const now = nowOf(context) const peerHints = values(args, '--peer') const meta: PrivateSpaceMeta = { spaceUri: ticket.space.uri, // The half of the ticket that makes the URI checkable. Written down at accept time because the // record it pins does not exist here yet and may not for days: without it, the first space // record any peer offered would be folded as the space, which is the whole thing a ticket is // handed over out of band to prevent. spaceCid: ticket.space.cid, founder: ticket.founder, protocol: ticket.version, peerHints: [...new Set([...(ticket.peerHints ?? []), ...peerHints])], ...(ticket.relays ? { relays: ticket.relays } : {}), ...(optional(args, '--label') !== undefined ? { label: optional(args, '--label') as string } : {}), createdAt: now, } const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [ticket.founder, context.actorDid, ...(meta.peerHints ?? [])], ...pinOption(meta), ...(context.now ? { now: context.now } : {}), }) try { // The directory, and only the directory: an invitee cannot have the space record yet, and this // is the moment the founder's published key is checked against the one the ticket names. await space.pollDirectory() const mismatch = ticketDirectoryMismatch(ticket, space.records.records()) if (mismatch?.kind === 'mismatch') throw new Error(mismatch.reason) if (mismatch?.kind === 'missing') { warnings.push( `${ticket.founder} has not published device ${ticket.device.deviceKeyId} where this machine can read it. ` + 'Their envelopes cannot be verified until it appears — check the ticket is current.', ) } await writeSpaceMeta(spaceDirectory(context.dataDir, meta.spaceUri), meta) // The checked write preserves the ordinary second-machine flow, but a concurrent collision is // safe only after it re-reads a valid bookmark pinning this exact genesis. await writeCheckedJoinBookmark(context.publicWriter, ticket, ticket.space, { peerHints: meta.peerHints as string[], ...(meta.label !== undefined ? { label: meta.label } : {}), now, }) return { meta, fingerprint: await ticketFingerprint(text.trim()), warnings } } finally { space.close() }}
async function readTicketFile(context: PrivateCliContext, path: string): Promise<string> { if (!context.readText) throw new Error('Cannot read --ticket-file without a file reader') return context.readText(path)}
export interface PrivateStatusReport { space: PrivateSpaceMeta directory: string devices: Array<{ deviceKeyId: string; kind: string; status: string; published: boolean }> members: number goals: number records: number envelopes: number /** Blobs this replica holds, and the ones its records name that it does not (design §18). */ blobs: { held: number; missing: number } outstanding: { quarantined: number; wants: number; blobs: number } /** * Whether a transport could deliver anything at all. False from the CLI, always: a command opens * `LocalOnlyBus`, because this machine's endpoint belongs to the daemon that serves the space * (ADR §20) and a second endpoint here would be a second replica of one space on one machine. * Reported as a fact about this process rather than as a count of unsent envelopes, which would * read as zero however many are waiting: the writes it is asking about happened in another one. */ connected: boolean /** * Set when the space record here is not the one the ticket pinned — the state every other command * refuses to run in. Reported rather than thrown because this is the command an operator reaches * for once one of those refusals has already happened. */ spaceMismatch?: SpacePinMismatch warnings: string[]}
/** `radial private status` — what this replica holds, and what it cannot yet reach. */export async function privateStatus( context: PrivateCliContext, args: string[],): Promise<PrivateStatusReport> { const meta = await requireSpace(context, optional(args, '--space')) const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { if (args.includes('--sync')) await space.pollDirectory() // Asked before the fold, because on a mismatch the fold is the thing not to trust: the counts // below still describe the store honestly, and the index is left out rather than reported for a // space this replica has no business folding. const mismatch = space.spaceMismatch() const index = mismatch ? undefined : space.indexOrNothing() const keys = await new FileDeviceKeyStore(context.dataDir).list(context.actorDid) const published = new Set( (index?.devices ?? []) .filter((device) => device.did === context.actorDid) .map((device) => device.deviceKeyId), ) const warnings: string[] = [] if (mismatch) { warnings.push(mismatch.reason) } else if (!index) { warnings.push( `this replica holds no space record for ${meta.spaceUri} yet — it has nothing to fold until a ` + 'peer hands it one, which is a daemon serving this space (radiald run), not this command', ) } if (!keys.some((key) => deviceKeyStatus(key) === 'active')) { warnings.push('this machine holds no active signing key for you; run "radial device rotate"') } for (const key of keys) { if (deviceKeyStatus(key) === 'active' && !published.has(key.deviceKeyId)) { warnings.push( `device ${key.deviceKeyId} is not visible in the directory this replica has read; ` + 'run "radial private sync" (and check the device record was published)', ) } } return { space: meta, directory: space.directory, devices: keys.map((key) => ({ deviceKeyId: key.deviceKeyId, kind: key.kind, status: deviceKeyStatus(key), published: published.has(key.deviceKeyId), })), members: (index?.members ?? []).filter((member) => member.active).length, goals: index?.goals.length ?? 0, records: space.records.records().length, envelopes: space.envelopes.all().length, blobs: { held: space.blobs.list().length, missing: space.ingestor.missingBlobs().length }, outstanding: space.ingestor.outstanding(), connected: false, ...(mismatch ? { spaceMismatch: mismatch } : {}), warnings, } } finally { space.close() }}
export interface PrivateSyncReport { meta: PrivateSpaceMeta /** The fold, or absent — a replica no peer has handed the space record to cannot be folded yet. */ index?: MaterializedIndex records: number envelopes: number /** What this cycle admitted. Zero from the CLI by construction; measured rather than assumed. */ admitted: number /** Envelopes still waiting on a key, records this replica knows it lost, and bytes it lacks. */ outstanding: { quarantined: number; wants: number; blobs: number } /** Anything this cycle refused outright, named. Empty is the ordinary answer. */ rejected: Array<{ uri: string; reason: string }> /** Set when the space record here is not the one the ticket pinned. Reported, never thrown. */ spaceMismatch?: SpacePinMismatch}
/** * `radial private sync` — one ingestion cycle against this machine's replica. * * A whole cycle, not the half of one this used to run. Polling the directory was all it did, and it * then reported `lastReport.admitted` — a field that only a cycle ever writes, so the number printed * was not a small count, it was a field nothing had set. A command whose output cannot vary is worse * than one that says nothing: it reads as an answer. * * So it runs the cycle and reports what the cycle and the replica actually say. The one number that * stays structurally zero here is `admitted`, and it stays for the honest reason: the CLI holds no * endpoint (ADR §20), so the catch-up in the middle of a cycle asks `LocalOnlyBus` and is answered * with nothing. Everything else is real — the directory poll, the fold, and what is outstanding — * and the directory poll is what this command is for: it is what makes a device record published a * minute ago visible to the replica, which is the state `radial private status` sends people here in. */export async function privateSync( context: PrivateCliContext, args: string[],): Promise<PrivateSyncReport> { const meta = await requireSpace(context, optional(args, '--space')) const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid, ...(meta.peerHints ?? [])], ...pinOption(meta), }) try { // The directory on its own first. It is the half of a cycle that talks to the network, and // running it here is what keeps a poll that failed a failure of this command rather than // something the tolerance below could hide. The cycle polls again; a checkpointed poller answers // the second one from the revision it just wrote. await space.pollDirectory() let index: MaterializedIndex | undefined let mismatch: SpacePinMismatch | undefined try { index = await space.sync() } catch (error) { // A cycle ends in the fold, and two of the fold's refusals are states this command reports // rather than fails on: a replica nobody has handed the space record to yet, and one whose pin // disagrees with the record it holds. Both are identified by asking the replica what it holds // — never by reading the exception — and anything else is rethrown. `lastReport` survives // either, because `sync()` records it before it folds. mismatch = space.spaceMismatch() if (!mismatch && space.records.records().some((record) => record.uri === meta.spaceUri)) { throw error } } const report = space.ingestor.lastReport return { meta, ...(index ? { index } : {}), records: space.records.records().length, envelopes: space.envelopes.all().length, admitted: report.admitted, outstanding: space.ingestor.outstanding(), rejected: report.rejected, ...(mismatch ? { spaceMismatch: mismatch } : {}), } } finally { space.close() }}
export interface PrivateExportReport { space: PrivateSpaceMeta header: ArchiveHeader /** The archive itself, so a caller with nowhere to write it can pipe it. */ text: string path?: string envelopes: number /** Blobs carried out with them: no PDS holds the bytes either (design §18). */ blobs: number warnings: string[]}
/** * `radial private export` — every envelope this replica holds, in a file. * * The command that makes "your SQLite is the backup" (§18) something an operator can act on rather * than a warning to feel bad about. Whole-corpus and idempotent by construction: envelope identity * is content-addressed, so re-exporting produces the same file and re-importing one changes nothing. * There is no incremental mode, because an incremental backup of a corpus with no central authority * would need a completeness proof, and the honest one is the same explicit want-list catch-up uses. * * A mismatched pin does not stop it. A replica holding a space record nobody promised it is exactly * one somebody may need to hand over to work out what happened, and an export writes to a file * rather than into a space — the refusals belong on the paths that *use* the fold. */export async function exportPrivateSpace( context: PrivateCliContext, args: string[],): Promise<PrivateExportReport> { const meta = await requireSpace(context, optional(args, '--space')) const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { const envelopes = space.envelopes.all() // Every blob this replica holds, not only the ones a record currently names: a corpus that grows // back a reference to bytes already here should not need the bytes fetched again. const blobs = space.blobs.list().map((cid) => ({ cid, bytes: space.blobs.get(cid) as Uint8Array })) const text = encodeArchive( { space: meta.spaceUri, ...(meta.spaceCid !== undefined ? { spaceCid: meta.spaceCid } : {}), founder: meta.founder, protocol: meta.protocol, exportedAt: nowOf(context), // What this replica knows it lost, carried with what it kept — otherwise a restore looks // complete and stops asking (design §18, "Bounds, and the want list"). wants: space.ingestor.wants.all(), }, envelopes, blobs, ) const path = optional(args, '--out') if (path !== undefined) await writeSecretFile(path, text) const warnings: string[] = [] const mismatch = space.spaceMismatch() if (mismatch) warnings.push(`${mismatch.reason} — this archive carries that store as it is`) if (envelopes.length === 0) { warnings.push( `this replica of ${meta.spaceUri} holds no envelopes yet, so the archive is a header and ` + 'nothing else', ) } return { space: meta, // Read back rather than remembered: the header this command reports is one that decoded from // the bytes it just wrote, so an archive nothing can read never gets reported as written. header: decodeArchive(text).header, text, ...(path !== undefined ? { path } : {}), envelopes: envelopes.length, blobs: blobs.length, warnings, } } finally { space.close() }}
export interface PrivateImportReport { space: PrivateSpaceMeta header: ArchiveHeader /** True when this import created the replica from a ticket rather than filling an existing one. */ bootstrapped: boolean /** Envelopes in the archive this replica already held — all of them, on a second import. */ held: number /** Passed gate 1 in this run, duplicates included. */ admitted: number /** Signed by a key this replica has not read from a public repo yet. Retryable. */ quarantined: number /** Blobs restored from the file, stored under the name their bytes hash to and no other. */ blobs: number rejected: Array<{ uri: string; reason: string }> outstanding: { quarantined: number; wants: number; blobs: number } index?: MaterializedIndex spaceMismatch?: SpacePinMismatch warnings: string[]}
/** * `radial private import` — restore a corpus from an archive. * * Every envelope goes through `offer()`, which is `admit()`, which is the identical gate a peer's * envelope passes: an archive is a bag of envelopes and never a trust input (design §18). So a file * somebody edited cannot land a record here that they could not have landed over a transport, and * the failure mode is a named rejection in the report rather than a corpus quietly one record * different from everyone else's. * * Three things a restore deliberately does NOT bring back, because none of them is the corpus: * * - **membership** — it is an admin's `addMember` grant, folded from the envelopes like everything * else, and if the archive holds it the fold finds it there; * - **the device directory** — public, and re-polled, so a restore on a machine that cannot reach * the network quarantines everything and says so instead of trusting the keys in the file; * - **this identity's signing key** — a restored replica can read a space and cannot write into it * until it mints and publishes a device (`radial device rotate`). */export async function importPrivateSpace( context: PrivateCliContext, args: string[],): Promise<PrivateImportReport> { const { header, envelopes, blobs } = decodeArchive(await readArchive(context, args)) const warnings: string[] = [] const wanted = optional(args, '--space') ?? header.space if (wanted !== header.space) { throw new Error(`this archive is of ${header.space}, not ${wanted}`) }
const ticket = await readImportTicket(context, args) const existing = await findPrivateSpace(context.dataDir, header.space) const bootstrapped = existing === undefined // The meta is built here and written *after* the directory is read, exactly as `ticket accept` // does: a ticket the founder's repo contradicts must leave no replica behind. const meta = existing ?? metaFromTicket(context, header, ticket) if (existing && ticket && ticket.space.uri !== existing.spaceUri) { throw new Error(`the ticket is for ${ticket.space.uri}, not ${existing.spaceUri}`) } // Compared and reported, never adopted: the header is one observer's word arriving in the same // file as the records it would vouch for, and this replica's pin came out of band. if (header.spaceCid !== undefined && meta.spaceCid !== undefined && header.spaceCid !== meta.spaceCid) { warnings.push( `this archive was exported from a replica pinned to space record ${header.spaceCid}, and this ` + `replica is pinned to ${meta.spaceCid}. One of the two was invited to a different space.`, ) }
const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid, ...(meta.peerHints ?? [])], ...pinOption(meta), ...(context.now ? { now: context.now } : {}), }) try { // The directory first, exactly as a sync does, and best-effort on purpose: a restore happens on // a machine that has just lost something, which is precisely when the network may also be gone. // Failing the whole import for it would leave the operator with a file they cannot use and no // report; quarantining instead is recoverable by re-running the same command later. try { await space.pollDirectory() } catch (error) { warnings.push( `could not read the public device directory (${(error as Error).message}), so envelopes whose ` + 'signing key is unknown here are quarantined rather than stored. Fix the connection, then ' + 'import the same archive again — it is unchanged by this run.', ) } if (bootstrapped) { // The same check `ticket accept` runs, on the same evidence: a founder's repo that publishes a // different key than the ticket names is either a stale ticket or somebody else writing there, // and restoring is not a reason to skip it. `missing` is not a refusal — a directory that has // not been read yet is the normal state of a machine that has just been handed a backup. const contradiction = ticketDirectoryMismatch(ticket as PrivateTicket, space.records.records()) if (contradiction?.kind === 'mismatch') throw new Error(contradiction.reason) if (contradiction?.kind === 'missing') { warnings.push( `${meta.founder} has not published device ${(ticket as PrivateTicket).device.deviceKeyId} where ` + 'this machine can read it, so nothing they signed can be verified here yet.', ) } await writeSpaceMeta(spaceDirectory(context.dataDir, meta.spaceUri), meta) }
// The wants before the envelopes, so any the archive itself satisfies are cleared as they land // rather than left outstanding by an ordering accident. for (const want of header.wants ?? []) space.ingestor.wants.add(want)
const report = { admitted: 0, quarantined: 0, rejected: [] as Array<{ uri: string; reason: string }>, blobs: { fetched: 0, missing: 0 }, } let held = 0 for (const envelope of envelopes) { if (space.envelopes.has(envelope)) held += 1 await space.ingestor.offer(envelope, report) }
// Bytes go through the store's own `put`, which names them by hashing them: a blob line somebody // edited lands under a name no record mentions, or fails outright. There is no separate gate for // it because content addressing IS the gate (design §18). let restoredBlobs = 0 for (const blob of blobs) { try { await space.blobs.put(blob.bytes, blob.cid) restoredBlobs += 1 } catch (error) { report.rejected.push({ uri: `blob:${blob.cid}`, reason: error instanceof Error ? error.message : String(error), }) } }
if (report.quarantined > 0) { warnings.push( `${report.quarantined} envelope(s) are signed by keys no public repo has published where this ` + 'machine can read them. They are not stored yet — run "radial private sync" and import the ' + 'same archive again.', ) } const missingBlobs = space.ingestor.missingBlobs().length if (missingBlobs > 0) { warnings.push( `${missingBlobs} blob(s) named by records in this archive are not in it, so those pictures are ` + 'missing here. A peer that holds them can still serve them; nothing else can.', ) } // Asked, not thrown, for the same reason `private status` asks: the store is now honest about // what it holds either way, and an operator who has just restored a replica needs to be told // that its space record is not the one they were promised — not handed a stack trace. const mismatch = space.spaceMismatch() const index = mismatch ? undefined : space.indexOrNothing() if (mismatch) warnings.push(mismatch.reason) else if (!index) { warnings.push(`this replica holds no space record for ${meta.spaceUri} yet, so it folds nothing`) } return { space: meta, header, bootstrapped, held, admitted: report.admitted, quarantined: report.quarantined, blobs: restoredBlobs, rejected: report.rejected, outstanding: space.ingestor.outstanding(), ...(index ? { index } : {}), ...(mismatch ? { spaceMismatch: mismatch } : {}), warnings, } } finally { space.close() }}
/** * The metadata for a replica of a space this machine has never held. * * From the ticket, and only from the ticket. Everything a replica needs beyond its envelopes — which * genesis record is the space, whose public repo to poll for the founder's key — is a bootstrap * input that has to arrive out of band, and taking it from the archive header instead would let an * archive define the space it is claiming to be a backup of. * * No public `join` is written, unlike `ticket accept`: restoring a replica is not joining a space. * The bookmark is already in the repo of whoever joined, and a member who never had one wants * `ticket accept` rather than this. */function metaFromTicket( context: PrivateCliContext, header: ArchiveHeader, ticket: PrivateTicket | undefined,): PrivateSpaceMeta { if (!ticket) { throw new Error( `no private replica of ${header.space} in ${context.dataDir}. Restoring one needs the creation ` + 'ticket as well as the archive: pass --ticket-file, or run "radial ticket accept" first. The ' + 'ticket is what says which space record is the space, and an archive cannot vouch for itself.', ) } assertBootstrapTicket(ticket) if (ticket.space.uri !== header.space) { throw new Error(`the ticket is for ${ticket.space.uri}, and this archive is of ${header.space}`) } const meta: PrivateSpaceMeta = { spaceUri: ticket.space.uri, spaceCid: ticket.space.cid, founder: ticket.founder, protocol: ticket.version, peerHints: [...ticket.peerHints], ...(ticket.relays ? { relays: ticket.relays } : {}), createdAt: nowOf(context), } return meta}
export interface PrivateBlobAddReport { space: PrivateSpaceMeta cid: string mimeType: string size: number /** The blob reference to hand to `radial image create --blob`, already JSON. */ ref: string /** True when this replica already held these exact bytes — adding the same file twice is one blob. */ held: boolean}
/** The four raster types `image.blob` accepts, by the extension people actually type. */const MIME_BY_EXTENSION: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif',}
/** * `radial private blob add` — put bytes in this replica, and print the reference a record names them * by. * * The private-path counterpart of `com.atproto.repo.uploadBlob`, and deliberately a separate command * from `image create` for the same reason the public path has two steps: uploading bytes and writing * the record that anchors them are different acts, and only the second one is a record anybody folds. * What differs here is only where the bytes went — into this replica, to be served to peers over the * private bus, and never to a PDS (design §18). * * The MIME type is checked against the lexicon's own accept list rather than a copy of it, so bytes * no `image` record could ever name are refused here instead of at the write. */export async function addPrivateBlob( context: PrivateCliContext, args: string[],): Promise<PrivateBlobAddReport> { const meta = await requireSpace(context, optional(args, '--space')) const path = required(args, '--file') if (!context.readBytes) throw new Error('Cannot read --file without a file reader') const bytes = await context.readBytes(path) const extension = (path.split('.').pop() ?? '').toLowerCase() const mimeType = optional(args, '--mime') ?? MIME_BY_EXTENSION[extension] if (!mimeType) { throw new Error(`Cannot tell what ${path} is: pass --mime (one of ${IMAGE_LIMITS.mimeTypes.join(', ')})`) } if (!isImageMimeType(mimeType)) { throw new Error(`${mimeType} is not a type a Radial record may carry (${IMAGE_LIMITS.mimeTypes.join(', ')})`) } if (bytes.length > IMAGE_LIMITS.maxBytes) { throw new Error(`${path} is ${bytes.length} bytes; the lexicon caps a blob at ${IMAGE_LIMITS.maxBytes}`) } const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { const ref = await privateBlobRef(bytes, mimeType) const held = space.blobs.has(ref.ref.$link) await space.blobs.put(bytes, ref.ref.$link) return { space: meta, cid: ref.ref.$link, mimeType, size: bytes.length, ref: JSON.stringify(ref), held, } } finally { space.close() }}
export interface PrivateBlobListReport { space: PrivateSpaceMeta held: string[] /** Bytes a record here names that this replica does not hold — derived, so never stale. */ missing: Array<{ cid: string; mimeType: string; size: number }>}
/** `radial private blob list` — what this replica holds, and what it is still missing. */export async function listPrivateBlobs( context: PrivateCliContext, args: string[],): Promise<PrivateBlobListReport> { const meta = await requireSpace(context, optional(args, '--space')) const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { return { space: meta, held: space.blobs.list(), missing: space.ingestor.missingBlobs() } } finally { space.close() }}
export interface PrivateBlobGetReport { space: PrivateSpaceMeta cid: string size: number path: string}
/** * `radial private blob get` — write held bytes back out. * * The only way to look at a private picture until the browser can, and the reason absence has to be * a first-class answer: a replica that has the record and not the bytes says so by name rather than * writing a truncated file. */export async function getPrivateBlob( context: PrivateCliContext, args: string[],): Promise<PrivateBlobGetReport> { const meta = await requireSpace(context, optional(args, '--space')) const cid = required(args, '--cid') const out = required(args, '--out') if (!context.writeBytes) throw new Error('Cannot write --out without a file writer') const space = await openPrivateSpace({ dataDir: context.dataDir, spaceUri: meta.spaceUri, actorDid: context.actorDid, transport: context.transport, bootstrapDids: [meta.founder, context.actorDid], ...pinOption(meta), }) try { const bytes = space.blobs.get(cid) if (!bytes) { throw new Error( `this replica of ${meta.spaceUri} does not hold ${cid}. A private blob has no PDS to fetch it ` + 'from: catch up from a peer that holds it, or import an archive that carries it.', ) } await context.writeBytes(out, bytes) return { space: meta, cid, size: bytes.length, path: out } } finally { space.close() }}
async function readArchive(context: PrivateCliContext, args: string[]): Promise<string> { const path = required(args, '--file') if (!context.readText) throw new Error('Cannot read --file without a file reader') return context.readText(path)}
/** The optional ticket an import may bootstrap from — the same two flags `ticket accept` takes. */async function readImportTicket( context: PrivateCliContext, args: string[],): Promise<PrivateTicket | undefined> { const inline = optional(args, '--ticket') const path = optional(args, '--ticket-file') if (inline && path) throw new Error('Provide at most one of --ticket or --ticket-file') const text = inline ?? (path !== undefined ? await readTicketFile(context, path) : undefined) return text === undefined ? undefined : decodeTicket(text)}
/** * `radial device rotate` — a new signing key, published, with the old one retired. * * Fold-neutral by construction: everything the retired key signed keeps counting and stays * verifiable forever. Rotation is routine hygiene, not a compromise response. */export async function rotateDevice( context: PrivateCliContext, options: { kind?: 'node' | 'browser' } = {},): Promise<{ created: DeviceKeyRecord retired: DeviceKeyRecord[] /** Key ids whose published address this withdrew — what other replicas will actually notice. */ addressesRetired: string[]}> { const rotated = await rotateDeviceKey( new FileDeviceKeyStore(context.dataDir), context.publicWriter, context.actorDid, { kind: options.kind ?? 'node', ...(context.now ? { now: context.now } : {}), }, ) context.log?.(`published device ${rotated.created.deviceKeyId} at ${rotated.ref.uri}`) return { created: rotated.created, retired: rotated.retired, addressesRetired: rotated.addressesRetired, }}
/** * `radial device retire <deviceKeyId>` — withdraw one device from the network. * * The compromise response that costs nothing, and the one this CLI can run about a machine it is not * (`retireDeviceAddress`): the address record is in the owner's repo, so retiring a stolen laptop's * key is a write from whatever machine still has the session. Within a directory poll no replica * dials that endpoint and none will serve it. * * It touches the local key store deliberately not at all. Withdrawing an address is a statement * about the network and `rotate` is the statement about this machine, and the device being retired * is usually not one this machine holds a key for in the first place. */export async function retireDevice( context: PrivateCliContext, deviceKeyId: string,): Promise<{ deviceKeyId: string; changed: boolean; local: boolean }> { const result = await retireDeviceAddress(context.publicWriter, deviceKeyId, { ...(context.now ? { now: context.now } : {}), }) const local = (await new FileDeviceKeyStore(context.dataDir).list(context.actorDid)).some( (key) => key.deviceKeyId === deviceKeyId, ) if (result.ref) context.log?.(`rewrote ${result.ref.uri}`) return { deviceKeyId, changed: result.changed, local }}
async function requireSpace( context: PrivateCliContext, spaceUri: string | undefined,): Promise<PrivateSpaceMeta> { const meta = await findPrivateSpace(context.dataDir, spaceUri) if (meta) return meta const known = await listPrivateSpaces(context.dataDir) if (spaceUri) throw new Error(`no private replica of ${spaceUri} in ${context.dataDir}`) throw new Error( known.length === 0 ? `no private space replica in ${context.dataDir}; create one with "radial space create --private" ` + 'or join one with "radial ticket accept"' : `--space is required: this machine holds ${known.map((entry) => entry.spaceUri).join(', ')}`, )}