agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
733 lines • 36.5 kB
TypeScript
/**
* hosting/errors — the refusals, authored once so every adapter refuses in the
* same words.
*
* A refusal that varies by adapter is a refusal nobody can write a test or a
* runbook against. These six carry a stable `code`, name WHO refused, and say
* what the caller should do instead. Adapters map the codes onto whatever their
* transport uses to say "no" — that mapping is the adapter's business and lives
* in the adapter, never here.
*
* Note what is NOT here: a run that paused. That is unfinished work rather than
* a refusal, and it leaves through `reply.awaiting(...)` — its own terminal —
* not through an error dressed up as one.
*
* `requireCapability` is the last refusal and the only one that is a
* programming mistake rather than a runtime condition, so it throws a plain
* `Error`: nothing branches on "I forgot to feature-detect", it just needs to
* say so loudly and name the adapter it is talking about.
*/
import type { AgentHost, ConversationHost, HostCapability, PendingAsk } from './types.js';
/**
* Thrown when a request arrives at a host that is shutting down or shut down.
*
* `close()` lets in-flight work finish and refuses everything after it; this is
* what "everything after it" receives.
*/
export declare class HostClosedError extends Error {
readonly code: "ERR_HOST_CLOSED";
/** Which adapter refused. */
readonly hostName: string;
constructor(hostName: string);
}
/**
* Thrown when a request arrives for a session that already has a run in flight
* and the policy is `'reject'`.
*
* The refusal is about the SESSION, not about load: two turns of one
* conversation racing each other would each answer from the state the other is
* about to replace. A request for any OTHER session is never refused — it
* simply waits.
*/
export declare class ConcurrentRunError extends Error {
readonly code: "ERR_CONCURRENT_RUN";
/** The session that already has a run going. */
readonly sessionId: string;
/** The run that is already going, when it has announced itself. */
readonly activeRunId?: string;
constructor(sessionId: string, activeRunId?: string);
}
/**
* Raised when a run paused to ask a person something and there is **nowhere to
* keep it**.
*
* **The run did not fail.** A pause is unfinished work: the agent stopped to ask
* and is waiting for an answer. Since 7.19 a paused run is stored as
* `'flowchart-v1'` and continued by a later request carrying a decision — so the
* one case left where a pause genuinely cannot be carried is a request with no
* session id. There is no session to store it under, and therefore no later
* request that could ever answer it.
*
* The other half of the old meaning — "the reply cannot carry a pause" — is
* gone: {@link HostReply.awaiting} carries it now. An adapter that has not
* implemented that terminal still gets its pause STORED (the store is not the
* transport's business) and this refusal on the wire, naming the session it can
* be answered on.
*/
export declare class PauseNotCarriedError extends Error {
readonly code: "ERR_PAUSE_NOT_CARRIED";
/** The tool that asked, when the run recorded which one it was. */
readonly toolName?: string;
/** The session the paused run was stored under, when there was one. */
readonly sessionId?: string;
/** Whether the paused run was stored. `false` means it is gone. */
readonly stored: boolean;
constructor(toolName?: string, sessionId?: string, stored?: boolean);
}
/**
* Thrown when a new message arrives for a session whose run is waiting on a
* person's decision.
*
* The message is NOT run and the pause is NOT discarded — those are the two ways
* this could have gone wrong. Answering the message would step over an
* outstanding consent gate; dropping the paused run to make room for the message
* would throw away work a person was asked about. So the request is refused, the
* pending question is named, and the session sits exactly where it was.
*
* Answer it by sending the same session a request carrying
* {@link HostRequest.decision}. A pause NO answer can reach any more — stored
* before requests were named, its call changed after the ask, or an ask made
* under rules the agent no longer runs — is not held this way: the door closes
* it and runs the message (`answeredPause.ts`).
*/
export declare class AwaitingDecisionError extends Error {
readonly code: "ERR_AWAITING_DECISION";
/** The session that is waiting. */
readonly sessionId: string;
/**
* What it is waiting on — the same payload `reply.awaiting()` delivered.
*
* NOT enumerable. It names the pending request's id (`checkIn.id` /
* `ask.id`), the one value a decision must quote, and an own enumerable
* field is serialized whole by `JSON.stringify(err)` — what a logger or a
* crash reporter does to an error. So it reads as a property for the code
* that caught the error and is left out of every copy made by enumeration,
* like the refusals that name no id at all (`ResumeDecisionError`).
*/
readonly pending: PendingAsk;
constructor(sessionId: string, pending: PendingAsk);
}
/**
* Thrown when a request carries a decision for a session that is not waiting on
* one.
*
* Usually a duplicate delivery: the run was already continued, or already
* answered, and the same decision arrived twice. Running it as an ordinary
* message would put a raw approval into the conversation as if the user had
* typed it, so it is refused by name instead.
*/
export declare class NoPendingAskError extends Error {
readonly code: "ERR_NO_PENDING_ASK";
/** The session the decision was addressed to. */
readonly sessionId: string;
constructor(sessionId: string);
}
/**
* Thrown when something is sent down a conversation that has already ended.
*
* A closed channel that accepts frames and drops them is the worst of the three
* options: the sender believes the far side got it, the far side never did, and
* nothing anywhere says so. Refusing by name is the only version of this that
* leaves a trace.
*
* `onClose` is how you avoid meeting this — subscribe, and stop sending.
*/
export declare class ConversationClosedError extends Error {
readonly code: "ERR_CONVERSATION_CLOSED";
/** Which adapter's door this was. */
readonly hostName: string;
/** The session the conversation claimed, when it claimed one. */
readonly sessionId?: string;
constructor(hostName: string, sessionId?: string);
}
/**
* Thrown when a frame is bigger than the ceiling the adapter DECLARED.
*
* This is the other half of {@link ConversationLimits} — a declared ceiling
* nothing enforces is a number in a doc comment. The port neither chunks nor
* truncates, on purpose: how a message is split, numbered and reassembled is
* the consumer's protocol question, and answering it inside the adapter would
* answer it for every consumer at once. So the ceiling is visible, the refusal
* names it, and the splitting happens above the port where the protocol lives.
*/
export declare class FrameTooLargeError extends Error {
readonly code: "ERR_FRAME_TOO_LARGE";
/** Which adapter's door refused. */
readonly hostName: string;
/** How big the frame was, in bytes of UTF-8. */
readonly bytes: number;
/** The declared ceiling it crossed. */
readonly maxFrameBytes: number;
constructor(hostName: string, bytes: number, maxFrameBytes: number);
}
/**
* Thrown when a request body names a wire operation this host does not speak,
* or names one and leaves out what it needs (an artifact op without a `ref`).
*
* The law it enforces: **a body that named an `op` never falls through to a
* model turn.** A caller who typo'd `'artifact-head'` and silently got a
* conversation turn — with the ref as garbage input — would be told nothing
* and billed anyway, which is the accepted-and-silently-wrong failure this
* refusal exists to prevent. Adapters answer it as that request's 400: the
* request is what is wrong, and the same caller can send the right shape.
*/
export declare class InvalidWireOpError extends Error {
readonly code: "ERR_INVALID_WIRE_OP";
constructor(detail: string);
}
/**
* The wire spelling of an artifact verb, for the refusals below. (Spelled here
* rather than imported: `wireOps` imports this file for its own refusal, and
* the two spellings are pinned equal by the wire tests.)
*/
/** A bound verb from `handle.artifactsForRequest` — spelled as the call. */
type SeamVerb = 'put' | 'delete' | 'list';
/**
* Thrown when an artifact operation arrives with no session id.
*
* Artifact resolution is governed by the requesting session's identity —
* a ref is redeemed under exactly the scope the run's tools minted it in, and
* a request that names no session presents no scope to resolve under. (A run
* served without a session scopes its artifacts to its own runId, which no
* later request can name — so there is genuinely nothing this request could
* ever redeem.) There is deliberately no bare-ref mode: an id alone opens
* nothing, ever.
*/
export declare class ArtifactSessionRequiredError extends Error {
readonly code: "ERR_ARTIFACT_SESSION_REQUIRED";
constructor(op: 'head' | 'get' | 'account');
}
/**
* Thrown when an artifact operation arrives and the agent serving this
* session has no artifact store attached.
*
* The teaching refusal of the fail-closed capability (`ctx.artifacts` with no
* store), spoken at the hosting door: it names the attach rather than
* answering "not found" — a deployment gap and a missing ref are different
* facts, and only one of them is the operator's to fix.
*/
export declare class NoArtifactStoreError extends Error {
readonly code: "ERR_NO_ARTIFACT_STORE";
constructor(op: 'head' | 'get' | 'account');
}
/**
* Thrown when a ref does not resolve for the requesting session — missing,
* expired, or minted under a scope this session's identity does not compose.
*
* **Deliberately one shape for all three.** Distinguishing "never existed"
* from "another session's" would let a caller probe scopes it does not own;
* the store already answers a wrong scope with "no data" rather than a
* cross-tenant error, and this refusal keeps that ambiguity on the wire.
* The screen renders a stated absence in place — the `present` result's
* description snapshot exists exactly so an expired pane can still say what
* is gone.
*/
export declare class ArtifactNotFoundError extends Error {
readonly code: "ERR_ARTIFACT_NOT_FOUND";
/** The ref that did not resolve. */
readonly ref: string;
constructor(ref: string);
}
/**
* Refused when one session already has as many artifact operations in flight
* as the host allows (`standingAgent({ artifactOpsPerSession })`, default 8) —
* `artifact-head`, `artifact-get` and `answer-account` counted together.
*
* Those ops are lane-free and turn admission does not see them, so without a
* bound one session could keep the event loop every session shares busy with
* payload reads. The refusal is for THIS session only (every other session is
* served), it names no session id, and the same request succeeds once one of
* the session's own requests has finished.
*/
export declare class ArtifactOpsBusyError extends Error {
readonly code: "ERR_ARTIFACT_OPS_BUSY";
/** The per-session bound that was met. */
readonly limit: number;
constructor(op: 'head' | 'get' | 'account' | SeamVerb, limit: number);
}
/**
* Refused when an `answer-account` request names a recording larger than the
* host's ceiling (`standingAgent({ answerAccounts: { maxRecordingBytes } })`,
* default 16 MiB).
*
* The account is computed on the event loop that serves every session, so its
* worst single parse is bounded by a number the operator chose — and a
* recording over it is refused BEFORE a byte of its payload is read (the size
* comes from the ticket's `bytes`). Named rather than folded into the one
* not-found: it reaches only a caller who already passed the ownership check,
* so it tells them nothing about anybody else's records, and "too large to
* explain here" is a different fact from "not available" — the operator can
* raise the ceiling, and a person should not be told their answer's record is
* gone when it is not.
*
* The message carries the ceiling and nothing from the recording.
*/
export declare class RecordingTooLargeForAccountError extends Error {
readonly code: "ERR_RECORDING_TOO_LARGE_FOR_ACCOUNT";
/** The ceiling the recording is over, in bytes. */
readonly maxRecordingBytes: number;
constructor(maxRecordingBytes: number);
}
/**
* Raised when an artifact RESOLVED and the reply cannot describe it — a host
* without {@link HostReply.artifact}, or an `HttpWire` dialect without an
* `artifact` body shape.
*
* The `PauseNotCarriedError` shape, for the other optional terminal: the
* resolution itself succeeded and nothing about the store is wrong — it is
* THIS REPLY that has no vocabulary for the answer. Named rather than
* improvised, because an adapter inventing a body shape on the spot would be
* a body no client was written against.
*/
export declare class ArtifactNotCarriedError extends Error {
readonly code: "ERR_ARTIFACT_NOT_CARRIED";
/** The ref that resolved but could not be delivered. */
readonly ref: string;
constructor(ref: string, hostName?: string);
}
/**
* Refused when host code uses a turn's artifact hand-over
* (`HostReply.turnArtifacts`) after that turn ended.
*
* The hand-over is live only while the hook runs: the composer awaits the
* hook while the session's lane is held, waits for every operation the hook
* started, then revokes the binding before the reply ends. A verb called
* after that is refused by name rather than performed. The footprintjs
* `ScopeFacade · assertLive` law, one layer up: a handle held past its stage
* is refused, because a write made outside its turn lands on whatever run the
* instance is serving by then — in the shared shape, another person's — and
* makes the record lie.
*
* The fix is always the same: file INSIDE the hook (`await
* turn.artifacts.put(...)`), and carry the ticket onward from there.
*/
export declare class TurnArtifactsExpiredError extends Error {
readonly code: "ERR_TURN_ARTIFACTS_EXPIRED";
/** Which verb was called after the turn ended. */
readonly op: 'put' | 'head' | 'get' | 'delete' | 'list';
constructor(op: 'put' | 'head' | 'get' | 'delete' | 'list');
}
/**
* Thrown by a verb of a binding `handle.artifactsForRequest` returned, called
* after the binding was revoked: the instance it was bound to was retired
* (evicted from the pool) or the composer was closed.
*
* The same law as {@link TurnArtifactsExpiredError}: a binding never outlives
* the instance whose store and record it writes to — a write after that would
* land on a stopped agent, and its fact on a record nobody holds. The
* rejection is created already handled, so a floating late call cannot crash
* the process. Ask the handle again for a fresh binding.
*/
export declare class RequestArtifactsRevokedError extends Error {
readonly code: "ERR_REQUEST_ARTIFACTS_REVOKED";
/** Which verb was called after the revocation. */
readonly op: 'put' | 'head' | 'get' | 'delete' | 'list';
constructor(op: 'put' | 'head' | 'get' | 'delete' | 'list');
}
/**
* Thrown when a store hands back something that is **present but unreadable**
* where a `CheckpointEnvelope` should be.
*
* The law, in the words of the field report that bought it:
*
* > *An unreadable stored conversation and an absent one are different facts,
* > and only one of them is safe to answer with a fresh start.*
*
* Absent is ordinary — a new session has no conversation, and answering it
* fresh is exactly right. Unreadable is not ordinary: a conversation EXISTS,
* somebody is in the middle of it, and starting fresh over the top of it looks
* identical to the happy path from the outside. Nobody finds that until a
* deployment boundary hands a user a stranger's blank slate. So this refuses,
* loudly, naming the session — the one thing a silent `undefined` could never
* do.
*
* It extends `TypeError` because that is what it always was: the refusal gained
* a name, a `code` and the session it is about, but a caller who was already
* catching a `TypeError` from a reader keeps working.
*
* `storedPreview` quotes at most 64 characters (`STORED_PREVIEW_LIMIT`, in
* `lib/storedPreview` — one cap, shared by every adapter that has to describe
* bytes it could not read) — enough to recognise a mangled encoding at a
* glance, never the conversation itself.
*/
export declare class UnreadableEnvelopeError extends TypeError {
readonly code: "ERR_UNREADABLE_ENVELOPE";
/** The session those bytes were stored under, when the refuser knows it. */
readonly sessionId?: string;
/** A short, deliberately truncated rendering of what the store handed back. */
readonly storedPreview: string;
constructor(stored: unknown, sessionId?: string);
/**
* The same refusal, naming the session — for a reader that knew the bytes
* were unreadable but not whose conversation they were.
*
* Returns a copy rather than mutating: an error already thrown past somebody
* is a fact about a moment, and editing it under them is how two stack traces
* end up disagreeing about what happened.
*/
withSession(sessionId: string): UnreadableEnvelopeError;
}
/**
* WHY a presented credential did not identify anybody — the whole vocabulary a
* refusal is allowed to say about a token.
*
* Every value here is a fact about the CHECK, never about the secret. That is
* the point of enumerating them at all: an operator needs to act differently on
* each one, and `expired` versus `wrong-audience` versus `unverifiable` is the
* entire difference between "the client should refresh", "the client is pointed
* at the wrong API" and "somebody is presenting something we cannot check" —
* none of which requires printing one character of the token.
*
* - `'no-token'` — no `Authorization: Bearer …` arrived at all.
* - `'expired'` — the token's own lifetime is over, or the sign-in the
* request named has ended (signed out, expired, idle, or never existed —
* one answer, so a guessed sign-in key learns nothing).
* - `'not-yet-valid'` — its `nbf` is in the future (a clock-skew smell).
* - `'wrong-audience'` — it was minted for a different API.
* - `'wrong-issuer'` — it came from an IdP this door does not accept.
* - `'unverifiable'` — the signature did not check out, no key matched, the
* algorithm is not allowed, or it is not a token at all. Deliberately ONE
* class: distinguishing "bad signature" from "unknown key" tells whoever is
* probing which half of a forgery to fix.
* - `'claimed-another-user'` — a valid token, and a request that signed
* somebody else's name beside it.
* - `'not-a-user-token'` — a valid token that does not stand for a PERSON:
* an app-only token (`idtyp: app`, `oid` equal to `sub`, roles and no
* scope) or one without the scope only this API's person tokens carry.
* Raised by `oidcIdentity`'s person test.
* - `'wrong-client'` — a person's token, obtained by a client this door does
* not list (`azp` / `appid` / `cid` / `client_id`), or naming none.
* - `'two-credentials'` — the request presented a token AND a sign-in.
* Which one to believe is not a question a door answers (rule 13).
* - `'roles-unknown'` — the token's roles claim was replaced by a pointer to
* somewhere else (an IdP's group-overage pointer). Unknown is not none: reading it
* as "no roles" would strip a person silently.
*
* The last four arrived with `oidcIdentity` and the sign-in seam. A
* consumer's exhaustive `switch` over this union must add them.
*/
export type IdentityFailureClass = 'no-token' | 'expired' | 'not-yet-valid' | 'wrong-audience' | 'wrong-issuer' | 'unverifiable' | 'claimed-another-user' | 'not-a-user-token' | 'wrong-client' | 'two-credentials' | 'roles-unknown';
/**
* The caller could not be identified, and this door was configured to insist.
*
* **The message never contains the token.** Not truncated, not hashed, not "the
* first eight characters". A bearer token in a refusal is a bearer token in the
* reply body, in the log line, in the trace and in every sink attached — one
* echo and the credential has been published. What travels is
* {@link IdentityFailureClass} and the sentence that tells the caller what to
* do about it.
*/
export declare class IdentityNotVerifiedError extends Error {
readonly code: "ERR_IDENTITY_NOT_VERIFIED";
/** What went wrong, in the one vocabulary. Never the token. */
readonly failure: IdentityFailureClass;
/** Did the request also NAME a user it could not prove? The impersonation
* shape, kept as a fact so a sink can count it separately. */
readonly claimedUser: boolean;
constructor(failure: IdentityFailureClass, claimedUser: boolean);
}
/**
* The verifier itself could not do its job — its key set was unreachable, its
* introspection endpoint timed out.
*
* A DIFFERENT fact from a token that failed, and the difference is who is at
* fault: this is the deployment's outage, not the caller's bad credential, and
* answering it with a 401 would send every client off to re-authenticate
* against an identity provider that is already down. It maps to 503.
*/
export declare class VerifierUnavailableError extends Error {
readonly code: "ERR_IDENTITY_VERIFIER_UNAVAILABLE";
/** Which verifier could not answer — its name, never its configuration. */
readonly verifier: string;
constructor(verifier: string, detail?: string);
}
/**
* An admission policy refused this request before any work started.
*
* The policy authors the sentence — this class only carries it — because the
* limit and its reset are the operator's facts, not the library's. What this
* class guarantees is the SHAPE: a refusal that names a limit is a refusal a
* client can act on, and one that says "denied" is a support ticket.
*/
export declare class AdmissionRefusedError extends Error {
readonly code: "ERR_ADMISSION_REFUSED";
/** WHO was refused, when the request identified anybody. Never a token. */
readonly userId?: string;
constructor(reason: string, userId?: string);
}
/**
* A session-history operation arrived at a door that does not verify identity.
*
* Listing a person's conversations is answering "which sessions belong to
* you?", and a door that reads WHO from an unverified header would answer that
* for anyone who guesses a user id — enumeration with a friendly interface.
* So the ops are refused outright rather than served under a claimed name.
*/
export declare class SessionOpNeedsIdentityError extends Error {
readonly code: "ERR_SESSION_OP_NEEDS_IDENTITY";
/** The op that was refused. */
readonly op: string;
constructor(op: string);
}
/**
* A session-history operation arrived at a session store that keeps no
* owner index.
*
* The two members are OPTIONAL on the port on purpose — most stores are a
* key/value map and owe nobody a secondary index — so this refusal names the
* store's limitation instead of pretending the answer is "you have no
* sessions". An empty list and an unanswerable question are different facts,
* and only one of them is safe to render as "nothing here".
*/
export declare class SessionIndexUnavailableError extends Error {
readonly code: "ERR_SESSION_INDEX_UNAVAILABLE";
readonly op: string;
constructor(op: string, missing: string);
}
/**
* Retention was asked of a session store that has no answer for it (9.42.0).
*
* `retention` is OPTIONAL on the port for the same reason the two index
* members are — most stores are a key/value map and owe nobody a way to expire
* one — so this refusal names the store's limitation rather than doing
* nothing quietly. That distinction is the whole point here: an unanswerable
* question and a sweep that deleted nothing look identical from a cron job's
* exit code, and a deployment can run for a year believing conversations are
* being expired because a call returned without complaining.
*
* It names which stores DO implement it, because the answer to "my store
* cannot do this" is usually "then use one that can", and a refusal that makes
* somebody go and find that out is a refusal that gets caught and ignored. The
* two it names by hand are the two that ship in this folder; the cloud
* adapters are pointed at rather than listed, because a hand-maintained list
* of them inside a PORT is both a vendor name where none belongs and a list
* that goes stale the release after somebody adds one.
*
* No identity material, like every refusal here: retention is about a store,
* and nothing about who was signed in belongs in it.
*/
export declare class SessionRetentionUnavailableError extends Error {
readonly code: "ERR_SESSION_RETENTION_UNAVAILABLE";
/** What was being attempted, in the caller's words. */
readonly purpose: string;
constructor(purpose: string);
}
/**
* The ONE answer for a session the caller may not have — read OR continued.
*
* A session that does not exist, one that belongs to somebody else, and one
* whose stored conversation names no owner all end here, byte-identical but
* for the id the caller already knew. Told apart, they would be an oracle for
* which session ids are real — the same law
* {@link ArtifactNotFoundError} follows, for the same reason.
*
* Raised by BOTH doors that open a session at a verifying host: the two
* session-history ops, and an ordinary turn that named somebody else's session
* (9.26.0). One refusal, so the answer cannot depend on which door a caller
* knocked at — the mistake this class of hole is always made of.
*/
export declare class SessionNotFoundError extends Error {
readonly code: "ERR_SESSION_NOT_FOUND";
/** The id that was asked for — the caller's own string, nothing learned. */
readonly sessionId: string;
constructor(sessionId: string);
}
/**
* A `persist` would have left the ownership index naming one person and the
* stored conversation naming another (9.36.1).
*
* ── The split brain this exists to prevent ──────────────────────────────────
* The write-once rule protects the INDEX and said nothing about the PAYLOAD.
* Two writers signing the same fresh session therefore produced a store where
* `ownerOf(session)` answered the FIRST writer and the stored envelope carried
* the SECOND writer's whole conversation — and the envelope carries its own
* identity. The first writer lists the session, opens it, and reads somebody
* else's conversation. That is not a race in one backend: it was reproduced in
* a live trial and then found, by inspection, in every store implementing this
* contract, because the flaw was in the contract rather than in any of them.
*
* So the rule gained its missing half: **a different non-empty identity is
* refused, and an absent one is allowed.** A leaner turn claims nobody and
* cannot contradict anybody — the contract explicitly blesses it — while a
* turn signed by somebody else is a contradiction, and a store that stored it
* would be a store where ownership is decided by who wrote last.
*
* ── What this refusal will not tell you ─────────────────────────────────────
* Neither name. Not the owner, not the caller, not a line of either
* conversation. An error is read by whoever provoked it, and a refusal that
* names the person who owns a session is an oracle for who is signed in — the
* same law {@link SessionNotFoundError} and {@link ArtifactNotFoundError}
* follow. The session id is here because it is the caller's own string, which
* is the one thing the message teaches nobody anything by repeating.
*/
export declare class SessionOwnershipConflictError extends Error {
readonly code: "ERR_SESSION_OWNERSHIP_CONFLICT";
/** The id that was written to — the caller's own string, nothing learned. */
readonly sessionId: string;
constructor(sessionId: string);
}
/**
* A session-history result resolved and the reply cannot describe it.
*
* The {@link ArtifactNotCarriedError} shape, for the other new terminal.
*/
export declare class SessionsNotCarriedError extends Error {
readonly code: "ERR_SESSIONS_NOT_CARRIED";
readonly op: string;
constructor(op: string, hostName?: string);
}
/**
* Assert that a host can do something, and throw a corrective error naming the
* adapter when it cannot.
*
* This is the feature-detection law with teeth: capabilities are read, never
* assumed, and asking for one that is absent tells you which adapter you are
* actually holding rather than failing quietly somewhere downstream.
*
* Takes either port, because both declare the same two facts — who they are and
* what they can do — and a caller holding a conversation-only host is entitled
* to the same answer as one holding a request host.
*
* @example
* requireCapability(host, 'streaming'); // throws unless this host streams
*
* // or branch instead of insisting:
* if (host.capabilities.includes('streaming')) { ... }
*/
/**
* Thrown when a request body exceeds the ceiling the host was given.
*
* A body is memory this process pays for while somebody else fills it, so a
* host with a ceiling stops reading at it rather than discovering the limit as
* an out-of-memory kill. The read is abandoned at the byte that crossed the
* line — nothing further is buffered, and the bytes already read are dropped.
*
* There is no default ceiling: see `HttpHostOptions.maxBodyBytes` for why the
* absence is deliberate and what it costs.
*/
export declare class RequestTooLargeError extends Error {
readonly limitBytes: number;
readonly code: "ERR_REQUEST_TOO_LARGE";
constructor(limitBytes: number, hostName: string);
}
/**
* A dialect's refusal of THIS request — its own status, its own code.
*
* `readRequest` is a wire's one chance to look at a body before a turn is paid
* for, and some dialects can tell from the shape alone that they cannot serve
* it: an input item kind this dialect does not carry, a field whose type is
* wrong. Throwing this says so with a status the caller can act on, instead of
* a 500 that reads as "the server broke" for a request the caller could fix.
*
* The distinction it keeps: this is the REQUEST being wrong. A dialect that
* throws anything else from `readRequest` still gets that request's 500, which
* is the honest answer when the dialect itself is what failed.
*
* @example A dialect refusing an input kind it does not carry
* throw new WireRequestRefusal(
* 'unsupported_input',
* 'image input is not carried by this dialect',
* );
*/
export declare class WireRequestRefusal extends Error {
readonly code: string;
/** HTTP status for this refusal. Default 400 — the request is what is wrong. */
readonly status: number;
constructor(code: string, detail: string,
/** HTTP status for this refusal. Default 400 — the request is what is wrong. */
status?: number);
}
/**
* A request that changes something did not say `content-type: application/json`.
*
* The door is a JSON API, and that is only half the reason. The other half is
* the web: a page on ANY site can make a browser send `text/plain`,
* `application/x-www-form-urlencoded` or `multipart/form-data` — or no content
* type at all — without asking the server first (a CORS "simple" request). A
* browser must ask first (a preflight, which this host never approves) before
* it sends `application/json` across origins. Requiring it is what turns a
* forged request into one the browser refuses to send.
*/
export declare class UnsupportedMediaTypeError extends Error {
readonly code: "ERR_UNSUPPORTED_MEDIA_TYPE";
/** 415 Unsupported Media Type. */
readonly status: 415;
constructor(hostName: string);
}
/**
* Which browser fact refused an {@link OriginNotAllowedError}.
*
* - `'same-host'` — `allowedOrigins` is unset and the page's Origin is not
* this door's own name.
* - `'listed'` — the page's Origin is not in `allowedOrigins`.
* - `'fetch-metadata'` — the browser marked the request `Sec-Fetch-Site:
* cross-site` and its Origin is not one `allowedOrigins` lists. This is the
* one that still fires when a proxy strips `Origin` but forwards the Fetch
* metadata beside it.
*/
export type OriginRefusalRule = 'same-host' | 'listed' | 'fetch-metadata';
/**
* A browser request came from a page whose origin this door does not allow —
* or from an opaque origin (`Origin: null`), which any sandboxed frame on any
* site can produce and which is therefore never allowed — or the browser said
* outright that it was sent cross-site.
*
* Only BROWSERS send `Origin` (on every request that changes something, and on
* every WebSocket handshake), so a request without one is a script or a
* server — unless the same request carries the browser's own `Sec-Fetch-Site:
* cross-site`, which is how a stripped `Origin` still gets caught.
*/
export declare class OriginNotAllowedError extends Error {
readonly code: "ERR_ORIGIN_NOT_ALLOWED";
/** 403 Forbidden. */
readonly status: 403;
/** Which browser fact refused it. */
readonly rule: OriginRefusalRule;
constructor(hostName: string, rule: OriginRefusalRule);
}
/**
* A request named a Host this door was not configured to answer for.
*
* The DNS-rebinding defence: a page on an attacker's domain re-points its OWN
* name at this server's address, and from then on the browser treats it as
* same-origin with the door — so every other check passes, and the Host header
* (which no page can set) is the one fact that still names the attacker.
* 421 is the status HTTP defines for exactly this (RFC 9110 §15.5.20: a target
* "that does not match an origin for which the server has been configured").
*/
export declare class HostNotAllowedError extends Error {
readonly code: "ERR_HOST_NOT_ALLOWED";
/** 421 Misdirected Request. */
readonly status: 421;
/**
* Where the list came from: `'listed'` — the deployment's own
* `allowedHosts`; `'loopback-default'` — nobody set one, and the door is
* bound to a loopback address, so it answers the loopback names only.
*/
readonly rule: 'listed' | 'loopback-default';
constructor(hostName: string, rule?: 'listed' | 'loopback-default');
}
/**
* A session id the door will not carry: empty, longer than the ceiling, or
* holding anything but visible ASCII (0x21–0x7E).
*
* A session id is caller data that travels everywhere the conversation does —
* the store's key, every log line, every exported trace span of every turn.
* Unbounded, one caller with no credentials could make every span of a turn
* carry megabytes; with a control, a line separator or a bidi override, it
* could forge or disguise a line in any log that prints it; with non-ASCII,
* the same id read from a header (latin1) and from a body (UTF-8) would be two
* different conversations. So it is bounded at the door, before any of that
* happens. The refusal says how long the id was, never what it said.
*/
export declare class InvalidSessionIdError extends Error {
readonly code: "ERR_INVALID_SESSION_ID";
/** 400 Bad Request — the caller can send a valid id and continue. */
readonly status: 400;
/** Which rule the id broke. Never the id. */
readonly reason: 'empty' | 'too-long' | 'invalid-character';
constructor(reason: 'empty' | 'too-long' | 'invalid-character', hostName: string, length: number, limit: number);
}
export declare function requireCapability(host: AgentHost | ConversationHost, capability: HostCapability): void;
export {};
//# sourceMappingURL=errors.d.ts.map