Roles and sharing-policy API
Routes, fields and refusal codes for roles and QoS. See Security model and trust boundaries for how roles and policy work, and Control which devices share with each other for the task.
Routes
| Route | What it does |
|---|---|
GET /api/v1/peer-policy |
Returns a count-only status: role and member counts, outbox size, the applied policy revision, and other summary fields. See Response fields. |
GET /api/v1/peer-policy/roles |
Returns every role definition, sorted, with the current revision and ETag. |
PUT /api/v1/peer-policy/roles/<name> |
Creates or replaces one role definition. Preview with ?dry_run=1 before you commit. See Role fields and validation. |
GET /api/v1/peer-policy/roles/export-csv |
Returns every role definition as CSV, in the format iris-role export writes. Returns 503 policy_unavailable instead of an export when the policy is degraded. |
POST /api/v1/peer-policy/roles/import-csv |
Replaces every role definition in one revision from an uploaded CSV file, up to 8 MiB. A role a device or a schedule still uses cannot be dropped; refuses with role_in_use. A CSV grammar problem refuses with invalid_roles_csv, naming the row or field. |
DELETE /api/v1/peer-policy/roles/<name> |
Deletes an unused role after preview. The preview returns 200 JSON with the candidate revision and ETag; the committed DELETE returns 204 with an empty body and the committed ETag. A role with members, or another role that still permits it, refuses with role_in_use. |
PUT /api/v1/peer-policy/qos |
Replaces global QoS, or one role's QoS, or the qos_state tracker overlay, or both together. See QoS keys. |
GET /api/v1/peer-policy/origin-enforcement |
Returns reviewed distribution-access settings, proposed counts and current application proof, with a separate control ETag. |
PUT /api/v1/peer-policy/origin-enforcement |
Previews or commits a bounded, exact set of origin restrictions. See Reviewed distribution access. |
POST /api/v1/devices/<id>/role |
Sets or clears one device's role. |
POST /api/v1/devices/bulk-role |
Applies one role change to a batch of devices as one policy revision, with a request body up to 2 MiB. |
GET /api/v1/devices/<id>/effective-qos |
Returns the QoS values that apply to one device, with their source. Add tracker_state=seeder or tracker_state=leecher to also get paired tracker_state/tracker_qos values for that state. Invalid or unknown query input refuses with 422 invalid_policy_request; an unknown device refuses with 404 device_not_found. delivery_state: pre-instructions is a deprecated legacy value. |
GET /api/v1/peer-policy/explain?a=&b= |
Resolves two devices, or a device and the server's own seeder, and returns whether each side permits the other, the matched rule, and the current revision. Each argument is a device id, device:<id>, or service:seeder. Refuses with 422 when a side cannot be resolved to exactly one match. |
PUT /api/v1/peer-policy/quarantine/<device_id> |
Quarantines or releases one device. The body must be exactly {"quarantined": <bool>, "if_revision": <int ≥ 1>}, with no other keys. if_revision is the revision from GET /api/v1/peer-policy. Success returns 200 {ok: true, revision, quarantined}. |
Role and QoS writes share one policy revision:
- Read the current ETag from
GET /api/v1/peer-policy. - Send your write with
If-Match: <ETag>and?dry_run=1. Result: aconfirm_token. - Send the same body again with
confirm_tokenset, using the original ETag. Result: the write commits.
A token is bound to the revision and body it previewed. If another commit lands first, reread the ETag and preview again. See Automate with the API for a full worked session.
On /api/v1, a missing or expired browser session returns 401 console-session-required; sign in and preview again. On the private management listener, a missing or invalid management credential returns 401 management-authentication-required. A browser request also needs its current CSRF token.
Role fields and validation
Role names match ^[a-z0-9][a-z0-9._-]{0,31}$, and at most 256 roles may exist. default, quarantine, origin, seeder and legacy are reserved names.
| Field | Default / bound | Meaning |
|---|---|---|
restricted |
false |
When true, the server compiles a virtual access list for this role; false keeps open, implicit permit. |
peers |
the role itself; at most 64 | Permitted role names. The list must contain the role itself. Creating or updating a role adds or removes the matching link in each other role's list in the same commit. |
origin |
true |
Whether the tracker may introduce the server's own seeder to this restricted role. |
nets |
empty | Optional IPv4 subnet hints for role management. |
on_stale |
keep for restricted roles, defaults otherwise, unless overridden globally |
After the last verified instruction expires, retain its transfer settings or use built-in device settings. See Choose behavior when instructions expire. |
qos |
empty | Overrides the global QoS values for this role, for the keys that allow a role scope. |
qos_state |
omitted | The seeder and leecher tracker cadence overlay for this role. Tracker-only; editable through the API or role CSV. |
Allow-list membership always falls back to tracker-only, independent of on_stale. peers must be a non-null array of unique role names. The role schema sets no per-role member cap, but the devices in one role still cannot exceed the devices in your inventory. A restricted role with origin: false is valid. origin_unreachable is an advisory the server returns when neither this role nor a permitted role can reach the origin. Every role and QoS object is closed and typed: an unknown key is refused.
nets accepts a bare IPv4 address or an IPv4 prefix with a decimal length, a netmask, or a hostmask; leading zeroes and IPv6 are refused. iris-role CSV import compares networks for you and keeps the first valid spelling.
A role's qos object cannot set defs.<role>.qos.on_stale: that fallback is a separate field on the role definition, not a QoS key. Stored ACLs, the older explicit per-device peer lists, hold at most 64 names with 256 rules each; a role's rules compile in memory and use none of those slots. One explicit stored-ACL assignment shadows a role rather than combining with it. iris-role migrate ACL ROLE --dry-run previews moving a device between them; iris-role migrate ACL ROLE --apply commits the move in two policy revisions. A quarantined device cannot be migrated. iris-role CSV import owns duplicate-network detection: it compares canonical networks while keeping the first valid spelling, and the raw policy validator does not promise duplicate-net rejection.
QoS keys
Values are integers. Rate keys are bytes per second, and 0 means unlimited; a nonzero rate must be at least 8,192 B/s. Numeric QoS values reject booleans. A key with a state overlay resolves in this order, most general to most specific: builtin → roles.qos_default → roles.qos_state_default.<state> → roles.defs.<role>.qos → roles.defs.<role>.qos_state.<state>. A partial state map overrides only the key it supplies; every other key keeps resolving through the rest of the chain.
| Key | Default | Range | Scope | Current behavior |
|---|---|---|---|---|
max_peers |
10 | 1–1,000 | global, role, device | Hard per-torrent peer cap, including pending outbound peers. |
per_peer_bps |
12,500,000 | 0 or 8,192–10,000,000,000 | global, role, device | A modelling input, not a cap by itself: when set, it derives any per-torrent rate you did not set as per_peer_bps × fanout. |
fanout |
1 | 1–1,000, and no greater than max_peers |
global, role, device | Modeling input for derived per-torrent rates. |
seed_up_bps, seed_down_bps |
0 | 0 or 8,192–10,000,000,000 | global, role, device | Device rate while seeding; unlimited by default. |
leech_up_bps, leech_down_bps |
0 | 0 or 8,192–10,000,000,000 | global, role, device | Device rate while leeching; unlimited by default. |
overall_up_bps, overall_down_bps |
0 | 0 or 8,192–10,000,000,000 | global, role, device | Device-wide image rate across all transfers; unlimited by default. Tracker announcements remain responsive when the download limit is saturated. |
max_concurrent |
100 | 1–1,000 | global, role, device | Concurrent torrents a device runs. |
request_peer_speed_limit_bps |
51,200 | 0 or 8,192–1,000,000,000 | global, role | Per-peer request rate a device applies. |
announce_min_interval_s |
30 s | 10–300 s | global, role, state overlay | The tracker applies one bounded jitter of plus or minus 10% to the interval it issues. |
numwant |
50 | 4–200 | global, role, state overlay | Ceiling on peers returned per announce; a client that asks for numwant=0 gets none. |
handout_budget |
0 (off) | 0–1,000 | global, role | Per-window handout limit accepted by the API. |
catalog_tick_s |
60 s | 60–900 s, multiple of 60 | global, role, device | Catalog and staging cadence. For a restricted device, the effective value may not exceed effective endpoint_ttl()/3; the endpoint TTL defaults to 900 s but is configurable. |
telemetry_every_ticks |
1 | 1–60 | global, role, device | Ticks between telemetry sends. |
telemetry_pause |
false |
boolean | global, role, device | Suspends telemetry sends without changing anything else. |
on_stale |
defaults |
keep or defaults |
global or role definition | Retain verified transfer settings after expiry, or restore built-in device settings. |
origin_up_bps |
0 | 0 or 8,192–10,000,000,000 | global only | Origin-wide upload limit; unlimited by default. |
origin_per_torrent_up_bps |
0 | 0 or 8,192–10,000,000,000 | global only | Per-image origin upload limit; unlimited by default. |
origin_max_peers |
55 | 1–1,000 | global only | Origin per-torrent peer cap. |
max_peers, max_concurrent and the peer caps count connections; fanout is a multiplier; numwant counts peers per announce; handout_budget counts handouts per window; telemetry_every_ticks counts ticks; interval and cadence fields are seconds. Convert a decimal rate to the *_bps value with Mbit/s × 1,000,000 ÷ 8.
A PUT /api/v1/peer-policy/qos write replaces the whole selected QoS object; {} clears that layer. Omitting qos_state preserves the stored state object. An explicit qos_state: {} removes only the selected state layer and preserves the rest of the scalar QoS unchanged. A full role-definition replacement, a PUT .../roles/<name> write, must include qos_state to retain it: that write is a complete replacement, not a merge.
Role CSV uses seeder_numwant, leecher_numwant, seeder_announce_min_interval_s and leecher_announce_min_interval_s for the corresponding qos_state values. The same bounds apply. Export includes these columns; import replaces the whole definition, so blank or omitted cells remove those overrides.
Zero is unlimited, so no rate key expresses never upload. Use role membership and access rules to keep a role off a swarm, and account for connections aria2 already holds. The Console edits per-role QoS as typed fields and preserves tracker overlays. Global QoS uses PUT /api/v1/peer-policy/qos. There is no per-device QoS write route. Pair explanations come only from GET /api/v1/peer-policy/explain.
Scalar QoS is the instruction intent the server sends to a device. tracker_qos explains the selected tracker state and its sources. tracker_qos is tracker-only: tracker state never enters instruction QoS or control, telemetry, semantic hashes, role artifacts, stamps, serials, envelopes, heartbeat, or device configuration.
Response fields
GET /api/v1/peer-policy is count-only: it never returns the raw deny list, peer addresses, aria2 option dictionaries, session IDs, or the device IDs behind the mutual-origin preflight. Role drift (the gap between a device's declared role and its compiled membership) is the one exception: up to ten device IDs return, flagged if the list was truncated.
| Field | Meaning |
|---|---|
roles_supported, roles_present |
Whether this server build understands roles, and whether role state has ever existed in this policy. Presence is not proof of enforcement. |
enforcement.state, applied_revision, stale |
The tracker's last reconciliation result and how fresh it is. A result older than five minutes is marked stale. |
enforcement.mutual_origin.mode = preflight |
The default observation-only mode. newly_denied_device_count is null when unavailable, and 0 when a completed check found no newly denied device; neither adds a device to the active deny list. |
enforcement.mutual_origin.mode = enforce |
Reviewed restrictions are requested. Read the dedicated origin-enforcement route for current application proof; the old preflight count is null. |
origin_qos.state, target_download_count, applied_download_count |
Whether the origin's rate limits reached every active download. These are counts, not per-role throughput. |
fleet_rollup.issued_revision, fleet_rollup.applied |
The highest policy revision among issued device instructions, or null when unavailable, and accepted-device counts grouped by revision. |
fleet_rollup.states.pre-instructions |
Devices whose last heartbeat did not claim the instruction protocol. |
GET /api/v1/devices/<id>/effective-qos delivery_state = pre-instructions |
A deprecated legacy sentinel; read the response's instruction object for current evidence instead. |
Devices can report different accepted revisions. A policy edit that leaves a device's effective settings unchanged does not by itself issue a new instruction.
Warning
Never log or export an aria2 option dictionary, because tracker authorization data can be present among those options.
Reviewed distribution access
The dedicated route has its own strong ETag, "origin-enforcement-<revision>". Keep it separate from the sharing-policy ETag.
- Read
GET /api/v1/peer-policy/origin-enforcement. -
Send a PUT with that ETag in
If-Matchand this JSON body:{"mode":"enforce","max_new_ips":10,"max_new_devices":10,"if_policy_revision":1,"dry_run":true} -
Check the counts and
blockers. Send the unchanged request withdry_run: falseand the returnedconfirm_tokenonly after review.
Use the returned policy_revision, not the example value above. mode is preflight (Preview only in the Console) or enforce (Enforce reviewed blocks). Both budgets are integers from 0 to 1,000 and default to 10 on a new installation. new_ip_count and new_device_count are additions to the retained approval; retained_ip_count, proposed_ip_count and proposed_device_count describe the full proposed set. Counts are null when the required evidence is unavailable.
The confirmation binds exact device identities and addresses, not just their counts. Changed endpoints, policy, control revision or request fields require a new preview. Preview returns the proposed mode but does not apply it. Commit advances the control revision and returns pending with no applied proof.
GET separates requested mode from actual state. The Console labels these Saved mode and Transfer-service status at last refresh. Only enforced, with non-null applied counts, last_verified_at, no reason and no blockers, confirms recent application. The Console shows Blocks confirmed for that state. Proof expires after 120 seconds. Select Refresh status for current confirmation. review_required, degraded or unavailable needs attention; never interpret Approved for blocking as Confirmed at last refresh. Shared-address conflicts cannot be fully enforced with an IP blocklist. Switching to preflight uses the same review procedure.
Missing If-Match returns 428 precondition_required; a stale control marker returns 412 precondition_failed. A changed policy returns 409 policy_revision_conflict, a stale confirmation 409 origin_review_stale, and an unsafe apply 409 origin_enforcement_blocked. Invalid input returns 422 invalid_origin_enforcement; unreadable private state returns 503 origin_enforcement_unavailable. After an uncertain commit, reread current state before starting another preview.
Refusals
Every policy write queues in an outbox the tracker must acknowledge; see Security model and trust boundaries for the mechanism. A write that finds a full backlog of 256 unacknowledged entries is refused before it changes anything.
New role and QoS routes use exact strong ETags and return 409 operation_backlog_full for a full outbox. The legacy quarantine route keeps its own refusals, which mean different things:
| Response | Meaning |
|---|---|
409 revision_conflict |
Policy changed elsewhere; retry against the revision the response returns. |
422 policy_error |
Policy is degraded and running on the last known good copy. Repair the file it read from. |
503 policy_fail_closed |
Policy is fail-closed; every mutation is refused. |
503 operation_backlog_full |
256 operations are unacknowledged; check the tracker and reduce write pressure. |
Full descriptions of every code, including precondition_required, precondition_failed, confirmation_required and role_in_use, are in API error codes (problem types).