What Nightjar refuses, and why
Status: explanation. Every refusal below is enforced in shipped code, not planned. Audience: a technically sophisticated evaluator deciding whether to trust this with credentials and network reach. Scope: the deliberate nos — what the platform will not do, and the reasoning that makes each one a design choice rather than a missing feature.
A product’s capabilities tell you what it can do for you. Its refusals tell you what it can do to you — and that is the question you are actually asking. This page collects the second kind.
The shape of a refusal here
Most refusals in Nightjar are structural: the unwanted thing is not blocked by a check that could be misconfigured, it is unrepresentable. Where a refusal is a check, we say so, because the two give you very different guarantees.
A control you can turn off is a policy. A shape that cannot express the failure is a guarantee. Both appear below, labelled.
Your content is returned by reference, not by value
Tools that touch page content — extraction, clipboard, downloads — hand your agent a session://artifacts/<hash> reference rather than the bytes. The content is materialised on the node
and never crosses the tool boundary inline.
Why: an agent’s context is a transcript. Anything returned by value is durable, quotable, and copied onward by default. A reference makes the decision to look explicit and separately governed.
What this does NOT mean: it is not encryption, and it is not redaction. The artifact holds whatever the page held. See What these things are not.
An org you are not a member of is indistinguishable from one that does not exist
When a request names an organisation, the platform resolves it against the session’s memberships. A non-member org returns exactly what an unknown org returns.
Why: the alternative leaks. If “exists but you lack access” were distinguishable from “no such org”, the error itself becomes a directory of every tenant on the platform, readable by anyone with an account.
How it is enforced: by a named test, so a future change that reintroduces the distinction fails the build rather than shipping quietly.
The platform refuses to guess which organisation you meant
A human who belongs to several organisations, arriving without naming one, is not defaulted to the first, the newest, or the largest. The request is refused as ambiguous.
Why: silently choosing an org means silently choosing a blast radius. A wrong guess is not a usability annoyance — it is work performed against the wrong tenant.
The cost, honestly: a multi-org human sees a prompt where a single-org human sees none. We took the prompt.
Secrets are filled, never handed over
A stored credential is typed into a page by the node and zeroised. It is never returned to the agent that asked for it, never written to the session journal, and never appears in a screenshot the agent requested.
Why: an agent that never receives a secret cannot leak one — through a transcript, a log, a bug report, or a model that quotes its own context back. The value-free boundary is what makes “an agent drives your browser” a bounded claim instead of an unbounded one.
Field-level refusals, from the specification
Each section below is moved verbatim from the OpenAPI description it used to live in, and the schema keeps a one-line pointer where it was. They are grouped by what makes the refusal hold — which is a different question from the arguments above, and the reason they sit together rather than being folded into them.
Refusals that are structural, not policy
The thing cannot be formed, so there is nothing to enforce. This is the largest group here, and it is the opening argument of this page stated at field level.
ExtractRequest
mode is a CLOSED enum ([ExtractMode]) — text (default) or structured (#1333). The
node-side raw-markup html mode remains unrepresentable here (see [ExtractMode]); together
with #[serde(deny_unknown_fields)] a stray field or a non-variant mode is a hard 400 rather
than a silently-ignored input — a future refactor cannot quietly re-widen the read past drive
(pinned by negative tests in the exact-set suite).
ProjectionSearchRequest
Whole-page by construction: there is deliberately NO selector, because scoping the search to one element would presuppose the answer it exists to find.
Gated on its OWN projection_search operation rather than folded into extract (CAA ruling,
#2962): a dominance argument for extract was proposed and ruled unproven, because reproducing
this search client-side needs the PER-ELEMENT innerText multiset and extract yields either flat
text or a closed block enum that collapses exactly the wrapper elements it ranges over.
StreamTicket
The ticket is bound to its own session AND its own stream kind — each mint writes into a dedicated
store, so a viewport ticket is structurally absent from the journal store and can never be replayed
to open a different stream. Authority is checked at mint time, on the authenticated POST; the
handshake itself reads no Authorization header (see the streamTicket security scheme).
⛔ BOTH FIELDS ARE SECRET-BEARING. See each field — this is not a struct-level caveat you can discharge by redacting one of them.
CredentialFillRequest
The model passes ONLY the credential SLOT name plus the target selector; there is NO text/value field by construction, so the model structurally cannot place the secret. Control
JIT-decrypts the named credential on the session’s bound identity into a Zeroizing buffer and
forwards a node type act (selector, origin allowlist, requireEditableField=true); the node
verifies live origin + editable-field before typing, then zeroizes. The fill act itself is
value-free: the secret is never returned in the result, journaled, screenshotted, or audited.
Filling a non-masked (text/email) field leaves the value visible on the rendered page, where
later perceives/screenshots can observe it — prefer masked (password) fields. The response
reuses [ActResult].
/v1/reach/occupancy/get
Discloses no more than a bind already does: reach/ensure’s contention refusal already returns
this exact holder to any org member who contends for the target, so this removes the side effect,
not a disclosure boundary. Org-anchored by construction (org_id predicate), so a cross-org answer
cannot be formed. Same reach:self entitlement as ensure: if you may take a target, you may ask
whether it is taken. /v1 only — deliberately NOT projected as an agent MCP tool (no consumer
asked for one, and adding one later is additive while removing one is breaking; un-gate is the first
agent consumer that asks).
Refusals that disclose nothing new
The answer was already reachable, so withholding it would buy nothing. Each is decided from state the caller already has.
AccessRequestCreated
⚠ THE ECHO DOES NOT WEAKEN THE ORACLE PROPERTY, and that is the whole reason it is safe (#2686
remedy 2). requestedScopes and delivery are the AGENT’S OWN INPUTS read back — never
server-resolved state. The agent learns nothing it did not send; what it gains is the ability to
verify BY EFFECT that the shape it sent is the shape that landed. requestedIdentityRef is
deliberately NOT echoed: it is also agent-supplied, but echoing it invites a reader to treat this
response as confirmation the identity resolved, which it never is.
ReachRefusalState
These are ordered cheapest-to-decide, and each is decided from state the caller already has access to — never from request input, so none of them is an oracle:
CredentialNotAgentBound ctx.agent_id.is_none() no DB read at all
OrgFlagDisabled governance_settings the read that was already here
CredentialNotReissued EXISTS(reach:self key for me) the added read
BindingPresentsOldKey otherwise What a refusal attests, and what it does not
A refusal carries exactly one claim. Reading a second one out of it is where callers go wrong.
ReachRefusal
R-d: this is DATA IN THE BODY. The approval URL is returned as a field for the caller to PRINT — never auto-opened, never written to stdout by the platform.
R-a: approvalUrl NAMES a request; it never encodes a delta. The id is the whole payload, so
the URL cannot be edited into a different grant, and what the operator approves is looked up
server-side rather than read off the link.
Shape follows [ReachRemedy] deliberately — the house pattern for handing a caller one
unambiguous next action, with a list of outstanding preconditions rather than a single sentence.
RecordingStartResult
This verb answered {} until #2419: a deliberate value-free ack. The gap that closed is narrow and
worth stating exactly, because the issue rejected three receipt-shaped alternatives for carrying zero bits — outcome_certainty is always COMPLETED on a success-only response, a timestamp attests when the ack was written, and a request_id is a correlation handle.
fps is none of those: the node clamps to 1..=30 (default 4) and spawns the encoder with the
clamped value, so it is observed node-side policy the caller cannot compute — the request carries fps as input only and the bounds appear nowhere on the wire. It therefore VARIES between two
SUCCESSFUL starts, which is precisely what a zero-bit field cannot do.
⛔ It does not attest that frames are flowing. Presence-on-success carries no information (every
field of a success-only response shares that property); the content is the rate capture was actually
configured at. For “did it capture anything?“, recording-stop reports frameCount.
Metadata only, never a frame — the ADR-046 authority floor is unchanged.
The gate is authority, never a content filter
A scope decides who may read; it has never decided what the bytes are.
ConsoleEntry.text
⛔ #2238 removed the secret-shape floor that once redacted tokens here (operator ruling: no
heuristics), so this exit performs no content filtering. The sessions:read org scope is
the gate, and it was never a content filter.
What still holds is the OVERSIZE branch, and it is structural rather than a filter: a message too large to have been inlined (persisted as an artifact or chunk-series ref) surfaces a placeholder rather than its bytes — those bytes are never fetched on this path at all, which is the objection that survived #2238 (it was never about flooring, it was about not opening a second exit).
What is not on this page
Refusals that exist only as configuration — things an operator can switch on or off — are documented with their surface rather than here, because they are policy and this page is about shape. If you need to know whether a specific control is armed in your deployment, the API reference and build status pages answer that; this page cannot, and would be lying if it tried.