UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

196 lines (195 loc) • 12.8 kB
/** * design/389 — ONE permission-rule store, partitioned by SOURCE. * * A rule comes from one of four sources — `org | user | project | session` — and the four differ in * polarity, authority and evolution (an org snapshot is replaced whole by a server administrator; a * user/project rule is one human click folded into an OR-Set CRDT; a session grant lives exactly as long * as its session). Those differences are WRITE LAWS, and each partition keeps its own. What is one is the * READ: "which rules stand for this owner, in this session, and what does each one's standing look like" * is a single question, so it has a single answer — {@link PermissionRuleStore.effective}, a query the * store answers across every partition with each row's `source` and `status`, the org shadow predicate * applied inside the store rather than at each caller. * * ## Composition, not inheritance * * {@link createPermissionRuleStoreProvider} is the ONE way to build the store. A deployment hands it * partition backends — the durable CRDT partition (`DurableRulePartitionProvider`: file, in-memory, an * out-of-repo SQL twin), an org partition config (its snapshot source and staleness bound), a session * partition — and the engine composes them. The org and session partition LAWS are engine code the * backend cannot weaken; the durable partition's law is the exported pure fold every backend runs. * A partition a deployment does not configure does not exist: its source answers nothing, and the * provider's `partitions` face says so, so a manifest reads a declaration rather than inferring one. * * ## Source is a projection of the row's address * * `user` ⟺ scope `global`, `project` ⟺ scope `project`, `session` ⟺ scope `session`. A row never carries * a second source byte that could drift from its scope — {@link ruleSourceOf} is the whole mapping. * * ## One write path, routed by scope * * `writerOf(store)` hands back a writer whose `apply` routes a `redemption-add` by the delta's scope: a * session scope lands in the session partition (idempotent by dot, refused `session_ended` past the * session's epoch seal), the two durable scopes take the CRDT partition's compare-and-set. The consent * protocol therefore has ONE write path with ONE result vocabulary ({@link RuleWriteOutcome}); the * durable backend keeps refusing a session row loudly should one ever reach it — the route is the * store's, the fence stays the partition's. */ import type { PersistedRule, RuleScope } from "./permission-rule-model.js"; import { type DurableRulePartitionProvider, type QuarantinedRuleAdd, type RuleOwner } from "./permission-rule-store.js"; import { type OrgRulePartitionConfig, type OrgRuleResolution } from "./permission-rule-org.js"; import type { SessionRulePartition } from "./permission-rule-session.js"; import type { StoreDurability, StoreFidelity } from "./checkpoint-store.js"; /** The four sources a rule can come from — CC's settings-layer vocabulary folded onto sema's four. */ export type RuleSource = "org" | "user" | "project" | "session"; /** The source of a row is a projection of its scope — `global` is the `user` source. */ export declare function ruleSourceOf(scope: RuleScope): Exclude<RuleSource, "org">; /** One live personal rule in the effective view: the row itself, its source, and its standing under the * org partition (`shadowed-by-org` = an org deny reaches an ALLOW row's command pattern; see * `orgRuleShadows` — a personal deny/ask row is never shadowed: it only ever tightens, and the org * partition's power over the personal store is to override an allow, not a refusal). */ export interface EffectivePermissionRule extends PersistedRule { source: Exclude<RuleSource, "org">; status: "live" | "shadowed-by-org"; } /** A durable rule identity every add of which is tombstoned — the rule no longer stands, and its * tombstone identity is still visible in the partition. Session rows never appear here (they die * with their session and leave no tombstone). */ export interface RemovedPermissionRule { behavior: import("./permission-rule-model.js").RuleBehavior; rule: string; scope: RuleScope; source: "user" | "project"; status: "removed"; } /** One read of the person's store as every consumer of it reads it, the failure law applied (a run's one reader of it, * `person-path-rows.ts`, takes it; the rule lanes and the read station read it). */ export interface PersonRowsRead { /** The rows this read stands on: each partition it read, as read; each it could not read, the deny and ask rows of the * run's last successful read of that partition (none before one). This session's rows first. */ readonly rules: readonly EffectivePermissionRule[]; /** The org partition's resolution off this read. Absent when the whole read failed — {@link unreadable} says why. */ readonly org?: OrgRuleResolution; /** Present iff a personal partition (or the whole read) could not be read: the failure's line. */ readonly unreadable?: string; } /** THE answer of the store's one read query. */ export interface EffectivePermissionRules { /** Live personal rows, in reporting order: this session's rows first (the narrowest standing yes), then * the durable partition in its own table order — byte-equivalent to the pre-389 splice. */ rules: EffectivePermissionRule[]; /** Tombstoned durable identities (introspection). */ removed: RemovedPermissionRule[]; /** The org partition's resolution. `ungoverned` on a deployment with no org partition. */ org: OrgRuleResolution; /** The durable partition's revision — the optimistic-concurrency anchor for its writes. */ rev: number; /** Present iff a PERSONAL partition could not be read — the durable one (its user / project rows; `removed` is then * empty and `rev` 0) or the session one: the failed partition's rows are absent from `rules`, while the rows of the * partition that WAS read stand, and so does the org resolution beside them (a flaky personal store must not become an * org governance outage). The line names the failure (both lines, `; `-joined, when both failed). A consumer that needs * the durable rows treats this as a read failure — {@link effectiveOrThrow} is that reading; the gate's personal lane * and the leg's path-row read skip the failed partition's rows, disclosed, and judge by the rest. */ unreadable?: string; /** Beside {@link unreadable}: the sources whose partition could not be read (`user` and `project` — the durable * partition; `session`), in that order — the rows `rules` does not carry. */ unreadableSources?: readonly Exclude<RuleSource, "org">[]; /** Disclosure lines from the personal partitions (a session partition that could not be read, rows it * served that are not canonical session rows of this session). The org partition's lines ride * `org.disclosures`. */ disclosures: string[]; /** The durable rows left out because their project scope root is not a complete absolute path (a foreign writer's or a * hand edit's row): each row's text and its scope root as served (a non-string root as its type). Absent when none. The * reader tells the person (`permission.rule_row_dropped`). */ droppedRows?: ReadonlyArray<{ readonly rule: string; readonly scopeRoot: string; }>; } /** * The host-visible store, already anchored to one owner. One read verb, which never throws: each * partition reports its own failure in the view — a PERSONAL partition's read failure (the durable one's, the * session one's) as `unreadable` / `unreadableSources`, with that partition's rows absent and the other's present * (served-but-non-canonical session rows stay a `disclosures` line), the ORG partition's as its `status`. The unit * that fails is the partition: the org verdict stands while a personal partition is down, each personal partition's * rows stand while the other is down, and the personal rows stand while an org source is down. */ export interface PermissionRuleStore { effective(query?: { sessionId?: string; }): Promise<EffectivePermissionRules>; /** design/182 §5.2 — the durable partition's quarantine area, for introspection. */ quarantined?(): Promise<QuarantinedRuleAdd[]>; /** The durable partition's declared durability; an undeclared backend reads fail-closed as `"process-local"`. */ readonly durability?: StoreDurability; readonly fidelity?: StoreFidelity; } /** * The unified provider — the ONE `RunnerDeps.permissionRuleStore` seam. `partitions` is the declaration * face: the wiring manifest's cells and the ask evidence's `not_wired` words read it verbatim instead of * inferring from three separate seams. */ export interface PermissionRuleStoreProvider { /** The store for one verified principal. `undefined` ⇒ zero personal rules — but the org partition * still adjudicates: governance applies to everyone, an anonymous task included. */ forPrincipal(principal: string | undefined): PermissionRuleStore; /** design/182 §4.5 — the identity-less local bucket. Present iff the durable backend has one. */ forLocalOwner?(): PermissionRuleStore; readonly partitions: Readonly<{ durable: boolean; org: boolean; session: boolean; }>; } /** * The store one task reads its person's rules from. The unauthenticated floor is enforced HERE, not merely asked of the * provider: an absent principal resolves to zero personal rules, and a provider that forgot it would hand one person's * standing approvals to an anonymous task. The ONE exception is an explicitly declared LOCAL-OWNER deployment, where "no * principal" is the typed absence of identity — the machine owner's own bucket (a provider with no such face resolves * the anonymous store; {@link createPermissionRuleLanes} refuses that declaration loudly). */ export declare function ownerStoreOf(provider: PermissionRuleStoreProvider, principal: string | undefined, localOwnerDeclared: boolean): PermissionRuleStore; /** What {@link createPermissionRuleStoreProvider} composes. */ export interface PermissionRuleStoreConfig { /** The durable CRDT partition (user + project sources). Absent ⇒ no personal rules exist on this * deployment (the rule lane does not arm), and no session grant can land either (the redemption * dot is minted by the durable replica). */ durable?: DurableRulePartitionProvider; /** The org partition. Its PRESENCE is the governance declaration (design/182 §7.4 D3): under it the * org adjudication surface must always be effective, and an unreadable snapshot fails the whole * decision boundary closed. */ org?: OrgRulePartitionConfig; /** The session partition. Absent ⇒ the session source does not exist: a session-scope redemption is * refused loudly (nowhere to land), and `effective()` carries no session rows. */ session?: SessionRulePartition; } /** * Compose the unified store over its partition backends. This is the boot gate of the org declaration * (`createOrgRulePartition` refuses a config naming no source) and the ONLY constructor of the store. */ export declare function createPermissionRuleStoreProvider(cfg: PermissionRuleStoreConfig): PermissionRuleStoreProvider; /** * The one read query for a consumer that NEEDS the durable rows (a write's optimistic-concurrency anchor, * a coverage judgment, a removal): an `unreadable` durable partition is a thrown read failure here, so * the caller's existing "could not read the permission-rule store" arm fires — the gate lane is the one * consumer that reads the word itself. */ export declare function effectiveOrThrow(store: PermissionRuleStore, query?: { sessionId?: string; }): Promise<EffectivePermissionRules>; /** * The one read query, addressed by owner (design/182 §7.3 introspection, design/389 form): resolve the * owner's store and ask it. Nothing is assembled here — a consumer that needs the effective view with * `source`/`status` per row and the org resolution beside it gets the store's own answer. */ export declare function effectivePermissionRules(opts: { provider: PermissionRuleStoreProvider; principal?: string; owner?: RuleOwner; sessionId?: string; }): Promise<EffectivePermissionRules>; /** Owner routing shared by the introspection entry: either a principal or the local owner, never both. */ export declare function resolveOwnerStore(opts: { provider: PermissionRuleStoreProvider; principal?: string; owner?: RuleOwner; }): PermissionRuleStore;