/** * The schedule kill switch (#1666), shared by both adapters so the gate that decides * whether a module's schedules run and the lever that moves it cannot disagree. * * A scope runs a module's schedules only while it holds a live `system:` grant * (#383) — "the grant IS the switch". That sentence was true of the gate and had no * lever: nothing could revoke the grant. This file is the lever, and it has to be more * than a revoke, for two reasons found while building it: * * 1. **A reconcile seats grants.** Since #1659 a reconcile leaves a revoked grant revoked, * but it still CREATES a missing one. A vertical whose next version declares a schedule * with a new permission gets that grant seated live on every install — and a gate that * reads "any live grant" is open again, on a scope an operator switched off. * 2. **An explicit grant clears a tombstone.** `grantToSystem` is `INSERT OR REPLACE` by * design (#1659's re-grant guarantee), so a stray grant of one permission would bring * that tuple back — and with it, under the old gate, the whole module. * * So the OFF position is a tuple of its own: a live `system:` / `switch:off` / * `scope:` marker. The gate is OFF while that marker is live, whatever grants are * live beside it. **Restore is the lever; a grant is not.** The marker is an ordinary * K-21 tuple: switching back on tombstones it, so the row stays as evidence of when the * switch was last pulled. The permission walk reads only `role:` and `granted:` relations, * so the marker authorizes nothing; what it denies, it denies through the one question the * evaluator asks before the walk (below). * * Switching off ALSO tombstones every live `granted:` tuple the module holds on the scope. * That is what makes it a kill switch rather than a schedule pause: anything that acts * with the module's system authority — a resumable job run (#1577), a `getSystemScope` * invoke — is denied by its own `ctx.check` while the switch is off. * * **And the checker reads the marker too** (#1823), which is what makes that denial hold * against a grant OFF cannot tombstone. A TENANT-level `system:` grant lives in the directory * (and in every scope's projection of it), not at `scope:`, so OFF leaves it live. The * permission evaluator asks the scope reader's `switchedOff` before it reads any tuple, and a * live marker denies the subject on that scope whatever it holds (`permission-eval.ts`). The * write side stays closed as well, so a switched-off scope does not accumulate grants that * ON would then hand back: * - `grantToSystem` refuses while the module is switched off on that scope (the adapters * ask `systemSwitchedOff` in the same unit as the write), and a tenant-level grant while * the directory records it off on any scope (#1743); * - provisioning's seat (`seatScopeTuple`) seats nothing for a subject whose marker is * live, so a reconcile cannot create a grant a newer version declares. * * **Restore returns exactly what OFF took, never more.** Each grant OFF tombstones gets a * `switched:` record, live for as long as the switch holds it. ON restores * only grants with a live record, then tombstones the records — so a grant that was * revoked independently BEFORE the switch was pulled stays revoked through OFF and ON. * * Params and results are plain SQL over `_substrat_tuples`, run through `SwitchSql`, * which each adapter implements over its own handle inside one transaction. */ /** The relation of the OFF marker. Not `granted:` and not `role:`, so no check reads it. */ export declare const SYSTEM_SWITCH_OFF_RELATION = "switch:off"; /** The two statement shapes the switch needs, over either adapter's SQLite handle. */ export interface SwitchSql { all(sql: string, ...params: (string | number | null)[]): Record[]; run(sql: string, ...params: (string | number | null)[]): void; } /** A `ScopedSql` as a `SwitchSql` — for kernel code holding the one and calling a writer typed on the other. */ export declare const switchSqlOf: (sql: { query(q: string, p?: readonly (string | number | null)[]): unknown[]; exec(q: string, p?: readonly (string | number | null)[]): unknown; }) => SwitchSql; /** * Where one module's schedules stand on one scope. * * - `on`: a live grant and no live OFF marker — due schedules fire. * - `off`: a live OFF marker. The schedules do not fire, and each is reported `skipped` * with `switchedOff: true` — the sweep reached the scope and chose not to run, which * is not the same fact as a sweep that never reached it. * - `ungranted`: no live grant and no marker. A foreign vertical's scope on a CP-full host * (the module is registered and this scope never ran it), or a grant removed by a raw * write. Quiet, exactly as before the switch existed: no run, no skip, no error. */ export type SystemScheduleState = 'on' | 'off' | 'ungranted'; /** * Which kill switch: a module's system authority (`system:`, #1666) or a peer * vertical's (`vertical:`, #1706). Both move the same marker through `switchSubjectGrants`. */ export type SwitchKind = 'system' | 'peer'; /** The tuple-subject prefix of a peer vertical (#1706) — `vertical:acme/board-room`. Re-exported by `peer.ts`. */ export declare const PEER_SUBJECT_PREFIX = "vertical:"; /** The tuple subject a peer's grants are seated under. The slug only, never the instance. Re-exported by `peer.ts`. */ export declare const peerSubjectRef: (vertical: string) => string; /** * "Is this subject's OFF marker live?", as a SQL predicate over ONE bound parameter (the * subject). The one spelling the gate, the grant refusal and the provisioning seat share, * so the three cannot disagree about what "switched off" means. */ export declare const SYSTEM_SWITCH_OFF_PREDICATE = "EXISTS (SELECT 1 FROM _substrat_tuples\n WHERE subject = ? AND relation = 'switch:off' AND revoked_at IS NULL)"; /** * The gate, one statement: is the OFF marker live, and is any `granted:` tuple live? * `substr` rather than `LIKE`: `LIKE` is case-insensitive in SQLite and a Durable * Object caps its patterns (#1655); an exact prefix compare is neither. */ export declare function systemScheduleState(db: SwitchSql, moduleId: string, now: string): SystemScheduleState; /** * The same gate for ANY switched subject (#1706) — `system:` above, `vertical:` * for a peer. One statement, one spelling: the peer switch's status read must not be able to * disagree with the schedule switch's about what "off" and "ungranted" mean, and the surest * way to hold that is for there to be one predicate rather than two that look alike. */ export declare function subjectGrantState(db: SwitchSql, subject: string, now: string): SystemScheduleState; /** One module's position, as `systemGrantsStatus` below enumerates them. */ export interface SystemGrantsEntry { moduleId: string; schedules: SystemScheduleState; } /** * Every module this scope holds or has ever held system authority for, and where each * stands (#1674) — the per-scope status read (`GET .../system-grants`). One SELECT * enumerating the `system:` subjects this scope's storage has a live `granted:` * tuple or OFF marker for (a module with neither has never run here, and has nothing to * report — not even `ungranted`), then `systemScheduleState` per one: the SAME predicate * `runDueSchedules` gates on, so the read and the runner cannot disagree. */ export declare function systemGrantsStatus(db: SwitchSql, now: string): SystemGrantsEntry[]; /** Is this module switched off on the scope `db` is? What `grantToSystem` refuses on. */ export declare function systemSwitchedOff(db: SwitchSql, moduleId: string): boolean; /** * `SYSTEM_SWITCH_OFF_PREDICATE` as a whole statement answering `off` (0/1), over the one bound * subject — what `subjectSwitchedOff` runs, and what each adapter's permission reader prepares * once and runs on every check of a switchable subject (#1823). */ export declare const SYSTEM_SWITCH_OFF_QUERY = "SELECT EXISTS (SELECT 1 FROM _substrat_tuples\n WHERE subject = ? AND relation = 'switch:off' AND revoked_at IS NULL) AS off"; /** Is this subject's OFF marker live on the scope `db` is? Any subject — see `switchSubjectGrants`. */ export declare function subjectSwitchedOff(db: SwitchSql, subject: string): boolean; export declare const isSwitchableSubjectKind: (kind: string) => boolean; /** The refusal `grantToSystem` throws while the switch is off — one wording, both adapters. */ export declare function systemSwitchedOffMessage(moduleId: string, scopeId: string): string; /** * The same refusal for a TENANT-level grant (#1743), naming the scopes the directory records * the module off on: a tenant tuple reaches every scope of the tenant, those included. */ export declare function tenantSystemSwitchedOffMessage(moduleId: string, scopeIds: readonly string[]): string; /** What one switch call did in the scope's own storage — `SystemSwitchOutcome`'s shape. */ export interface SwitchOutcome { held: boolean; changed: boolean; permissions: string[]; /** * #1823: this code's evaluator denies a module on a scope whose marker is live, its * TENANT-level grants included. Set by `switchSystemSchedules` on every answer, because it * is a fact about the code that answered, not about the call: a deployment built before * #1823 omits it, and its OFF leaves a tenant-level grant authorizing. The platform reads * its absence on an OFF of a module it found tenant-held as "this deployment cannot hold * that OFF", and refuses it rather than record a scope off that is not. */ deniesTenantGrants?: true; /** * #2045: the move was refused, and nothing written, because the scope has already applied a * NEWER switch call on this subject (`SWITCH_FENCES_DDL`). The scope stays where the newer call * put it, which is also where the directory's record is. Absent from a deployment built before * the fence, which applies every move it is sent. */ superseded?: true; /** * #2045 (Codex r2): this code honoured the call's fence — set on every answer to a move that * carried one, superseded or not. A deployment built before the fence drops the field and the * fence with it, and would apply an older call's move after a newer one, so the platform refuses * a fenced move whose answer lacks this, as it refuses an OFF without `deniesTenantGrants`. */ fenced?: true; } /** * #2045: the fence a scope keeps per switched subject — the operation id (a ULID) of the newest * switch call it has applied. The directory's record (`system-switch-record.ts`) refuses a write * older than the one it holds, and the scope refuses a move older than the one it applied, so the * two always end on the SAME call's position, whatever order two concurrent calls' writes and moves * arrive in. Without it, operator A recording OFF, then B recording ON and moving ON, then A's OFF * landing, left the record ON beside a scope that is OFF, and a later restore re-admitted the * subject. Keyed by (subject, object) as the marker is, so a copy of another scope's rows fences * nothing here. Part of the scope's own storage: a restore or rewind takes it back with the marker. */ export declare const SWITCH_FENCES_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_switch_fences (\n subject TEXT NOT NULL,\n object TEXT NOT NULL,\n -- The operation id of the newest switch call applied here. A ULID, so it orders by text.\n fence TEXT NOT NULL,\n PRIMARY KEY (subject, object)\n );\n"; /** * Move one module's switch on one scope. Idempotent: a repeat changes nothing and says so. * * OFF tombstones every live `granted:` tuple the module holds at `scope:`, records each * as `switched:`, and makes the marker live. ON un-tombstones exactly the * grants with a live `switched:` record, tombstones those records, and tombstones the * marker. A repeated OFF re-asserts the whole position: a grant that became live meanwhile * (only a raw write can do that now) is tombstoned and recorded too. * * `held: false`, with nothing written, when the module has no authority reaching the scope: * no grant and no marker here, and no live TENANT-level grant (`tenantHeld`, which the caller * reads from the directory, since the scope's storage cannot answer it). The caller named * something this scope never ran, and turning "nothing" off must not write a marker that * silently disables a module the day it is installed. A module whose only authority here is a * tenant-level grant IS held (#1823): OFF writes the marker, which the evaluator then denies * the module on, and tombstones nothing, because nothing of it is here to tombstone. * * Run it inside one transaction; the reads and writes here are meant to be one unit. */ export declare function switchSystemSchedules(db: SwitchSql, input: { moduleId: string; scopeId: string; to: 'on' | 'off'; at: string; tenantHeld?: boolean; fence?: string; }): SwitchOutcome; /** * One subject `switchRecordedOff` switched off — `SwitchedOffInUnit`'s shape: a module * (`moduleId`), or since #2029 a peer vertical (`vertical`). */ export type SwitchedOff = SwitchOutcome & ({ moduleId: string; vertical?: never; } | { vertical: string; moduleId?: never; }); /** * What the platform carries into a unit that re-creates a scope's grants (#1742): the subjects * its directory records OFF on that scope, and of those, the ones held there only by a * tenant-level grant (#1823, #2030). Subjects only, never a scope: the unit names its own. */ export interface RecordedOffCarry { moduleIds: readonly string[]; tenantHeld?: readonly string[]; /** #2029: the recorded-off peers. Absent from a platform that predates the peer record. */ verticals?: readonly string[]; tenantHeldVerticals?: readonly string[]; /** * #2045: the fence of each recorded-off subject, by tuple subject (`system:`, `vertical:`): * the operation id of the call the record holds. A subject the scope has since moved under a newer * call is left as that call put it. Absent from a platform built before the fence. */ fences?: Readonly>; } /** * The same carry as it crosses the wire to a vertical's deployment (#1742): the provision, * reconcile and restore bodies, and `SwitchCarry` on the platform side. `switchedOffPeers` and * `tenantHeldPeers` are #2029's; a deployment built before them strips both, and the platform's * re-assert after the call switches those peers off instead. */ export interface SwitchCarryWire { switchedOff?: readonly string[]; tenantHeld?: readonly string[]; switchedOffPeers?: readonly string[]; tenantHeldPeers?: readonly string[]; /** #2045: `RecordedOffCarry.fences`, as it crosses the wire. A deployment built before it strips it. */ switchFences?: Readonly>; } /** A wire carry as the unit runs it, or undefined when it names nothing to switch off. */ export declare function recordedOffFromWire(wire: SwitchCarryWire): RecordedOffCarry | undefined; /** * Switch the directory's recorded-off subjects off again, inside the unit that re-created the * scope's grants (#1742): a provision's or reconcile's seat, or a restore's replay. The caller * runs it in that same unit, AFTER the seat, so the grants the seat created are the ones OFF * tombstones and records, and a later restore gives back exactly those (#1674's order). No * schedule can run, and no peer call be admitted, between the seat and this, because nothing * can run between them. * * `moduleIds` are the recorded-off modules, `verticals` the recorded-off peers (#2029). `scopeId` * is the scope `db` is, and the caller passes the one the request provisions or restores, never * a second id: the lists name subjects, never scopes, so they can only reach the scope that unit * is already writing. A subject the scope holds nothing for answers `held: false` and writes * nothing, as the switch always does — unless it is in `tenantHeld` / `tenantHeldVerticals`, the * recorded-off subjects the caller found a live tenant-level grant for (#1823, #2030). Off only: * nothing here turns anything on. */ export declare function switchRecordedOff(db: SwitchSql, input: RecordedOffCarry & { scopeId: string; at: string; }): SwitchedOff[]; /** * Move one kill switch of either kind on one scope (#2029): `switchSystemSchedules` for a * module, `switchSubjectGrants` over `vertical:` for a peer. `key` is the module id or the * peer's slug. The one mover the adapters' shared switch body and their re-asserts call. */ export declare function moveSwitch(db: SwitchSql, kind: SwitchKind, input: { key: string; scopeId: string; to: 'on' | 'off'; at: string; tenantHeld?: boolean; fence?: string; }): SwitchOutcome; //# sourceMappingURL=system-switch.d.ts.map