Documentation
¶
Overview ¶
Package command holds the durable command vocabulary Factory admits.
It is a VOCABULARY, not a dependency seam, which is why it is shared rather than restated by each caller. Specification section 8.1 makes the REST control routes and the ClientLink RPCs two spellings of one admission contract: the same principal, the same authorization decision and the same durable record. Two packages holding two private copies of "gate_response" would satisfy every test either package could write and still let a ClientLink RPC be authorized under a kind the REST route never admits.
sessionstore deliberately does not enumerate these. Its CommandKind doc says the set is left open so a later Core wire version can add one, and a closed set there would refuse a command Core itself considers valid. Factory's own surface is closed -- it serves exactly the operations routeTable and the ClientLink method vocabulary name -- so the enumeration belongs here.
Index ¶
- Constants
- func Kinds() []sessionstore.CommandKind
- func RefusalCodes() []sessionwire.ErrorCode
- func RefusalStatus(code sessionwire.ErrorCode) (int, bool)
- func RetryableStatus(status int) bool
- func SessionAbsent(err error) bool
- func StatusFor(entry sessionstore.InboxEntry) (sessionwire.CommandStatus, bool)
- func StatusForDisposition(entry sessionstore.DispositionInboxEntry) (sessionwire.CommandStatus, bool)
Constants ¶
const ( // KindCreateSession allocates a session. KindCreateSession sessionstore.CommandKind = "create" // KindInput delivers a turn's input. KindInput sessionstore.CommandKind = "input" // KindInterrupt asks a running turn to stop. KindInterrupt sessionstore.CommandKind = "interrupt" // KindRestore resumes a session that is not resident. KindRestore sessionstore.CommandKind = "restore" // KindGateResponse answers a gate. KindGateResponse sessionstore.CommandKind = "gate_response" )
The command kinds this Factory admits. The values are the durable record's, so they are wire-visible and may not be changed to suit a caller.
Variables ¶
This section is empty.
Functions ¶
func Kinds ¶
func Kinds() []sessionstore.CommandKind
Kinds returns the vocabulary in a stable order.
It is a function rather than a package variable so no caller can hold a reference that lets it add a kind at run time.
func RefusalCodes ¶
func RefusalCodes() []sessionwire.ErrorCode
RefusalCodes lists the codes this authority classifies.
The order is map order, which is to say unspecified, and nothing sorts it: its readers are sweeps over the whole set. It exists so a test can range over the authority's own subject rather than over a list beside it.
func RefusalStatus ¶
func RefusalStatus(code sessionwire.ErrorCode) (int, bool)
RefusalStatus reports the REST status for a classified admission refusal.
The second result is false for a code this authority has no ruling for, and a caller must fail closed on it rather than defaulting: a public code with no status is a code somebody added to admission without deciding what it means to a client, and answering it with whatever the zero value happens to be would make that decision silently.
func RetryableStatus ¶
RetryableStatus reports whether a client may usefully repeat the identical request.
The set includes throttling, a bad gateway, an unavailable dependency, and an expired deadline. A 500 is deliberately absent -- a fault Factory could not classify is not one a client should be told to hammer -- and so is 403 csrf_token_expired, which is recoverable but only after the client changes the request by fetching a new token, which is not the same claim.
It is here rather than in `internal/httpapi` because the ClientLink has to reach the same answer, and a second literal there is the divergence this file exists to remove.
func SessionAbsent ¶
SessionAbsent reports whether a SessionStore failure means "there is no such session in this tenant".
Why it is one function and not one per caller ¶
The released store has FOUR spellings of that one public fact, and the two callers had two different subsets of them. `internal/httpapi`'s catalogFailure read all four; `internal/admission`'s catalogNotFound read two, omitting CatalogErrorDeleted and CatalogErrorIdentity, so the same session answered a GET with 404 session_not_found and a control command with a bare fault -- 500 over REST, a temporary transport error over the ClientLink. That is the `A9.1-notfound` carry-forward, and it is settled here by making the two readers one.
The four reasons the set is exactly these four ¶
not_found and deleted are the same public fact -- there is no session here -- and reporting them apart would tell a caller that an identifier they cannot read WAS once real. binding_not_found is the keyspace's answer for a session whose collision witnesses do not resolve, which the store checks BEFORE it reads a record at all, and it is how the CROSS-TENANT case arrives: outside the legacy single-tenant layout sessionstore hashes the tenant into the session's scope, so another tenant's session fails at verifySessionScope and never reaches a record.
identity is the fourth, and it is the one that had to be ruled rather than listed. An earlier wording of this paragraph credited it with the cross-tenant case and called it "unreachable today against the released store". BOTH halves are false, and they were checked at the pin rather than recalled: GetCatalogEntry (catalog.go:336) calls readCatalogEntry (:596) calls catalogEntry (:718), which mints CatalogErrorIdentity at :730 when a DECODED record's own TenantID or SessionID disagrees with the one asked for. That is the live read path this module uses, and catalogEntry's own doc says what the check is for: "a provider that hashes the stable key stores the original bytes for exactly this verification". It is the store's stable-key COLLISION guard, not a tenant check -- the tenant check already ran, one layer up, and answered binding_not_found.
So it is ruled deliberately, and the ruling is 404 ¶
What the condition means is that the store cannot vouch that the record filed at this session's id IS this session's record. Two things follow.
The record it declined to vouch for may be ANOTHER TENANT's -- that is precisely what a stable-key collision produces -- so any answer that distinguishes this condition from absence risks confirming that some other session occupies the id a caller named. Absence is the disclosure-safe direction.
And the two edges must not disagree about one session. internal/httpapi has answered 404 for this code since before A3.3 (catalogFailure read all four spellings), so a READ of such a session already answers "there is no such session". Had admission kept it a fault, a control command and a read of the identical session would have disagreed about whether it exists, which is the divergence this predicate exists to remove.
The COST is real and is stated rather than discovered: an operator loses the 500 that would have paged them about a genuinely corrupt or colliding record on the control path -- exactly as they already had on the read path. The condition is not silent, it is in the store's own error and in an operator's log; it is only the PUBLIC answer that is absence.
Everything else stays a fault ¶
An ambiguous marker, a layout mismatch or the keyspace's OWN hash-collision code is the deployment disagreeing with itself, not a session that is not there. Note that KeyspaceHashCollision and CatalogErrorIdentity are two different detectors and are ruled differently on purpose: the first is the keyspace reporting that two sessions' witnesses collide, which is a fact about the deployment and nothing about whether this session exists; the second is a record failing to vouch for itself, which is exactly "there is no session here that I can give you". An earlier wording of this doc used "a hash collision stays a fault" as though it covered both.
The predicate is an enumeration of ABSENCE rather than of faults, so a code a later sessionstore adds lands on "fault" instead of on a 404 reporting a session missing because the store misbehaved.
func StatusFor ¶
func StatusFor(entry sessionstore.InboxEntry) (sessionwire.CommandStatus, bool)
StatusFor projects the authoritative SessionInbox record into Core's public command status.
It is the RECORD that is described, never the call: admission's "this call created it" boolean is deliberately not an input, because a retry after an unknown outcome must receive the same answer the original acceptance received. If the two differed, a client could tell whether its earlier attempt landed -- and the whole point of the durable CommandID is that it does not have to know.
The state projection ¶
SessionStore's inbox has five states and Core's status has four, and they are not the same partition. What this acknowledges is durable ACCEPTANCE, which Core's own doc defines as "the inbox commit succeeded; it does not promise that a Host has already applied the command" -- true of pending, claimed and applying alike, so all three are accepted. Applied is applied. Rejected is rejected and carries the record's own stable ErrorDetail; reporting it as accepted would be a client believing a rejected command is in flight.
Core's CommandStatePending is never minted here. It has no counterpart in the released inbox vocabulary -- a claimed command is still exactly "accepted, not yet applied" -- and inventing a mapping for it would put a state on the wire that no durable record means.
An unrecognised state is a FAULT rather than an acceptance, which is what the second result is for. A record whose state this build does not know is a store disagreeing with this build, and defaulting it to accepted would report the one case that must never be reported optimistically.
func StatusForDisposition ¶
func StatusForDisposition(entry sessionstore.DispositionInboxEntry) (sessionwire.CommandStatus, bool)
StatusForDisposition is StatusFor over the DISPOSITION family.
It is a second ENTRANCE, not a second authority: both call statusFor, so the InboxState -> CommandState mapping and the five-state vocabulary exist in exactly one place. Adding a family must not add a second opinion about what "claimed" means publicly, which is the rule A9.1 fixed for RefusalStatus and which a per-family copy of this switch would immediately reintroduce.
Two rejections, and only one of them is describable ¶
The one genuine asymmetry between the families is the rejection detail. sessionstore's DispositionInboxRecord has NO member equivalent to the legacy record's `Rejection *sessionwire.ErrorDetail` -- rejection is a state there and nothing more -- while Core REQUIRES an error on a rejected status (CommandStateRejected with a nil Error is missing_field). So the detail has to come from WHO can have written the rejection, and the record says that much:
- An ATTEMPTLESS rejection is a refusal before any dispatch. Its only producer in this fleet is Factory's own disposition deadline sweep (internal/admission DispositionReconciler): a Host declares no seam for RejectDispositionCommand at all and leaves the deadline to Factory. So it is described exactly as the legacy sweep described its own: runtime_unavailable -- accepted, and no runtime applied it before its deadline. This is what keeps a retry of an expired input answering "rejected, runtime_unavailable" now that inputs are disposition commands, as they did as legacy ones.
- A rejection carrying an ATTEMPT is the store's not_applied settlement, decided from a runtime's evidence this module never saw. It stays UNREADABLE: an invented code would be a public, stable, client-switchable answer for a cause nobody here observed, and the edge answers a fault instead, which is recorded as owed against sessionstore.
Types ¶
This section is empty.