UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

130 lines (129 loc) • 8.57 kB
/** * design/382 §4.3 / design/389 — the SESSION partition of the permission-rule store: where a * `{kind:"session"}` scoped allow rule lives. * * A session authorization's home is the session's own lifetime — never the durable CRDT partition. The * rows are ordinary {@link PersistedRule}s (scope `session`), and the unified store's ONE read * query (`PermissionRuleStore.effective({ sessionId })`) splices this partition's rows IN FRONT of the * durable partition's (the §4.1 narrowest-first reporting order: session > project > global), so every * read-side consumer — the gate's conjunction arm, the coverage table, the consent prepare — takes them * through the ONE eligibility predicate with zero second judges. The store's routing writer lands a * session-scope `redemption-add` here; the durable partition never sees one. * * ## The contract (design/382 §8-Q6, settled here) * * TWO verbs, and deliberately no third: * · `read(sessionId)` — the live rows of ONE session, session-scoped, that session's only. * · `apply(sessionId, add)` — land one redeemed authorization. Idempotent BY DOT (the redemption * replay contract: `alreadyRedeemed` marks the dot as RECORDED, not the overlay as LANDED, so a * replay must RE-APPLY, and this verb is what makes re-applying safe); refuses `session_ended` * once the session's lifetime epoch is sealed — a late in-flight write after termination must not * revive an authorization for a session that no longer exists. * * There is deliberately NO delete verb: a session row is not individually deletable — it dies with * its session (design/382 §10, ruled). Termination is the HOST's act on its own implementation (see * {@link InMemorySessionRulePartition.endSession}), not an engine verb. * * ## Lifecycle and durability * * The overlay lives WITH the session: a host that parks/checkpoints a session serializes its rows * beside it and rehydrates them on resume (`snapshotSession`/`restoreSession` on the reference * implementation) — that is how a session authorization survives a park without ever touching the * persisted store, and why it still ends when the session ends. `sessionId` uniqueness is the host's * duty (the `principal` posture): reusing an id declares "the same session". * * ## What this is NOT * * Not a durable backend: no dots are minted here (the redemption leg mints the audit dot exactly as the * store leg does), no tombstones exist here, nothing here syncs. Every durable entrance — write * deltas, sync in both directions, at-rest bytes — refuses or drops a session row * (`permission-rule-store.ts`, the durable two-member face); this module is the OTHER side of that * wall, and the wall is the design. */ import type { PersistedRule, RuleAdd, RuleBehavior, PersistedRuleMatch, PersistedRuleTool } from "./permission-rule-model.js"; /** One redeemed authorization landing in a session's overlay: the rule's canonical shape plus the * redemption's own add (dot + provenance) — the same fields a `redemption-add` delta carries, minus * the scope (the verb's `sessionId` IS the scope, constructed here so a caller cannot land a row * whose scope names a different session than the one it addressed). */ export interface SessionRuleAdd { behavior: RuleBehavior; rule: string; tool: PersistedRuleTool; match: PersistedRuleMatch; command: string; add: RuleAdd; } /** What one overlay apply answered. `session_ended` is the epoch fence speaking: the write arrived at * (or after) termination and landed nothing — the redemption reports the member `refused`. */ export type SessionRuleApplyResult = { landed: true; } | { refused: "session_ended"; }; /** * The session partition a deployment hands to `createPermissionRuleStoreProvider({ session })`. See the * module doc for the contract; {@link InMemorySessionRulePartition} is the reference implementation and * the semantics. A backend that keeps a session's rows beside its own session record (a host whose * sessions are durable) may implement this over that storage — the law is LIFETIME-bound, not * medium-bound: no tombstones, no sync, nothing outlives the session. */ export interface SessionRulePartition { /** The live session-scoped rows of ONE session. An ended (or never-written) session reads as `[]`. */ read(sessionId: string): Promise<readonly PersistedRule[]>; /** Land one redeemed authorization — idempotent by dot, refused once the session ended. */ apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>; } /** * The reference overlay — in-memory, per-process, the semantics every host implementation must keep: * * · APPLY is idempotent by dot identity (a replayed redemption re-submits its recorded dot and * changes nothing — the store's `foldDelta` discipline, verbatim); * · TERMINATION seals FIRST, clears SECOND ({@link endSession}): the sealed-set membership is the * session's lifetime epoch, flipped atomically before any state is dropped, so an in-flight write * that lost the race lands on the seal and answers `session_ended` — never on a half-cleared * session, and never as a row that outlives its session (design/382 §4.3, the r2-⑥ fence); * · a sealed session never reopens — `apply` and `restoreSession` both refuse it, so "session ended" * is a one-way fact exactly like a quarantined dot's; * · SNAPSHOT/RESTORE ({@link snapshotSession}/{@link restoreSession}) are the park/resume carriage: * the host serializes a session's rows into its checkpoint and rehydrates them on resume. Restore * is fail-closed on shape — every row must be a session row of THE session being restored (a * checkpoint is host-owned bytes, and a global row smuggled through this door would ride the * merge point straight into adjudication). */ export declare class InMemorySessionRulePartition implements SessionRulePartition { private readonly rows; private readonly sealed; /** How many times one session's row set has CHANGED. Reading a caller-owned value runs caller code, * so a verb that reads its argument and then replaces a session's rows wholesale must be able to * tell that something landed in between — otherwise it overwrites a grant that was already answered * `landed:true`, and a consent nobody withdrew stops existing. Bumped by every landing, restore and * termination; read across `restoreSession`'s snapshot. */ private readonly epochs; private bumpEpoch; read(sessionId: string): Promise<readonly PersistedRule[]>; apply(sessionId: string, add: SessionRuleAdd): Promise<SessionRuleApplyResult>; /** * The termination transfer: SEAL the lifetime epoch first (one atomic membership flip — after this * line every in-flight `apply` holding this id answers `session_ended`), THEN drop the rows. The * order is the whole fence: clear-then-seal would leave a window in which a late write re-grows a * rule set for a session that is already gone. Idempotent; a never-seen id seals to the same place. */ endSession(sessionId: string): void; /** The park half of the checkpoint carriage: this session's rows, serializable as-is. */ snapshotSession(sessionId: string): Promise<readonly PersistedRule[]>; /** * The resume half. Fail-closed on every axis: a sealed session never rehydrates (termination is * one-way), and every row must be a well-formed SESSION row of exactly this session — anything else * in the checkpoint bytes is refused whole, loudly, because the merge point trusts this overlay to * serve only what its contract says it holds. * * The rows also pass the SAME `normalizePersistedRule` re-projection the sync face runs on store * rows (rule text parses, canonical spelling, stored tool/match/command agree with the text): this * door takes host-owned checkpoint bytes with no redemption validator in front of them, and a row * the engine could not re-project would otherwise sit one merge away from adjudication. Rows this * overlay landed itself always pass — {@link InMemorySessionRulePartition.apply} runs the same screen, * so the two doors agree on what a session row is and a snapshot of this overlay always restores — * which is what makes a refusal here foreign damage, refused whole. */ restoreSession(sessionId: string, rows: readonly PersistedRule[]): Promise<void>; }