Radicle
window.radicle
Seed, sync, disclose identity, and write collaborative objects through the user's Radicle node. Repository reads are plain rad: URL fetches.
Source on GitHubOn this page
- Reads are rad: URL fetches
- Provider object
- Events
- Timeouts
- Errors
- Consent and permission tiers
- Input rules
- Methods at a glance
- Connection
radicle_requestAccessradicle_disconnectradicle_getCapabilities- Node
radicle_getNodeStatusradicle_listSeededReposradicle_seedradicle_unseedradicle_getSeedStatusradicle_sync- Identity and writes
radicle_getIdentityradicle_createIssueradicle_commentIssueradicle_editIssueStateradicle_commentPatch- Not in this version
- Example
window.radicle lets a web page perform actions against the user's
Radicle node: seed repositories, sync them, disclose the user's Radicle
identity, and write collaborative objects (issues, issue comments, issue
state changes and patch comments). Every action is scoped to the page's
origin and gated by the user's consent. The provider reports
specVersion: '0.2'.
Reads are rad: URL fetches#
The provider has no read methods for repository data. Freedom resolves
the rad: URL scheme itself, so any page can fetch() repository metadata,
trees, files, commits, issues and patches as JSON from the user's own node
without a prompt. See rad: URLs for the routes.
Two limits apply: only public repositories are served (a private one is
403), and file, tree and commit routes take a full 40-character commit SHA,
not a branch name.
The provider exists for everything that is not a public read:
| Concern | Why it needs consent |
|---|---|
| Seeding | Commits the user's disk and bandwidth indefinitely |
| Syncing | Uses bandwidth on demand |
| Identity disclosure | The user's DID is a persistent cross-site identifier |
| COB writes | Signed with the user's key; cannot be taken back once gossiped |
| Node introspection | The seeded-repo list and node details are private information about the user |
Provider object#
window.radicle is installed in the page's main world of every web page's
top frame before any of the page's own scripts run, alongside window.swarm
and window.ethereum. A page's Content Security Policy does not affect it.
It is not installed in private windows, in sub-frames, or on Freedom's
internal pages. Detect it with window.radicle?.isFreedomBrowser === true.
window.radicle.request({ method: string, params?: object }): Promise<any>
Every method also has a convenience wrapper without the radicle_ prefix,
for example window.radicle.seed({ rid }) or window.radicle.getCapabilities().
Events#
window.radicle.on(event, handler) // also addListener
window.radicle.removeListener(event, handler)
window.radicle.removeAllListeners(event?)
| Event | Payload | When |
|---|---|---|
connect |
{ origin } |
The origin is granted access for the first time (a repeat radicle_requestAccess does not emit it) |
disconnect |
{ origin } |
Only after the page's own radicle_disconnect call |
seedStatus |
a seed status object | Replication progress for seed and sync actions this origin started |
Freedom v0.8.8 has no settings screen that lists or revokes Radicle grants,
so there is no user-side revocation that would emit disconnect. Treat a
4100 from a later call as the reliable signal that access is gone.
Timeouts#
The page shim rejects unanswered requests with
{ code: -32603, message: 'Request timed out' } after:
- 300 s for methods that can open a consent prompt:
radicle_requestAccess,radicle_seed,radicle_getIdentityand the four COB writes; - 60 s for everything else, including
radicle_unseed, which does prompt. A user who takes longer than a minute to answer the unseed prompt gets a timeout on the page even though the decision still takes effect. This is a bug, tracked in #1001.
A timed-out promise does not cancel a grant or write that already reached
the node, so retry COB writes carefully. A response that arrives after the
tab navigated to another document is dropped, and the request rejects with
4900.
Errors#
Rejections are { code, message, data? }, aligned with window.swarm:
| Code | Meaning | data.reason |
|---|---|---|
4001 |
The user rejected the prompt | — |
4100 |
Not authorized | not_connected (no connection grant), signing_not_granted (a signing-tier method without the signing grant), agent_custody (see Consent) |
4200 |
Unknown method | — |
4900 |
Radicle unavailable | profile-disabled (on every method, including radicle_getCapabilities), node-stopped (the node is not running or still starting) |
-32602 |
Invalid params | invalid_rid, invalid_id, invalid_title, invalid_body, invalid_labels, invalid_state, payload_too_large, not_seeded |
-32603 |
Internal or node error | repo_not_found, announce_failed, native_failed, seed_failed, unseed_failed, sync_failed, status_failed |
The checks run in this order: unknown method (4200), Radicle disabled for
the profile (4900 profile-disabled), then for every method except
radicle_getCapabilities: no connection grant (4100 not_connected),
signing tier without the signing grant (4100 signing_not_granted), and
node not running (4900 node-stopped). Only radicle_getNodeStatus,
radicle_requestAccess and radicle_disconnect work while the node is
stopped.
Note the asymmetry while the node is starting: method calls fail with
node-stopped, while radicle_getCapabilities reports
reason: 'node-not-ready'.
Consent and permission tiers#
Grants are keyed by the origin of the tab's address bar, derived by Freedom
and never by anything the page controls. The key rules are the same as for
window.swarm; see its origin key table.
| Tier | Methods | Prompt |
|---|---|---|
| None | radicle_getCapabilities |
never |
| Connection | radicle_requestAccess |
once per origin |
radicle_getNodeStatus, radicle_listSeededRepos, radicle_sync, radicle_getSeedStatus, radicle_disconnect |
never (need the connection grant) | |
radicle_seed, radicle_unseed |
every call, per repository | |
| Signing | radicle_getIdentity, radicle_createIssue, radicle_commentIssue, radicle_editIssueState, radicle_commentPatch |
the first call asks for the signing grant; later writes do not prompt |
seedandunseedalways prompt: one commits disk and bandwidth, the other drops a policy the user chose. The permission record hasautoApproveflags, but v0.8.8 ships no UI to set them.syncdoes not prompt, so it only accepts repositories that already have a seeding policy (-32602 not_seededotherwise).- Writes sign with the user's node identity. There are no per-origin sub-identities: a forge needs the user to be the same author everywhere.
- On a tab that Freedom Agent holds, the signing grant does not answer a
write on its own: each write prompts with its content, and a write
answered only by the grant is refused with
4100 agent_custody. - There is no per-origin rate limit on writes in v0.8.8.
Input rules#
- Repository ids (
rid) are accepted asrad:z…,rad://z…or a barez…, where thez…part is 20 to 60 base58 characters. They are normalized torad:z…. Anything else isinvalid_rid. - COB ids (
issueId,patchId,revisionId,replyTo) are 6 to 40 lowercase hex characters (invalid_id). - Text: titles, descriptions, comment bodies and labels must be non-empty after trimming whitespace.
- Sizes: title at most 200 bytes, description and body at most
65,536 bytes (
payload_too_large), each label at most 100 bytes, at most 10 labels (invalid_labels).
Methods at a glance#
| Method | Tier | Returns |
|---|---|---|
radicle_requestAccess |
connection | { connected, origin, capabilities } |
radicle_disconnect |
connection | { connected: false } |
radicle_getCapabilities |
none | capability object |
radicle_getNodeStatus |
connection | node status |
radicle_listSeededRepos |
connection | seeded repositories |
radicle_seed |
connection, prompts | { rid, seeded: true, status } |
radicle_unseed |
connection, prompts | { rid, seeded: false } |
radicle_getSeedStatus |
connection | seed status |
radicle_sync |
connection | { rid, status } |
radicle_getIdentity |
signing | { did, nid, alias } |
radicle_createIssue |
signing | { id } |
radicle_commentIssue |
signing | { id } |
radicle_editIssueState |
signing | { id, state } |
radicle_commentPatch |
signing | { id } |
Connection#
radicle_requestAccess#
Params none · Returns { connected: true, origin, capabilities } · Tier connection (prompts once)
Prompts the user to connect the origin. Repeat calls return the current
state without a prompt. capabilities is the
radicle_getCapabilities object. Works while the
node is stopped. Emits connect on the first grant only.
radicle_disconnect#
Params none · Returns { connected: false } · Tier connection
The page gives up its own access: the connection grant, the signing grant
and any auto-approvals. No prompt. Works while the node is stopped. Emits
disconnect.
radicle_getCapabilities#
Params none · Returns capability object · Tier none
{
specVersion: '0.2',
canUseNode: boolean, // connected AND node running
reason: null | 'not-connected' | 'node-not-ready' | 'node-stopped',
writes: ['issue', 'issueComment', 'issueState', 'patchComment']
}
When Radicle is disabled for the profile, this call fails with
4900 profile-disabled instead of returning an object.
Node#
radicle_getNodeStatus#
Params none · Returns node status · Tier connection
{
running: boolean, // status === 'running'
status: string, // the node's lifecycle status
peers?: number, // when running and the connection count can be read
alias?: string, // the public node alias, when running
nid?: string // only when the origin also has the signing grant
}
nid identifies the user, so it stays behind the signing grant. alias is
gossiped network-wide anyway.
radicle_listSeededRepos#
Params none · Returns the repositories the node seeds · Tier connection
The repositories the user's node has a seeding policy for, including one
whose first fetch has not finished yet. The list comes straight from the
native Radicle library; expect at least each repository's rid, and treat
metadata such as name and description as optional (it is missing until
the first fetch lands). Unlike swarm_listFeeds, this needs a connection:
the list is private information about the user.
radicle_seed#
Params { rid } · Returns { rid, seeded: true, status } · Tier connection, prompts per repository
Writes the seeding policy and starts a background fetch from the
network. It resolves straight away; status has the
radicle_getSeedStatus shape. Subscribe to
seedStatus before you call it, and use radicle_getSeedStatus to restore
the latest snapshot after a reload. This is how a page gets a repository the
node does not have yet. A rejected prompt is 4001 and the node is not
touched. Errors: invalid_rid, seed_failed.
radicle_unseed#
Params { rid } · Returns { rid, seeded: false } · Tier connection, prompts per repository
Removes the seeding policy and cancels any fetch in flight. Errors:
invalid_rid, unseed_failed. Mind the
60 s timeout.
radicle_getSeedStatus#
Params { rid } · Returns seed status · Tier connection
A replication snapshot. seedStatus events carry the same shape.
{
rid,
state: 'fetched' | 'fetching' | 'failed' | 'cancelled' | 'idle',
inStorage: boolean, // the repo is served locally
seedersKnown: number | null, // network seeders found for the fetch
attemptCount: number,
recentAttempts: [{ nid, ok, error?, at }], // last 5 per-seed results
progress: { phase, candidates?, nid?, addr?, index?, total?, reason? } | null,
lastError: string | null,
startedAt: number | null,
finishedAt: number | null
}
progress.phase is one of starting, resolving, connecting,
fetching, peer-failed, done, failed or cancelled. There are no
byte-level percentages. idle means nothing is known about the repository
this session. A failed repository can still turn fetched later, because
the node keeps retrying in the background. Errors: invalid_rid,
status_failed.
radicle_sync#
Params { rid } · Returns { rid, status } · Tier connection
Restarts the background fetch for a repository that already has a seeding
policy, for example after state: 'failed', without a second prompt.
Follow seedStatus for progress. Errors: invalid_rid, not_seeded,
sync_failed.
Identity and writes#
radicle_getIdentity#
Params none · Returns { did, nid, alias } · Tier signing
The user's Radicle identity. The first call asks for the signing grant, so this is the natural way to request it before the first write.
radicle_createIssue#
Params { rid, title, description, labels? } · Returns { id } · Tier signing
Opens an issue signed by the user and announces it to the network before
resolving. Errors: invalid_rid, invalid_title, invalid_body,
invalid_labels, payload_too_large, repo_not_found, announce_failed,
native_failed.
radicle_commentIssue#
Params { rid, issueId, body, replyTo? } · Returns { id } · Tier signing
Comments on an issue, optionally as a reply to the comment replyTo.
Errors as radicle_createIssue, plus invalid_id.
radicle_editIssueState#
Params { rid, issueId, state } · Returns { id, state } · Tier signing
state is 'open', 'closed' or 'solved' (invalid_state otherwise).
radicle_commentPatch#
Params { rid, patchId, body, revisionId? } · Returns { id } · Tier signing
Comments on a patch revision. Without revisionId, the comment goes on the
patch's first revision (the patch id doubles as that revision's id).
Not in this version#
- Creating patches or repositories (
rad init): both need a working copy. Issues and patch comments cover the collaboration around existing patches. - Private repositories, through
rad:URLs or the provider. - A settings screen to list or revoke Radicle grants, or to turn on auto-approval.
- Per-origin write rate limits.
Design notes
One identity. Every origin with the signing grant acts as the user's
single Radicle identity. A code forge is a reputation system: per-origin
sub-identities, as window.swarm uses for feeds, would split the user into
unlinkable authors and break Radicle's delegate model. The identity is only
disclosed behind the signing grant.
Policy and replication are separate. A fetch can take seconds, fail per
seed, or never complete, so radicle_seed resolves once the policy is
written and reports replication through seedStatus.
In-process node. Every method calls Freedom's embedded Radicle library against the profile's own node and storage. No Radicle CLI, HTTP daemon or local port is involved. See Architecture.
Example#
if (!window.radicle?.isFreedomBrowser) throw new Error('Open this app in Freedom');
const rid = 'rad:z…'; // your repository id
window.radicle.on('seedStatus', (status) => {
if (status.rid === rid) console.log(status.state, status.progress?.phase);
});
await window.radicle.requestAccess(); // prompts once
await window.radicle.seed({ rid }); // prompts for this repo
await window.radicle.getIdentity(); // asks for the signing grant
const { id } = await window.radicle.createIssue({
rid,
title: 'Typo in README',
description: 'Second paragraph, "teh" should be "the".',
});