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
paramsnever clears them. - First-class trusted actors. A per-webhook
trusted_actorsfield 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,quarantinedorfaulted. 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"]andparams = {"trusted": "alice"}(a string, not a list) - WHEN an issue from
malloryis delivered - THEN r1 faults, r2 is not evaluated, no todo is created on
lane-m, and the disposition isfaulted
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_rulesis called withparams = {"trusted": {"alice": true}} - THEN the call fails with
invalid_paramsnamingtrusted
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_rulesis called withrulesanddefault_action, and noparams - THEN the stored params are still
{"trusted_humans": ["joestump"]}, and the response echoes them
Scenario: Explicit clear
- WHEN
set_webhook_rulesis called withparams: {} - 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
githubandgiteawebhooks:{"logins": [string], "match": "sender"|"author"|"both"}, wherematchdefaults to"sender"; - for
cairnwebhooks:{"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_webhookMUST accept an optionaltrusted_actors. On agithub,giteaorcairnwebhook, omitting it MUST store an empty list, and the result MUST say that every delivery will be quarantined until trusted actors, orallow_all, are set.set_trusted_actors {webhook_id, trusted_actors}MUST replace the field.trusted_actorsMUST 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 withlogins,actor_idsandmatch, MUST be off unless set explicitly, and MUST be flagged bylist_webhooks(allow_all: true) and by a warning on the webhook's row on its endpoint card.list_webhooksMUST echo the field, and aquarantinedcount 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:
senderissender.login.authoris the first present ofcomment.user.login,review.user.login,pull_request.user.login,issue.user.loginanddiscussion.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 fromauthor, and the delivery still carries the thread's text. A gitea review'sreviewobject is{type, content}and names no user, so when a gitea body carries a non-nullreviewand neithercomment.user.loginnorreview.user.login,authorMUST besender, who wrote the review. - cairn:
senderandauthorare both the signedactor_id.on_behalf_ofMUST 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_trustedMUST be whethersenderis in the list.author_trustedMUST be true only whenauthoris 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 hasauthor_trusted = false, because the outsider's text still travels with it. Whenauthoris null,author_trustedMUST equalsender_trusted.trustedMUST besender_trustedformatch = "sender",author_trustedformatch = "author", and both formatch = "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,trustedMUST be true andsender_trustedandauthor_trustedMUST be null. - A missing
trusted_actorsMUST NOT be treated as "gate off" by any code path. The migration MUST write{"allow_all": true}onto every existinggithub,giteaandcairnwebhook, so existing webhooks keep routing as before, visibly.
Scenario: Maintainer label promotes an outsider's issue
- GIVEN a
githubwebhook withtrusted_actors = {"logins": ["joestump"], "match": "sender"} - WHEN
malloryopens an issue, andjoestumpthen labels it - THEN the
openeddelivery is quarantined, and thelabeleddelivery 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
githubwebhook withouttrusted_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
giteawebhook withtrusted_actors = {"allow_all": true} - WHEN an outsider opens an issue
- THEN it reaches the rules with
.actor.trusted = trueandauthor_trusted = null, andlist_webhooksshowsallow_all: true
Scenario: Existing webhook after the upgrade
- GIVEN a
githubwebhook created before this change - WHEN the migration has run
- THEN its
trusted_actorsis{"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.loginisjoestump - THEN the sender is trusted
Scenario: A maintainer's comment keeps the outsider's thread untrusted
- GIVEN a
githubwebhook withtrusted_actors = {"logins": ["joestump"], "match": "sender"} - WHEN
joestumpcomments on an issue thatmalloryopened - THEN the delivery reaches the rules with
.actor = {sender: "joestump", author: "joestump", sender_trusted: true, author_trusted: false, trusted: true}, and undermatch = "author"it would be quarantined
Scenario: Token-trust webhook refused
- WHEN an agent calls
set_trusted_actorson agenericwebhook - THEN the call fails with
invalid_argument, saying the webhook's body is not signed
Scenario: Self-reported Cairn identity ignored
- GIVEN a
cairnwebhook withactor_ids = ["acct_joe"] - WHEN a signed artifact event arrives with
actor_id = "acct_other"andon_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": "…"}withouttrusted_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,claimorclaim_nexton 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
quarantinequeue, 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, callslist_todos, or callsclaimwith the todo's id - THEN
claim_nextreturns nothing from quarantine,list_todosomits it,claimfails withnot_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:")tolane-m - WHEN the owner releases it from the Quarantine view
- THEN it becomes a
lane-mtodo whose work order carriesauthor_trusted = falseandreleased_by = "human:<id>", andlane-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, andreceived_at. It MUST NOT return payloads.get_quarantined {id}MUST return one item, including the payload.release_quarantined {id, queue?}anddiscard_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
metacontain a count and the fixed instruction only
Scenario: A foreign classifier
- WHEN a classifier endpoint owned by human B calls
get_quarantinedwith 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 underallow_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
githubwebhook, installed at the GitHub organization level forissuesandissue_comment; trusted_actorswith the maintainers' logins andmatch = "sender";- rules that route trusted
labeledevents 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 = falsedownstream.
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
openedevent and a maintainerlabeledevent - 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
| Surface | Auth | Description |
|---|---|---|
MCP set_trusted_actors, clear_trusted_actors, rules verbs | Required | Endpoint credential, webhook-family grant, owner scope |
MCP list_quarantined, get_quarantined, release_quarantined, discard_quarantined | Required | Classifier-role endpoint credential only |
| Web UI Quarantine view and actions | Required | Signed-in human, owner scope, CSRF token |
POST /webhooks/w/{token} | Public | Inbound 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.