@agentcomms/resend 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -4322,26 +4322,7 @@ declare namespace external_d_exports {
4322
4322
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4323
4323
  }
4324
4324
  //#endregion
4325
- //#region ../core/dist/launcher-BuZRjGA8.d.mts
4326
- //#endregion
4327
- //#region src/config-hooks.d.ts
4328
- /**
4329
- * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
4330
- * neither lost nor let in between its scan and its write.
4331
- *
4332
- * Deliberately not exported from the package root, as `enableNamesMigrationForTests` is not (`release-gate.ts`): a
4333
- * store is given them through its constructor under this symbol, which only a test importing this file by path can
4334
- * name. No consumer of the package can reach it.
4335
- */
4336
- declare const CONVERSION_HOOKS: unique symbol;
4337
- interface ConversionHooks {
4338
- /** After the read before the lock, before the config lock is taken. */
4339
- beforeLock?: (() => Promise<void> | void) | undefined;
4340
- /** Under the lock, after the approvals were scanned and before the version-3 write is built. */
4341
- afterScan?: (() => Promise<void> | void) | undefined;
4342
- /** Under the lock, immediately before the version-3 write. */
4343
- beforeWrite?: (() => Promise<void> | void) | undefined;
4344
- }
4325
+ //#region ../core/dist/launcher-CD40rL4F.d.mts
4345
4326
  interface RegisteredServer {
4346
4327
  client: string;
4347
4328
  path: string;
@@ -4415,19 +4396,67 @@ interface ResolvedPaths {
4415
4396
  */
4416
4397
  downloadsDir: string;
4417
4398
  }
4418
- type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "NEEDS_NEWER_RELEASE" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SOURCE_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "WHATSAPP_VISIBILITY_SEAM_REQUIRED" | "CONFIG";
4419
- interface CommsErrorOptions {
4420
- hint?: string;
4421
- details?: Record<string, unknown>;
4422
- cause?: unknown;
4399
+ //#endregion
4400
+ //#region src/approval-binding.d.ts
4401
+ /**
4402
+ * What a version-2 approval binds, defined once (design 2026-10-05 §D1, "Digest integrity").
4403
+ *
4404
+ * A record stores two digests. `contentDigest` is the outward content or change, computed as before: the message, the
4405
+ * change, the download. `bindingDigest` is this module's: the content digest together with the record's route, its
4406
+ * lifetimes and its own identity — who it belongs to, which draft, which channel, which send epoch. Every one of those
4407
+ * is a field a claim or a report acts on, so a record whose binding no longer recomputes from its own fields has been
4408
+ * altered since it was made, and nothing it says can be trusted.
4409
+ *
4410
+ * There is no second copy of anything: the binding is recomputed from the stored fields alone, never stored apart
4411
+ * from them as a key.
4412
+ */
4413
+ /**
4414
+ * How an approval is decided, fixed when it is made: `chat` — a yes in the conversation is enough — or `confirm` — a
4415
+ * person outside the chat decides. A send's route is the stricter of its live send policy and any escalation; a
4416
+ * change's is the live change policy. It never changes when the configuration does.
4417
+ *
4418
+ * `standing` — the send is one use of a standing approval the person gave in advance, at a terminal with its code
4419
+ * (design 2026-10-19-standing-approvals, S9): a child, approved when it is made, for one claim within minutes. A
4420
+ * standing record itself is on `confirm`, whatever the change policy.
4421
+ */
4422
+ type ApprovalRoute = "chat" | "confirm" | "standing";
4423
+ /** The four activation documents that may receive standing disclosure authority. */
4424
+ type DisclosureActivationKind = "rule" | "judge-budget" | "judge-kind" | "enable-all";
4425
+ /** An immutable version named by a standing disclosure authorisation. */
4426
+ type DisclosureVersionKind = "rule" | "target" | "subscriber" | "judge" | "judge-budget" | "judge-kind";
4427
+ interface DisclosureVersion {
4428
+ readonly kind: DisclosureVersionKind;
4429
+ readonly id: string;
4430
+ readonly version: number;
4423
4431
  }
4424
- declare class CommsError extends Error {
4425
- readonly code: ErrorCode;
4426
- readonly hint: string | undefined;
4427
- readonly details: Record<string, unknown> | undefined;
4428
- constructor(code: ErrorCode, message: string, options?: CommsErrorOptions);
4429
- get exitCode(): number;
4430
- get retryable(): boolean;
4432
+ /**
4433
+ * The content-free, canonical authority for a future disclosure (local-event design D2).
4434
+ *
4435
+ * The event daemon derives this from one canonical activation document. Core does not construct
4436
+ * that document; it verifies the closed shape that makes an independently supplied version list
4437
+ * impossible to smuggle into a disclosure record.
4438
+ */
4439
+ interface DisclosureBinding {
4440
+ readonly digest: string;
4441
+ readonly activationIntentId: string;
4442
+ readonly activationKind: DisclosureActivationKind;
4443
+ readonly versions: readonly DisclosureVersion[];
4444
+ }
4445
+ /**
4446
+ * What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
4447
+ * download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
4448
+ * change to no owner at all, stored with `inboxId: ''`.
4449
+ */
4450
+ type OwnerScope = "owner" | "prospective" | "global";
4451
+ /**
4452
+ * One use of a standing approval, as its child is bound to it (design 2026-10-19-standing-approvals, S9): the channel
4453
+ * its act goes to, the thread it is in, and the digest of the content it sends — what a claim checks its standing
4454
+ * approval still covers.
4455
+ */
4456
+ interface StandingUseBinding {
4457
+ channel: string;
4458
+ thread: string;
4459
+ content: string;
4431
4460
  }
4432
4461
  /**
4433
4462
  * The file operations a lock is made of, one method each, so a test can count and interrupt every one of them: the
@@ -4449,1039 +4478,804 @@ interface LockIo {
4449
4478
  remove(path: string): Promise<void>;
4450
4479
  }
4451
4480
  //#endregion
4452
- //#region src/cli-command.d.ts
4453
- /**
4454
- * The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
4455
- *
4456
- * A bare `agent-gmail approve …` is not runnable for most of the people it is printed for: a package run by npx, by a
4457
- * managed runtime or from a checkout puts nothing on their PATH. So every such command names this process's Node and
4458
- * a checked file of the product it runs, with the suite directories it uses pinned as options — and when no file can
4459
- * be checked, there is no command at all, only a sentence saying what is missing. The caller says which package it is
4460
- * and what to run; it never supplies the program or quotes the line.
4461
- *
4462
- * Three directions. A product's own CLI is found from the product itself, walking from one of its own modules to its
4463
- * manifest: no registration is ever asked. A channel's handoff to core is found the way the channel itself finds core,
4464
- * through its installed dependency, and only the same release. Another product's command is found among the servers
4465
- * registered with the person's MCP clients, which the caller scanned and passes in: only a file-backed registration of
4466
- * exactly the printing package's version, checked on disk. Nothing a registration names is run, or asked anything —
4467
- * not even its interpreter for a version: no installer wrote down where a scanned entry came from, so none is trusted
4468
- * to run (D1). This module never scans, spawns or probes.
4469
- *
4470
- * What is checked is checked when the command is printed, not for ever (D6): the file it names is real, readable and
4471
- * inside its package then, and a later change to that installation is a later fact.
4472
- */
4473
- /** The gate a constructor call has to pass: only this module holds it. */
4474
- declare const GATE$1: unique symbol;
4481
+ //#region src/approval-io.d.ts
4475
4482
  /**
4476
- * A command that runs one of this suite's own CLIs, made only by `locateCliCommand`. Rendered as every printed command
4477
- * is — a line to paste, or on Windows its words as JSON when no line is safe in every shell (`inlineCommand`,
4478
- * `commandText`).
4483
+ * The file operations approval maintenance and the unsent report are made of (design 2026-10-05 §D9), one method
4484
+ * each, so that a test can count every one of them — every directory read, metadata stat, record open, lock try,
4485
+ * renewal, ownership check and unlink — and none is hidden behind a helper. The store passes the real ones unless it
4486
+ * was opened with others.
4479
4487
  *
4480
- * A class with a private field, so the type is nominal: an object literal with the same fields is not one, and nor is
4481
- * a spread copy of one with other words in it, as it would be with a symbol-keyed brand. Only the type is exported;
4482
- * the class and its gate are not.
4488
+ * Only these two passes go through it: every other read and write of the store is as it was.
4483
4489
  */
4484
- declare class PrintedCommand {
4485
- #private;
4486
- /** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
4487
- readonly words: readonly string[];
4488
- /** The line to paste; null when no supported common or labelled shell rendering is safe. */
4489
- readonly line: string | null;
4490
- readonly platform: NodeJS.Platform;
4491
- readonly shell?: "powershell" | undefined;
4492
- constructor(gate: typeof GATE$1, words: readonly string[], platform: NodeJS.Platform, entry: string);
4493
- /**
4494
- * The real path of the file it runs, as it was checked when the command was located: a fact about then (D6). The
4495
- * command names this path and nothing else, so what runs is whatever is there when the person runs it.
4496
- */
4497
- get entry(): string;
4498
- /**
4499
- * As JSON — a result's field written out by `--json`, or returned to an MCP client — it is the text it always was:
4500
- * its line, or its words as JSON with what to do when no Windows line is safe (`commandText`). Rendered where it
4501
- * leaves the process, so a field can hold the command itself and the wire stays the same.
4502
- */
4503
- toJSON(): string;
4504
- /** Never interpolated: a sentence gives a command through `handoffSentence`, a value through `handoffText`. */
4505
- [Symbol.toPrimitive](): never;
4506
- }
4507
- /** The package asking: a module of its own (`import.meta.url`), and the package that module is expected to be in. */
4508
- interface CliCommandCaller {
4509
- readonly url: string;
4510
- readonly packageName: string;
4490
+ interface ApprovalIo {
4491
+ /** One enumeration of a directory: its entries' names. */
4492
+ readdir(directory: string): Promise<string[]>;
4493
+ /** One metadata stat: the modification time, in milliseconds. */
4494
+ stat(path: string): Promise<{
4495
+ readonly mtimeMs: number;
4496
+ }>;
4497
+ /** One open of a file's content, as text. */
4498
+ readText(path: string): Promise<string>;
4499
+ /** Replaces a file atomically — a temporary file, flushed, renamed — and, when `durable`, syncs its directory too. */
4500
+ writeAtomic(path: string, text: string, options?: {
4501
+ durable?: boolean;
4502
+ }): Promise<void>;
4503
+ /** One unlink of an approval artifact: a record or its claim marker. A missing one rejects with `ENOENT`. */
4504
+ unlink(path: string): Promise<void>;
4505
+ /** The lock files' own operations (`tryFileLock`). */
4506
+ readonly lock: LockIo;
4511
4507
  }
4512
- /** Why there is no command: the step that found nothing it could check. */
4513
- type CliNotLocatedReason = "caller" | "manifest" | "core" | "version" | "engine" | "bin" | "entry" | "interpreter" | "arguments" | "direction" | "no-registrations" | "not-registered";
4514
- /** Why one registration of the product was not used. */
4515
- type RegistrationRejection = "npx" | "arguments" | "no-file" | "no-package" | "version" | "engine" | "bin" | "entry";
4516
- interface RegistrationNotUsed {
4517
- readonly client: string;
4518
- readonly path: string;
4519
- readonly name: string;
4520
- readonly why: RegistrationRejection;
4508
+ /** A version-1 record, as 0.13.0 wrote it: a send has no `kind`, and one `digest` stands for the content. */
4509
+ interface LegacyApprovalRecord {
4510
+ approvalId: string;
4511
+ /** Absent on a send. */
4512
+ kind?: ApprovalKind | undefined;
4513
+ digestVersion: 1;
4514
+ inboxId: string;
4515
+ inboxSub?: string | undefined;
4516
+ draftId: string;
4517
+ draftMessageId: string;
4518
+ digest: string;
4519
+ approvedDigest?: string | undefined;
4520
+ approvedVia?: ApprovalChannel | undefined;
4521
+ policy: SendPolicy;
4522
+ requiredPolicy: SendPolicy;
4523
+ riskFlags: string[];
4524
+ expect: Expectation;
4525
+ challengeHash?: string | undefined;
4526
+ challengeAttempts: number;
4527
+ state: ApprovalState;
4528
+ createdAt: string;
4529
+ expiresAt: string;
4530
+ updatedAt: string;
4531
+ sentMessageId?: string | undefined;
4532
+ reason?: string | undefined;
4533
+ change?: ChangeBinding | undefined;
4534
+ download?: DownloadBinding | undefined;
4521
4535
  }
4522
4536
  /**
4523
- * No command: nothing to run, and a sentence naming the product, its package and the exact version it needs. An
4524
- * installation that has gone or changed since is said so, never replaced by a suite command found by name on PATH.
4537
+ * A version-1 record as every surface shows it: `{ approvalId, kind, state, legacy: true, createdAt, expiresAt }` and
4538
+ * the summary a version-2 record shows — its owner, what it was for, and which channel made it when that can be told.
4539
+ * Never claimable, and never given a version-2 timestamp it does not hold.
4525
4540
  */
4526
- interface CliCommandNotLocated {
4527
- readonly ok: false;
4528
- readonly reason: CliNotLocatedReason;
4529
- /** How the product is named to a person: `Gmail`. */
4530
- readonly product: string;
4531
- readonly package: string;
4532
- /** The exact version needed: the printing package's own, since the suite is released together. */
4533
- readonly version: string;
4534
- /** For `engine`: the Node range the target declares. */
4535
- readonly nodeRange?: string | undefined;
4536
- /** For another product: each registration of it that was looked at, and why it was not used. */
4537
- readonly registrations?: readonly RegistrationNotUsed[] | undefined;
4538
- /** What was found, in a clause. */
4539
- readonly detail: string;
4540
- /** The whole sentence to show. */
4541
- readonly message: string;
4541
+ interface LegacyView {
4542
+ readonly approvalId: string;
4543
+ readonly kind: ApprovalKind;
4544
+ /** Derived as 0.13.0 derived it (`deriveLegacyV1State`). */
4545
+ readonly state: ApprovalState;
4546
+ readonly legacy: true;
4547
+ readonly createdAt: string;
4548
+ readonly expiresAt: string;
4549
+ /** The stored owner, trusted as stored: a version-1 record has no binding to check it against. */
4550
+ readonly inboxId: string;
4551
+ readonly ownerScope: "owner" | "global";
4552
+ /**
4553
+ * The channel that made it, when only one could have: an `ibx_` owner is a Gmail mailbox, an `acc_` owner the
4554
+ * stored platform of an account that still exists. Null when nothing says — it then takes part in no channel's
4555
+ * grouping and gets only generic wording.
4556
+ */
4557
+ readonly channel: string | null;
4558
+ readonly expect: Expectation;
4559
+ readonly reason?: string | undefined;
4560
+ readonly sentMessageId?: string | undefined;
4542
4561
  }
4543
4562
  //#endregion
4544
- //#region src/handoff-text.d.ts
4563
+ //#region src/audit.d.ts
4545
4564
  /**
4546
- * A handoff as words a person reads, and what a core function that prints one is given (CUE-403; CONTRIBUTING.md,
4547
- * "Telling a person what to run"). `handoffs.ts` makes located handoffs and re-exports all of this.
4548
- *
4549
- * Kept apart from the locator on purpose: the configuration store, the secret stores and the attachment jail print
4550
- * handoffs too, and the locator's own imports reach the configuration store, so importing it from them would be a
4551
- * cycle. Nothing here locates anything or imports a module that does: a located command is made by `handoffs.ts` and
4552
- * handed in, and what is here only renders one. With none handed in, there is nothing to render: a package that did not
4553
- * give core its caller and is asked for a command has a programming error, not a bare name to fall back on.
4565
+ * Every mailbox write leaves one line here: what was done, to which inbox, with which ids and outcome. Never message
4566
+ * bodies, never secrets — recipient *domains* only, so the log can be shared when asking for help.
4554
4567
  */
4555
- /** A command for a person to run, located, or the sentence saying why there is none here. */
4556
- type Handoff = PrintedCommand | CliCommandNotLocated;
4557
- interface HandoffUse {
4558
- /** The suite folders the command reads or writes; the four every command opens when left out. */
4559
- readonly uses?: readonly PathName[] | undefined;
4560
- /** It also reads or writes downloads, so the downloads folder is pinned too. */
4561
- readonly downloads?: boolean | undefined;
4562
- }
4563
- /**
4564
- * The commands a package tells a person to run, made once per printing process from its caller (`handoffs.ts`,
4565
- * `cliHandoffs`): `openCore({ caller })` puts them on `core.handoffs`. Each method takes the CLI's own words, never the
4566
- * program, and returns a `Handoff`.
4567
- */
4568
- interface CliHandoffs {
4569
- /** The printing package: one of its own modules and its name. */
4570
- readonly caller: CliCommandCaller;
4571
- /** The shell the commands are quoted for. */
4572
- readonly platform: NodeJS.Platform;
4573
- /** The printing package's own CLI, with these words after the program. */
4574
- own(words: readonly string[], use?: HandoffUse): Handoff;
4575
- /** The core CLI: the printing package's own when that is core, the core it has installed otherwise. */
4576
- core(words: readonly string[], use?: HandoffUse): Handoff;
4577
- /** The CLI of `channel` — `core`, `gmail`, `slack` …: own, core, or another product found among its registrations. */
4578
- of(channel: string, words: readonly string[], use?: HandoffUse): Handoff;
4579
- /** The same commands, quoted for another shell. */
4580
- on(platform: NodeJS.Platform): CliHandoffs;
4581
- /**
4582
- * The same, able to find another product: with the servers registered with this machine's MCP clients read now, from
4583
- * the environment these were made with. Read once per call; nothing a registration names is run.
4584
- */
4585
- registered(): Promise<CliHandoffs>;
4586
- /** The same, finding another product among these registrations: a scan the caller has already read. */
4587
- withRegistrations(registrations: readonly RegisteredServer[]): CliHandoffs;
4588
- }
4589
- type SendPolicy = "chat" | "confirm" | "never";
4590
- /**
4591
- * How a loosening of the configuration is approved: `chat` — the person says yes in the conversation and the agent
4592
- * claims the approval — or `confirm` — the person types a code at a terminal, as for a send under `confirm`.
4593
- *
4594
- * There is no `never`. A setting the software refused ever to loosen would leave editing the file by hand as the only
4595
- * way to change it, and that passes no gate at all.
4596
- */
4597
- type ChangePolicy = "chat" | "confirm";
4598
- /** 0.18's switch for the daily update check: what `defaults.updates` reads from until a level is set. */
4599
- type UpdateCheckSetting = "on" | "off";
4600
- type StoreKind = "keychain" | "file";
4601
- interface ClientConfig {
4602
- provider: string;
4603
- clientId: string;
4604
- projectId?: string | undefined;
4605
- secretRef: string;
4606
- addedAt: string;
4607
- /**
4608
- * The organisation profile that made this row and owns it (design 2026-10-02 §D4) — absent on a row a person
4609
- * registered, or one a profile adopted. A row carrying it is the profile's to change: `org update` and `org remove`
4610
- * change it, and `client add --replace` and `client remove` refuse it while its organisation has a record.
4611
- *
4612
- * Additive: client rows are loose, so a release that does not know the key keeps it through its writes.
4613
- */
4614
- organisation?: string | undefined;
4615
- }
4616
- interface InboxConfig {
4617
- /** Immutable id (`ibx_` + 16 base32 characters). Secrets and all state are keyed by it; the alias is a label. */
4618
- id: string;
4619
- provider: string;
4620
- email: string;
4621
- /** Stable account id (OIDC `sub`). Absent for inboxes imported from tools that never asked for it. */
4622
- sub?: string | undefined;
4623
- identity: "oidc" | "legacy";
4624
- client: string;
4625
- tier: string;
4626
- contacts: boolean;
4627
- grantedScopes: string[];
4628
- secretRef: string;
4629
- sendPolicy?: SendPolicy | undefined;
4630
- /** Overrides `defaults.changePolicy` for changes to this inbox. */
4631
- changePolicy?: ChangePolicy | undefined;
4632
- internalDomains: string[];
4633
- createdAt: string;
4634
- }
4635
- interface Defaults {
4636
- sendPolicy: SendPolicy;
4637
- /**
4638
- * How a loosening is approved wherever an inbox or account does not say otherwise. Absent reads as `chat`.
4639
- *
4640
- * Absent rather than filled in by the schema, unlike `sendPolicy`. A config written before this setting existed
4641
- * reads as `chat` from then on, and the release note says so; a default the schema supplied would be written into
4642
- * the file by the next unrelated change, where it would look like a choice somebody made.
4643
- */
4644
- changePolicy?: ChangePolicy | undefined;
4645
- riskEscalation: boolean;
4646
- sendCaps: {
4647
- perHour: number;
4648
- perDay: number;
4649
- };
4650
- attachRoots: string[];
4651
- /** Extra deny entries on top of the built-in list. */
4652
- attachDeny: string[];
4653
- downloadsDir?: string | undefined;
4654
- timezone: string;
4655
- confirm: {
4656
- /**
4657
- * Fail-closed allowlist of MCP `clientInfo.name` values whose form elicitation is trusted to reach a human.
4658
- * Empty by default; only the CLI adds entries, after a probe.
4659
- */
4660
- elicitationClients: string[];
4661
- };
4662
- /**
4663
- * The daily update check (design 2026-09-28): once a day this machine asks npm whether a newer release is out, and
4664
- * every server and command stops until it is updated or put off until tomorrow. Absent reads as `on`.
4665
- *
4666
- * Absent rather than filled in, as `changePolicy` is: a default the schema supplied would be written into the file
4667
- * by the next unrelated change, where it would look like a choice somebody made. Turning it off is a loosening —
4668
- * it is what keeps a machine from quietly falling behind a release — so it is approved like one; on applies at once.
4669
- */
4670
- updateCheck?: UpdateCheckSetting | undefined;
4671
- /**
4672
- * How this machine updates (design 2026-10-16, U2): `auto`, `every-14-days` or `never`. Absent reads from
4673
- * `updateCheck` — `on` as `auto`, `off` as `never` — and both absent as `auto`. A word this release does not know
4674
- * reads as `auto`, and the file stays readable: a later release may add a level.
4675
- *
4676
- * Moving to a level that updates less is a loosening, approved like one; moving back applies at once. Every write of
4677
- * a level writes `updateCheck` beside it, `off` for `never` and `on` otherwise, so a release before 0.20 sharing the
4678
- * file checks, or does not, as the person chose.
4679
- */
4680
- updates?: string | undefined;
4681
- }
4682
- /**
4683
- * A connected account on a platform that is not mail: a Slack workspace, and every channel after it (design
4684
- * 2026-09-26) — one generic record, whatever the platform.
4685
- *
4686
- * `id`, `platform`, `workspace` (the container: a Slack team, a Resend team, a WhatsApp store), `userId` (who it acts
4687
- * as), `tier`, `mode`, `grantedScopes`, `secretRef`, the two policies and `createdAt` are every channel's. A channel
4688
- * may keep keys of its own beside them — Slack's `appId`, `redirectPort` — and they are kept through every write. It
4689
- * may not add a safety setting: only `sendPolicy`, `changePolicy` and `mode` are judged by `classifyChange`, so a key
4690
- * of a channel's own that loosened something would loosen it unasked.
4691
- *
4692
- * `secretRef` stays required, although the 2026-09-26 contract made it optional for a channel with no credential:
4693
- * every earlier release requires it, and they share this file — an account without one would make the whole
4694
- * configuration unreadable to a server started last week. Such a channel records a reference that names no secret,
4695
- * `<platform>:none:<id>`, until a config version can say otherwise.
4696
- *
4697
- * It sits beside `inboxes` rather than replacing it. The spec asked for one `accounts` map holding everything, and
4698
- * that rename is the one change this file cannot take: `version: 1` is additive precisely because an MCP server
4699
- * started last week and a CLI run today share the file, and a release that moved every mailbox out of `inboxes`
4700
- * would read, to the older of the two, as a config with no mailboxes in it. What the rename was for — one list of
4701
- * everything connected, whatever the platform — is a question about the shape of the answer, not the shape of the
4702
- * file, so `connectedAccounts()` provides it and the file grows one key.
4703
- */
4704
- interface AccountConfig {
4705
- id: string;
4706
- /** `slack`. */
4707
- platform: string;
4708
- /** The workspace or team id. Every other id this account sees is only meaningful inside it. */
4709
- workspace: string;
4710
- /** "Acme Corp" — shown to people, never matched on: a workspace can be renamed and stays the same workspace. */
4711
- workspaceName?: string | undefined;
4712
- /** This account's own user id in that workspace, so its own messages can be told from everyone else's. */
4713
- userId: string;
4714
- tier: string;
4715
- grantedScopes: string[];
4716
- /** Where its credential is in the secret store, `<platform>:…` — for a channel that stores none, `<platform>:none:<id>`. */
4717
- secretRef: string;
4718
- sendPolicy?: SendPolicy | undefined;
4719
- /** Overrides `defaults.changePolicy` for changes to this account. */
4720
- changePolicy?: ChangePolicy | undefined;
4721
- createdAt: string;
4722
- /**
4723
- * The OAuth client this account's token was issued by, and the app it belongs to.
4724
- *
4725
- * Recorded because the Gmail release found the opposite: a reauth used the first OAuth client in the config
4726
- * rather than the inbox's own, and then did not record which one it had used. Slack makes that worse — D8
4727
- * means **one app per workspace**, so "the first app" is wrong more often than it is right — and a reauth
4728
- * that silently moves an account onto a different app changes what it can do without saying so.
4729
- *
4730
- * Optional because the key is additive: a config written before these existed parses unchanged.
4731
- */
4732
- oauthClientId?: string | undefined;
4733
- appId?: string | undefined;
4734
- /**
4735
- * `read` or `send`, as installed.
4736
- *
4737
- * Kept beside `grantedScopes` rather than derived from them, because the two answer different questions: the
4738
- * scopes are what Slack granted, and this is what the person asked for. A disagreement between them is drift
4739
- * worth reporting, and a value derived from the scopes could never disagree.
4740
- */
4741
- mode?: string | undefined;
4742
- /**
4743
- * The loopback port the last sign-in used, which is the one in the app's redirect URL.
4744
- *
4745
- * Slack matches redirect URLs exactly, so every later sign-in and every set of steps that edits the app has to
4746
- * name this number. Without it the steps said `<port>`, or — from the MCP server — guessed 51234, and a person who
4747
- * had chosen another port followed them to a sign-in that failed on the way back.
4748
- */
4749
- redirectPort?: number | undefined;
4750
- /** The organisation profile whose Slack app created this account. Absent for a person's own app. */
4751
- organisation?: string | undefined;
4752
- /** Which app in that profile issued the account's current credential. */
4753
- profileApp?: "read" | "send" | undefined;
4754
- }
4755
- type PendingRevocationStatus = "pending" | "revoked" | "expired";
4756
- /** One bearer token in a superseded rotating bundle, tracked independently from its pair. */
4757
- interface PendingRevocationTokenState {
4758
- status: PendingRevocationStatus;
4759
- /** The fixed absolute time after which the old token is known to be unusable. */
4760
- deadline: string;
4761
- }
4762
- /**
4763
- * A superseded credential kept until each token in it is conclusively revoked or expired.
4764
- *
4765
- * `store` belongs to the entry rather than being inferred from `secrets.store`: an older release can move the root
4766
- * store without knowing to move this bundle, and cleanup must still find the only copy that exists.
4767
- */
4768
- interface PendingRevocation {
4769
- ref: string;
4770
- store: StoreKind;
4771
- platform: string;
4772
- workspace: string;
4773
- createdAt: string;
4774
- tokens: {
4775
- access: PendingRevocationTokenState;
4776
- refresh?: PendingRevocationTokenState | undefined;
4777
- };
4778
- }
4779
- interface ConfigBody {
4780
- /**
4781
- * The one secret backend for this config directory: client secrets, refresh tokens and the approval key. Absent
4782
- * until the first command that stores a secret chooses it.
4783
- */
4784
- secrets?: {
4785
- store: StoreKind;
4786
- } | undefined;
4787
- clients: Record<string, ClientConfig>;
4788
- inboxes: Record<string, InboxConfig>;
4789
- /** Non-mail accounts. Absent in every config written before this key existed, hence the default. */
4790
- accounts: Record<string, AccountConfig>;
4791
- /** Superseded rotating credentials which still need conclusive per-token cleanup. */
4792
- pendingRevocations?: PendingRevocation[] | undefined;
4793
- defaults: Defaults;
4794
- /**
4795
- * How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
4796
- * who a message appears to come from, and each Slack account's agent emoji.
4797
- *
4798
- * `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
4799
- * as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
4800
- * later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
4801
- * judged by `classifyChange`.
4802
- */
4803
- agentLabel?: unknown;
4804
- }
4805
- interface ConfigV1 extends ConfigBody {
4806
- version: 1;
4807
- }
4808
- /** A name an account used to have, and the account that had it. */
4809
- interface FormerName {
4810
- /** What it was renamed to — at the time. The account's current name is found by `id`, so a later rename is followed. */
4811
- name: string;
4812
- id: string;
4813
- }
4814
- /**
4815
- * Names that were replaced, per kind, and never reusable.
4816
- *
4817
- * Per kind because version 1 lets a mailbox and a workspace share a word: `work` the mailbox and `work` the workspace
4818
- * become `work/gmail` and `work/slack`, and one flat record could not say which old `work` is which.
4819
- */
4820
- interface FormerNames {
4821
- inboxes: Record<string, FormerName>;
4822
- accounts: Record<string, FormerName>;
4823
- }
4824
- /** Who an organisation's Google client is for, as its administrator states it (design 2026-10-02 §D2). */
4825
- type OrganisationServes = "any" | {
4826
- domains: string[];
4827
- };
4828
- /**
4829
- * One Google client an organisation profile brought here: a *generation* (design 2026-10-02 §D4).
4830
- *
4831
- * `owned` — the profile made the client row `name` (`rgc-1`), which carries `organisation`, and its secret is the
4832
- * profile's to rewrite. `adopted` — the row was already here under a name a person chose (they ran `client add` with
4833
- * the organisation's file), and stays theirs: no `org` command changes or removes it. `serves` is per generation,
4834
- * because it is a fact about one client, not about the organisation.
4835
- */
4836
- interface OrganisationGeneration {
4837
- name: string;
4838
- clientId: string;
4839
- projectId?: string | undefined;
4840
- ownership: "owned" | "adopted";
4841
- serves: OrganisationServes;
4842
- addedAt: string;
4843
- }
4844
- interface OrganisationSlackApp {
4845
- clientId: string;
4846
- appId?: string | undefined;
4847
- }
4848
- /**
4849
- * An organisation's update caps, as the file beside its profile names them (design 2026-10-16, U8), and the SHA-256 of
4850
- * that file, so `org update` can tell the organisation changing them from the record drifting. A `max` word this release
4851
- * does not know reads as `never`, the strictest it could mean: caps only ever tighten.
4852
- */
4853
- interface OrganisationUpdates {
4854
- max?: string | undefined;
4855
- background?: boolean | undefined;
4856
- sha256: string;
4857
- }
4858
- /**
4859
- * The organisation's bot-only agent app, as its profile's agent-app file names it (CUE-493): the app's id, and the
4860
- * SHA-256 of that file, so `org update` can tell the organisation changing it from the record drifting. No client id:
4861
- * nobody signs in through it — its administrator installs it once and its members paste its bot token.
4862
- */
4863
- interface OrganisationAgentApp {
4864
- appId: string;
4865
- sha256: string;
4866
- }
4867
- /**
4868
- * What this machine records of one organisation profile: never its bytes and never its secret — where it was read
4869
- * from, the SHA-256 of what was read and when, the generations of its Google client, and its Slack apps.
4870
- */
4871
- interface OrganisationRecord {
4872
- label: string;
4873
- /** Read again, from this absolute path, by every `org update`. `file` is the one kind version 1 knows (§D3). */
4874
- source: {
4875
- kind: string;
4876
- path: string;
4877
- };
4878
- sha256: string;
4879
- readAt: string;
4880
- addedAt: string;
4881
- /** Whether the member asked for this organisation's client to serve addresses outside it too (§D6). */
4882
- forOtherAddresses: boolean;
4883
- /** `active` names the generation new mailboxes get — none once a profile stops naming a Google client. */
4884
- gmail?: {
4885
- active: string | null;
4886
- generations: OrganisationGeneration[];
4887
- } | undefined;
4888
- slack?: {
4889
- workspace: string;
4890
- workspaceName: string;
4891
- redirectPort: number;
4892
- apps: {
4893
- read?: OrganisationSlackApp | undefined;
4894
- send?: OrganisationSlackApp | undefined;
4895
- agent?: OrganisationAgentApp | undefined;
4896
- };
4897
- } | undefined;
4898
- /** The organisation's update caps, from the file beside its profile; absent when it names none. */
4899
- updates?: OrganisationUpdates | undefined;
4568
+ interface AuditRecord {
4569
+ at: string;
4570
+ /** Immutable inbox id; `alias` is the label at the time. */
4571
+ inboxId: string;
4572
+ alias?: string;
4573
+ operation: string;
4574
+ /** `started` marks the first half of a change recorded before it is made; a later record for the same ids ends it. */
4575
+ outcome: "ok" | "refused" | "failed" | "started";
4576
+ ids?: Record<string, string | string[] | CondensedIds>;
4577
+ recipientDomains?: string[];
4900
4578
  /**
4901
- * The entries of the profile's `extensions` this release did not know and skipped, being optional (design 2026-10-19,
4902
- * X3): the release that first understands each, and the SHA-256 of the entry's canonical JSON — as
4903
- * `Record<string, OrganisationIgnoredExtension>`. Kept in step with the profile. Absent when none was skipped.
4904
- *
4905
- * Held as the file holds it, whatever its shape, and read only through `ignoredListOf`, which keeps the well-formed
4906
- * entries and names the rest: 0.21.0 is the first release to read it, so a shape a later release or a hand gave it
4907
- * must never make the whole configuration unreadable, nor be dropped by an unrelated write. Losing it is safe: a
4908
- * later release that understands an entry judges applying it by its classifier whether or not the list names it —
4909
- * a tightening at once, anything else approved first, never a repair of drift (X4).
4579
+ * Full canonical recipient addresses. Only a send writes these: for every other operation the domains are enough,
4580
+ * and storing less is better — but when mail has actually left, "who did it go to" is the first question anyone
4581
+ * asks afterwards, and a domain does not answer it.
4910
4582
  */
4911
- ignoredExtensions?: unknown;
4912
- }
4913
- interface ConfigV2 extends ConfigBody {
4914
- version: 2;
4915
- formerNames: FormerNames;
4583
+ recipients?: string[];
4584
+ approvalId?: string;
4585
+ reason?: string;
4586
+ /** The process that performed the work; daemon work keeps its caller separately in `origin`. */
4587
+ surface?: "cli" | "mcp" | "app" | "daemon";
4588
+ /** The requesting client surface; autonomous work is `daemon`/`daemon`. */
4589
+ origin?: "cli" | "mcp" | "app" | "daemon";
4916
4590
  /**
4917
- * The organisation profiles added here, by organisation word (design 2026-10-02 §D4). Absent reads as none, and
4918
- * stays absent until the first `org add`: a key the schema filled in would be written into every file by the next
4919
- * unrelated change.
4591
+ * The policy that decided how this was approved. Written by change approvals, where it is the whole question
4592
+ * afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
4593
+ * the configuration, and only this line tells them apart.
4920
4594
  */
4921
- organisations?: Record<string, OrganisationRecord> | undefined;
4922
- }
4923
- /** What a legacy drain records of one tracked record: still open, or what became of it. */
4924
- type LegacyDrainOutcome = "revoked" | "expired" | "used" | "failed" | "sending" | "unknown";
4925
- /**
4926
- * The approvals an earlier release prepared that were still waiting, or already being sent, when this configuration
4927
- * became version 3, and what has become of each (design 2026-10-05 §D1, "Old releases are locked out before the fence
4928
- * is relied on").
4929
- */
4930
- interface LegacyDrain {
4931
- /** When the conversion that opened the drain was written. */
4932
- since: string;
4595
+ policy?: SendPolicy;
4933
4596
  /**
4934
- * Every tracked version-1 send record by id: `open` until it has an outcome — or `sending` from the start, for one an
4935
- * earlier release was already sending when the conversion ran.
4597
+ * What daily retention deleted (`operation: 'approval.retained'`, design 2026-10-05 §D9): the approval's kind, its
4598
+ * final state, when it finished and any provider id — never its content or a recipient. Appended durably before the
4599
+ * record is unlinked, so the audit log keeps the terminal history the approval directory no longer does.
4936
4600
  */
4937
- tracked: Record<string, "open" | LegacyDrainOutcome>;
4601
+ retained?: RetainedApproval;
4938
4602
  }
4939
- /** What version 3 adds to a body of either naming. */
4940
- interface Version3Fields {
4941
- /** Each owner's send epoch by id; absent reads as 0. Raised by every write that turns its send policy to `never`. */
4942
- sendEpochs?: Record<string, number> | undefined;
4943
- /** Present while records from an earlier release are being retired. */
4944
- legacyDrain?: LegacyDrain | undefined;
4603
+ /** Large id lists are condensed so every audit line stays small enough to be appended atomically. */
4604
+ interface CondensedIds {
4605
+ count: number;
4606
+ sha256: string;
4607
+ first: string[];
4945
4608
  }
4946
- /** Version 3 over a version-1 body: one plain word per account. */
4947
- interface ConfigV3Naming1 extends ConfigBody, Version3Fields {
4948
- version: 3;
4949
- naming: 1;
4609
+ declare class AuditLog {
4610
+ #private;
4611
+ readonly directory: string;
4612
+ constructor(stateDir: string, now?: () => Date);
4613
+ append(record: Omit<AuditRecord, "at"> & {
4614
+ at?: string;
4615
+ }, options?: {
4616
+ durable?: boolean;
4617
+ }): Promise<AuditRecord>;
4618
+ /** The most recent records, newest last, optionally filtered by inbox and a lower time bound. */
4619
+ tail(options?: {
4620
+ limit?: number;
4621
+ inbox?: string;
4622
+ since?: string;
4623
+ }): Promise<AuditRecord[]>;
4950
4624
  }
4951
- /** Version 3 over a version-2 body: `organisation/platform` names, and the names they replaced. */
4952
- interface ConfigV3Naming2 extends Omit<ConfigV2, "version">, Version3Fields {
4953
- version: 3;
4954
- naming: 2;
4625
+ /** The step of a batch that failed. */
4626
+ type MaintenanceStep = "lock" | "state" | "enumerate" | "stat" | "read" | "write" | "append" | "claim-unlink" | "record-unlink" | "lock-lost" | "cursor";
4627
+ /** A failure a batch met: which step, for which record, and the error's code — never anything a record holds. */
4628
+ interface MaintenanceError {
4629
+ readonly step: MaintenanceStep;
4630
+ readonly approvalId?: string | undefined;
4631
+ readonly code: string;
4955
4632
  }
4956
4633
  /**
4957
- * Version 3: the send epoch and the legacy drain (design 2026-10-05 §D1), over either naming — converted from version 1
4958
- * or 2 without renaming anything, so its body keeps the rules of the version it came from.
4634
+ * What one call of `ensurePruned` did. `attempted` — it ran a batch; when it did not, `skipped` says why: not due
4635
+ * (a batch ran less than a day ago), busy (another process holds the maintenance lock), or no records (there is no
4636
+ * approvals directory at all). `processed` counts the slots whose record work ran to its end, `pruned` the records
4637
+ * deleted, `skippedBusy` the records whose lock was held elsewhere and were left. `complete` — the batch reached the end
4638
+ * of the directory inside both bounds; hitting 200 slots or the deadline, or losing the lock, leaves it false.
4959
4639
  */
4960
- type ConfigV3 = ConfigV3Naming1 | ConfigV3Naming2;
4961
- type Config$1 = ConfigV1 | ConfigV2 | ConfigV3;
4962
- /** A configuration whose accounts are named `organisation/platform`: version 2, or version 3 over it. */
4963
- type NamedConfig = ConfigV2 | ConfigV3Naming2;
4964
- /** A configuration whose accounts have one plain word each: version 1, or version 3 over it. */
4965
- type PlainConfig = ConfigV1 | ConfigV3Naming1;
4966
- /** The committed config returned by an update; process-only commit metadata is deliberately stored out of band. */
4967
- type ConfigUpdateResult = Config$1;
4968
- declare class ConfigStore {
4969
- #private;
4970
- readonly path: string;
4971
- /**
4972
- * @param options.handoffs the printing package's handoffs (`core.handoffs`, CUE-403), for the command a refusal
4973
- * names; left out, a refusal that has to name one is a programming error (`requiredHandoffs`).
4974
- */
4975
- constructor(configDir: string, options?: {
4976
- readonly handoffs?: CliHandoffs | undefined;
4977
- /** Test-only pauses inside the version-3 conversion (`config-hooks.ts`); unreachable from the package root. */
4978
- readonly [CONVERSION_HOOKS]?: ConversionHooks | undefined;
4979
- });
4980
- /** The current config; an empty one when the file does not exist yet. */
4981
- load(): Promise<Config$1>;
4982
- /**
4983
- * Read-modify-write under a lock, so a CLI command and a running MCP server never lose each other's changes. The
4984
- * mutator receives a fresh copy read inside the lock and returns the new config, which is validated before writing.
4985
- */
4986
- update(mutator: (config: Config$1) => Config$1 | Promise<Config$1>, options?: {
4987
- consent?: LooseningConsent;
4988
- signal?: AbortSignal | undefined;
4989
- }): Promise<ConfigUpdateResult>;
4990
- /**
4991
- * The one way a version-1 config becomes version 2.
4992
- *
4993
- * Under the credentials lock and then the config lock — the order everything takes them in — so it cannot land
4994
- * between a removal's read and its write, or in the middle of moving secrets between backends.
4995
- *
4996
- * `expected` is the fingerprint of the config the caller previewed. The file is read again inside the locks, and
4997
- * if it is not that config any more the whole thing is refused: somebody confirmed a mapping computed from
4998
- * something else. Nothing waits for a person while holding a lock; the preview happens before this is called.
4999
- *
5000
- * `build` produces version 2 from the locked snapshot. What it may change is checked rather than trusted: the same
5001
- * accounts, byte for byte, under new keys — a rename grants nothing, so a build that changed anything else is a
5002
- * bug and is refused before it is written.
5003
- *
5004
- * Idempotent, but only for this plan. A retry after a write that committed — even one whose lock release then
5005
- * failed — finds version 2, recognises its own mapping in it, and says so. Version 2 that does *not* carry this
5006
- * mapping is somebody else's migration; saying "already migrated" there would report a mapping nobody applied,
5007
- * and a caller updating registrations from it would point them at names that do not exist.
5008
- *
5009
- * `rows` is the plan, and the question is asked of the mapping rather than of the whole file: between a
5010
- * committed write and its retry, something else may have changed a policy or a timezone, and a retry refused
5011
- * over that would be idempotency in name only. The rows are checked here rather than by whoever built them,
5012
- * for the same reason `build` is: a caller that could answer its own question could answer it wrongly.
5013
- *
5014
- * The file it replaces is copied beside it first — see `backUpBeforeMigration` — and the copy's path returned.
5015
- */
5016
- migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: PlainConfig) => NamedConfig): Promise<{
5017
- status: "migrated" | "already-migrated";
5018
- config: NamedConfig;
5019
- backup?: string;
5020
- }>;
4640
+ interface MaintenanceStatus {
4641
+ readonly attempted: boolean;
4642
+ readonly processed: number;
4643
+ readonly pruned: number;
4644
+ readonly skippedBusy: number;
4645
+ readonly complete: boolean;
4646
+ readonly errors: readonly MaintenanceError[];
4647
+ readonly skipped?: "not-due" | "busy" | "no-records" | undefined;
4648
+ }
4649
+ /** What a batch reports of a record it is about to delete: never a body, an address, or anything a sender wrote. */
4650
+ interface RetainedApproval {
4651
+ readonly kind: ApprovalKind;
4652
+ readonly state: ApprovalState;
4653
+ /** When it finished: its validated terminal time, or a legacy used record's safe `updatedAt`. */
4654
+ readonly finishedAt: string;
4655
+ /** A used send's provider id, when it has a non-empty one. */
4656
+ readonly providerId?: string | undefined;
4657
+ /** A record an earlier release wrote. */
4658
+ readonly legacy?: true | undefined;
4659
+ }
4660
+ interface MaintenanceOptions {
5021
4661
  /**
5022
- * The one way a configuration becomes version 3 (design 2026-10-05 §D1, "Old releases are locked out before the
5023
- * fence is relied on").
5024
- *
5025
- * Version 3 carries the send epoch, which 0.13 does not know, and 0.13 refuses to read it — so from this write on, no
5026
- * 0.13 process can start a prepare, a claim, an approval or a policy change through this file. Nothing is renamed:
5027
- * a version-1 body stays version 1's (`naming: 1`) until the names migration, which needs its own approval.
5028
- *
5029
- * Under the config lock, the file is read again — never from the cache, never from the read before the lock — and an
5030
- * existing version 3 returned unchanged: a second converter that lost the race keeps the winner's epochs and drain.
5031
- * `scan` runs under the same lock, so a record written while this waited for it is seen, and an update cannot land
5032
- * between the scan and the write; the version-3 write carries `legacyDrain` only when the scan tracked something.
5033
- * With no config file at all there is nothing to convert, and nothing is written.
4662
+ * When no further step may start, on the store's clock, in milliseconds: five seconds from the call when left out.
4663
+ * A report passes the one deadline it shares with its own reads.
5034
4664
  */
5035
- convertToVersion3(scan: () => Promise<LegacyDrain["tracked"]>, options?: {
5036
- now?: (() => Date) | undefined;
5037
- }): Promise<{
5038
- status: "converted" | "already" | "empty";
5039
- config: Config$1;
5040
- }>;
4665
+ readonly deadline?: number | undefined;
4666
+ }
4667
+ //#endregion
4668
+ //#region src/approval-validate.d.ts
4669
+ /**
4670
+ * The integrity check of a version-2 approval record (design 2026-10-05 §D1, "Digest integrity" and "Version-2
4671
+ * timestamps fail closed"). Pure: it reads the record and the id its file is named by, and decides.
4672
+ *
4673
+ * A record either passes, or is `corrupt` for one fixed reason — never a sentence built from the record, so nothing a
4674
+ * file holds is ever echoed — and of one of two attribution classes. `verified`: its `bindingDigest` recomputes, so its
4675
+ * owner and identity can be trusted and it is corrupt for another reason (a state, a timestamp, its evidence).
4676
+ * `unverifiable`: its binding is missing, malformed or does not match, or it sits in another record's file, so nothing
4677
+ * it says about whom it belongs to can be trusted.
4678
+ *
4679
+ * Nothing here is wired into a read yet; the store's decoder does that.
4680
+ */
4681
+ /** Why a version-2 record is corrupt: a fixed category, never the record's own words. */
4682
+ type IntegrityReason = "file-name-mismatch" | "binding-missing" | "binding-malformed" | "binding-mismatch" | "wrong-shape" | "digest-version" | "content-digest-malformed" | "content-digest-mismatch" | "identity-contradiction" | "listing-mismatch" | "narrowing-invalid" | "state-impossible" | "lifetime-mismatch" | "timestamp-invalid" | "timestamp-missing" | "timestamp-misplaced" | "timestamp-misordered" | "sent-id-missing" | "evidence-missing" | "evidence-invalid" | "evidence-contradictory";
4683
+ type Attribution = "verified" | "unverifiable";
4684
+ type ErrorCode = "UNEXPECTED" | "APPROVAL_REQUIRED" | "APPROVAL_PENDING" | "APPROVAL_EXPIRED" | "APPROVAL_VOID" | "SEND_OUTCOME_UNKNOWN" | "SEND_REFUSED" | "POLICY_NEVER" | "ACT_NOT_ALLOWED" | "STANDING_REFUSED" | "STANDING_LIMIT" | "RATE_CAPPED" | "UNSENDABLE_HTML" | "LOOSENING_REFUSED" | "NEEDS_NEWER_RELEASE" | "USAGE" | "CURSOR_MISMATCH" | "BAD_DATA" | "REPLY_INVALID" | "NOT_FOUND" | "PROVIDER_UNAVAILABLE" | "SOURCE_UNAVAILABLE" | "SECRET_STORE_UNAVAILABLE" | "TRANSIENT" | "KEYCHAIN_APPROVAL_PENDING" | "LOCK_TIMEOUT" | "AUTH_REQUIRED" | "SCOPE_MISSING" | "WHATSAPP_VISIBILITY_SEAM_REQUIRED" | "CONFIG";
4685
+ interface CommsErrorOptions {
4686
+ hint?: string;
4687
+ details?: Record<string, unknown>;
4688
+ cause?: unknown;
5041
4689
  }
5042
- /** One account's rename, as the migration planned it. */
5043
- interface RenamedAccount {
5044
- readonly kind: "inbox" | "account";
5045
- readonly from: string;
5046
- readonly to: string;
5047
- readonly id: string;
4690
+ declare class CommsError extends Error {
4691
+ readonly code: ErrorCode;
4692
+ readonly hint: string | undefined;
4693
+ readonly details: Record<string, unknown> | undefined;
4694
+ constructor(code: ErrorCode, message: string, options?: CommsErrorOptions);
4695
+ get exitCode(): number;
4696
+ get retryable(): boolean;
5048
4697
  }
5049
- /** A value a loosened setting had or will have: a policy, a mode, a list, a pair of caps, a path, or nothing. */
5050
- type SettingValue = string | number | boolean | readonly string[] | {
5051
- readonly [key: string]: unknown;
5052
- } | null;
4698
+ //#endregion
4699
+ //#region src/approval-stored.d.ts
5053
4700
  /**
5054
- * One loosened setting, with the values the classifier compared.
4701
+ * Every approval file, read one way (design 2026-10-05 §D2, "Legacy version-1 records, one decoder everywhere" and
4702
+ * "Two classes of parseable corrupt record").
5055
4703
  *
5056
- * The values are the ones it judged, not the raw fields: an inbox that inherits a looser default reports the policy
5057
- * it inherits, and a new inbox reports the default it was measured against. A preview built from anything else could
5058
- * show a person one change while the classifier judged another.
4704
+ * A file is one of four things, and every read says which:
4705
+ *
4706
+ * - `v2` — a version-2 record that passed `validateV2`: the only form anything may act on;
4707
+ * - `legacy` — a structurally valid version-1 record, read by its own release's rules: reported, never claimed;
4708
+ * - `corrupt` — a parseable version-2 record that failed its integrity check, attributable to its owner or not;
4709
+ * - `unreadable` — a file that cannot safely be read at all: shown only as a stub naming its id (from the file name)
4710
+ * and a fixed reason, never anything the file holds.
4711
+ *
4712
+ * Nothing is silently omitted, and the decoder writes nothing.
5059
4713
  */
5060
- interface Loosening {
5061
- /** As `ConfigStore.update` names it in a refusal: `accounts.rgc/slack.mode`, `defaults.sendPolicy`. */
5062
- readonly path: string;
5063
- readonly before: SettingValue;
5064
- readonly after: SettingValue;
5065
- /**
5066
- * The inbox or account measured on the before side, by id. Absent for one new in this change and for a setting
5067
- * that belongs to the whole configuration. Carried so that an approval of this loosening cannot be spent on a
5068
- * different account that took the same name in the meantime.
5069
- */
5070
- readonly id?: string | undefined;
4714
+ /** Why a file cannot safely be read: a fixed category, never the file's own bytes. */
4715
+ type UnreadableReason = "invalid-json" | "truncated" | "missing-ownership" | "missing-kind" | "wrong-shape" | "file-name-mismatch";
4716
+ /** What may be said of a record that cannot be trusted: its id, that it is corrupt, and why — nothing else. */
4717
+ interface CorruptStub {
4718
+ readonly approvalId: string;
4719
+ readonly state: "corrupt";
4720
+ readonly reason: UnreadableReason | IntegrityReason;
4721
+ }
4722
+ /** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
4723
+ interface OrdinarySafeFields {
4724
+ readonly kind: ApprovalKind;
4725
+ readonly channel: string;
4726
+ readonly ownerScope: OwnerScope;
4727
+ readonly inboxId: string;
4728
+ readonly inboxSub?: string | undefined;
4729
+ readonly draftId: string;
4730
+ readonly draftMessageId: string;
4731
+ readonly expect: Expectation;
4732
+ }
4733
+ /** A disclosure record has no channel owner or sender-controlled operational fields to expose. */
4734
+ interface DisclosureSafeFields {
4735
+ readonly kind: "disclosure";
5071
4736
  }
4737
+ type SafeFields = OrdinarySafeFields | DisclosureSafeFields;
4738
+ type StoredApproval = {
4739
+ readonly form: "v2";
4740
+ readonly record: ApprovalRecord;
4741
+ } | {
4742
+ readonly form: "legacy";
4743
+ readonly view: LegacyView;
4744
+ readonly record: LegacyApprovalRecord;
4745
+ } | {
4746
+ readonly form: "corrupt";
4747
+ readonly approvalId: string;
4748
+ readonly reason: IntegrityReason;
4749
+ readonly attribution: Attribution;
4750
+ /** Only when attribution is verified. */
4751
+ readonly safe: SafeFields | null;
4752
+ } | {
4753
+ readonly form: "unreadable";
4754
+ readonly stub: CorruptStub;
4755
+ };
4756
+ //#endregion
4757
+ //#region src/approval-outcome.d.ts
5072
4758
  /**
5073
- * One setting a change writes, from the value in the file before to the value after — whichever way it moves.
4759
+ * One locked classification of every record into an outcome (design 2026-10-05 §D2).
5074
4760
  *
5075
- * Unlike a `Loosening`, these are the raw values, not the effective ones: `null` for a setting the file does not
5076
- * hold, so a mailbox set to `chat` itself is told apart from one inheriting `chat`, which only the first survives a
5077
- * later change to the default. `id` is the inbox or account measured on the before side, as for a loosening.
4761
+ * `approvalOutcome` says what a record is now — its state, whether it can be claimed, and what an action on it would be
4762
+ * told — from the record as it was read under its own lock and the configuration read in that same lock (`LiveGate`).
4763
+ * Nothing here reads a file or a clock of its own, and nothing here writes: the store persists what the outcome
4764
+ * derives, and only from inside the lock it was classified under.
5078
4765
  */
5079
- interface SettingChange {
5080
- readonly path: string;
5081
- readonly before: SettingValue;
5082
- readonly after: SettingValue;
5083
- readonly id?: string | undefined;
5084
- }
4766
+ /** What is being done with the record: an approval, a claim, a wait, a revoke, or a look. */
4767
+ type OutcomeAction = "approve" | "claim" | "wait" | "revoke" | "inspect";
4768
+ /** A record's state as anyone may be told it: a download's answered question says `answered`. */
4769
+ type PublicApprovalState = ApprovalState | "corrupt" | "answered";
5085
4770
  /**
5086
- * Proof that exactly these paths may loosen: produced by a CLI after a person at a terminal typed a challenge, or by
5087
- * `claimChange` from a change approval a person gave.
4771
+ * Where an approval stands, as every surface may show it (design 2026-10-05 §D8): its id, kind, channel, state and
4772
+ * whether it can be claimed, with the timestamps its state carries. Never a challenge hash, and nothing a sender wrote.
5088
4773
  */
5089
- interface LooseningConsent {
5090
- kind: "loosening-consent";
5091
- paths: readonly string[];
4774
+ interface ApprovalObject {
4775
+ readonly id: string;
4776
+ readonly kind: ApprovalKind | null;
4777
+ readonly channel: string | null;
4778
+ readonly state: PublicApprovalState;
4779
+ readonly claimable: boolean;
4780
+ readonly route?: ApprovalRoute | undefined;
4781
+ readonly ownerRemoved?: true | undefined;
4782
+ /** A record from an earlier release, read by its own rules: shown, never claimed. */
4783
+ readonly legacy?: true | undefined;
4784
+ readonly reason?: string | undefined;
4785
+ readonly createdAt?: string | undefined;
4786
+ readonly expiresAt?: string | undefined;
4787
+ readonly approvedAt?: string | undefined;
4788
+ readonly usableUntil?: string | undefined;
4789
+ readonly sendingAt?: string | undefined;
4790
+ readonly sendingHeartbeatAt?: string | undefined;
4791
+ /** When a `sending` record reads `unknown` (`unknownAtOf`): derived, never stored. Also on an `unknown` one. */
4792
+ readonly unknownAt?: string | undefined;
4793
+ readonly usedAt?: string | undefined;
4794
+ readonly sentAt?: string | undefined;
4795
+ readonly sentMessageId?: string | undefined;
4796
+ readonly failedAt?: string | undefined;
4797
+ readonly revokedAt?: string | undefined;
4798
+ readonly expiredAt?: string | undefined;
4799
+ }
4800
+ /** What an action on a record is, now. */
4801
+ interface ApprovalOutcome {
4802
+ readonly state: PublicApprovalState;
4803
+ readonly claimable: boolean;
4804
+ readonly ownerRemoved?: true | undefined;
4805
+ readonly reason?: string | undefined;
5092
4806
  /**
5093
- * What each path was approved to move between, when the consent came from a change approval.
5094
- *
5095
- * A terminal challenge is answered moments before the write it permits, so the paths are enough. An approval is
5096
- * prepared first and applied later — after a sign-in, possibly minutes later — and in between the configuration
5097
- * can move underneath it: the same path, loosened from a different value, or on a different account that took
5098
- * the name. With these, `ConfigStore.update` refuses any loosening that is not exactly one of them.
4807
+ * A version-2 record as this classification reads it: with a revocation it derived applied (an owner removed, a
4808
+ * stale send epoch, or — for a claim or an approval — a live `never`). Null for every other form.
5099
4809
  */
5100
- changes?: readonly Loosening[] | undefined;
4810
+ readonly record: ApprovalRecord | null;
4811
+ /** Whether `record` carries a revocation the classification derived, for a locked action to persist. */
4812
+ readonly revokes: boolean;
4813
+ /** Where it stands, as D8 shows it (`publicApproval`). */
4814
+ readonly approval: ApprovalObject;
4815
+ /** What the action is refused with, when it is refused by the record's state alone. */
4816
+ readonly error?: CommsError | undefined;
5101
4817
  }
4818
+ /**
4819
+ * What became of a used send, in the words of the surface that says it, from the time the provider accepted it (D2's
4820
+ * used-send row): Gmail and Slack, where acceptance is sending, say it was "sent at …"; Resend's own surfaces that it
4821
+ * was "accepted by Resend at …"; core's generic surfaces "accepted by <provider> at …".
4822
+ */
4823
+ type UsedSaid = (usedAt: string) => string;
5102
4824
  //#endregion
5103
- //#region src/approval-binding.d.ts
4825
+ //#region src/handoff-text.d.ts
5104
4826
  /**
5105
- * What a version-2 approval binds, defined once (design 2026-10-05 §D1, "Digest integrity").
5106
- *
5107
- * A record stores two digests. `contentDigest` is the outward content or change, computed as before: the message, the
5108
- * change, the download. `bindingDigest` is this module's: the content digest together with the record's route, its
5109
- * lifetimes and its own identity — who it belongs to, which draft, which channel, which send epoch. Every one of those
5110
- * is a field a claim or a report acts on, so a record whose binding no longer recomputes from its own fields has been
5111
- * altered since it was made, and nothing it says can be trusted.
4827
+ * A handoff as words a person reads, and what a core function that prints one is given (CUE-403; CONTRIBUTING.md,
4828
+ * "Telling a person what to run"). `handoffs.ts` makes located handoffs and re-exports all of this.
5112
4829
  *
5113
- * There is no second copy of anything: the binding is recomputed from the stored fields alone, never stored apart
5114
- * from them as a key.
4830
+ * Kept apart from the locator on purpose: the configuration store, the secret stores and the attachment jail print
4831
+ * handoffs too, and the locator's own imports reach the configuration store, so importing it from them would be a
4832
+ * cycle. Nothing here locates anything or imports a module that does: a located command is made by `handoffs.ts` and
4833
+ * handed in, and what is here only renders one. With none handed in, there is nothing to render: a package that did not
4834
+ * give core its caller and is asked for a command has a programming error, not a bare name to fall back on.
5115
4835
  */
4836
+ /** A command for a person to run, located, or the sentence saying why there is none here. */
4837
+ type Handoff = PrintedCommand | CliCommandNotLocated;
4838
+ interface HandoffUse {
4839
+ /** The suite folders the command reads or writes; the four every command opens when left out. */
4840
+ readonly uses?: readonly PathName[] | undefined;
4841
+ /** It also reads or writes downloads, so the downloads folder is pinned too. */
4842
+ readonly downloads?: boolean | undefined;
4843
+ }
5116
4844
  /**
5117
- * How an approval is decided, fixed when it is made: `chat` — a yes in the conversation is enough — or `confirm` — a
5118
- * person outside the chat decides. A send's route is the stricter of its live send policy and any escalation; a
5119
- * change's is the live change policy. It never changes when the configuration does.
4845
+ * The commands a package tells a person to run, made once per printing process from its caller (`handoffs.ts`,
4846
+ * `cliHandoffs`): `openCore({ caller })` puts them on `core.handoffs`. Each method takes the CLI's own words, never the
4847
+ * program, and returns a `Handoff`.
5120
4848
  */
5121
- type ApprovalRoute = "chat" | "confirm";
5122
- /** The four activation documents that may receive standing disclosure authority. */
5123
- type DisclosureActivationKind = "rule" | "judge-budget" | "judge-kind" | "enable-all";
5124
- /** An immutable version named by a standing disclosure authorisation. */
5125
- type DisclosureVersionKind = "rule" | "target" | "subscriber" | "judge" | "judge-budget" | "judge-kind";
5126
- interface DisclosureVersion {
5127
- readonly kind: DisclosureVersionKind;
5128
- readonly id: string;
5129
- readonly version: number;
4849
+ interface CliHandoffs {
4850
+ /** The printing package: one of its own modules and its name. */
4851
+ readonly caller: CliCommandCaller;
4852
+ /** The shell the commands are quoted for. */
4853
+ readonly platform: NodeJS.Platform;
4854
+ /** The printing package's own CLI, with these words after the program. */
4855
+ own(words: readonly string[], use?: HandoffUse): Handoff;
4856
+ /** The core CLI: the printing package's own when that is core, the core it has installed otherwise. */
4857
+ core(words: readonly string[], use?: HandoffUse): Handoff;
4858
+ /** The CLI of `channel` — `core`, `gmail`, `slack` …: own, core, or another product found among its registrations. */
4859
+ of(channel: string, words: readonly string[], use?: HandoffUse): Handoff;
4860
+ /** The same commands, quoted for another shell. */
4861
+ on(platform: NodeJS.Platform): CliHandoffs;
4862
+ /**
4863
+ * The same, able to find another product: with the servers registered with this machine's MCP clients read now, from
4864
+ * the environment these were made with. Read once per call; nothing a registration names is run.
4865
+ */
4866
+ registered(): Promise<CliHandoffs>;
4867
+ /** The same, finding another product among these registrations: a scan the caller has already read. */
4868
+ withRegistrations(registrations: readonly RegisteredServer[]): CliHandoffs;
5130
4869
  }
5131
4870
  /**
5132
- * The content-free, canonical authority for a future disclosure (local-event design D2).
5133
- *
5134
- * The event daemon derives this from one canonical activation document. Core does not construct
5135
- * that document; it verifies the closed shape that makes an independently supplied version list
5136
- * impossible to smuggle into a disclosure record.
4871
+ * Why a saved name has {@link DOWNLOAD_SUFFIX} after it: a name tools read or run on their own (`auto-read`), an
4872
+ * extension that is not one of the inert ones (`type`), or no extension at all (`no-extension`).
5137
4873
  */
5138
- interface DisclosureBinding {
5139
- readonly digest: string;
5140
- readonly activationIntentId: string;
5141
- readonly activationKind: DisclosureActivationKind;
5142
- readonly versions: readonly DisclosureVersion[];
4874
+ type RenameReason = "auto-read" | "type" | "no-extension";
4875
+ //#endregion
4876
+ //#region src/ledger.d.ts
4877
+ interface Caps {
4878
+ perHour: number;
4879
+ perDay: number;
4880
+ }
4881
+ interface CapStatus {
4882
+ hour: number;
4883
+ day: number;
4884
+ /** When the next slot frees up, if a cap is reached. */
4885
+ resetAt?: string;
4886
+ }
4887
+ declare class SendLedger {
4888
+ #private;
4889
+ readonly directory: string;
4890
+ constructor(stateDir: string, now?: () => Date);
4891
+ /** Counts sends (including in-flight reservations) in the last hour and day. */
4892
+ status(inboxId: string, caps: Caps): Promise<CapStatus>;
4893
+ /** Reserves a slot or refuses with APPROVAL_REQUIRED and the time the cap resets. Atomic across processes. */
4894
+ reserve(inboxId: string, approvalId: string, caps: Caps): Promise<CapStatus>;
4895
+ /** Releases a reservation whose send did not happen. */
4896
+ release(inboxId: string, approvalId: string): Promise<void>;
5143
4897
  }
5144
- /**
5145
- * What a record's `inboxId` names. `owner` — a mailbox or account that existed when it was made (every send and
5146
- * download, and a change to one). `prospective` — a change that creates its owner, which has no id yet. `global` — a
5147
- * change to no owner at all, stored with `inboxId: ''`.
5148
- */
5149
- type OwnerScope = "owner" | "prospective" | "global";
5150
4898
  //#endregion
5151
- //#region src/approval-io.d.ts
4899
+ //#region src/plans.d.ts
5152
4900
  /**
5153
- * The file operations approval maintenance and the unsent report are made of (design 2026-10-05 §D9), one method
5154
- * each, so that a test can count every one of them — every directory read, metadata stat, record open, lock try,
5155
- * renewal, ownership check and unlink — and none is hidden behind a helper. The store passes the real ones unless it
5156
- * was opened with others.
5157
- *
5158
- * Only these two passes go through it: every other read and write of the store is as it was.
4901
+ * Bulk mailbox changes (more than 20 messages, anything driven by a query, and every trash) are two-step: a dry run
4902
+ * returns a plan token bound to the exact set of ids and the exact change; executing requires that token, once, within
4903
+ * ten minutes. The ids are resolved at dry-run time, so a query cannot silently grow between preview and execution.
5159
4904
  */
5160
- interface ApprovalIo {
5161
- /** One enumeration of a directory: its entries' names. */
5162
- readdir(directory: string): Promise<string[]>;
5163
- /** One metadata stat: the modification time, in milliseconds. */
5164
- stat(path: string): Promise<{
5165
- readonly mtimeMs: number;
5166
- }>;
5167
- /** One open of a file's content, as text. */
5168
- readText(path: string): Promise<string>;
5169
- /** Replaces a file atomically — a temporary file, flushed, renamed — and, when `durable`, syncs its directory too. */
5170
- writeAtomic(path: string, text: string, options?: {
5171
- durable?: boolean;
5172
- }): Promise<void>;
5173
- /** One unlink of an approval artifact: a record or its claim marker. A missing one rejects with `ENOENT`. */
5174
- unlink(path: string): Promise<void>;
5175
- /** The lock files' own operations (`tryFileLock`). */
5176
- readonly lock: LockIo;
5177
- }
5178
- /** A version-1 record, as 0.13.0 wrote it: a send has no `kind`, and one `digest` stands for the content. */
5179
- interface LegacyApprovalRecord {
5180
- approvalId: string;
5181
- /** Absent on a send. */
5182
- kind?: ApprovalKind | undefined;
5183
- digestVersion: 1;
4905
+ interface PlanRecord {
4906
+ token: string;
5184
4907
  inboxId: string;
5185
- inboxSub?: string | undefined;
5186
- draftId: string;
5187
- draftMessageId: string;
5188
- digest: string;
5189
- approvedDigest?: string | undefined;
5190
- approvedVia?: ApprovalChannel | undefined;
5191
- policy: SendPolicy;
5192
- requiredPolicy: SendPolicy;
5193
- riskFlags: string[];
5194
- expect: Expectation;
5195
- challengeHash?: string | undefined;
5196
- challengeAttempts: number;
5197
- state: ApprovalState;
4908
+ operation: string;
4909
+ /** Digest of the operation parameters (labels added/removed, archive, trash…). */
4910
+ paramsDigest: string;
4911
+ /** Digest of the sorted id list. */
4912
+ idsDigest: string;
4913
+ count: number;
5198
4914
  createdAt: string;
5199
4915
  expiresAt: string;
5200
- updatedAt: string;
5201
- sentMessageId?: string | undefined;
5202
- reason?: string | undefined;
5203
- change?: ChangeBinding | undefined;
5204
- download?: DownloadBinding | undefined;
5205
4916
  }
5206
- /**
5207
- * A version-1 record as every surface shows it: `{ approvalId, kind, state, legacy: true, createdAt, expiresAt }` and
5208
- * the summary a version-2 record shows — its owner, what it was for, and which channel made it when that can be told.
5209
- * Never claimable, and never given a version-2 timestamp it does not hold.
5210
- */
5211
- interface LegacyView {
5212
- readonly approvalId: string;
5213
- readonly kind: ApprovalKind;
5214
- /** Derived as 0.13.0 derived it (`deriveLegacyV1State`). */
5215
- readonly state: ApprovalState;
5216
- readonly legacy: true;
5217
- readonly createdAt: string;
5218
- readonly expiresAt: string;
5219
- /** The stored owner, trusted as stored: a version-1 record has no binding to check it against. */
5220
- readonly inboxId: string;
5221
- readonly ownerScope: "owner" | "global";
4917
+ declare class PlanStore {
4918
+ #private;
4919
+ readonly directory: string;
4920
+ constructor(stateDir: string, now?: () => Date);
4921
+ create(input: {
4922
+ inboxId: string;
4923
+ operation: string;
4924
+ params: unknown;
4925
+ ids: readonly string[];
4926
+ }): Promise<PlanRecord>;
5222
4927
  /**
5223
- * The channel that made it, when only one could have: an `ibx_` owner is a Gmail mailbox, an `acc_` owner the
5224
- * stored platform of an account that still exists. Null when nothing says — it then takes part in no channel's
5225
- * grouping and gets only generic wording.
4928
+ * Consumes a plan exactly once. It must be for the same inbox, operation, parameters and ids; the file is deleted
4929
+ * inside the lock, so a second consume fails.
5226
4930
  */
5227
- readonly channel: string | null;
5228
- readonly expect: Expectation;
5229
- readonly reason?: string | undefined;
5230
- readonly sentMessageId?: string | undefined;
4931
+ consume(token: string, expected: {
4932
+ inboxId: string;
4933
+ operation: string;
4934
+ params: unknown;
4935
+ ids: readonly string[];
4936
+ }): Promise<PlanRecord>;
5231
4937
  }
5232
4938
  //#endregion
5233
- //#region src/audit.d.ts
4939
+ //#region src/secrets.d.ts
5234
4940
  /**
5235
- * Every mailbox write leaves one line here: what was done, to which inbox, with which ids and outcome. Never message
5236
- * bodies, never secrets — recipient *domains* only, so the log can be shared when asking for help.
4941
+ * Where refresh tokens, client secrets and approval keys live. Two backends, chosen per inbox or client and recorded
4942
+ * in config; the choice is never switched at runtime and there is no silent fallback from one to the other.
4943
+ *
4944
+ * - `keychain`: the OS store (macOS Keychain, Windows Credential Manager, Linux Secret Service — pinned, never the
4945
+ * kernel keyring, which forgets everything on reboot) through the optional `@napi-rs/keyring` package.
4946
+ * - `file`: one owner-only JSON file per secret in an owner-only directory. Chosen explicitly, never by default.
5237
4947
  */
5238
- interface AuditRecord {
5239
- at: string;
5240
- /** Immutable inbox id; `alias` is the label at the time. */
5241
- inboxId: string;
5242
- alias?: string;
5243
- operation: string;
5244
- /** `started` marks the first half of a change recorded before it is made; a later record for the same ids ends it. */
5245
- outcome: "ok" | "refused" | "failed" | "started";
5246
- ids?: Record<string, string | string[] | CondensedIds>;
5247
- recipientDomains?: string[];
4948
+ type SecretStoreKind = "keychain" | "file";
4949
+ interface SecretStore {
4950
+ readonly kind: SecretStoreKind;
4951
+ get(ref: string): Promise<string | null>;
4952
+ set(ref: string, value: string): Promise<void>;
4953
+ delete(ref: string): Promise<boolean>;
4954
+ /** Forgets any cached value, so the next read goes to the backend (e.g. after another process re-authorised). */
4955
+ invalidate(ref: string): void;
5248
4956
  /**
5249
- * Full canonical recipient addresses. Only a send writes these: for every other operation the domains are enough,
5250
- * and storing less is better — but when mail has actually left, "who did it go to" is the first question anyone
5251
- * asks afterwards, and a domain does not answer it.
4957
+ * Resolves once no earlier call is still occupying the backend. Absent on a store with nothing to wait for.
4958
+ *
4959
+ * For a caller that must not give up on a write: the keychain fails every call fast while a timed-out native call
4960
+ * is still held by an OS dialog, so retrying on a timer spends every attempt against the same stuck call. A
4961
+ * refresh that has already spent Slack's single-use token is that caller — it has one value it cannot get again,
4962
+ * and waiting for the dialog to be answered is the only retry that can succeed.
5252
4963
  */
5253
- recipients?: string[];
5254
- approvalId?: string;
5255
- reason?: string;
5256
- /** The process that performed the work; daemon work keeps its caller separately in `origin`. */
5257
- surface?: "cli" | "mcp" | "app" | "daemon";
5258
- /** The requesting client surface; autonomous work is `daemon`/`daemon`. */
5259
- origin?: "cli" | "mcp" | "app" | "daemon";
4964
+ settled?(): Promise<void>;
4965
+ }
4966
+ //#endregion
4967
+ //#region src/standing-ledger.d.ts
4968
+ /**
4969
+ * The acknowledgement ledger (design 2026-10-19-standing-approvals, S6, S9): one JSONL file per account,
4970
+ * `<state>/standing/<accountId>.jsonl`, shared by every process and locked at `<path>.lock`.
4971
+ *
4972
+ * Every use of a standing approval of the account is a `use` line, written under the lock before its one-shot child
4973
+ * is minted; what became of the act is an `outcome` line after it. The counts — per thread across every standing
4974
+ * approval of the account, per hour and per day of each one — are read from the `use` lines alone, so nothing an
4975
+ * outcome says frees one, and nothing ever removes a line. A count is spent when it is written, never refunded.
4976
+ */
4977
+ /** One use: which standing approval, which child, which content, where, and what the acknowledged message said. */
4978
+ interface AckUse {
4979
+ kind: "use";
4980
+ at: string;
4981
+ standingId: string;
4982
+ childId: string;
4983
+ act: StandingAct;
4984
+ contentDigest: string;
4985
+ channel: string;
4986
+ ts: string;
4987
+ /** The thread the message is in — its parent's `ts` — or the message's own `ts` outside one. */
4988
+ thread: string;
4989
+ /** Who wrote the acknowledged message, by their Slack id: what `perSender` counts by, across the account. */
4990
+ sender: string;
4991
+ /** SHA-256 of the acknowledged message's text at the act, so an edit since shows. */
4992
+ targetDigest: string;
4993
+ edited: boolean;
4994
+ }
4995
+ /** What became of one use's act. It records; it frees nothing. */
4996
+ interface AckOutcome {
4997
+ kind: "outcome";
4998
+ at: string;
5260
4999
  /**
5261
- * The policy that decided how this was approved. Written by change approvals, where it is the whole question
5262
- * afterwards: a loosening a person typed a code for and one an agent claimed after a yes in chat look the same in
5263
- * the configuration, and only this line tells them apart.
5000
+ * When the act itself started, after the pause between the decision and the act (S9, the lead's call after review):
5001
+ * a use's own `at` is when it was decided.
5264
5002
  */
5265
- policy?: SendPolicy;
5003
+ actAt?: string;
5004
+ childId: string;
5005
+ outcome: "sent" | "failed" | "unknown";
5006
+ link?: string;
5007
+ notice: string;
5008
+ }
5009
+ declare class AckLedger {
5010
+ #private;
5011
+ readonly directory: string;
5012
+ constructor(stateDir: string, now?: () => Date);
5013
+ /** The clock this ledger was opened with: what a use is timed by, and its counts' windows measured from. */
5014
+ now(): Date;
5015
+ /** Where an account's ledger is kept: `<state>/standing/<accountId>.jsonl`, for a person told to move it aside. */
5016
+ pathOf(accountId: string): string;
5017
+ /**
5018
+ * Every account's ledger on this machine that holds a line this release cannot read, with the number of each such
5019
+ * line, for the doctor (design 2026-10-19-standing-approvals, the lead's call after review): while one does, no
5020
+ * standing approval of that account can be used — what it counts cannot be known — and nothing else says so. Reads
5021
+ * without the lock: a look, and every line it reads was written whole.
5022
+ */
5023
+ unreadable(): Promise<{
5024
+ accountId: string;
5025
+ path: string;
5026
+ lines: number[];
5027
+ }[]>;
5266
5028
  /**
5267
- * What daily retention deleted (`operation: 'approval.retained'`, design 2026-10-05 §D9): the approval's kind, its
5268
- * final state, when it finished and any provider id — never its content or a recipient. Appended durably before the
5269
- * record is unlinked, so the audit log keeps the terminal history the approval directory no longer does.
5029
+ * Runs `fn` under the account's ledger lock, with every entry as it reads there and `append`, which writes a `use`
5030
+ * line under the lock already held. `fn` must never take this lock again.
5031
+ *
5032
+ * A line this release cannot read refuses the whole call, BAD_DATA, before `fn` runs: a count read without it could
5033
+ * be lower than the truth, and a count is never guessed downwards.
5270
5034
  */
5271
- retained?: RetainedApproval;
5035
+ withLock<T>(accountId: string, fn: (entries: (AckUse | AckOutcome)[], append: (entry: AckUse) => Promise<void>) => Promise<T>): Promise<T>;
5036
+ /** Records what became of a use's act, taking the lock itself. It never frees a count. */
5037
+ recordOutcome(accountId: string, entry: AckOutcome): Promise<void>;
5038
+ /** Every entry this release can read, oldest first, for a report: a line it cannot read is left out of it. */
5039
+ entries(accountId: string): Promise<(AckUse | AckOutcome)[]>;
5272
5040
  }
5273
- /** Large id lists are condensed so every audit line stays small enough to be appended atomically. */
5274
- interface CondensedIds {
5275
- count: number;
5276
- sha256: string;
5277
- first: string[];
5041
+ //#endregion
5042
+ //#region src/state.d.ts
5043
+ /**
5044
+ * Per-inbox runtime facts that any process may update (last successful refresh, last use, health). Kept out of
5045
+ * config.json so a running server never rewrites user intent — a stale in-memory copy of config written back by a
5046
+ * server could otherwise silently undo a policy the user just tightened.
5047
+ */
5048
+ interface InboxRuntimeState {
5049
+ lastRefreshOkAt?: string | undefined;
5050
+ lastUsedAt?: string | undefined;
5051
+ /** Set to `undefined` to clear it: the merged value is dropped when the file is written. */
5052
+ lastError?: {
5053
+ code: string;
5054
+ message: string;
5055
+ at: string;
5056
+ } | undefined;
5057
+ grantedScopes?: string[] | undefined;
5058
+ refreshTokenExpiresAt?: string | undefined;
5278
5059
  }
5279
- declare class AuditLog {
5060
+ declare class InboxStateStore {
5280
5061
  #private;
5281
5062
  readonly directory: string;
5282
- constructor(stateDir: string, now?: () => Date);
5283
- append(record: Omit<AuditRecord, "at"> & {
5284
- at?: string;
5285
- }, options?: {
5286
- durable?: boolean;
5287
- }): Promise<AuditRecord>;
5288
- /** The most recent records, newest last, optionally filtered by inbox and a lower time bound. */
5289
- tail(options?: {
5290
- limit?: number;
5291
- inbox?: string;
5292
- since?: string;
5293
- }): Promise<AuditRecord[]>;
5063
+ constructor(stateDir: string);
5064
+ get(inboxId: string): Promise<InboxRuntimeState>;
5065
+ /** Merges `patch` into the stored state under a per-inbox lock. */
5066
+ update(inboxId: string, patch: Partial<InboxRuntimeState>): Promise<InboxRuntimeState>;
5067
+ }
5068
+ /**
5069
+ * A platform identifier that is not an email address: a Slack user or conversation id.
5070
+ *
5071
+ * It carries its scope because, unlike an address, it is not globally unique — `U024BE7LH` names a different person
5072
+ * in every workspace that happens to mint that id. The address store is deliberately cross-inbox, on the reasoning
5073
+ * that a message read in one mailbox can ask for a send from another; that reasoning does not transfer here, and a
5074
+ * store that matched ids across workspaces would flag an unrelated person every time two workspaces collided.
5075
+ *
5076
+ * The id, never the display name. `@sam` is set by the account that bears it and can be changed to `@finance-bot`
5077
+ * between the message being read and the send being checked; the id cannot.
5078
+ */
5079
+ interface TaintHandle {
5080
+ /** `slack`. Lower-cased on the way in. */
5081
+ platform: string;
5082
+ /** The workspace or team the id belongs to — a Slack team id. */
5083
+ scope: string;
5084
+ /** `U024BE7LH`, `C0123`, `D0456`. Passed in the platform's own canonical form; core does not know its shape. */
5085
+ id: string;
5086
+ }
5087
+ type TaintSource = "header" | "body";
5088
+ /** Why the content reached this process. This is separate from `TaintSource`, which remains the cap priority. */
5089
+ type TaintOrigin = "read" | "event";
5090
+ interface TaintObservation {
5091
+ address: string;
5092
+ source: TaintSource;
5093
+ /** Old readers did not record this, so an omitted origin remains an ordinary read. */
5094
+ origin?: TaintOrigin | undefined;
5095
+ inboxId: string;
5096
+ messageId?: string | undefined;
5294
5097
  }
5295
- /** The step of a batch that failed. */
5296
- type MaintenanceStep = "lock" | "state" | "enumerate" | "stat" | "read" | "write" | "append" | "claim-unlink" | "record-unlink" | "lock-lost" | "cursor";
5297
- /** A failure a batch met: which step, for which record, and the error's code — never anything a record holds. */
5298
- interface MaintenanceError {
5299
- readonly step: MaintenanceStep;
5300
- readonly approvalId?: string | undefined;
5301
- readonly code: string;
5098
+ /** The same observation for a handle. `inboxId` is the account that did the reading, as it is for an address. */
5099
+ interface TaintHandleObservation {
5100
+ handle: TaintHandle;
5101
+ source: TaintSource;
5102
+ /** Old readers did not record this, so an omitted origin remains an ordinary read. */
5103
+ origin?: TaintOrigin | undefined;
5104
+ inboxId: string;
5105
+ messageId?: string | undefined;
5106
+ }
5107
+ /** What is never recorded: the user's own addresses and the domains they call internal. */
5108
+ interface TaintExclusions {
5109
+ ownAddresses: readonly string[];
5110
+ internalDomains: readonly string[];
5111
+ /**
5112
+ * Handles that are never recorded: the account's own user id, and whoever the caller treats as internal — the
5113
+ * counterpart of `internalDomains`, decided per platform because "internal" means a domain for mail and
5114
+ * workspace membership for Slack, and core should not be the thing that knows the difference.
5115
+ */
5116
+ ownHandles?: readonly TaintHandle[] | undefined;
5302
5117
  }
5303
5118
  /**
5304
- * What one call of `ensurePruned` did. `attempted` — it ran a batch; when it did not, `skipped` says why: not due
5305
- * (a batch ran less than a day ago), busy (another process holds the maintenance lock), or no records (there is no
5306
- * approvals directory at all). `processed` counts the slots whose record work ran to its end, `pruned` the records
5307
- * deleted, `skippedBusy` the records whose lock was held elsewhere and were left. `complete` — the batch reached the end
5308
- * of the directory inside both bounds; hitting 200 slots or the deadline, or losing the lock, leaves it false.
5119
+ * One stored aggregate, exactly as the store holds it (design 2026-10-05 §D4): the newest time anything matched it, the
5120
+ * strongest source ever seen — `header` is kept once seen — and every mailbox that recorded it. Three separate facts,
5121
+ * never one sighting: `touch` replaces the time, keeps a header and unions the mailboxes independently.
5309
5122
  */
5310
- interface MaintenanceStatus {
5311
- readonly attempted: boolean;
5312
- readonly processed: number;
5313
- readonly pruned: number;
5314
- readonly skippedBusy: number;
5315
- readonly complete: boolean;
5316
- readonly errors: readonly MaintenanceError[];
5317
- readonly skipped?: "not-due" | "busy" | "no-records" | undefined;
5123
+ interface TaintAggregate {
5124
+ readonly at: string;
5125
+ readonly source: TaintSource;
5126
+ readonly inboxIds: readonly string[];
5127
+ /** Sorted, duplicate-free provenance. A base entry with no sidecar record is a legacy read. */
5128
+ readonly origins: readonly TaintOrigin[];
5318
5129
  }
5319
- /** What a batch reports of a record it is about to delete: never a body, an address, or anything a sender wrote. */
5320
- interface RetainedApproval {
5321
- readonly kind: ApprovalKind;
5322
- readonly state: ApprovalState;
5323
- /** When it finished: its validated terminal time, or a legacy used record's safe `updatedAt`. */
5324
- readonly finishedAt: string;
5325
- /** A used send's provider id, when it has a non-empty one. */
5326
- readonly providerId?: string | undefined;
5327
- /** A record an earlier release wrote. */
5328
- readonly legacy?: true | undefined;
5130
+ interface TaintCheck {
5131
+ address: boolean;
5132
+ domain: boolean;
5133
+ /** The exact address's aggregate, when it is in the window. */
5134
+ addressSeen?: TaintAggregate | undefined;
5135
+ /** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
5136
+ domainSeen?: TaintAggregate | undefined;
5329
5137
  }
5330
- interface MaintenanceOptions {
5138
+ /** Testable interruption point for the crash-safe, two-lock sidecar protocol. */
5139
+ interface TaintStoreHooks {
5140
+ afterOriginsCommit?: (() => Promise<void>) | undefined;
5141
+ onLockAcquired?: ((lock: "origins" | "taint" | "handles") => void) | undefined;
5142
+ }
5143
+ declare class TaintStore {
5144
+ #private;
5145
+ readonly directory: string;
5146
+ constructor(stateDir: string, now?: () => Date, hooks?: TaintStoreHooks);
5331
5147
  /**
5332
- * When no further step may start, on the store's clock, in milliseconds: five seconds from the call when left out.
5333
- * A report passes the one deadline it shares with its own reads.
5148
+ * Records observations. Throws if it cannot — callers must fail the read rather than return content whose taint
5149
+ * was not recorded.
5334
5150
  */
5335
- readonly deadline?: number | undefined;
5151
+ record(observations: readonly TaintObservation[], exclusions: TaintExclusions, handleObservations?: readonly TaintHandleObservation[]): Promise<void>;
5152
+ /**
5153
+ * Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox — and the
5154
+ * aggregate each match is made of, for a caller to explain it. Nothing is written.
5155
+ */
5156
+ check(address: string): Promise<TaintCheck>;
5157
+ /**
5158
+ * Whether a handle was seen in message content in the window.
5159
+ *
5160
+ * Only within its own workspace — see `TaintHandle`. There is no second answer to give, the way an address also
5161
+ * carries a domain: two ids sharing a workspace says nothing about either of them.
5162
+ */
5163
+ checkHandle(handle: TaintHandle): Promise<boolean>;
5164
+ /** The merged provenance of a retained handle, or no origins when it is not in the taint window. */
5165
+ checkHandleOrigins(handle: TaintHandle): Promise<readonly TaintOrigin[]>;
5336
5166
  }
5337
5167
  //#endregion
5338
- //#region src/approval-validate.d.ts
5339
- /**
5340
- * The integrity check of a version-2 approval record (design 2026-10-05 §D1, "Digest integrity" and "Version-2
5341
- * timestamps fail closed"). Pure: it reads the record and the id its file is named by, and decides.
5342
- *
5343
- * A record either passes, or is `corrupt` for one fixed reason — never a sentence built from the record, so nothing a
5344
- * file holds is ever echoed — and of one of two attribution classes. `verified`: its `bindingDigest` recomputes, so its
5345
- * owner and identity can be trusted and it is corrupt for another reason (a state, a timestamp, its evidence).
5346
- * `unverifiable`: its binding is missing, malformed or does not match, or it sits in another record's file, so nothing
5347
- * it says about whom it belongs to can be trusted.
5348
- *
5349
- * Nothing here is wired into a read yet; the store's decoder does that.
5350
- */
5351
- /** Why a version-2 record is corrupt: a fixed category, never the record's own words. */
5352
- type IntegrityReason = "file-name-mismatch" | "binding-missing" | "binding-malformed" | "binding-mismatch" | "wrong-shape" | "digest-version" | "content-digest-malformed" | "content-digest-mismatch" | "identity-contradiction" | "listing-mismatch" | "state-impossible" | "lifetime-mismatch" | "timestamp-invalid" | "timestamp-missing" | "timestamp-misplaced" | "timestamp-misordered" | "sent-id-missing" | "evidence-missing" | "evidence-invalid" | "evidence-contradictory";
5353
- type Attribution = "verified" | "unverifiable";
5354
- //#endregion
5355
- //#region src/approval-stored.d.ts
5356
- /**
5357
- * Every approval file, read one way (design 2026-10-05 §D2, "Legacy version-1 records, one decoder everywhere" and
5358
- * "Two classes of parseable corrupt record").
5359
- *
5360
- * A file is one of four things, and every read says which:
5361
- *
5362
- * - `v2` — a version-2 record that passed `validateV2`: the only form anything may act on;
5363
- * - `legacy` — a structurally valid version-1 record, read by its own release's rules: reported, never claimed;
5364
- * - `corrupt` — a parseable version-2 record that failed its integrity check, attributable to its owner or not;
5365
- * - `unreadable` — a file that cannot safely be read at all: shown only as a stub naming its id (from the file name)
5366
- * and a fixed reason, never anything the file holds.
5367
- *
5368
- * Nothing is silently omitted, and the decoder writes nothing.
5369
- */
5370
- /** Why a file cannot safely be read: a fixed category, never the file's own bytes. */
5371
- type UnreadableReason = "invalid-json" | "truncated" | "missing-ownership" | "missing-kind" | "wrong-shape" | "file-name-mismatch";
5372
- /** What may be said of a record that cannot be trusted: its id, that it is corrupt, and why — nothing else. */
5373
- interface CorruptStub {
5374
- readonly approvalId: string;
5375
- readonly state: "corrupt";
5376
- readonly reason: UnreadableReason | IntegrityReason;
5377
- }
5378
- /** What an attribution-verified corrupt record may still show its owner: the fields its binding was made over. */
5379
- interface OrdinarySafeFields {
5380
- readonly kind: ApprovalKind;
5381
- readonly channel: string;
5382
- readonly ownerScope: OwnerScope;
5383
- readonly inboxId: string;
5384
- readonly inboxSub?: string | undefined;
5385
- readonly draftId: string;
5386
- readonly draftMessageId: string;
5387
- readonly expect: Expectation;
5388
- }
5389
- /** A disclosure record has no channel owner or sender-controlled operational fields to expose. */
5390
- interface DisclosureSafeFields {
5391
- readonly kind: "disclosure";
5168
+ //#region src/core.d.ts
5169
+ /** Everything a provider package needs from the core, wired to one config directory. */
5170
+ interface Core {
5171
+ paths: ResolvedPaths;
5172
+ /** The subset of `paths` explicitly pinned by global CLI options. */
5173
+ pathOverrides: Readonly<PathOverrides>;
5174
+ config: ConfigStore;
5175
+ states: InboxStateStore;
5176
+ approvals: ApprovalStore;
5177
+ ledger: SendLedger;
5178
+ /**
5179
+ * The acknowledgement ledger (design 2026-10-19-standing-approvals, S6, S9): every use of a standing approval,
5180
+ * counted per account, under its own lock, before the use's child is minted — and never refunded.
5181
+ */
5182
+ acks: AckLedger;
5183
+ plans: PlanStore;
5184
+ taint: TaintStore;
5185
+ audit: AuditLog;
5186
+ /** Opens the config directory's one secret backend (as recorded in config, or `kind` before the first write). */
5187
+ secrets(kind?: SecretStoreKind): Promise<SecretStore>;
5188
+ /**
5189
+ * The commands this process tells a person to run — its own CLI's, core's, another product's — located from the
5190
+ * caller it was opened with (`OpenCoreOptions.caller`), for these folders. Undefined when none was given: then there
5191
+ * is no command to print at all, and anything that asks for one meets the programming error `requireHandoffs` throws —
5192
+ * never a bare name. Every package opens core with its caller.
5193
+ */
5194
+ handoffs?: CliHandoffs | undefined;
5392
5195
  }
5393
- type SafeFields = OrdinarySafeFields | DisclosureSafeFields;
5394
- type StoredApproval = {
5395
- readonly form: "v2";
5396
- readonly record: ApprovalRecord;
5397
- } | {
5398
- readonly form: "legacy";
5399
- readonly view: LegacyView;
5400
- readonly record: LegacyApprovalRecord;
5401
- } | {
5402
- readonly form: "corrupt";
5403
- readonly approvalId: string;
5404
- readonly reason: IntegrityReason;
5405
- readonly attribution: Attribution;
5406
- /** Only when attribution is verified. */
5407
- readonly safe: SafeFields | null;
5408
- } | {
5409
- readonly form: "unreadable";
5410
- readonly stub: CorruptStub;
5411
- };
5412
5196
  //#endregion
5413
- //#region src/approval-outcome.d.ts
5197
+ //#region src/standing.d.ts
5414
5198
  /**
5415
- * One locked classification of every record into an outcome (design 2026-10-05 §D2).
5199
+ * Standing approvals (design 2026-10-19-standing-approvals): the person's yes given in advance, for acknowledging — an
5200
+ * exact set of contents they wrote, a bounded set of places, a bounded number of uses, and a time limit. The agent
5201
+ * decides whether to acknowledge a message that has just arrived, and with which of the contents; it never writes the
5202
+ * words, and it cannot pick an old message or a new place.
5416
5203
  *
5417
- * `approvalOutcome` says what a record is now — its state, whether it can be claimed, and what an action on it would be
5418
- * told — from the record as it was read under its own lock and the configuration read in that same lock (`LiveGate`).
5419
- * Nothing here reads a file or a clock of its own, and nothing here writes: the store persists what the outcome
5420
- * derives, and only from inside the lock it was classified under.
5421
- */
5422
- /** What is being done with the record: an approval, a claim, a wait, a revoke, or a look. */
5423
- type OutcomeAction = "approve" | "claim" | "wait" | "revoke" | "inspect";
5424
- /** A record's state as anyone may be told it: a download's answered question says `answered`. */
5425
- type PublicApprovalState = ApprovalState | "corrupt" | "answered";
5426
- /**
5427
- * Where an approval stands, as every surface may show it (design 2026-10-05 §D8): its id, kind, channel, state and
5428
- * whether it can be claimed, with the timestamps its state carries. Never a challenge hash, and nothing a sender wrote.
5429
- */
5430
- interface ApprovalObject {
5431
- readonly id: string;
5432
- readonly kind: ApprovalKind | null;
5433
- readonly channel: string | null;
5434
- readonly state: PublicApprovalState;
5435
- readonly claimable: boolean;
5436
- readonly route?: ApprovalRoute | undefined;
5437
- readonly ownerRemoved?: true | undefined;
5438
- /** A record from an earlier release, read by its own rules: shown, never claimed. */
5439
- readonly legacy?: true | undefined;
5440
- readonly reason?: string | undefined;
5441
- readonly createdAt?: string | undefined;
5442
- readonly expiresAt?: string | undefined;
5443
- readonly approvedAt?: string | undefined;
5444
- readonly usableUntil?: string | undefined;
5445
- readonly sendingAt?: string | undefined;
5446
- readonly sendingHeartbeatAt?: string | undefined;
5447
- /** When a `sending` record reads `unknown` (`unknownAtOf`): derived, never stored. Also on an `unknown` one. */
5448
- readonly unknownAt?: string | undefined;
5449
- readonly usedAt?: string | undefined;
5450
- readonly sentAt?: string | undefined;
5451
- readonly sentMessageId?: string | undefined;
5452
- readonly failedAt?: string | undefined;
5453
- readonly revokedAt?: string | undefined;
5454
- readonly expiredAt?: string | undefined;
5455
- }
5456
- /** What an action on a record is, now. */
5457
- interface ApprovalOutcome {
5458
- readonly state: PublicApprovalState;
5459
- readonly claimable: boolean;
5460
- readonly ownerRemoved?: true | undefined;
5461
- readonly reason?: string | undefined;
5462
- /**
5463
- * A version-2 record as this classification reads it: with a revocation it derived applied (an owner removed, a
5464
- * stale send epoch, or — for a claim or an approval — a live `never`). Null for every other form.
5465
- */
5466
- readonly record: ApprovalRecord | null;
5467
- /** Whether `record` carries a revocation the classification derived, for a locked action to persist. */
5468
- readonly revokes: boolean;
5469
- /** Where it stands, as D8 shows it (`publicApproval`). */
5470
- readonly approval: ApprovalObject;
5471
- /** What the action is refused with, when it is refused by the record's state alone. */
5472
- readonly error?: CommsError | undefined;
5204
+ * This module is the model: the contents, each named by its digest and never by its position (S3); the places (S5);
5205
+ * the limits (S6); the digest a standing record is bound to; and what widens one, which only a person may approve at a
5206
+ * terminal (S8). Pure, but for `raiseStandingEpoch`: nothing else here reads a file or a clock.
5207
+ */
5208
+ /** S1: the acts a standing approval may cover. A reaction is only ever added, never removed. */
5209
+ declare const STANDING_ACTS: readonly ["reaction", "reply"];
5210
+ type StandingAct = (typeof STANDING_ACTS)[number];
5211
+ /** One content the person wrote: a standard emoji's name, or a fixed reply. Named by `digest`, never by position. */
5212
+ interface StandingContent {
5213
+ kind: StandingAct;
5214
+ text: string;
5215
+ /** `sha256Hex(canonicalJson({ kind, text }))`: the kind and the exact text, and nothing else. */
5216
+ digest: string;
5473
5217
  }
5474
5218
  /**
5475
- * What became of a used send, in the words of the surface that says it, from the time the provider accepted it (D2's
5476
- * used-send row): Gmail and Slack, where acceptance is sending, say it was "sent at …"; Resend's own surfaces that it
5477
- * was "accepted by Resend at …"; core's generic surfaces "accepted by <provider> at …".
5219
+ * Who shares a place, as recorded when it was named (S5). `externalMembers` is null when the place has 50 or more
5220
+ * members, which are not read one by one.
5478
5221
  */
5479
- type UsedSaid = (usedAt: string) => string;
5222
+ interface SharedState {
5223
+ extShared: boolean;
5224
+ pendingExtShared: boolean;
5225
+ externalMembers: boolean | null;
5226
+ }
5227
+ /** A channel or DM named exactly, with what the person was shown of it: its name, its member count and its sharing. */
5228
+ interface ExactPlace {
5229
+ id: string;
5230
+ name: string | null;
5231
+ members: number;
5232
+ shared: SharedState;
5233
+ }
5234
+ /** S5: exact channels or DMs, or the threads in one workspace where the person posted themselves. */
5235
+ type StandingPlaces = {
5236
+ kind: "exact";
5237
+ places: ExactPlace[];
5238
+ } | {
5239
+ kind: "threads-i-am-in";
5240
+ };
5480
5241
  /**
5481
- * Why a saved name has {@link DOWNLOAD_SUFFIX} after it: a name tools read or run on their own (`auto-read`), an
5482
- * extension that is not one of the inert ones (`type`), or no extension at all (`no-extension`).
5242
+ * S6: the limits. `perThread` and `perSender` count across every standing approval of the account: `perSender` is how
5243
+ * many acts a day may answer messages from any one person (the lead's call, after review), the cap on what an injected
5244
+ * agent can say to one recipient by choosing which of their messages to acknowledge.
5483
5245
  */
5484
- type RenameReason = "auto-read" | "type" | "no-extension";
5246
+ interface StandingLimits {
5247
+ perThread: number;
5248
+ perHour: number;
5249
+ perDay: number;
5250
+ perSender: number;
5251
+ expiresDays: number;
5252
+ }
5253
+ /** Exactly what a standing record permits, stored on it so a terminal can show it again and prove it is what was made. */
5254
+ interface StandingBinding {
5255
+ /** One line about it, for a listing. Shown; not in the digest. */
5256
+ summary: string;
5257
+ target: {
5258
+ kind: "account";
5259
+ name: string;
5260
+ id: string;
5261
+ };
5262
+ /** Reactions first, then replies, as written. */
5263
+ contents: StandingContent[];
5264
+ places: StandingPlaces;
5265
+ limits: StandingLimits;
5266
+ /** Who proposed it: an agent, over MCP or without a person at the terminal, or the person at their terminal. */
5267
+ proposedBy: "agent" | "person";
5268
+ /** The standing id this widens, which approving it ends. */
5269
+ replaces?: string | undefined;
5270
+ /** S3's warnings, as the preview shows them. */
5271
+ warnings: string[];
5272
+ }
5273
+ /** What a narrowing keeps: content digests, exact place ids, and lower limits. It applies at once. */
5274
+ interface StandingNarrowing {
5275
+ keep?: string[];
5276
+ places?: string[];
5277
+ limits?: Partial<StandingLimits>;
5278
+ }
5485
5279
  //#endregion
5486
5280
  //#region src/approvals.d.ts
5487
5281
  /**
@@ -5516,7 +5310,7 @@ type ApprovalChannel = "elicitation" | "terminal";
5516
5310
  * and a kind on every record, so that none can be spent as another. Without it, a person who approved a post at a
5517
5311
  * terminal would also have approved whatever change an agent claimed under the same id.
5518
5312
  */
5519
- type ApprovalKind = "send" | "change" | "download" | "disclosure";
5313
+ type ApprovalKind = "send" | "change" | "download" | "disclosure" | "standing";
5520
5314
  /** The inbox or account a change is about. `id` is absent when the change connects it, and it does not exist yet. */
5521
5315
  interface ChangeTarget {
5522
5316
  kind: "inbox" | "account";
@@ -5656,8 +5450,11 @@ interface ApprovalRecord {
5656
5450
  * terminal or in a trusted form. Never on a send or a change, which carry `approvedBindingDigest` instead.
5657
5451
  */
5658
5452
  approvedDigest?: string | undefined;
5659
- /** Where a person approved, or answered: the terminal, or a trusted client's form. Absent on a chat-route claim. */
5660
- approvedVia?: ApprovalChannel | undefined;
5453
+ /**
5454
+ * Where a person approved, or answered: the terminal, or a trusted client's form. Absent on a chat-route claim.
5455
+ * `standing` on a standing approval's child, and on nothing else: the person approved it in advance, at a terminal.
5456
+ */
5457
+ approvedVia?: ApprovalChannel | "standing" | undefined;
5661
5458
  /** A send's or a change's approval: when, and the binding the person approved, which must still be the record's. */
5662
5459
  approvedAt?: string | undefined;
5663
5460
  /** `approvedAt + approvedMs`: until when an approved send or change may be claimed. */
@@ -5695,6 +5492,29 @@ interface ApprovalRecord {
5695
5492
  download?: DownloadBinding | undefined;
5696
5493
  /** For a standing disclosure: the canonical, content-free event activation authority. */
5697
5494
  disclosure?: DisclosureBinding | undefined;
5495
+ /**
5496
+ * For a standing approval (`kind: 'standing'`, design 2026-10-19-standing-approvals): exactly what it permits.
5497
+ * `contentDigest` is `standingDigest` of this.
5498
+ */
5499
+ standing?: StandingBinding | undefined;
5500
+ /** For a standing approval: what has been narrowed since, at once. Not bound: it can only take away (S8). */
5501
+ narrowed?: StandingNarrowing | undefined;
5502
+ /** On a standing approval and its children: the standing epoch as read when it was made. Bound. */
5503
+ standingEpoch?: number | undefined;
5504
+ /** On a standing approval's child: the standing approval it is one use of. Bound. */
5505
+ standingId?: string | undefined;
5506
+ /**
5507
+ * On a standing approval's child: where its act goes and which of its standing approval's contents it sends, by
5508
+ * digest — what its claim checks the standing approval still covers. Bound.
5509
+ */
5510
+ standingUse?: StandingUseBinding | undefined;
5511
+ /**
5512
+ * On an approved standing approval: the change policy that governed its account when the person approved it — its
5513
+ * own, else the default. Written with the approval, beside `approvedAt`. A live change policy stricter than this has
5514
+ * ended it (`changePolicy-tightened`), whatever the standing epoch says: a release that knows no standing epoch can
5515
+ * still tighten the policy (design 2026-10-19-standing-approvals, S8, the lead's call after review).
5516
+ */
5517
+ approvedChangePolicy?: ChangePolicy | undefined;
5698
5518
  }
5699
5519
  /**
5700
5520
  * The stored branch for standing disclosure. Its runtime decoder refuses every field belonging to send, change or
@@ -5834,511 +5654,976 @@ interface ApprovalStoreOptions {
5834
5654
  * The file operations daily maintenance and the unsent report go through — the real ones when left out; a test's to
5835
5655
  * count and interrupt every step. Every other read and write of the store is unaffected.
5836
5656
  */
5837
- io?: ApprovalIo | undefined;
5657
+ io?: ApprovalIo | undefined;
5658
+ /**
5659
+ * Told when a person has answered a send or post: approved at a terminal, or claimed to be sent — the person's yes
5660
+ * under the chat policy (design 2026-10-16, U7: the security line a preview carried counts as asked once its approval
5661
+ * is answered). Never when it is only prepared, refused or revoked. Awaited, and whatever it throws is ignored:
5662
+ * nothing it does may stop a send. `openCore` passes `securityAnswered`.
5663
+ */
5664
+ answered?: ((approvalId: string) => Promise<void>) | undefined;
5665
+ }
5666
+ declare class ApprovalStore {
5667
+ #private;
5668
+ readonly directory: string;
5669
+ /** `ApprovalStoreOptions.answered`, never throwing. */
5670
+ readonly answered: ((approvalId: string) => Promise<void>) | undefined;
5671
+ constructor(stateDir: string, options?: ApprovalStoreOptions);
5672
+ /**
5673
+ * Daily retention (design 2026-10-05 §D9): once a day per state directory, a bounded batch deletes valid finished
5674
+ * records ninety days past their finish. Awaited before every creation, list, status and wait — with its own
5675
+ * five-second budget — and before a report, on the report's shared deadline. Never a gate: whatever it finds, it
5676
+ * returns its status and the caller goes on.
5677
+ */
5678
+ ensurePruned(options?: MaintenanceOptions): Promise<MaintenanceStatus>;
5679
+ create(input: CreateApprovalInput): Promise<ApprovalRecord>;
5680
+ /** Creates only a pending standing disclosure approval from a daemon-derived canonical binding. */
5681
+ createDisclosure(binding: DisclosureBinding): Promise<DisclosureApprovalRecord>;
5682
+ /**
5683
+ * One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
5684
+ * or null when there is none. Nothing is written.
5685
+ */
5686
+ get(approvalId: string): Promise<StoredApproval | null>;
5687
+ /**
5688
+ * A look at one approval, under its lock: the record as it stands and its classification for `expect` — the same
5689
+ * `NOT_FOUND` as every action for an id that is not what the caller expects, before anything of the record is
5690
+ * classified. It writes only what reading derives (an expiry, a dead send's `unknown`), never a revocation: an owner
5691
+ * removed or a stale epoch is shown as revoked here, and written by the next action.
5692
+ *
5693
+ * `action` is for a surface about to act on what it reads — a terminal about to show a preview, an execute about to
5694
+ * read its draft — classified for that action (`approve`, `claim`, `revoke`), so the refusal is the one the action
5695
+ * would get. A revocation the classification derives for it is written then, as the action itself would write it:
5696
+ * the first locked action persists it, and this is that action's first step (design 2026-10-05 §D2).
5697
+ */
5698
+ inspect(approvalId: string, expect?: ApprovalExpectation, options?: {
5699
+ action?: OutcomeAction | undefined;
5700
+ usedSaid?: UsedSaid | undefined;
5701
+ }): Promise<{
5702
+ stored: StoredApproval;
5703
+ outcome: ApprovalOutcome;
5704
+ }>;
5705
+ /**
5706
+ * D8's object for a record a call has just written, claimed or finished — what its result reports. Classified as a
5707
+ * look at it would be, against a fresh read of the configuration, without the lock and writing nothing: what decided
5708
+ * was the transition that wrote it. A configuration that cannot be read leaves only what the record says (nothing
5709
+ * claimable): a report never fails the call it reports on, least of all one that has already sent.
5710
+ */
5711
+ approvalOf(record: ApprovalRecord): Promise<ApprovalObject>;
5712
+ /**
5713
+ * The classification of a record a call has just written, in any of its forms — a revoke's, a cancel's — for its
5714
+ * result to show through `publicApproval`, as a status, a wait and a list show theirs: what a sender wrote only inside
5715
+ * the untrusted-content envelope (design 2026-10-05 §D8). Classified as a look at it would be, against a fresh read of
5716
+ * the configuration, without the lock and writing nothing, as `approvalOf` is; a configuration that cannot be read
5717
+ * leaves nothing claimable, and never fails the call it reports on.
5718
+ */
5719
+ outcomeOf(stored: StoredApproval): Promise<ApprovalOutcome>;
5720
+ /** Issues a new challenge to show a human; only its hash is kept. */
5721
+ issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5722
+ usedSaid?: UsedSaid | undefined;
5723
+ }): Promise<string>;
5724
+ /** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
5725
+ issueDisclosureChallenge(approvalId: string): Promise<string>;
5726
+ /**
5727
+ * Approves one exact standing disclosure authority. MCP/chat forms are never an approval channel here: `app` means
5728
+ * the desktop application's trusted typed-challenge bridge, not a client form.
5729
+ */
5730
+ approveDisclosure(approvalId: string, liveBinding: DisclosureBinding, answer: string, via: "terminal" | "app"): Promise<DisclosureApprovalRecord>;
5731
+ /**
5732
+ * Claims an approved disclosure once, persisting the exact `usedAt` that recovery must subsequently reuse.
5733
+ * A specialised caller may impose a shorter deadline; it is evaluated while this record's transition lock is held,
5734
+ * so a check made before waiting on that lock cannot accidentally consume the approval after it has expired.
5735
+ */
5736
+ claimForDisclosure(approvalId: string, liveBinding: DisclosureBinding, options?: {
5737
+ readonly deadlineMs?: number | undefined;
5738
+ }): Promise<DisclosureApprovalRecord>;
5739
+ /**
5740
+ * A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
5741
+ * record was prepared for; otherwise the record is voided, because the human would be approving content the record
5742
+ * does not describe. Three wrong answers void it too.
5743
+ *
5744
+ * A change is approved the same way, with `kind: 'change'` and its digest standing in for the draft (see
5745
+ * `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
5746
+ */
5747
+ approve(approvalId: string, via: ApprovalChannel, live: LiveDraft, answer: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5748
+ usedSaid?: UsedSaid | undefined;
5749
+ }): Promise<ApprovalRecord>;
5750
+ /**
5751
+ * Claims the record for sending, once. Non-consuming refusals (not yet approved) leave the record untouched so the
5752
+ * human can still approve it; integrity failures (another account, an edited or changed draft, different recipients
5753
+ * or subject) void it. Success creates the O_EXCL claim marker.
5754
+ *
5755
+ * `live.inboxId` is the owner the caller claims for: a record of another owner is the one `NOT_FOUND`, and left as it
5756
+ * is. The policy is the live gate's, from the configuration read under the record's lock (decision 4): a value the
5757
+ * caller read before the lock is exactly what a `never` change can land between.
5758
+ */
5759
+ claimForSend(approvalId: string, live: LiveDraft & {
5760
+ inboxId: string;
5761
+ inboxSub?: string | undefined;
5762
+ expect: Expectation;
5763
+ }, options?: ClaimOptions): Promise<SendClaim>;
5764
+ /**
5765
+ * Renews the sending lease of a send this call claimed: writes `sendingHeartbeatAt` while the claim still owns a
5766
+ * `sending` record, and nothing otherwise. `renewed` while it is sending; `lost` once it reads `unknown` (or anything
5767
+ * else) — a renewal never moves a record back to `sending`. Refused, writing nothing, without the claim's token.
5768
+ */
5769
+ heartbeat(approvalId: string, claimToken: string): Promise<"renewed" | "lost">;
5770
+ /**
5771
+ * The check before each provider step (design 2026-10-05 §D1): `go`, with the lease renewed, while this claim still
5772
+ * owns a `sending` record; `stop` once it reads `unknown` — another caller has decided its outcome cannot be known,
5773
+ * and no further step may start. Refused, writing nothing, without the claim's token.
5774
+ */
5775
+ fence(approvalId: string, claimToken: string): Promise<"go" | "stop">;
5776
+ /**
5777
+ * A change approval, pending, bound to `changeDigest(input.change)`.
5778
+ *
5779
+ * The digest is computed here, never taken from the caller, so a record cannot claim to be bound to one change while
5780
+ * describing another. It stands in for the draft revision too, as a reaction's does: a change has no draft, and the
5781
+ * same value means the same change.
5782
+ */
5783
+ createChange(input: CreateChangeApprovalInput): Promise<ApprovalRecord>;
5784
+ /**
5785
+ * Claims a change approval, once, for the change the caller is about to write.
5786
+ *
5787
+ * `live.change` is the change as the caller computes it now, and it has to digest to what was prepared: a different
5788
+ * path, a different value, a different account or a different effect voids the approval, because the person agreed
5789
+ * to something else. `live.policy` is the change policy in force now, and the stricter of it and the one at prepare
5790
+ * decides — so tightening the policy after an agent prepared a change takes effect on that change, and loosening it
5791
+ * does not release one prepared under `confirm`.
5792
+ *
5793
+ * Under `chat` a pending approval is claimable: the yes was given in the conversation. Anything stricter needs a
5794
+ * person to have typed the code at a terminal first; until then the refusal leaves the record as it is.
5795
+ */
5796
+ claimForChange(approvalId: string, live: LiveChange, options?: ClaimOptions): Promise<ApprovalRecord>;
5797
+ /**
5798
+ * A standing approval (design 2026-10-19-standing-approvals, S8), pending, bound to `standingDigest` of its binding —
5799
+ * computed here, never taken from the caller — and to the send epoch of its account and the standing epoch, both as
5800
+ * read when it is made.
5801
+ *
5802
+ * Its route is `confirm` and its `requiredPolicy` `confirm` whatever the change policy is, because what it permits
5803
+ * happens with nobody asked: it is approved only with the code typed at a terminal (`approve`, `via: 'terminal'`),
5804
+ * never by a yes in the chat, a client's form, or an answer carried in a call. It lasts its days once approved
5805
+ * (`approvedMs`). Nothing ever claims it: each use mints a child (`createStandingChild`).
5806
+ *
5807
+ * The binding is checked before anything is written (`canonicalStandingBinding`): a custom emoji, a reply with a
5808
+ * link, a limit outside the table or a place that is no Slack id is refused, however the caller assembled it.
5809
+ */
5810
+ createStanding(input: {
5811
+ binding: StandingBinding;
5812
+ inboxSub: string;
5813
+ sendEpoch: number;
5814
+ standingEpoch: number;
5815
+ }): Promise<ApprovalRecord>;
5816
+ /**
5817
+ * Runs `use` under the standing record's own lock, with the record as it reads there and its classification for a
5818
+ * use (`claim`): its expiry, a revocation the configuration derives — its account removed, `never`, a send epoch or
5819
+ * a standing epoch behind the live one — written before `use` runs, and `outcome.error` set for every state that
5820
+ * refuses a use. `owner` is the account the caller acts for: any other's record is the one `NOT_FOUND`.
5821
+ *
5822
+ * What `use` decides is decided while no revocation, narrowing or approval of this record can land. `use` must not
5823
+ * take this record's lock again: it is held.
5824
+ */
5825
+ withStanding<T>(standingId: string, owner: string, use: (record: ApprovalRecord, outcome: ApprovalOutcome) => Promise<T>): Promise<T>;
5826
+ /**
5827
+ * One use of a standing approval (S9): a send record, approved the moment it is made — `route: 'standing'`,
5828
+ * `approvedVia: 'standing'`, `approvedAt` its creation — bound to the standing id, the standing epoch and the send
5829
+ * epoch, and usable once, within `APPROVAL_LIFETIMES.standingChild`. It then takes the one send path every send
5830
+ * takes: `claimForSend`, its O_EXCL marker, the fence and the outcome.
5831
+ *
5832
+ * Minted only by the use (`mintStandingUse`), under the account's acknowledgement ledger lock and then the standing
5833
+ * record's, after every check there. Checked again here, read without that lock — which the caller holds: the
5834
+ * standing record must be this account's, approved at a terminal, under the same epochs and the same account user,
5835
+ * and not ended, or nothing is written. A child never names anything but what its use was checked for.
5836
+ */
5837
+ createStandingChild(input: {
5838
+ /**
5839
+ * The id the child is made under, when its use was counted under it first (`mintStandingUse`): an id this store
5840
+ * would make, and one no record holds. A new one when left out.
5841
+ */
5842
+ approvalId?: string | undefined;
5843
+ standingId: string;
5844
+ standingEpoch: number;
5845
+ inboxId: string;
5846
+ inboxSub: string;
5847
+ draftId: string;
5848
+ draftMessageId: string;
5849
+ contentDigest: string;
5850
+ sendEpoch: number;
5851
+ riskFlags: string[];
5852
+ expect: Expectation;
5853
+ /** Where the act goes, and which of the standing approval's contents it sends: what its claim checks again. */
5854
+ use: StandingUseBinding;
5855
+ }): Promise<ApprovalRecord>;
5856
+ /**
5857
+ * Narrows a standing approval at once (S8): fewer contents or places, a lower limit, an earlier end. Nothing is asked
5858
+ * for, because nothing more is permitted: anything `classifyStandingChange` calls a widening of what is in force is
5859
+ * refused with USAGE, and the record is left as it was. The binding the person approved stays the record's own, and
5860
+ * what is narrowed is kept beside it, where it can only take away.
5861
+ */
5862
+ narrowStanding(standingId: string, owner: string, narrowing: StandingNarrowing): Promise<ApprovalRecord>;
5863
+ /**
5864
+ * A download's question, pending, bound to `downloadDigest(input.download)` — computed here, never taken from the
5865
+ * caller, as a change's digest is. It stands in for the draft revision too.
5866
+ *
5867
+ * `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
5868
+ * asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
5869
+ * other changes are approved. A claim holds the question to the stricter of this and the policy then.
5870
+ */
5871
+ createDownload(input: {
5872
+ /** The manifest channel the files come from. */
5873
+ channel: string;
5874
+ download: DownloadBinding;
5875
+ policy: ChangePolicy;
5876
+ }): Promise<ApprovalRecord & {
5877
+ download: DownloadBinding;
5878
+ }>;
5879
+ /**
5880
+ * Records the person's answer to a download's question, given where an agent cannot give it: at their own terminal,
5881
+ * or in a form a trusted client showed them. The question moves to `approved`, carrying the answer, and waits for
5882
+ * the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
5883
+ *
5884
+ * Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
5885
+ */
5886
+ answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer, _platform?: NodeJS.Platform): Promise<ApprovalRecord & {
5887
+ download: DownloadBinding;
5888
+ }>;
5889
+ /**
5890
+ * Claims a download's question, once, for the download the caller is about to make — and returns it, with the
5891
+ * folders it offered, so that `downloads` and `current` in the answer mean the paths the person read, and with the
5892
+ * answer the person recorded, when they recorded one.
5893
+ *
5894
+ * `live` is the download as the caller computes it now: the same account, the same request, the same files under
5895
+ * the same names — the person answered for those files and no others. Any other is refused, and the question left
5896
+ * open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
5897
+ * question for it would make the person answer again for nothing. It still expires, and is claimed once.
5898
+ *
5899
+ * Whether it can be claimed is the download `claimable` matrix (`downloadClaimable`, design 2026-10-05 §D2), decided
5900
+ * under the question's lock against the live change policy read there — never a policy the caller read before it.
5901
+ * While both the policy the question was asked under and the live one are `chat`, a pending question is claimed
5902
+ * with the answer the caller carries: the person gave it in the conversation, and it is never kept. Otherwise only a
5903
+ * question the person answered at a terminal or in a trusted form can be claimed; a pending one is refused, and left
5904
+ * as it is, so they can still answer it. One used, voided, expired or corrupt is refused for what it is.
5905
+ *
5906
+ * `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
5907
+ * download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
5908
+ */
5909
+ claimForDownload(approvalId: string, live: DownloadRequest, options?: {
5910
+ pendingHint?: string | undefined;
5911
+ signal?: AbortSignal | undefined;
5912
+ platform?: NodeJS.Platform | undefined;
5913
+ handoffs?: CliHandoffs | undefined;
5914
+ }): Promise<ApprovalRecord & {
5915
+ download: DownloadBinding;
5916
+ }>;
5917
+ /**
5918
+ * Records the outcome of the one send attempt: `used` with the provider's id for it, when the provider accepted it,
5919
+ * or `failed`.
5920
+ *
5921
+ * `used` needs a non-empty id. A provider that accepted a send without one has not given anything to record it by,
5922
+ * so the record is left as it is — `sending`, and then `unknown` — rather than made `used` with an empty id that
5923
+ * every later reader would take for one.
5924
+ *
5925
+ * Only the claimant records it, by its claim token, from `sending` or from `unknown`: a provider's answer that
5926
+ * arrives after the lease ran out is still the truth. A late success clears the stale-lease reason; a late failure
5927
+ * replaces it with its own.
5928
+ */
5929
+ complete(approvalId: string, claimToken: string, outcome: SendOutcome): Promise<ApprovalRecord>;
5930
+ /**
5931
+ * Voids a pending or approved record. Other records are left as they are, and returned as they read.
5932
+ *
5933
+ * `disposition` says whose decision this is, and has no default: every caller chooses (`RevokeDisposition`). A
5934
+ * version-1 record is retired in its own shape when a person or an owner's removal revokes it (`revokeLegacy`), and
5935
+ * refused with the version refusal, nothing written, when this release's integrity check would have voided it. A
5936
+ * corrupt or unreadable record is never revoked or rewritten: it is refused with the integrity refusal, which repeats
5937
+ * only its stub.
5938
+ */
5939
+ revoke(approvalId: string, reason: string, options: {
5940
+ disposition: RevokeDisposition;
5941
+ expect?: ApprovalExpectation | undefined;
5942
+ }): Promise<StoredApproval>;
5838
5943
  /**
5839
- * Told when a person has answered a send or post: approved at a terminal, or claimed to be sent — the person's yes
5840
- * under the chat policy (design 2026-10-16, U7: the security line a preview carried counts as asked once its approval
5841
- * is answered). Never when it is only prepared, refused or revoked. Awaited, and whatever it throws is ignored:
5842
- * nothing it does may stop a send. `openCore` passes `securityAnswered`.
5944
+ * Retires a record from an earlier release (version 1) in that release's own shape, so a process still running it
5945
+ * can no longer claim the record — and nothing more.
5946
+ *
5947
+ * Under the record's own lock, `<id>.json.lock`, the one 0.13.0's transitions take, so the two serialise. The file
5948
+ * is read again there, decoded, and its state derived by its own release's rules (`deriveLegacyV1State`). Only a
5949
+ * `pending` or `approved` record is rewritten, as itself plus `state: 'revoked'`, the reason and `updatedAt`: its
5950
+ * digest version stays 1, a send stays without a `kind`, and no field of this release's is added — no claim marker
5951
+ * either. Any other derived state is returned as it reads and written nothing: an expired record is never rewritten,
5952
+ * as `revoke` leaves every finished record as it is.
5953
+ *
5954
+ * It reads no configuration and needs no owner, so it works on a record whose account is already gone. Its only
5955
+ * callers are a person's revoke, the removal of the record's owner, and the legacy drain.
5843
5956
  */
5844
- answered?: ((approvalId: string) => Promise<void>) | undefined;
5845
- }
5846
- declare class ApprovalStore {
5847
- #private;
5848
- readonly directory: string;
5849
- /** `ApprovalStoreOptions.answered`, never throwing. */
5850
- readonly answered: ((approvalId: string) => Promise<void>) | undefined;
5851
- constructor(stateDir: string, options?: ApprovalStoreOptions);
5957
+ revokeLegacy(approvalId: string, reason: string): Promise<LegacyApprovalRecord>;
5852
5958
  /**
5853
- * Daily retention (design 2026-10-05 §D9): once a day per state directory, a bounded batch deletes valid finished
5854
- * records ninety days past their finish. Awaited before every creation, list, status and wait — with its own
5855
- * five-second budget — and before a report, on the report's shared deadline. Never a gate: whatever it finds, it
5856
- * returns its status and the caller goes on.
5959
+ * Every approval file, each looked at under its own lock and classified there, as `inspect` looks at one — against one
5960
+ * configuration snapshot, loaded once before any file is read (design 2026-10-05 §D2, §D8). What a list shows: each
5961
+ * record's form and its outcome, nothing skipped. Like a look, it writes only what reading derives — an expiry at its
5962
+ * boundary, a stale send's `unknown` — never a revocation: an owner removed or a stale epoch is shown as revoked, and
5963
+ * written by the next action.
5964
+ *
5965
+ * `inboxId` keeps the records whose owner is that id (`ownerOf`), so one whose owner cannot be trusted never matches;
5966
+ * `states` keeps those whose classified state is listed — a stored state, `corrupt`, and for a download's answered
5967
+ * question its stored `approved` or `used`.
5857
5968
  */
5858
- ensurePruned(options?: MaintenanceOptions): Promise<MaintenanceStatus>;
5859
- create(input: CreateApprovalInput): Promise<ApprovalRecord>;
5860
- /** Creates only a pending standing disclosure approval from a daemon-derived canonical binding. */
5861
- createDisclosure(binding: DisclosureBinding): Promise<DisclosureApprovalRecord>;
5969
+ inspectAll(filter?: {
5970
+ inboxId?: string;
5971
+ states?: (ApprovalState | "corrupt")[];
5972
+ }): Promise<Array<{
5973
+ stored: StoredApproval;
5974
+ outcome: ApprovalOutcome;
5975
+ }>>;
5862
5976
  /**
5863
- * One approval, in whichever of its four forms it is (`StoredApproval`), read against one configuration snapshot —
5864
- * or null when there is none. Nothing is written.
5977
+ * Every approval file, each in one of its four forms — nothing skipped, nothing omitted — read against one
5978
+ * configuration snapshot, loaded once before any file is read: a loader that fails fails the whole list.
5979
+ *
5980
+ * `inboxId` keeps the records whose owner is that id (`ownerOf`), so a file whose owner cannot be trusted never
5981
+ * matches one; `states` keeps those whose state (`stateOf`, `corrupt` for one that cannot be used) is listed.
5865
5982
  */
5866
- get(approvalId: string): Promise<StoredApproval | null>;
5983
+ list(filter?: {
5984
+ inboxId?: string;
5985
+ states?: (ApprovalState | "corrupt")[];
5986
+ }): Promise<StoredApproval[]>;
5987
+ }
5988
+ //#endregion
5989
+ //#region src/config-hooks.d.ts
5990
+ /**
5991
+ * Test-only pauses inside `ConfigStore.convertToVersion3`, for the tests that prove a write racing the conversion is
5992
+ * neither lost nor let in between its scan and its write.
5993
+ *
5994
+ * Deliberately not exported from the package root, as `enableNamesMigrationForTests` is not (`release-gate.ts`): a
5995
+ * store is given them through its constructor under this symbol, which only a test importing this file by path can
5996
+ * name. No consumer of the package can reach it.
5997
+ */
5998
+ declare const CONVERSION_HOOKS: unique symbol;
5999
+ interface ConversionHooks {
6000
+ /** After the read before the lock, before the config lock is taken. */
6001
+ beforeLock?: (() => Promise<void> | void) | undefined;
6002
+ /** Under the lock, after the approvals were scanned and before the version-3 write is built. */
6003
+ afterScan?: (() => Promise<void> | void) | undefined;
6004
+ /** Under the lock, immediately before the version-3 write. */
6005
+ beforeWrite?: (() => Promise<void> | void) | undefined;
6006
+ }
6007
+ type SendPolicy = "chat" | "confirm" | "never";
6008
+ /**
6009
+ * How a loosening of the configuration is approved: `chat` — the person says yes in the conversation and the agent
6010
+ * claims the approval — or `confirm` — the person types a code at a terminal, as for a send under `confirm`.
6011
+ *
6012
+ * There is no `never`. A setting the software refused ever to loosen would leave editing the file by hand as the only
6013
+ * way to change it, and that passes no gate at all.
6014
+ */
6015
+ type ChangePolicy = "chat" | "confirm";
6016
+ /** 0.18's switch for the daily update check: what `defaults.updates` reads from until a level is set. */
6017
+ type UpdateCheckSetting = "on" | "off";
6018
+ type StoreKind = "keychain" | "file";
6019
+ interface ClientConfig {
6020
+ provider: string;
6021
+ clientId: string;
6022
+ projectId?: string | undefined;
6023
+ secretRef: string;
6024
+ addedAt: string;
5867
6025
  /**
5868
- * A look at one approval, under its lock: the record as it stands and its classification for `expect` — the same
5869
- * `NOT_FOUND` as every action for an id that is not what the caller expects, before anything of the record is
5870
- * classified. It writes only what reading derives (an expiry, a dead send's `unknown`), never a revocation: an owner
5871
- * removed or a stale epoch is shown as revoked here, and written by the next action.
6026
+ * The organisation profile that made this row and owns it (design 2026-10-02 §D4) — absent on a row a person
6027
+ * registered, or one a profile adopted. A row carrying it is the profile's to change: `org update` and `org remove`
6028
+ * change it, and `client add --replace` and `client remove` refuse it while its organisation has a record.
5872
6029
  *
5873
- * `action` is for a surface about to act on what it reads — a terminal about to show a preview, an execute about to
5874
- * read its draft — classified for that action (`approve`, `claim`, `revoke`), so the refusal is the one the action
5875
- * would get. A revocation the classification derives for it is written then, as the action itself would write it:
5876
- * the first locked action persists it, and this is that action's first step (design 2026-10-05 §D2).
6030
+ * Additive: client rows are loose, so a release that does not know the key keeps it through its writes.
5877
6031
  */
5878
- inspect(approvalId: string, expect?: ApprovalExpectation, options?: {
5879
- action?: OutcomeAction | undefined;
5880
- usedSaid?: UsedSaid | undefined;
5881
- }): Promise<{
5882
- stored: StoredApproval;
5883
- outcome: ApprovalOutcome;
5884
- }>;
6032
+ organisation?: string | undefined;
6033
+ }
6034
+ interface InboxConfig {
6035
+ /** Immutable id (`ibx_` + 16 base32 characters). Secrets and all state are keyed by it; the alias is a label. */
6036
+ id: string;
6037
+ provider: string;
6038
+ email: string;
6039
+ /** Stable account id (OIDC `sub`). Absent for inboxes imported from tools that never asked for it. */
6040
+ sub?: string | undefined;
6041
+ identity: "oidc" | "legacy";
6042
+ client: string;
6043
+ tier: string;
6044
+ contacts: boolean;
6045
+ grantedScopes: string[];
6046
+ secretRef: string;
6047
+ sendPolicy?: SendPolicy | undefined;
6048
+ /** Overrides `defaults.changePolicy` for changes to this inbox. */
6049
+ changePolicy?: ChangePolicy | undefined;
6050
+ internalDomains: string[];
6051
+ createdAt: string;
6052
+ }
6053
+ interface Defaults {
6054
+ sendPolicy: SendPolicy;
5885
6055
  /**
5886
- * D8's object for a record a call has just written, claimed or finished — what its result reports. Classified as a
5887
- * look at it would be, against a fresh read of the configuration, without the lock and writing nothing: what decided
5888
- * was the transition that wrote it. A configuration that cannot be read leaves only what the record says (nothing
5889
- * claimable): a report never fails the call it reports on, least of all one that has already sent.
6056
+ * How a loosening is approved wherever an inbox or account does not say otherwise. Absent reads as `chat`.
6057
+ *
6058
+ * Absent rather than filled in by the schema, unlike `sendPolicy`. A config written before this setting existed
6059
+ * reads as `chat` from then on, and the release note says so; a default the schema supplied would be written into
6060
+ * the file by the next unrelated change, where it would look like a choice somebody made.
5890
6061
  */
5891
- approvalOf(record: ApprovalRecord): Promise<ApprovalObject>;
6062
+ changePolicy?: ChangePolicy | undefined;
6063
+ riskEscalation: boolean;
6064
+ sendCaps: {
6065
+ perHour: number;
6066
+ perDay: number;
6067
+ };
6068
+ attachRoots: string[];
6069
+ /** Extra deny entries on top of the built-in list. */
6070
+ attachDeny: string[];
6071
+ downloadsDir?: string | undefined;
6072
+ timezone: string;
6073
+ confirm: {
6074
+ /**
6075
+ * Fail-closed allowlist of MCP `clientInfo.name` values whose form elicitation is trusted to reach a human.
6076
+ * Empty by default; only the CLI adds entries, after a probe.
6077
+ */
6078
+ elicitationClients: string[];
6079
+ };
5892
6080
  /**
5893
- * The classification of a record a call has just written, in any of its forms — a revoke's, a cancel's — for its
5894
- * result to show through `publicApproval`, as a status, a wait and a list show theirs: what a sender wrote only inside
5895
- * the untrusted-content envelope (design 2026-10-05 §D8). Classified as a look at it would be, against a fresh read of
5896
- * the configuration, without the lock and writing nothing, as `approvalOf` is; a configuration that cannot be read
5897
- * leaves nothing claimable, and never fails the call it reports on.
6081
+ * The daily update check (design 2026-09-28): once a day this machine asks npm whether a newer release is out, and
6082
+ * every server and command stops until it is updated or put off until tomorrow. Absent reads as `on`.
6083
+ *
6084
+ * Absent rather than filled in, as `changePolicy` is: a default the schema supplied would be written into the file
6085
+ * by the next unrelated change, where it would look like a choice somebody made. Turning it off is a loosening —
6086
+ * it is what keeps a machine from quietly falling behind a release — so it is approved like one; on applies at once.
5898
6087
  */
5899
- outcomeOf(stored: StoredApproval): Promise<ApprovalOutcome>;
5900
- /** Issues a new challenge to show a human; only its hash is kept. */
5901
- issueChallenge(approvalId: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5902
- usedSaid?: UsedSaid | undefined;
5903
- }): Promise<string>;
5904
- /** Issues the typed challenge that a terminal or the trusted desktop app must answer for disclosure. */
5905
- issueDisclosureChallenge(approvalId: string): Promise<string>;
6088
+ updateCheck?: UpdateCheckSetting | undefined;
5906
6089
  /**
5907
- * Approves one exact standing disclosure authority. MCP/chat forms are never an approval channel here: `app` means
5908
- * the desktop application's trusted typed-challenge bridge, not a client form.
6090
+ * How this machine updates (design 2026-10-16, U2): `auto`, `every-14-days` or `never`. Absent reads from
6091
+ * `updateCheck` — `on` as `auto`, `off` as `never` — and both absent as `auto`. A word this release does not know
6092
+ * reads as `auto`, and the file stays readable: a later release may add a level.
6093
+ *
6094
+ * Moving to a level that updates less is a loosening, approved like one; moving back applies at once. Every write of
6095
+ * a level writes `updateCheck` beside it, `off` for `never` and `on` otherwise, so a release before 0.20 sharing the
6096
+ * file checks, or does not, as the person chose.
5909
6097
  */
5910
- approveDisclosure(approvalId: string, liveBinding: DisclosureBinding, answer: string, via: "terminal" | "app"): Promise<DisclosureApprovalRecord>;
6098
+ updates?: string | undefined;
6099
+ }
6100
+ /**
6101
+ * A connected account on a platform that is not mail: a Slack workspace, and every channel after it (design
6102
+ * 2026-09-26) — one generic record, whatever the platform.
6103
+ *
6104
+ * `id`, `platform`, `workspace` (the container: a Slack team, a Resend team, a WhatsApp store), `userId` (who it acts
6105
+ * as), `tier`, `mode`, `grantedScopes`, `secretRef`, the two policies and `createdAt` are every channel's. A channel
6106
+ * may keep keys of its own beside them — Slack's `appId`, `redirectPort` — and they are kept through every write. It
6107
+ * may not add a safety setting: only `sendPolicy`, `changePolicy` and `mode` are judged by `classifyChange`, so a key
6108
+ * of a channel's own that loosened something would loosen it unasked.
6109
+ *
6110
+ * `secretRef` stays required, although the 2026-09-26 contract made it optional for a channel with no credential:
6111
+ * every earlier release requires it, and they share this file — an account without one would make the whole
6112
+ * configuration unreadable to a server started last week. Such a channel records a reference that names no secret,
6113
+ * `<platform>:none:<id>`, until a config version can say otherwise.
6114
+ *
6115
+ * It sits beside `inboxes` rather than replacing it. The spec asked for one `accounts` map holding everything, and
6116
+ * that rename is the one change this file cannot take: `version: 1` is additive precisely because an MCP server
6117
+ * started last week and a CLI run today share the file, and a release that moved every mailbox out of `inboxes`
6118
+ * would read, to the older of the two, as a config with no mailboxes in it. What the rename was for — one list of
6119
+ * everything connected, whatever the platform — is a question about the shape of the answer, not the shape of the
6120
+ * file, so `connectedAccounts()` provides it and the file grows one key.
6121
+ */
6122
+ interface AccountConfig {
6123
+ id: string;
6124
+ /** `slack`. */
6125
+ platform: string;
6126
+ /** The workspace or team id. Every other id this account sees is only meaningful inside it. */
6127
+ workspace: string;
6128
+ /** "Acme Corp" — shown to people, never matched on: a workspace can be renamed and stays the same workspace. */
6129
+ workspaceName?: string | undefined;
6130
+ /** This account's own user id in that workspace, so its own messages can be told from everyone else's. */
6131
+ userId: string;
6132
+ tier: string;
6133
+ grantedScopes: string[];
6134
+ /** Where its credential is in the secret store, `<platform>:…` — for a channel that stores none, `<platform>:none:<id>`. */
6135
+ secretRef: string;
6136
+ sendPolicy?: SendPolicy | undefined;
6137
+ /** Overrides `defaults.changePolicy` for changes to this account. */
6138
+ changePolicy?: ChangePolicy | undefined;
6139
+ createdAt: string;
5911
6140
  /**
5912
- * Claims an approved disclosure once, persisting the exact `usedAt` that recovery must subsequently reuse.
5913
- * A specialised caller may impose a shorter deadline; it is evaluated while this record's transition lock is held,
5914
- * so a check made before waiting on that lock cannot accidentally consume the approval after it has expired.
6141
+ * The OAuth client this account's token was issued by, and the app it belongs to.
6142
+ *
6143
+ * Recorded because the Gmail release found the opposite: a reauth used the first OAuth client in the config
6144
+ * rather than the inbox's own, and then did not record which one it had used. Slack makes that worse — D8
6145
+ * means **one app per workspace**, so "the first app" is wrong more often than it is right — and a reauth
6146
+ * that silently moves an account onto a different app changes what it can do without saying so.
6147
+ *
6148
+ * Optional because the key is additive: a config written before these existed parses unchanged.
5915
6149
  */
5916
- claimForDisclosure(approvalId: string, liveBinding: DisclosureBinding, options?: {
5917
- readonly deadlineMs?: number | undefined;
5918
- }): Promise<DisclosureApprovalRecord>;
6150
+ oauthClientId?: string | undefined;
6151
+ appId?: string | undefined;
5919
6152
  /**
5920
- * A human approved through a confirm channel by typing the issued challenge. The draft must still be exactly what the
5921
- * record was prepared for; otherwise the record is voided, because the human would be approving content the record
5922
- * does not describe. Three wrong answers void it too.
6153
+ * `read` or `send`, as installed.
5923
6154
  *
5924
- * A change is approved the same way, with `kind: 'change'` and its digest standing in for the draft (see
5925
- * `createChange`), so a person's typed code means one thing whichever kind of approval it is typed for.
6155
+ * Kept beside `grantedScopes` rather than derived from them, because the two answer different questions: the
6156
+ * scopes are what Slack granted, and this is what the person asked for. A disagreement between them is drift
6157
+ * worth reporting, and a value derived from the scopes could never disagree.
5926
6158
  */
5927
- approve(approvalId: string, via: ApprovalChannel, live: LiveDraft, answer: string, kind?: ApprovalKind, _platform?: NodeJS.Platform, options?: {
5928
- usedSaid?: UsedSaid | undefined;
5929
- }): Promise<ApprovalRecord>;
6159
+ mode?: string | undefined;
5930
6160
  /**
5931
- * Claims the record for sending, once. Non-consuming refusals (not yet approved) leave the record untouched so the
5932
- * human can still approve it; integrity failures (another account, an edited or changed draft, different recipients
5933
- * or subject) void it. Success creates the O_EXCL claim marker.
6161
+ * The loopback port the last sign-in used, which is the one in the app's redirect URL.
5934
6162
  *
5935
- * `live.inboxId` is the owner the caller claims for: a record of another owner is the one `NOT_FOUND`, and left as it
5936
- * is. The policy is the live gate's, from the configuration read under the record's lock (decision 4): a value the
5937
- * caller read before the lock is exactly what a `never` change can land between.
6163
+ * Slack matches redirect URLs exactly, so every later sign-in and every set of steps that edits the app has to
6164
+ * name this number. Without it the steps said `<port>`, or — from the MCP server — guessed 51234, and a person who
6165
+ * had chosen another port followed them to a sign-in that failed on the way back.
5938
6166
  */
5939
- claimForSend(approvalId: string, live: LiveDraft & {
5940
- inboxId: string;
5941
- inboxSub?: string | undefined;
5942
- expect: Expectation;
5943
- }, options?: ClaimOptions): Promise<SendClaim>;
6167
+ redirectPort?: number | undefined;
6168
+ /** The organisation profile whose Slack app created this account. Absent for a person's own app. */
6169
+ organisation?: string | undefined;
6170
+ /** Which app in that profile issued the account's current credential. */
6171
+ profileApp?: "read" | "send" | undefined;
6172
+ }
6173
+ type PendingRevocationStatus = "pending" | "revoked" | "expired";
6174
+ /** One bearer token in a superseded rotating bundle, tracked independently from its pair. */
6175
+ interface PendingRevocationTokenState {
6176
+ status: PendingRevocationStatus;
6177
+ /** The fixed absolute time after which the old token is known to be unusable. */
6178
+ deadline: string;
6179
+ }
6180
+ /**
6181
+ * A superseded credential kept until each token in it is conclusively revoked or expired.
6182
+ *
6183
+ * `store` belongs to the entry rather than being inferred from `secrets.store`: an older release can move the root
6184
+ * store without knowing to move this bundle, and cleanup must still find the only copy that exists.
6185
+ */
6186
+ interface PendingRevocation {
6187
+ ref: string;
6188
+ store: StoreKind;
6189
+ platform: string;
6190
+ workspace: string;
6191
+ createdAt: string;
6192
+ tokens: {
6193
+ access: PendingRevocationTokenState;
6194
+ refresh?: PendingRevocationTokenState | undefined;
6195
+ };
6196
+ }
6197
+ interface ConfigBody {
5944
6198
  /**
5945
- * Renews the sending lease of a send this call claimed: writes `sendingHeartbeatAt` while the claim still owns a
5946
- * `sending` record, and nothing otherwise. `renewed` while it is sending; `lost` once it reads `unknown` (or anything
5947
- * else) — a renewal never moves a record back to `sending`. Refused, writing nothing, without the claim's token.
6199
+ * The one secret backend for this config directory: client secrets, refresh tokens and the approval key. Absent
6200
+ * until the first command that stores a secret chooses it.
5948
6201
  */
5949
- heartbeat(approvalId: string, claimToken: string): Promise<"renewed" | "lost">;
6202
+ secrets?: {
6203
+ store: StoreKind;
6204
+ } | undefined;
6205
+ clients: Record<string, ClientConfig>;
6206
+ inboxes: Record<string, InboxConfig>;
6207
+ /** Non-mail accounts. Absent in every config written before this key existed, hence the default. */
6208
+ accounts: Record<string, AccountConfig>;
6209
+ /** Superseded rotating credentials which still need conclusive per-token cleanup. */
6210
+ pendingRevocations?: PendingRevocation[] | undefined;
6211
+ defaults: Defaults;
5950
6212
  /**
5951
- * The check before each provider step (design 2026-10-05 §D1): `go`, with the lease renewed, while this claim still
5952
- * owns a `sending` record; `stop` once it reads `unknown` — another caller has decided its outcome cannot be known,
5953
- * and no further step may start. Refused, writing nothing, without the claim's token.
6213
+ * How what an agent sends is labelled (design 2026-10-15-agent-label, L5): the word, the name, the rules that say
6214
+ * who a message appears to come from, and each Slack account's agent emoji.
6215
+ *
6216
+ * `unknown` here, on purpose: it is read only through `agentLabelOf`, which reads a value it does not understand
6217
+ * as the stricter one — an unknown word as `agent`, a rule list it cannot read as no rules — so a hand edit or a
6218
+ * later release's key can only tighten. Kept through every write as the schemas keep every key they do not name, and
6219
+ * judged by `classifyChange`.
5954
6220
  */
5955
- fence(approvalId: string, claimToken: string): Promise<"go" | "stop">;
6221
+ agentLabel?: unknown;
5956
6222
  /**
5957
- * A change approval, pending, bound to `changeDigest(input.change)`.
6223
+ * What an agent may do (design 2026-10-19-agent-behaviour-rules, B3): `{ rules: [...] }`, each rule the acts allowed
6224
+ * in one account or one of its conversations.
5958
6225
  *
5959
- * The digest is computed here, never taken from the caller, so a record cannot claim to be bound to one change while
5960
- * describing another. It stands in for the draft revision too, as a reaction's does: a change has no draft, and the
5961
- * same value means the same change.
6226
+ * `unknown` here, as `agentLabel` is, and for the same reason the other way round: it is read only through
6227
+ * `readBehaviourSettings`, which reads a value it does not understand as allowing less — an unknown act dropped, a
6228
+ * rule it cannot read as its account allowing nothing — never as no rules. Kept through every write as the schemas
6229
+ * keep every key they do not name, so older releases keep it too, and judged by `classifyChange`.
5962
6230
  */
5963
- createChange(input: CreateChangeApprovalInput): Promise<ApprovalRecord>;
6231
+ behaviour?: unknown;
6232
+ }
6233
+ interface ConfigV1 extends ConfigBody {
6234
+ version: 1;
6235
+ }
6236
+ /** A name an account used to have, and the account that had it. */
6237
+ interface FormerName {
6238
+ /** What it was renamed to — at the time. The account's current name is found by `id`, so a later rename is followed. */
6239
+ name: string;
6240
+ id: string;
6241
+ }
6242
+ /**
6243
+ * Names that were replaced, per kind, and never reusable.
6244
+ *
6245
+ * Per kind because version 1 lets a mailbox and a workspace share a word: `work` the mailbox and `work` the workspace
6246
+ * become `work/gmail` and `work/slack`, and one flat record could not say which old `work` is which.
6247
+ */
6248
+ interface FormerNames {
6249
+ inboxes: Record<string, FormerName>;
6250
+ accounts: Record<string, FormerName>;
6251
+ }
6252
+ /** Who an organisation's Google client is for, as its administrator states it (design 2026-10-02 §D2). */
6253
+ type OrganisationServes = "any" | {
6254
+ domains: string[];
6255
+ };
6256
+ /**
6257
+ * One Google client an organisation profile brought here: a *generation* (design 2026-10-02 §D4).
6258
+ *
6259
+ * `owned` — the profile made the client row `name` (`rgc-1`), which carries `organisation`, and its secret is the
6260
+ * profile's to rewrite. `adopted` — the row was already here under a name a person chose (they ran `client add` with
6261
+ * the organisation's file), and stays theirs: no `org` command changes or removes it. `serves` is per generation,
6262
+ * because it is a fact about one client, not about the organisation.
6263
+ */
6264
+ interface OrganisationGeneration {
6265
+ name: string;
6266
+ clientId: string;
6267
+ projectId?: string | undefined;
6268
+ ownership: "owned" | "adopted";
6269
+ serves: OrganisationServes;
6270
+ addedAt: string;
6271
+ }
6272
+ interface OrganisationSlackApp {
6273
+ clientId: string;
6274
+ appId?: string | undefined;
6275
+ }
6276
+ /**
6277
+ * An organisation's update caps, as the file beside its profile names them (design 2026-10-16, U8), and the SHA-256 of
6278
+ * that file, so `org update` can tell the organisation changing them from the record drifting. A `max` word this release
6279
+ * does not know reads as `never`, the strictest it could mean: caps only ever tighten.
6280
+ */
6281
+ interface OrganisationUpdates {
6282
+ max?: string | undefined;
6283
+ background?: boolean | undefined;
6284
+ sha256: string;
6285
+ }
6286
+ /**
6287
+ * The organisation's bot-only agent app, as its profile's agent-app file names it (CUE-493): the app's id, and the
6288
+ * SHA-256 of that file, so `org update` can tell the organisation changing it from the record drifting. No client id:
6289
+ * nobody signs in through it — its administrator installs it once and its members paste its bot token.
6290
+ */
6291
+ interface OrganisationAgentApp {
6292
+ appId: string;
6293
+ sha256: string;
6294
+ }
6295
+ /**
6296
+ * What this machine records of one organisation profile: never its bytes and never its secret — where it was read
6297
+ * from, the SHA-256 of what was read and when, the generations of its Google client, and its Slack apps.
6298
+ */
6299
+ interface OrganisationRecord {
6300
+ label: string;
6301
+ /** Read again, from this absolute path, by every `org update`. `file` is the one kind version 1 knows (§D3). */
6302
+ source: {
6303
+ kind: string;
6304
+ path: string;
6305
+ };
6306
+ sha256: string;
6307
+ readAt: string;
6308
+ addedAt: string;
6309
+ /** Whether the member asked for this organisation's client to serve addresses outside it too (§D6). */
6310
+ forOtherAddresses: boolean;
6311
+ /** `active` names the generation new mailboxes get — none once a profile stops naming a Google client. */
6312
+ gmail?: {
6313
+ active: string | null;
6314
+ generations: OrganisationGeneration[];
6315
+ } | undefined;
6316
+ slack?: {
6317
+ workspace: string;
6318
+ workspaceName: string;
6319
+ redirectPort: number;
6320
+ apps: {
6321
+ read?: OrganisationSlackApp | undefined;
6322
+ send?: OrganisationSlackApp | undefined;
6323
+ agent?: OrganisationAgentApp | undefined;
6324
+ };
6325
+ } | undefined;
6326
+ /** The organisation's update caps, from the file beside its profile; absent when it names none. */
6327
+ updates?: OrganisationUpdates | undefined;
5964
6328
  /**
5965
- * Claims a change approval, once, for the change the caller is about to write.
5966
- *
5967
- * `live.change` is the change as the caller computes it now, and it has to digest to what was prepared: a different
5968
- * path, a different value, a different account or a different effect voids the approval, because the person agreed
5969
- * to something else. `live.policy` is the change policy in force now, and the stricter of it and the one at prepare
5970
- * decides — so tightening the policy after an agent prepared a change takes effect on that change, and loosening it
5971
- * does not release one prepared under `confirm`.
6329
+ * The entries of the profile's `extensions` this release did not know and skipped, being optional (design 2026-10-19,
6330
+ * X3): the release that first understands each, and the SHA-256 of the entry's canonical JSON — as
6331
+ * `Record<string, OrganisationIgnoredExtension>`. Kept in step with the profile. Absent when none was skipped.
5972
6332
  *
5973
- * Under `chat` a pending approval is claimable: the yes was given in the conversation. Anything stricter needs a
5974
- * person to have typed the code at a terminal first; until then the refusal leaves the record as it is.
6333
+ * Held as the file holds it, whatever its shape, and read only through `ignoredListOf`, which keeps the well-formed
6334
+ * entries and names the rest: 0.21.0 is the first release to read it, so a shape a later release or a hand gave it
6335
+ * must never make the whole configuration unreadable, nor be dropped by an unrelated write. Losing it is safe: a
6336
+ * later release that understands an entry judges applying it by its classifier whether or not the list names it —
6337
+ * a tightening at once, anything else approved first, never a repair of drift (X4).
5975
6338
  */
5976
- claimForChange(approvalId: string, live: LiveChange, options?: ClaimOptions): Promise<ApprovalRecord>;
6339
+ ignoredExtensions?: unknown;
6340
+ }
6341
+ interface ConfigV2 extends ConfigBody {
6342
+ version: 2;
6343
+ formerNames: FormerNames;
5977
6344
  /**
5978
- * A download's question, pending, bound to `downloadDigest(input.download)` — computed here, never taken from the
5979
- * caller, as a change's digest is. It stands in for the draft revision too.
5980
- *
5981
- * `policy` is the change policy of the mailbox or workspace the files come from, as it stands when the question is
5982
- * asked: where a stranger's files land on this machine is a change to it, and is answered the way that account's
5983
- * other changes are approved. A claim holds the question to the stricter of this and the policy then.
6345
+ * The organisation profiles added here, by organisation word (design 2026-10-02 §D4). Absent reads as none, and
6346
+ * stays absent until the first `org add`: a key the schema filled in would be written into every file by the next
6347
+ * unrelated change.
5984
6348
  */
5985
- createDownload(input: {
5986
- /** The manifest channel the files come from. */
5987
- channel: string;
5988
- download: DownloadBinding;
5989
- policy: ChangePolicy;
5990
- }): Promise<ApprovalRecord & {
5991
- download: DownloadBinding;
5992
- }>;
6349
+ organisations?: Record<string, OrganisationRecord> | undefined;
6350
+ }
6351
+ /** What a legacy drain records of one tracked record: still open, or what became of it. */
6352
+ type LegacyDrainOutcome = "revoked" | "expired" | "used" | "failed" | "sending" | "unknown";
6353
+ /**
6354
+ * The approvals an earlier release prepared that were still waiting, or already being sent, when this configuration
6355
+ * became version 3, and what has become of each (design 2026-10-05 §D1, "Old releases are locked out before the fence
6356
+ * is relied on").
6357
+ */
6358
+ interface LegacyDrain {
6359
+ /** When the conversion that opened the drain was written. */
6360
+ since: string;
6361
+ /**
6362
+ * Every tracked version-1 send record by id: `open` until it has an outcome — or `sending` from the start, for one an
6363
+ * earlier release was already sending when the conversion ran.
6364
+ */
6365
+ tracked: Record<string, "open" | LegacyDrainOutcome>;
6366
+ }
6367
+ /** What version 3 adds to a body of either naming. */
6368
+ interface Version3Fields {
6369
+ /** Each owner's send epoch by id; absent reads as 0. Raised by every write that turns its send policy to `never`. */
6370
+ sendEpochs?: Record<string, number> | undefined;
6371
+ /**
6372
+ * The standing epoch (design 2026-10-19-standing-approvals, S8); absent reads as 0. Raised by `standing off` and by
6373
+ * every write that turns a change policy from `chat` to `confirm`, and never lowered: a standing approval made under
6374
+ * an earlier one has ended.
6375
+ */
6376
+ standingEpoch?: number | undefined;
6377
+ /** Present while records from an earlier release are being retired. */
6378
+ legacyDrain?: LegacyDrain | undefined;
6379
+ }
6380
+ /** Version 3 over a version-1 body: one plain word per account. */
6381
+ interface ConfigV3Naming1 extends ConfigBody, Version3Fields {
6382
+ version: 3;
6383
+ naming: 1;
6384
+ }
6385
+ /** Version 3 over a version-2 body: `organisation/platform` names, and the names they replaced. */
6386
+ interface ConfigV3Naming2 extends Omit<ConfigV2, "version">, Version3Fields {
6387
+ version: 3;
6388
+ naming: 2;
6389
+ }
6390
+ /**
6391
+ * Version 3: the send epoch and the legacy drain (design 2026-10-05 §D1), over either naming — converted from version 1
6392
+ * or 2 without renaming anything, so its body keeps the rules of the version it came from.
6393
+ */
6394
+ type ConfigV3 = ConfigV3Naming1 | ConfigV3Naming2;
6395
+ type Config$1 = ConfigV1 | ConfigV2 | ConfigV3;
6396
+ /** A configuration whose accounts are named `organisation/platform`: version 2, or version 3 over it. */
6397
+ type NamedConfig = ConfigV2 | ConfigV3Naming2;
6398
+ /** A configuration whose accounts have one plain word each: version 1, or version 3 over it. */
6399
+ type PlainConfig = ConfigV1 | ConfigV3Naming1;
6400
+ /** The committed config returned by an update; process-only commit metadata is deliberately stored out of band. */
6401
+ type ConfigUpdateResult = Config$1;
6402
+ declare class ConfigStore {
6403
+ #private;
6404
+ readonly path: string;
5993
6405
  /**
5994
- * Records the person's answer to a download's question, given where an agent cannot give it: at their own terminal,
5995
- * or in a form a trusted client showed them. The question moves to `approved`, carrying the answer, and waits for
5996
- * the download to claim it — which it may then do under `confirm`, and which saves where this answer says.
5997
- *
5998
- * Only a pending question can be answered, and only once: a second answer is refused, not taken over the first.
6406
+ * @param options.handoffs the printing package's handoffs (`core.handoffs`, CUE-403), for the command a refusal
6407
+ * names; left out, a refusal that has to name one is a programming error (`requiredHandoffs`).
5999
6408
  */
6000
- answerDownload(approvalId: string, via: ApprovalChannel, answer: RecordedSaveAnswer, _platform?: NodeJS.Platform): Promise<ApprovalRecord & {
6001
- download: DownloadBinding;
6002
- }>;
6409
+ constructor(configDir: string, options?: {
6410
+ readonly handoffs?: CliHandoffs | undefined;
6411
+ /** Test-only pauses inside the version-3 conversion (`config-hooks.ts`); unreachable from the package root. */
6412
+ readonly [CONVERSION_HOOKS]?: ConversionHooks | undefined;
6413
+ });
6414
+ /** The current config; an empty one when the file does not exist yet. */
6415
+ load(): Promise<Config$1>;
6003
6416
  /**
6004
- * Claims a download's question, once, for the download the caller is about to make — and returns it, with the
6005
- * folders it offered, so that `downloads` and `current` in the answer mean the paths the person read, and with the
6006
- * answer the person recorded, when they recorded one.
6007
- *
6008
- * `live` is the download as the caller computes it now: the same account, the same request, the same files under
6009
- * the same names — the person answered for those files and no others. Any other is refused, and the question left
6010
- * open, as it was: a second call that got an argument wrong is the agent's slip, not the person's, and voiding the
6011
- * question for it would make the person answer again for nothing. It still expires, and is claimed once.
6012
- *
6013
- * Whether it can be claimed is the download `claimable` matrix (`downloadClaimable`, design 2026-10-05 §D2), decided
6014
- * under the question's lock against the live change policy read there — never a policy the caller read before it.
6015
- * While both the policy the question was asked under and the live one are `chat`, a pending question is claimed
6016
- * with the answer the caller carries: the person gave it in the conversation, and it is never kept. Otherwise only a
6017
- * question the person answered at a terminal or in a trusted form can be claimed; a pending one is refused, and left
6018
- * as it is, so they can still answer it. One used, voided, expired or corrupt is refused for what it is.
6019
- *
6020
- * `options.signal` is the download's cancellation, asked under the lock as a send's is (`ClaimOptions.signal`): a
6021
- * download cancelled while this waited saves nothing and leaves the question open, the answer still unused.
6417
+ * Read-modify-write under a lock, so a CLI command and a running MCP server never lose each other's changes. The
6418
+ * mutator receives a fresh copy read inside the lock and returns the new config, which is validated before writing.
6022
6419
  */
6023
- claimForDownload(approvalId: string, live: DownloadRequest, options?: {
6024
- pendingHint?: string | undefined;
6420
+ update(mutator: (config: Config$1) => Config$1 | Promise<Config$1>, options?: {
6421
+ consent?: LooseningConsent;
6025
6422
  signal?: AbortSignal | undefined;
6026
- platform?: NodeJS.Platform | undefined;
6027
- handoffs?: CliHandoffs | undefined;
6028
- }): Promise<ApprovalRecord & {
6029
- download: DownloadBinding;
6030
- }>;
6423
+ }): Promise<ConfigUpdateResult>;
6031
6424
  /**
6032
- * Records the outcome of the one send attempt: `used` with the provider's id for it, when the provider accepted it,
6033
- * or `failed`.
6425
+ * The one way a version-1 config becomes version 2.
6034
6426
  *
6035
- * `used` needs a non-empty id. A provider that accepted a send without one has not given anything to record it by,
6036
- * so the record is left as it is — `sending`, and then `unknown` — rather than made `used` with an empty id that
6037
- * every later reader would take for one.
6427
+ * Under the credentials lock and then the config lock — the order everything takes them in — so it cannot land
6428
+ * between a removal's read and its write, or in the middle of moving secrets between backends.
6038
6429
  *
6039
- * Only the claimant records it, by its claim token, from `sending` or from `unknown`: a provider's answer that
6040
- * arrives after the lease ran out is still the truth. A late success clears the stale-lease reason; a late failure
6041
- * replaces it with its own.
6042
- */
6043
- complete(approvalId: string, claimToken: string, outcome: SendOutcome): Promise<ApprovalRecord>;
6044
- /**
6045
- * Voids a pending or approved record. Other records are left as they are, and returned as they read.
6430
+ * `expected` is the fingerprint of the config the caller previewed. The file is read again inside the locks, and
6431
+ * if it is not that config any more the whole thing is refused: somebody confirmed a mapping computed from
6432
+ * something else. Nothing waits for a person while holding a lock; the preview happens before this is called.
6046
6433
  *
6047
- * `disposition` says whose decision this is, and has no default: every caller chooses (`RevokeDisposition`). A
6048
- * version-1 record is retired in its own shape when a person or an owner's removal revokes it (`revokeLegacy`), and
6049
- * refused with the version refusal, nothing written, when this release's integrity check would have voided it. A
6050
- * corrupt or unreadable record is never revoked or rewritten: it is refused with the integrity refusal, which repeats
6051
- * only its stub.
6052
- */
6053
- revoke(approvalId: string, reason: string, options: {
6054
- disposition: RevokeDisposition;
6055
- expect?: ApprovalExpectation | undefined;
6056
- }): Promise<StoredApproval>;
6057
- /**
6058
- * Retires a record from an earlier release (version 1) in that release's own shape, so a process still running it
6059
- * can no longer claim the record — and nothing more.
6434
+ * `build` produces version 2 from the locked snapshot. What it may change is checked rather than trusted: the same
6435
+ * accounts, byte for byte, under new keys — a rename grants nothing, so a build that changed anything else is a
6436
+ * bug and is refused before it is written.
6060
6437
  *
6061
- * Under the record's own lock, `<id>.json.lock`, the one 0.13.0's transitions take, so the two serialise. The file
6062
- * is read again there, decoded, and its state derived by its own release's rules (`deriveLegacyV1State`). Only a
6063
- * `pending` or `approved` record is rewritten, as itself plus `state: 'revoked'`, the reason and `updatedAt`: its
6064
- * digest version stays 1, a send stays without a `kind`, and no field of this release's is added — no claim marker
6065
- * either. Any other derived state is returned as it reads and written nothing: an expired record is never rewritten,
6066
- * as `revoke` leaves every finished record as it is.
6438
+ * Idempotent, but only for this plan. A retry after a write that committed — even one whose lock release then
6439
+ * failed — finds version 2, recognises its own mapping in it, and says so. Version 2 that does *not* carry this
6440
+ * mapping is somebody else's migration; saying "already migrated" there would report a mapping nobody applied,
6441
+ * and a caller updating registrations from it would point them at names that do not exist.
6067
6442
  *
6068
- * It reads no configuration and needs no owner, so it works on a record whose account is already gone. Its only
6069
- * callers are a person's revoke, the removal of the record's owner, and the legacy drain.
6070
- */
6071
- revokeLegacy(approvalId: string, reason: string): Promise<LegacyApprovalRecord>;
6072
- /**
6073
- * Every approval file, each looked at under its own lock and classified there, as `inspect` looks at one — against one
6074
- * configuration snapshot, loaded once before any file is read (design 2026-10-05 §D2, §D8). What a list shows: each
6075
- * record's form and its outcome, nothing skipped. Like a look, it writes only what reading derives — an expiry at its
6076
- * boundary, a stale send's `unknown` — never a revocation: an owner removed or a stale epoch is shown as revoked, and
6077
- * written by the next action.
6443
+ * `rows` is the plan, and the question is asked of the mapping rather than of the whole file: between a
6444
+ * committed write and its retry, something else may have changed a policy or a timezone, and a retry refused
6445
+ * over that would be idempotency in name only. The rows are checked here rather than by whoever built them,
6446
+ * for the same reason `build` is: a caller that could answer its own question could answer it wrongly.
6078
6447
  *
6079
- * `inboxId` keeps the records whose owner is that id (`ownerOf`), so one whose owner cannot be trusted never matches;
6080
- * `states` keeps those whose classified state is listed — a stored state, `corrupt`, and for a download's answered
6081
- * question its stored `approved` or `used`.
6448
+ * The file it replaces is copied beside it first — see `backUpBeforeMigration` — and the copy's path returned.
6082
6449
  */
6083
- inspectAll(filter?: {
6084
- inboxId?: string;
6085
- states?: (ApprovalState | "corrupt")[];
6086
- }): Promise<Array<{
6087
- stored: StoredApproval;
6088
- outcome: ApprovalOutcome;
6089
- }>>;
6450
+ migrateNames(expected: string, rows: readonly RenamedAccount[], build: (current: PlainConfig) => NamedConfig): Promise<{
6451
+ status: "migrated" | "already-migrated";
6452
+ config: NamedConfig;
6453
+ backup?: string;
6454
+ }>;
6090
6455
  /**
6091
- * Every approval file, each in one of its four forms — nothing skipped, nothing omitted — read against one
6092
- * configuration snapshot, loaded once before any file is read: a loader that fails fails the whole list.
6456
+ * The one way a configuration becomes version 3 (design 2026-10-05 §D1, "Old releases are locked out before the
6457
+ * fence is relied on").
6093
6458
  *
6094
- * `inboxId` keeps the records whose owner is that id (`ownerOf`), so a file whose owner cannot be trusted never
6095
- * matches one; `states` keeps those whose state (`stateOf`, `corrupt` for one that cannot be used) is listed.
6459
+ * Version 3 carries the send epoch, which 0.13 does not know, and 0.13 refuses to read it — so from this write on, no
6460
+ * 0.13 process can start a prepare, a claim, an approval or a policy change through this file. Nothing is renamed:
6461
+ * a version-1 body stays version 1's (`naming: 1`) until the names migration, which needs its own approval.
6462
+ *
6463
+ * Under the config lock, the file is read again — never from the cache, never from the read before the lock — and an
6464
+ * existing version 3 returned unchanged: a second converter that lost the race keeps the winner's epochs and drain.
6465
+ * `scan` runs under the same lock, so a record written while this waited for it is seen, and an update cannot land
6466
+ * between the scan and the write; the version-3 write carries `legacyDrain` only when the scan tracked something.
6467
+ * With no config file at all there is nothing to convert, and nothing is written.
6096
6468
  */
6097
- list(filter?: {
6098
- inboxId?: string;
6099
- states?: (ApprovalState | "corrupt")[];
6100
- }): Promise<StoredApproval[]>;
6101
- }
6102
- //#endregion
6103
- //#region src/ledger.d.ts
6104
- interface Caps {
6105
- perHour: number;
6106
- perDay: number;
6107
- }
6108
- interface CapStatus {
6109
- hour: number;
6110
- day: number;
6111
- /** When the next slot frees up, if a cap is reached. */
6112
- resetAt?: string;
6469
+ convertToVersion3(scan: () => Promise<LegacyDrain["tracked"]>, options?: {
6470
+ now?: (() => Date) | undefined;
6471
+ }): Promise<{
6472
+ status: "converted" | "already" | "empty";
6473
+ config: Config$1;
6474
+ }>;
6113
6475
  }
6114
- declare class SendLedger {
6115
- #private;
6116
- readonly directory: string;
6117
- constructor(stateDir: string, now?: () => Date);
6118
- /** Counts sends (including in-flight reservations) in the last hour and day. */
6119
- status(inboxId: string, caps: Caps): Promise<CapStatus>;
6120
- /** Reserves a slot or refuses with APPROVAL_REQUIRED and the time the cap resets. Atomic across processes. */
6121
- reserve(inboxId: string, approvalId: string, caps: Caps): Promise<CapStatus>;
6122
- /** Releases a reservation whose send did not happen. */
6123
- release(inboxId: string, approvalId: string): Promise<void>;
6476
+ /** One account's rename, as the migration planned it. */
6477
+ interface RenamedAccount {
6478
+ readonly kind: "inbox" | "account";
6479
+ readonly from: string;
6480
+ readonly to: string;
6481
+ readonly id: string;
6124
6482
  }
6125
- //#endregion
6126
- //#region src/plans.d.ts
6483
+ /** A value a loosened setting had or will have: a policy, a mode, a list, a pair of caps, a path, or nothing. */
6484
+ type SettingValue = string | number | boolean | readonly string[] | {
6485
+ readonly [key: string]: unknown;
6486
+ } | null;
6127
6487
  /**
6128
- * Bulk mailbox changes (more than 20 messages, anything driven by a query, and every trash) are two-step: a dry run
6129
- * returns a plan token bound to the exact set of ids and the exact change; executing requires that token, once, within
6130
- * ten minutes. The ids are resolved at dry-run time, so a query cannot silently grow between preview and execution.
6488
+ * One loosened setting, with the values the classifier compared.
6489
+ *
6490
+ * The values are the ones it judged, not the raw fields: an inbox that inherits a looser default reports the policy
6491
+ * it inherits, and a new inbox reports the default it was measured against. A preview built from anything else could
6492
+ * show a person one change while the classifier judged another.
6131
6493
  */
6132
- interface PlanRecord {
6133
- token: string;
6134
- inboxId: string;
6135
- operation: string;
6136
- /** Digest of the operation parameters (labels added/removed, archive, trash…). */
6137
- paramsDigest: string;
6138
- /** Digest of the sorted id list. */
6139
- idsDigest: string;
6140
- count: number;
6141
- createdAt: string;
6142
- expiresAt: string;
6143
- }
6144
- declare class PlanStore {
6145
- #private;
6146
- readonly directory: string;
6147
- constructor(stateDir: string, now?: () => Date);
6148
- create(input: {
6149
- inboxId: string;
6150
- operation: string;
6151
- params: unknown;
6152
- ids: readonly string[];
6153
- }): Promise<PlanRecord>;
6494
+ interface Loosening {
6495
+ /** As `ConfigStore.update` names it in a refusal: `accounts.rgc/slack.mode`, `defaults.sendPolicy`. */
6496
+ readonly path: string;
6497
+ readonly before: SettingValue;
6498
+ readonly after: SettingValue;
6154
6499
  /**
6155
- * Consumes a plan exactly once. It must be for the same inbox, operation, parameters and ids; the file is deleted
6156
- * inside the lock, so a second consume fails.
6500
+ * The inbox or account measured on the before side, by id. Absent for one new in this change and for a setting
6501
+ * that belongs to the whole configuration. Carried so that an approval of this loosening cannot be spent on a
6502
+ * different account that took the same name in the meantime.
6157
6503
  */
6158
- consume(token: string, expected: {
6159
- inboxId: string;
6160
- operation: string;
6161
- params: unknown;
6162
- ids: readonly string[];
6163
- }): Promise<PlanRecord>;
6504
+ readonly id?: string | undefined;
6164
6505
  }
6165
- //#endregion
6166
- //#region src/secrets.d.ts
6167
6506
  /**
6168
- * Where refresh tokens, client secrets and approval keys live. Two backends, chosen per inbox or client and recorded
6169
- * in config; the choice is never switched at runtime and there is no silent fallback from one to the other.
6507
+ * One setting a change writes, from the value in the file before to the value after — whichever way it moves.
6170
6508
  *
6171
- * - `keychain`: the OS store (macOS Keychain, Windows Credential Manager, Linux Secret Service — pinned, never the
6172
- * kernel keyring, which forgets everything on reboot) through the optional `@napi-rs/keyring` package.
6173
- * - `file`: one owner-only JSON file per secret in an owner-only directory. Chosen explicitly, never by default.
6509
+ * Unlike a `Loosening`, these are the raw values, not the effective ones: `null` for a setting the file does not
6510
+ * hold, so a mailbox set to `chat` itself is told apart from one inheriting `chat`, which only the first survives a
6511
+ * later change to the default. `id` is the inbox or account measured on the before side, as for a loosening.
6174
6512
  */
6175
- type SecretStoreKind = "keychain" | "file";
6176
- interface SecretStore {
6177
- readonly kind: SecretStoreKind;
6178
- get(ref: string): Promise<string | null>;
6179
- set(ref: string, value: string): Promise<void>;
6180
- delete(ref: string): Promise<boolean>;
6181
- /** Forgets any cached value, so the next read goes to the backend (e.g. after another process re-authorised). */
6182
- invalidate(ref: string): void;
6513
+ interface SettingChange {
6514
+ readonly path: string;
6515
+ readonly before: SettingValue;
6516
+ readonly after: SettingValue;
6517
+ readonly id?: string | undefined;
6518
+ }
6519
+ /**
6520
+ * Proof that exactly these paths may loosen: produced by a CLI after a person at a terminal typed a challenge, or by
6521
+ * `claimChange` from a change approval a person gave.
6522
+ */
6523
+ interface LooseningConsent {
6524
+ kind: "loosening-consent";
6525
+ paths: readonly string[];
6183
6526
  /**
6184
- * Resolves once no earlier call is still occupying the backend. Absent on a store with nothing to wait for.
6527
+ * What each path was approved to move between, when the consent came from a change approval.
6185
6528
  *
6186
- * For a caller that must not give up on a write: the keychain fails every call fast while a timed-out native call
6187
- * is still held by an OS dialog, so retrying on a timer spends every attempt against the same stuck call. A
6188
- * refresh that has already spent Slack's single-use token is that caller — it has one value it cannot get again,
6189
- * and waiting for the dialog to be answered is the only retry that can succeed.
6529
+ * A terminal challenge is answered moments before the write it permits, so the paths are enough. An approval is
6530
+ * prepared first and applied later — after a sign-in, possibly minutes later — and in between the configuration
6531
+ * can move underneath it: the same path, loosened from a different value, or on a different account that took
6532
+ * the name. With these, `ConfigStore.update` refuses any loosening that is not exactly one of them.
6190
6533
  */
6191
- settled?(): Promise<void>;
6534
+ changes?: readonly Loosening[] | undefined;
6192
6535
  }
6193
6536
  //#endregion
6194
- //#region src/state.d.ts
6195
- /**
6196
- * Per-inbox runtime facts that any process may update (last successful refresh, last use, health). Kept out of
6197
- * config.json so a running server never rewrites user intent — a stale in-memory copy of config written back by a
6198
- * server could otherwise silently undo a policy the user just tightened.
6199
- */
6200
- interface InboxRuntimeState {
6201
- lastRefreshOkAt?: string | undefined;
6202
- lastUsedAt?: string | undefined;
6203
- /** Set to `undefined` to clear it: the merged value is dropped when the file is written. */
6204
- lastError?: {
6205
- code: string;
6206
- message: string;
6207
- at: string;
6208
- } | undefined;
6209
- grantedScopes?: string[] | undefined;
6210
- refreshTokenExpiresAt?: string | undefined;
6211
- }
6212
- declare class InboxStateStore {
6213
- #private;
6214
- readonly directory: string;
6215
- constructor(stateDir: string);
6216
- get(inboxId: string): Promise<InboxRuntimeState>;
6217
- /** Merges `patch` into the stored state under a per-inbox lock. */
6218
- update(inboxId: string, patch: Partial<InboxRuntimeState>): Promise<InboxRuntimeState>;
6219
- }
6537
+ //#region src/cli-command.d.ts
6220
6538
  /**
6221
- * A platform identifier that is not an email address: a Slack user or conversation id.
6539
+ * The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
6222
6540
  *
6223
- * It carries its scope because, unlike an address, it is not globally unique — `U024BE7LH` names a different person
6224
- * in every workspace that happens to mint that id. The address store is deliberately cross-inbox, on the reasoning
6225
- * that a message read in one mailbox can ask for a send from another; that reasoning does not transfer here, and a
6226
- * store that matched ids across workspaces would flag an unrelated person every time two workspaces collided.
6541
+ * A bare `agent-gmail approve …` is not runnable for most of the people it is printed for: a package run by npx, by a
6542
+ * managed runtime or from a checkout puts nothing on their PATH. So every such command names this process's Node and
6543
+ * a checked file of the product it runs, with the suite directories it uses pinned as options — and when no file can
6544
+ * be checked, there is no command at all, only a sentence saying what is missing. The caller says which package it is
6545
+ * and what to run; it never supplies the program or quotes the line.
6227
6546
  *
6228
- * The id, never the display name. `@sam` is set by the account that bears it and can be changed to `@finance-bot`
6229
- * between the message being read and the send being checked; the id cannot.
6547
+ * Three directions. A product's own CLI is found from the product itself, walking from one of its own modules to its
6548
+ * manifest: no registration is ever asked. A channel's handoff to core is found the way the channel itself finds core,
6549
+ * through its installed dependency, and only the same release. Another product's command is found among the servers
6550
+ * registered with the person's MCP clients, which the caller scanned and passes in: only a file-backed registration of
6551
+ * exactly the printing package's version, checked on disk. Nothing a registration names is run, or asked anything —
6552
+ * not even its interpreter for a version: no installer wrote down where a scanned entry came from, so none is trusted
6553
+ * to run (D1). This module never scans, spawns or probes.
6554
+ *
6555
+ * What is checked is checked when the command is printed, not for ever (D6): the file it names is real, readable and
6556
+ * inside its package then, and a later change to that installation is a later fact.
6230
6557
  */
6231
- interface TaintHandle {
6232
- /** `slack`. Lower-cased on the way in. */
6233
- platform: string;
6234
- /** The workspace or team the id belongs to — a Slack team id. */
6235
- scope: string;
6236
- /** `U024BE7LH`, `C0123`, `D0456`. Passed in the platform's own canonical form; core does not know its shape. */
6237
- id: string;
6238
- }
6239
- type TaintSource = "header" | "body";
6240
- /** Why the content reached this process. This is separate from `TaintSource`, which remains the cap priority. */
6241
- type TaintOrigin = "read" | "event";
6242
- interface TaintObservation {
6243
- address: string;
6244
- source: TaintSource;
6245
- /** Old readers did not record this, so an omitted origin remains an ordinary read. */
6246
- origin?: TaintOrigin | undefined;
6247
- inboxId: string;
6248
- messageId?: string | undefined;
6249
- }
6250
- /** The same observation for a handle. `inboxId` is the account that did the reading, as it is for an address. */
6251
- interface TaintHandleObservation {
6252
- handle: TaintHandle;
6253
- source: TaintSource;
6254
- /** Old readers did not record this, so an omitted origin remains an ordinary read. */
6255
- origin?: TaintOrigin | undefined;
6256
- inboxId: string;
6257
- messageId?: string | undefined;
6258
- }
6259
- /** What is never recorded: the user's own addresses and the domains they call internal. */
6260
- interface TaintExclusions {
6261
- ownAddresses: readonly string[];
6262
- internalDomains: readonly string[];
6263
- /**
6264
- * Handles that are never recorded: the account's own user id, and whoever the caller treats as internal — the
6265
- * counterpart of `internalDomains`, decided per platform because "internal" means a domain for mail and
6266
- * workspace membership for Slack, and core should not be the thing that knows the difference.
6267
- */
6268
- ownHandles?: readonly TaintHandle[] | undefined;
6269
- }
6558
+ /** The gate a constructor call has to pass: only this module holds it. */
6559
+ declare const GATE: unique symbol;
6270
6560
  /**
6271
- * One stored aggregate, exactly as the store holds it (design 2026-10-05 §D4): the newest time anything matched it, the
6272
- * strongest source ever seen — `header` is kept once seen — and every mailbox that recorded it. Three separate facts,
6273
- * never one sighting: `touch` replaces the time, keeps a header and unions the mailboxes independently.
6561
+ * A command that runs one of this suite's own CLIs, made only by `locateCliCommand`. Rendered as every printed command
6562
+ * is — a line to paste, or on Windows its words as JSON when no line is safe in every shell (`inlineCommand`,
6563
+ * `commandText`).
6564
+ *
6565
+ * A class with a private field, so the type is nominal: an object literal with the same fields is not one, and nor is
6566
+ * a spread copy of one with other words in it, as it would be with a symbol-keyed brand. Only the type is exported;
6567
+ * the class and its gate are not.
6274
6568
  */
6275
- interface TaintAggregate {
6276
- readonly at: string;
6277
- readonly source: TaintSource;
6278
- readonly inboxIds: readonly string[];
6279
- /** Sorted, duplicate-free provenance. A base entry with no sidecar record is a legacy read. */
6280
- readonly origins: readonly TaintOrigin[];
6281
- }
6282
- interface TaintCheck {
6283
- address: boolean;
6284
- domain: boolean;
6285
- /** The exact address's aggregate, when it is in the window. */
6286
- addressSeen?: TaintAggregate | undefined;
6287
- /** Its domain's, when that counts — not a public mailbox provider — and is in the window. */
6288
- domainSeen?: TaintAggregate | undefined;
6289
- }
6290
- /** Testable interruption point for the crash-safe, two-lock sidecar protocol. */
6291
- interface TaintStoreHooks {
6292
- afterOriginsCommit?: (() => Promise<void>) | undefined;
6293
- onLockAcquired?: ((lock: "origins" | "taint" | "handles") => void) | undefined;
6294
- }
6295
- declare class TaintStore {
6569
+ declare class PrintedCommand {
6296
6570
  #private;
6297
- readonly directory: string;
6298
- constructor(stateDir: string, now?: () => Date, hooks?: TaintStoreHooks);
6299
- /**
6300
- * Records observations. Throws if it cannot — callers must fail the read rather than return content whose taint
6301
- * was not recorded.
6302
- */
6303
- record(observations: readonly TaintObservation[], exclusions: TaintExclusions, handleObservations?: readonly TaintHandleObservation[]): Promise<void>;
6571
+ /** The words, as the program is to receive them: this Node, its flags, the entry, then the CLI's own words. */
6572
+ readonly words: readonly string[];
6573
+ /** The line to paste; null when no supported common or labelled shell rendering is safe. */
6574
+ readonly line: string | null;
6575
+ readonly platform: NodeJS.Platform;
6576
+ readonly shell?: "powershell" | undefined;
6577
+ constructor(gate: typeof GATE, words: readonly string[], platform: NodeJS.Platform, entry: string);
6304
6578
  /**
6305
- * Whether an address, or its (non-public) domain, was seen in email content in the window — from any inbox — and the
6306
- * aggregate each match is made of, for a caller to explain it. Nothing is written.
6579
+ * The real path of the file it runs, as it was checked when the command was located: a fact about then (D6). The
6580
+ * command names this path and nothing else, so what runs is whatever is there when the person runs it.
6307
6581
  */
6308
- check(address: string): Promise<TaintCheck>;
6582
+ get entry(): string;
6309
6583
  /**
6310
- * Whether a handle was seen in message content in the window.
6311
- *
6312
- * Only within its own workspace — see `TaintHandle`. There is no second answer to give, the way an address also
6313
- * carries a domain: two ids sharing a workspace says nothing about either of them.
6584
+ * As JSON — a result's field written out by `--json`, or returned to an MCP client — it is the text it always was:
6585
+ * its line, or its words as JSON with what to do when no Windows line is safe (`commandText`). Rendered where it
6586
+ * leaves the process, so a field can hold the command itself and the wire stays the same.
6314
6587
  */
6315
- checkHandle(handle: TaintHandle): Promise<boolean>;
6316
- /** The merged provenance of a retained handle, or no origins when it is not in the taint window. */
6317
- checkHandleOrigins(handle: TaintHandle): Promise<readonly TaintOrigin[]>;
6588
+ toJSON(): string;
6589
+ /** Never interpolated: a sentence gives a command through `handoffSentence`, a value through `handoffText`. */
6590
+ [Symbol.toPrimitive](): never;
6318
6591
  }
6319
- //#endregion
6320
- //#region src/core.d.ts
6321
- /** Everything a provider package needs from the core, wired to one config directory. */
6322
- interface Core {
6323
- paths: ResolvedPaths;
6324
- /** The subset of `paths` explicitly pinned by global CLI options. */
6325
- pathOverrides: Readonly<PathOverrides>;
6326
- config: ConfigStore;
6327
- states: InboxStateStore;
6328
- approvals: ApprovalStore;
6329
- ledger: SendLedger;
6330
- plans: PlanStore;
6331
- taint: TaintStore;
6332
- audit: AuditLog;
6333
- /** Opens the config directory's one secret backend (as recorded in config, or `kind` before the first write). */
6334
- secrets(kind?: SecretStoreKind): Promise<SecretStore>;
6335
- /**
6336
- * The commands this process tells a person to run — its own CLI's, core's, another product's — located from the
6337
- * caller it was opened with (`OpenCoreOptions.caller`), for these folders. Undefined when none was given: then there
6338
- * is no command to print at all, and anything that asks for one meets the programming error `requireHandoffs` throws —
6339
- * never a bare name. Every package opens core with its caller.
6340
- */
6341
- handoffs?: CliHandoffs | undefined;
6592
+ /** The package asking: a module of its own (`import.meta.url`), and the package that module is expected to be in. */
6593
+ interface CliCommandCaller {
6594
+ readonly url: string;
6595
+ readonly packageName: string;
6596
+ }
6597
+ /** Why there is no command: the step that found nothing it could check. */
6598
+ type CliNotLocatedReason = "caller" | "manifest" | "core" | "version" | "engine" | "bin" | "entry" | "interpreter" | "arguments" | "direction" | "no-registrations" | "not-registered";
6599
+ /** Why one registration of the product was not used. */
6600
+ type RegistrationRejection = "npx" | "arguments" | "no-file" | "no-package" | "version" | "engine" | "bin" | "entry";
6601
+ interface RegistrationNotUsed {
6602
+ readonly client: string;
6603
+ readonly path: string;
6604
+ readonly name: string;
6605
+ readonly why: RegistrationRejection;
6606
+ }
6607
+ /**
6608
+ * No command: nothing to run, and a sentence naming the product, its package and the exact version it needs. An
6609
+ * installation that has gone or changed since is said so, never replaced by a suite command found by name on PATH.
6610
+ */
6611
+ interface CliCommandNotLocated {
6612
+ readonly ok: false;
6613
+ readonly reason: CliNotLocatedReason;
6614
+ /** How the product is named to a person: `Gmail`. */
6615
+ readonly product: string;
6616
+ readonly package: string;
6617
+ /** The exact version needed: the printing package's own, since the suite is released together. */
6618
+ readonly version: string;
6619
+ /** For `engine`: the Node range the target declares. */
6620
+ readonly nodeRange?: string | undefined;
6621
+ /** For another product: each registration of it that was looked at, and why it was not used. */
6622
+ readonly registrations?: readonly RegistrationNotUsed[] | undefined;
6623
+ /** What was found, in a clause. */
6624
+ readonly detail: string;
6625
+ /** The whole sentence to show. */
6626
+ readonly message: string;
6342
6627
  }
6343
6628
  //#endregion
6344
6629
  //#region ../core/dist/index.d.mts