import type { CrossSessionInbound, HeldEntry, Mailbox, PermissionClassLabel } from "@yanlinglabs/winter-agent-sdk/messaging"; import type { HeldMessageRecord, RuntimeDirectoryEntry, RuntimeDirectoryStore } from "../seams/directory-store.js"; import type { DeliveryOutcome, GlobalAgentMessage, SerializedRuntimeAddress } from "../seams/messaging-contract.js"; export interface InboundPolicyHooks { /** * The receiver's own explicit `crossSessionInbound` setting, when the host has one. * * WS-10 §13: an explicit setting ALWAYS WINS over the class matrix. It is a host fact (the settings * hierarchy is the host's), so it is asked for rather than assumed; absent means "no explicit * setting", which is what makes the matrix the default rather than the only rule. */ explicitSetting?: (receiver: RuntimeDirectoryEntry) => Promise | CrossSessionInbound | undefined; /** * WS-10 §13: "an unauthenticated route is refused BEFORE the matrix." * * The router's own default answer is "authenticated exactly when the sender is an object this * directory authored" — a canonical address the router itself issued and can still resolve. That is * the strongest claim a library can make on its own; a cross-machine or phone-originated route * "requires an authenticated Winter transport plus its own policy gate", which is the host's, and * this hook is where the host says so. */ authenticatedRoute?: (input: { message: GlobalAgentMessage; receiver: RuntimeDirectoryEntry; senderKnown: boolean; }) => Promise | boolean; /** * Called when a held message LEAVES THE MAILBOX WITHOUT BEING DELIVERED — swept by the five-minute * dialog expiry, or re-evaluated to `refuse` (review r3, NEW-10). * * The router supplies it and writes the delivery receipt through, because the ledger is the router's. * Without it a sender's durable receipt reads `held` forever for a message that no longer exists — * and "held" reads as "waiting for you". */ onHoldTerminal?: (message: GlobalAgentMessage, outcome: DeliveryOutcome) => Promise | void; /** An explicit hold is the receiver's own choice to queue rather than to take; the kind is its own. */ holdKind?: (input: { message: GlobalAgentMessage; receiver: RuntimeDirectoryEntry; decision: CrossSessionInbound; }) => "default" | "explicit"; } export interface InboundPolicyDeps extends InboundPolicyHooks { store: RuntimeDirectoryStore; now(): number; /** The receiver's permission class, asked of the runtime that actually holds it (WS-10 §13). */ receiverClass(receiver: RuntimeDirectoryEntry): Promise; } /** The two receiver-scoped facts a decision needs, read once per sweep (NEW-9). */ export interface ReceiverFacts { explicitSetting: CrossSessionInbound | undefined; receiverClass: PermissionClassLabel; } export type InboundVerdict = /** Deliver. `release()` MUST be called with the eventual outcome so the accepted-queue slot is freed. */ { kind: "accept"; release(outcome: DeliveryOutcome): void; } | { kind: "settled"; outcome: DeliveryOutcome; }; export interface InboundPolicy { /** Runs the whole of WS-10 §13 for one envelope against one receiver. */ decide(receiver: RuntimeDirectoryEntry, message: GlobalAgentMessage, senderKnown: boolean): Promise; /** * The DECISION alone, with no mailbox effect — the matrix, the explicit setting and the * authentication gate, and nothing else. * * WS-10 §14's returning idle notice is the caller that needs this: "inbound policy applies to the * returning notice", and a held subscription "delivers a REDUCED-STATUS notice, not treated as * ordinary delivered text". A notice is therefore never held in the mailbox and never occupies an * accepted slot — it is reduced or dropped — so it needs the verdict without the bookkeeping that * ordinary mail earns. */ classify(receiver: RuntimeDirectoryEntry, message: GlobalAgentMessage, senderKnown: boolean): Promise; /** * WS-10 §13's "held messages are re-evaluated when the receiver's mode or settings change". * * Sweeps the 5-minute expiry first, then re-runs the decision for every DEFAULT-class hold (an * explicit hold "persists until a later accept/refusal/session end", so it is never auto-promoted), * and hands back the envelopes that are now deliverable. Delivering them is the router's job — this * module never calls an adapter. */ reevaluate(receiver: RuntimeDirectoryEntry): Promise<{ released: GlobalAgentMessage[]; expired: HeldEntry[]; }>; /** * Take an accepted-queue slot WITHOUT re-running the decision (review r1, low 2). * * A RELEASED HOLD HAS ALREADY BEEN DECIDED, by `reevaluate`, which removed its durable record when * it promoted it. Re-running `decide` on the way out would let a second, divergent answer hold a * message whose record is gone — losing it — while still costing the host's hooks another call. The * cap is still honoured, because that is the half a release genuinely changes: a held message * occupies no accepted slot, and a delivered one does. */ reserveAccepted(receiverKey: SerializedRuntimeAddress, messageId: string): InboundVerdict; /** WS-10 §13's 5-minute dialog expiry, applied on its own. Returns what it swept. */ sweepExpired(receiverKey: SerializedRuntimeAddress): Promise; /** What is held for a receiver right now, durably. */ listHeld(receiverKey: SerializedRuntimeAddress): Promise; readonly caps: { held: number; accepted: number; }; /** Exposed for the router's own counters; the mailbox is this module's, not the router's. */ readonly mailbox: Mailbox; } export declare function createInboundPolicy(deps: InboundPolicyDeps): InboundPolicy;