Skip to main content

SPEC-0026: Fail-Closed Trusted-Actor Intake and Quarantine

SPEC · SPEC-0026 · Status · approved · Date · 2026-09-22 · Implements · ADR-0031 · Requires · SPEC-0001, SPEC-0003, SPEC-0006, SPEC-0011, SPEC-0020

Overview​

This spec makes Switchboard's intake gate fail closed. It has four parts:

  • Fail-closed routing. A faulting rule stops evaluation, and the delivery is quarantined instead of falling through.
  • Params survive omission. Omitting params never clears them.
  • First-class trusted actors. A per-webhook trusted_actors field is evaluated in Go from verified fields.
  • Quarantine. A reserved queue that no tool-bearing agent is ever pushed or handed, drained by a human or by a tool-less classifier endpoint.

It also specifies the routing recipe for the public GitHub mirrors. See ADR-0031.

It extends SPEC-0020 (event routing): REQ "Deterministic Rule Evaluation" (fault semantics), REQ "Rule Validation at Save Time", REQ "Rule Parameters", REQ "Routing Envelope" (.actor, .release), REQ "Drop Action Semantics" (the new quarantine action), and REQ "Work Orders" (author_trusted). It amends the SPEC-0011 sender gate so that quarantined todos are never doorbell-eligible for ordinary endpoints.

Terms:

  • Disposition: the outcome of intake for one verified delivery. It is routed, dropped, quarantined or faulted. A faulted delivery is also quarantined once REQ-6 is implemented.
  • Owner endpoint: the endpoint that owns the receiving webhook (endpoint_webhooks.endpoint_id).
  • Classifier endpoint: an endpoint vended with the classifier role (REQ-8).

Requirements​

REQ-1: Faults Stop Evaluation​

When evaluating a rule produces a fault (timeout, error, compile failure, or an exhausted per-event budget), evaluation MUST stop at that rule. No later rule MUST be evaluated, and the default action MUST NOT apply. This MUST hold for every rule, whatever its action. The delivery's disposition MUST be faulted, and the fault MUST be recorded on the routing trace with the rule id, the rule index, the cause and the detail.

Until REQ-6 is implemented, a faulted delivery MUST be persisted as an event with its trace and no todo, spending its dedup slot exactly as a drop does. After REQ-6, it MUST be quarantined with quarantine_reason = "rule_fault".

Each faulted delivery MUST increment switchboard_routing_faults_total{cause}, and MUST log one warning with the webhook id, rule id, rule index and cause. The owner MUST be able to see faulted deliveries through list_webhook_events (filterable by disposition) and as a warning on the webhook's row on its endpoint card (REQ-9).

Scenario: A mistyped trust rule no longer admits everyone​

  • GIVEN rules [r1: any($params.trusted[]; . == .issue.author) | not → drop, r2: → queue "lane-m"] and params = {"trusted": "alice"} (a string, not a list)
  • WHEN an issue from mallory is delivered
  • THEN r1 faults, r2 is not evaluated, no todo is created on lane-m, and the disposition is faulted

Scenario: A faulting drop rule does not route its delivery​

  • GIVEN a drop rule that times out on a large payload, followed by a default action of queue inbox
  • WHEN such a payload is delivered
  • THEN no todo is created on inbox, and the fault is recorded and counted

REQ-2: Unavailable Sandbox Refuses the Delivery​

When the routing sandbox cannot evaluate at all for a webhook that has rules (it failed to start, or it failed wholesale for this delivery), the receiver MUST answer 503 with {"error": "routing unavailable"}, MUST NOT persist an event or a todo, and MUST log an error, so that the producer retries. A webhook with no rules MUST be unaffected.

"Cannot evaluate at all" means a failure that says nothing about the delivery: no sandbox, no free evaluation slot, an evaluation process that failed to start or died before it began a rule, a deadline kill while no rule had run longer than the per-event budget, or output that cannot be trusted. An evaluation process killed at its memory limit or its deadline WHILE RUNNING A RULE is a fault of that rule on that delivery, not an unavailable sandbox: it MUST be handled under REQ-1 (disposition faulted, cause budget_exhausted for memory or timeout for the deadline, and the rule named), and the save-time dry-run (REQ-3) MUST refuse it with invalid_argument naming the rule. Answering 503 for it would fail the same way on every retry and leave no record.

Scenario: Sandbox down​

  • GIVEN a webhook with rules, on an instance whose sandbox failed to start
  • WHEN a verified delivery arrives
  • THEN the response is 503, nothing is persisted, and the producer's retry succeeds once the sandbox is healthy

REQ-3: Save-Time Fault Refusal and Param Typing​

set_webhook_rules, add_webhook_rule, update_webhook_rule and move_webhook_rule MUST dry-run the resulting configuration against up to the 50 most recent stored events of that webhook before saving. If any rule faults on any of them, the save MUST fail with invalid_argument and keep the previous configuration. The error MUST name each faulting rule id, the event id and the cause. A webhook with no stored events MUST skip the dry-run.

On a webhook with a trust gate (REQ-5), the dry-run MUST skip every stored event the gate would hold under the current trusted_actors, because rules never run on those, and MUST count only events the gate passes toward the 50. An event an untrusted actor sent MUST NOT refuse a save. The scan MUST be bounded, and reads at most the webhook's 500 most recent stored events. When the bound stops it before 50 events pass, the save MUST still proceed, so an outsider's flood cannot block the owner's edits. The result MUST report how many events were checked and how many were skipped as held, and MUST carry a warning that names the shortfall.

Every params value MUST be a string, a number, a boolean, or a list whose elements are all strings or all numbers. Any other shape, including nested objects and mixed lists, MUST be refused with invalid_params naming the key: routing's own validation code, which SPEC-0020 requires the rule verbs to surface verbatim and which the params size limit already uses. The shape check applies to a save that sets or changes params. A save that carries the stored params forward unchanged (every verb but a set_webhook_rules that passes new params) MUST NOT be refused over the shape of params stored before this check existed, so an owner can always edit or remove the rules that read them.

Scenario: A rule that faults on real traffic is refused​

  • GIVEN a webhook with 20 stored deliveries
  • WHEN an agent adds a rule that faults on 3 of them
  • THEN the save fails, the error lists the rule and the 3 event ids, and the stored rules are unchanged

Scenario: A flood of held deliveries is reported, not blocking​

  • GIVEN a webhook with one trusted delivery followed by 500 deliveries the gate would hold
  • WHEN an agent adds a rule that faults on the trusted one
  • THEN the save succeeds, and its result reports 0 checked, 500 skipped as held, and a warning that the rules went unchecked

Scenario: Nested params refused​

  • WHEN set_webhook_rules is called with params = {"trusted": {"alice": true}}
  • THEN the call fails with invalid_params naming trusted

REQ-4: Params Are Never Cleared by Omission​

set_webhook_rules MUST leave the stored params unchanged when the request omits params. It MUST clear them only when the request passes params: {} explicitly. Every rules verb that returns a configuration MUST echo the resulting params. The tool's schema description MUST state the omit and clear semantics.

Scenario: Omitting params keeps them​

  • GIVEN a webhook whose params are {"trusted_humans": ["joestump"]}
  • WHEN set_webhook_rules is called with rules and default_action, and no params
  • THEN the stored params are still {"trusted_humans": ["joestump"]}, and the response echoes them

Scenario: Explicit clear​

  • WHEN set_webhook_rules is called with params: {}
  • THEN the stored params are empty, and the response echoes {}

REQ-5: Trusted Actors​

A self-managed webhook MAY carry trusted_actors, owned by the webhook's owner scope:

  • for github and gitea webhooks: {"logins": [string], "match": "sender"|"author"|"both"}, where match defaults to "sender";
  • for cairn webhooks: {"actor_ids": [string]}.

Lists MUST hold 0 to 256 non-empty strings of at most 128 bytes each. A field that does not belong to the webhook's source type MUST be refused.

Management:

  • create_webhook MUST accept an optional trusted_actors. On a github, gitea or cairn webhook, omitting it MUST store an empty list, and the result MUST say that every delivery will be quarantined until trusted actors, or allow_all, are set.
  • set_trusted_actors {webhook_id, trusted_actors} MUST replace the field. trusted_actors MUST be a required argument.
  • clear_trusted_actors {webhook_id} MUST reset the field to an empty list.
  • {"allow_all": true} MUST trust every verified sender. It MUST be exclusive with logins, actor_ids and match, MUST be off unless set explicitly, and MUST be flagged by list_webhooks (allow_all: true) and by a warning on the webhook's row on its endpoint card.
  • list_webhooks MUST echo the field, and a quarantined count of the webhook's open quarantine items.

These verbs MUST join the webhook-management verb family for grants, the vend wizard and consent. Endpoint scope stays immutable (SPEC-0007): the upgrade MUST NOT add them to an existing endpoint's scope. An endpoint without set_trusted_actors gets them by re-vending, and create_webhook's empty-list warning MUST tell such an endpoint to recreate the webhook with trusted_actors or to re-vend. A webhook_id that another endpoint owns MUST be answered with not_found, exactly like an unknown id.

trusted_actors MUST be refused with invalid_argument on a token-trust (generic) webhook, and on a source with no actor projection (stripe, slack). The error MUST say why.

Actor projection. Switchboard MUST parse, in Go and only from a verified body:

  • github and gitea: sender is sender.login. author is the first present of comment.user.login, review.user.login, pull_request.user.login, issue.user.login and discussion.user.login, or null. The thread author is the first present of the last three, or null. On a comment or review it can differ from author, and the delivery still carries the thread's text. A gitea review's review object is {type, content} and names no user, so when a gitea body carries a non-null review and neither comment.user.login nor review.user.login, author MUST be sender, who wrote the review.
  • cairn: sender and author are both the signed actor_id. on_behalf_of MUST NOT be used.

The projection MUST read keys exactly, as .payload reads them: no case-insensitive key matching, with a duplicate key resolved the same way .payload resolves it. A present field of the wrong type MUST name no one. Logins MUST compare with an ASCII-only case fold. Actor ids MUST compare exactly.

Evaluation. On every github, gitea and cairn webhook, the gate MUST run after signature verification and before any rule:

  • sender_trusted MUST be whether sender is in the list. author_trusted MUST be true only when author is in the list and, when there is a thread author, the thread author is in the list too. A trusted maintainer's comment on an outsider's issue therefore has author_trusted = false, because the outsider's text still travels with it. When author is null, author_trusted MUST equal sender_trusted.
  • trusted MUST be sender_trusted for match = "sender", author_trusted for match = "author", and both for match = "both".
  • An untrusted delivery MUST be quarantined with quarantine_reason = "untrusted_actor", and rules MUST NOT be evaluated for it.
  • An empty list MUST trust no one.
  • Under allow_all, trusted MUST be true and sender_trusted and author_trusted MUST be null.
  • A missing trusted_actors MUST NOT be treated as "gate off" by any code path. The migration MUST write {"allow_all": true} onto every existing github, gitea and cairn webhook, so existing webhooks keep routing as before, visibly.

Scenario: Maintainer label promotes an outsider's issue​

  • GIVEN a github webhook with trusted_actors = {"logins": ["joestump"], "match": "sender"}
  • WHEN mallory opens an issue, and joestump then labels it
  • THEN the opened delivery is quarantined, and the labeled delivery reaches the rules with .actor = {sender: "joestump", author: "mallory", sender_trusted: true, author_trusted: false, trusted: true}

Scenario: A new webhook fails closed​

  • WHEN an agent creates a github webhook without trusted_actors, and an outsider opens an issue
  • THEN the delivery is quarantined with quarantine_reason = "untrusted_actor", and the create result said that the list is empty

Scenario: Allow-all is explicit and flagged​

  • GIVEN a gitea webhook with trusted_actors = {"allow_all": true}
  • WHEN an outsider opens an issue
  • THEN it reaches the rules with .actor.trusted = true and author_trusted = null, and list_webhooks shows allow_all: true

Scenario: Existing webhook after the upgrade​

  • GIVEN a github webhook created before this change
  • WHEN the migration has run
  • THEN its trusted_actors is {"allow_all": true}, it routes exactly as before, and its card shows the allow-all warning

Scenario: Case-insensitive login​

  • GIVEN logins = ["JoeStump"]
  • WHEN a delivery's sender.login is joestump
  • THEN the sender is trusted

Scenario: A maintainer's comment keeps the outsider's thread untrusted​

  • GIVEN a github webhook with trusted_actors = {"logins": ["joestump"], "match": "sender"}
  • WHEN joestump comments on an issue that mallory opened
  • THEN the delivery reaches the rules with .actor = {sender: "joestump", author: "joestump", sender_trusted: true, author_trusted: false, trusted: true}, and under match = "author" it would be quarantined

Scenario: Token-trust webhook refused​

  • WHEN an agent calls set_trusted_actors on a generic webhook
  • THEN the call fails with invalid_argument, saying the webhook's body is not signed

Scenario: Self-reported Cairn identity ignored​

  • GIVEN a cairn webhook with actor_ids = ["acct_joe"]
  • WHEN a signed artifact event arrives with actor_id = "acct_other" and on_behalf_of = "acct_joe"
  • THEN the delivery is quarantined as untrusted_actor

Scenario: Omitted argument never clears​

  • WHEN an agent calls set_trusted_actors {"webhook_id": "…"} without trusted_actors
  • THEN the call fails with invalid_argument, and the stored list is unchanged

REQ-6: Quarantine​

quarantine MUST be a reserved queue name. It MUST be refused as a create_webhook target queue, as a queue in any endpoint scope or webhook-queue ceiling, as a route queue, and as the queue of a rule's queue action.

A delivery MUST be quarantined when:

  • the trust gate finds it untrusted (untrusted_actor);
  • a rule faults (rule_fault, REQ-1);
  • the first matching rule's action is {"quarantine": true} (rule_action).

Quarantining MUST create exactly one todo on the owner endpoint, whatever the webhook's fan-out targets, with queue = "quarantine", quarantine_reason, and quarantine_detail (the fault or the actor, as structured JSON). It MUST keep the delivery's event, trace and idempotency key, so a redelivery collapses onto it.

A quarantined todo:

  • MUST NOT ring a doorbell on any endpoint other than a classifier endpoint (REQ-8);
  • MUST NOT fire a notify hook (SPEC-0024);
  • MUST NOT be returned by list_todos, claim or claim_next on any endpoint;
  • MUST be visible only to its owner scope and to that scope's classifier endpoints. The owner releases or discards it only in the Quarantine view (REQ-9). The owner's Board MAY list it read-only under the quarantine queue, but MUST NOT offer it as work (no claim, complete or retry control) or count it as awaiting a claim;
  • its event MUST NOT be deleted by retention while the item is held;
  • MUST be auto-discarded 30 days after creation, or sooner if the operator's retention bound is shorter, with result = {"expired": true}.

The store MUST apply the quarantine exclusion as a default filter on every todo read and lifecycle path, so that no individual caller has to remember it.

Scenario: Quarantine is owned by the webhook's owner​

  • GIVEN endpoint A's webhook, which fans out to A and to friend endpoint F
  • WHEN an untrusted delivery arrives
  • THEN one quarantine todo exists on A, and F has nothing new

Scenario: No agent is handed quarantined work​

  • GIVEN a quarantined todo on endpoint A
  • WHEN A's worker calls claim_next, calls list_todos, or calls claim with the todo's id
  • THEN claim_next returns nothing from quarantine, list_todos omits it, claim fails with not_found, and no doorbell or hook fired when it was created

Scenario: Reserved name refused​

  • WHEN an agent calls create_webhook {"source_type": "github", "target_queue": "quarantine"}
  • THEN the call fails with invalid_argument

REQ-7: Release and Discard​

A quarantined todo MUST leave quarantine only by release, discard, or expiry.

Release MUST route the delivery again, through the webhook's current rules, with the trust gate treated as satisfied. The envelope MUST carry .release = {by, at}, where by is "human:<human_id>" or "classifier:<endpoint slug>". .actor MUST be unchanged, so author_trusted and sender_trusted still reflect the original delivery. When the release names a queue, that queue MUST be within the owner endpoint's webhook-queue ceiling, and rules MUST be skipped. A released todo MUST keep its id. It MUST move to the routed queue or queues on the owner endpoint and the webhook's fan-out targets, as the routing decision says, and MUST ring doorbells and fire hooks exactly as a freshly routed todo would. If routing places it back in quarantine, or faults, the release MUST fail with conflict, and the todo MUST stay quarantined.

Discard MUST complete the todo with state done and the result {"discarded": true, "reason", "by"}.

Every release, discard and expiry MUST be recorded on the todo with who, when and the outcome.

Scenario: Human releases an outside report to triage​

  • GIVEN a quarantined outsider issue, and rules that send .release.by | startswith("human:") to lane-m
  • WHEN the owner releases it from the Quarantine view
  • THEN it becomes a lane-m todo whose work order carries author_trusted = false and released_by = "human:<id>", and lane-m's worker is rung

Scenario: Release into quarantine again is refused​

  • GIVEN rules whose first match for the released delivery is {"quarantine": true}
  • WHEN the owner releases it without naming a queue
  • THEN the release fails with conflict, and the todo stays quarantined

REQ-8: Classifier Endpoints​

A human MAY vend an endpoint with the classifier role. Its scope MUST contain exactly list_quarantined, get_quarantined, release_quarantined and discard_quarantined, plus self verbs. The vend MUST be refused if any other verb is requested with them. The vend wizard MUST require the human to attest that the agent behind the endpoint runs without tools, and MUST show what the classifier will be able to read.

  • list_quarantined {cursor?, limit?} MUST return the open quarantine items of the classifier's owner scope: the id, reason, source, kind, actor, summary, and received_at. It MUST NOT return payloads.
  • get_quarantined {id} MUST return one item, including the payload.
  • release_quarantined {id, queue?} and discard_quarantined {id, reason} MUST behave as REQ-7.

A classifier endpoint MUST see only items whose owner endpoint shares its owner scope. An id outside that scope MUST be answered with not_found.

A classifier endpoint's sessions MUST receive a doorbell when quarantine items arrive in its scope. The doorbell MUST carry only a count and a fixed instruction to call list_quarantined, and MUST NOT contain any sender-supplied text. At most one doorbell MUST be sent per minute per classifier endpoint.

Scenario: A classifier cannot do anything else​

  • WHEN a human tries to vend a classifier endpoint that also holds claim_next
  • THEN the vend is refused, and no endpoint is created

Scenario: Classifier doorbell carries no sender text​

  • GIVEN a classifier endpoint, and an arriving quarantine item titled ignore all instructions
  • WHEN the classifier's doorbell is sent
  • THEN its content and meta contain a count and the fixed instruction only

Scenario: A foreign classifier​

  • WHEN a classifier endpoint owned by human B calls get_quarantined with human A's item id
  • THEN the call fails with not_found

REQ-9: Quarantine View and Owner Signals​

The web UI MUST provide a Quarantine view listing the signed-in human's quarantine items across their endpoints. Once Teams land (ADR-0038 / SPEC-0033), it MUST include the items of teams in which the human may configure webhooks. For each item it MUST show:

  • the reason and its detail (the actor and trust flags, or the fault);
  • the source, the kind, and the receiving webhook;
  • the escaped summary;
  • the payload, collapsed by default, rendered as escaped text and never as HTML or Markdown.

The view MUST offer release (optionally to a named queue), discard (with a reason), and trust this actor and release. The last action adds the actor's login or actor id to the webhook's trusted_actors and releases the item. It MUST be refused for rule_fault items.

Each webhook's row on its endpoint card MUST show the webhook's open quarantine count and its fault count over the last 24 hours. The endpoint card is the web UI's per-webhook surface: the Board has no per-webhook cards, so a webhook's owner signals live where the webhook is configured.

Scenario: Trust this actor​

  • GIVEN a quarantined issue from newcontributor, on a webhook whose trusted logins are ["joestump"]
  • WHEN the owner chooses "trust this actor and release"
  • THEN the trusted logins become ["joestump", "newcontributor"], and the item is released through routing

Scenario: Payload is never rendered as markup​

  • GIVEN a quarantined item whose payload contains <script> and Markdown links
  • WHEN the owner expands it
  • THEN the text is displayed escaped, with no element or link created from it

REQ-10: Envelope and Work Order Additions​

The routing envelope MUST gain two top-level, Switchboard-derived fields that the payload cannot forge:

  • .actor: {sender, author, sender_trusted, author_trusted, trusted}. The two names MUST be populated for github, gitea and cairn whenever they can be parsed. The trust flags MUST be null on sources with no actor projection, and the per-actor flags MUST be null under allow_all.
  • .release: {by, at} on a released delivery, and null otherwise.

A work order (ADR-0025) built from a delivery with .actor MUST carry author_trusted, and, when released, released_by. These paths MUST be added to the SPEC-0020 envelope documentation, and MUST NOT move once shipped.

Scenario: Rules can read trust flags​

  • GIVEN a trusted delivery with an untrusted author
  • WHEN a rule tests .actor.author_trusted == false
  • THEN the rule matches

REQ-11: Metrics​

Switchboard MUST add these series to the SPEC-0023 registry, with bounded labels:

switchboard_routing_faults_total{cause} counter # timeout|error|compile|budget
switchboard_quarantine_items_total{reason} counter # untrusted_actor|rule_fault|rule_action
switchboard_quarantine_resolved_total{outcome,by} counter # outcome: released|discarded|expired; by: human|classifier|system
switchboard_quarantine_oldest_seconds gauge

The existing switchboard_queue_todos{queue="quarantine",…} series reports quarantine like any other queue. quarantine is a reserved queue label: it MUST NOT fold into the SPEC-0023 overflow value __other__, whatever the queue cap. The metrics guide's queue-liveness alert MUST exclude queue="quarantine", and MUST document a separate alert on switchboard_quarantine_oldest_seconds.

switchboard_routing_faults_total MUST be present at 0 for each of its four causes from the first scrape, as SPEC-0023 REQ-6 requires of the collection-error series, so an increase() alert fires on the first fault after a restart.

Scenario: Liveness alert ignores quarantine​

  • GIVEN 5 open quarantine items and no claims
  • WHEN the documented liveness alert expression is evaluated
  • THEN it does not fire for queue="quarantine"

REQ-12: Public Mirror Intake Recipe​

The routing documentation MUST include a recipe, "Outside intake from the public GitHub mirrors". It MUST cover:

  • one signed github webhook, installed at the GitHub organization level for issues and issue_comment;
  • trusted_actors with the maintainers' logins and match = "sender";
  • rules that route trusted labeled events to lanes, and map the mirror repository to its canonical tracker in the work order;
  • everything else left to quarantine, except trusted deliveries with no issue subject (a maintainer's own comments), which the recipe drops on purpose;
  • the promotion flow, in which a maintainer's label moves an item on;
  • the warning that an outsider's text keeps author_trusted = false downstream.

The recipe's example rules MUST be exercised by a routing test against recorded mirror payloads.

Scenario: Recipe test​

  • WHEN the recipe's rules are run against a recorded outsider opened event and a maintainer labeled event
  • THEN the first is quarantined and the second routes to a lane, in CI

REQ-13: Error Handling, Concurrency and Database Standards​

Every error MUST be wrapped with the webhook id and the stage (verify, trust gate, route, quarantine, release). A quarantine write MUST happen in the same transaction as the event insert, so a delivery is never persisted without its disposition. Release MUST lock the todo row (FOR UPDATE) so that concurrent release and discard resolve once. The loser MUST get conflict. Queries MUST be parameterized. Background expiry MUST be a single statement, safe under concurrent instances, and MUST pass go test -race.

Scenario: Concurrent release and discard​

  • WHEN a human releases an item at the same moment a classifier discards it
  • THEN exactly one succeeds, the other gets conflict, and the todo records one outcome

Security Requirements​

Authentication​

SurfaceAuthDescription
MCP set_trusted_actors, clear_trusted_actors, rules verbsRequiredEndpoint credential, webhook-family grant, owner scope
MCP list_quarantined, get_quarantined, release_quarantined, discard_quarantinedRequiredClassifier-role endpoint credential only
Web UI Quarantine view and actionsRequiredSigned-in human, owner scope, CSRF token
POST /webhooks/w/{token}PublicInbound delivery, authenticated by signature or unguessable URL (unchanged, SPEC-0001 and SPEC-0006)

Rate Limiting​

Inbound webhooks keep the existing per-IP limiter. The save-time dry-run is bounded to 50 events and runs inside the existing per-event CPU budget. Classifier doorbells are limited to one per minute per endpoint.

Security Headers​

The Quarantine view MUST carry the existing secureHeaders set, including a CSP with no inline script, because it displays attacker-supplied text.

Request Body Size Limits​

Inbound bodies stay capped at 5 MiB (SPEC-0001). A quarantine item stores the event's existing payload and does not copy it. get_quarantined returns the payload as data inside the MCP response limit.

CSRF Protection​

Every Quarantine view action MUST be a POST carrying the existing CSRF token.

Redirect Validation​

Quarantine actions MUST redirect only to the same-origin Quarantine view.

Tenancy​

Trust lists and quarantine items MUST be readable and changeable only within the webhook's owner scope. The instance operator role MAY bound retention and MUST NOT read, release or discard tenant items through the product. Unknown ids and foreign ids MUST both answer not_found.