@sema-agent/core
Version:
Stateless, task-oriented AI agent core
196 lines (195 loc) • 12.8 kB
TypeScript
/**
* 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;