/** * design/99 §E6 — per-session permission rules: a durable, session-scoped policy narrowing folded into the * task-time {@link import("./tool-policy.js").ToolPolicy} (see {@link import("./runner/session-rule-policy.js").createSessionRulePolicy}). * * Why core-owned (not a parallel service store): the rules must be ENFORCED by the same policy fold that runs * every tool call, so storage and enforcement can't drift (the [210] capability-token invariant). core defines * the seam + an InMemory default; a deployment supplies a DURABLE cross-replica backend (its CAS-rev semantics * must match this InMemory one byte-for-byte — a cross-backend equivalence test is the contract). * * Safety model: * - **tighten-only by default**: a NORMAL write may only ADD denies / NARROW allowlists / NARROW allowDirs. A * write that LOOSENS (removes a deny, widens an allowlist/allowDirs) is REFUSED unless it is an OPERATOR * write. core has NO role system — `operator` is a boolean the caller FREEZES from a verified principal * (mirrors `steer.trusted`); core never adjudicates operator-ness, it only honors the flag. * - **CAS rev** (optimistic concurrency): a `putRules` with a stale `expectedRev` is rejected, so two replicas * can't lose an update (mirrors the checkpoint store's `resolve` OCC). * - **structured fields only**: core interprets `toolAllow/Deny`, `allowDirs`, `commandAllow/Deny` (argv[0] * names). A richer rule DSL is the service's/profile's job — core grows no rule interpreter. * - **NOT A SANDBOX**: command rules inherit the coarse argv[0] matching (wrappers like `sh -c`/`sudo` bypass); * the real isolation boundary is the executionEnv, never these rules. */ /** The structured per-session rules. All optional; an absent field imposes no constraint of that kind. */ export interface SessionPermissionRules { /** If set, ONLY these tool names are allowed (others denied). A narrowing allowlist. */ toolAllow?: string[]; /** Tool names always denied (deny wins). */ toolDeny?: string[]; /** If set, WRITE tools (`write_file`/`edit_file`) may only target paths within these dirs; a write-capable * tool that can't be path-confined (e.g. `bash`) is denied while this is set. Stored RAW; resolved at run time. */ allowDirs?: string[]; /** If set, ONLY these `bash` command names (argv[0]) are allowed. */ commandAllow?: string[]; /** `bash` command names (argv[0]) always denied. */ commandDeny?: string[]; } /** {@link SessionPermissionRules} plus the monotonic `rev` the store stamps (the OCC key). */ export interface StoredSessionRules extends SessionPermissionRules { rev: number; } /** Options for {@link SessionPolicyStore.putRules}. */ export interface PutRulesOptions { /** CAS: reject the write if the stored `rev` is not this (optimistic concurrency). Omit to skip the check. */ expectedRev?: number; /** When true, this write MAY loosen the rules (remove a deny / widen an allowlist or allowDirs). A NORMAL * (non-operator) write is TIGHTEN-ONLY. core does NOT verify operator-ness — the caller (service) FREEZES * this from a verified, role-checked principal (mirrors `steer.trusted`). */ operator?: boolean; } /** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden` * = a non-operator write tried to relax the rules. */ export declare class SessionPolicyError extends Error { readonly code: "conflict" | "loosen_forbidden"; constructor(code: "conflict" | "loosen_forbidden", message: string); } /** * The seam a deployment implements for DURABLE, cross-replica per-session rules. Unset on `RunnerDeps` ⇒ the * feature is OFF (no rules read, zero behavior change). Keyed on `(sessionId, principal-owner)`: core passes * the running task's `principal` as the owner key; ownership/operator AUTHORIZATION is the implementation's job * (core has no identity system). `getRules` returns the rules to ENFORCE for a task; `putRules` writes them. */ /** A (principal, rules) pair for one session — the unit {@link SessionPolicyStore.listBySession} returns. */ export interface SessionRulesRecord { /** The principal these rules apply to (undefined = the session-wide default). */ principal: string | undefined; rules: StoredSessionRules; } export interface SessionPolicyStore { getRules(sessionId: string, principal?: string): Promise; putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise; /** * 2c session-sync ([271]): every (principal, rules) record for `sessionId` across ALL principals — `getRules` * is per-(sessionId, principal), but a cross-backend session EXPORT must bundle the WHOLE session's policy. * The importer replays each via `putRules(sessionId, record.principal, record.rules)`. Optional — a backend * that supports cross-backend policy export implements it (local backend uses the in-memory/file impl). */ listBySession?(sessionId: string): Promise; } /** Normalize a rules object's `allowDirs` (lexical `posix.normalize`) so stored rules are `..`-free and the * tighten diff compares the same canonical-ish form the runtime resolves. */ export declare function normalizeRules(rules: SessionPermissionRules): SessionPermissionRules; /** * Pure tighten-only diff: the reasons `next` LOOSENS `prior` (empty ⇒ `next` is a pure tightening or no-op). * Direction per field — deny-lists: removing an entry loosens. allowlists: removing the list (→ everything * allowed) or adding an entry outside the prior set loosens; prior-undefined → any defined next only narrows. * allowDirs: removing confinement, or adding a dir not within some prior dir, loosens (coarse raw containment). */ export declare function loosenReasons(prior: SessionPermissionRules, next: SessionPermissionRules): string[]; /** strip any caller-passed `rev` from a rules object so the store is the sole authority on it. */ export declare function stripRev(rules: SessionPermissionRules): SessionPermissionRules; /** * In-process default {@link SessionPolicyStore} (single-replica). A deployment that needs cross-replica * durability supplies its own backend with the SAME CAS-rev + tighten-only semantics. Single-threaded JS makes * the read-check-write trivially atomic here; a durable backend folds it into a CAS WHERE clause. */ export declare class InMemorySessionPolicyStore implements SessionPolicyStore { private readonly map; private key; getRules(sessionId: string, principal?: string): Promise; putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise; listBySession(sessionId: string): Promise; } //# sourceMappingURL=session-policy-store.d.ts.map