@cello-protocol/daemon 0.0.44 → 0.0.46

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.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * DOD-AGENT-ID-JOINKEY-1 — finish the migration REMOVE-001 started.
3
+ *
4
+ * `agent_name` was the original PRIMARY KEY of `agents`. REMOVE-001 added the stable surrogate
5
+ * `agent_id` **specifically to make names reusable** (retire keeps the rows and FREES the name), and
6
+ * migrated only the parent table. SEVEN child tables were left joining on the column that had just
7
+ * become a mutable, reusable attribute:
8
+ *
9
+ * sessions · seal_interrupted_artifacts · session_tree_leaves · transcript · message_watermarks ·
10
+ * contacts · retry_queue
11
+ *
12
+ * (`retry_queue` is the seventh — it predates REMOVE-001, so REMOVE-001 skipped seven, not six.)
13
+ *
14
+ * The hazard is not stale data. Retire an agent, create a new one with the freed name — a different
15
+ * agent_id and a different K_local keypair — and every `WHERE agent_name = ?` hands the new identity
16
+ * the dead one's transcript, its contact whitelist (an ACCESS-CONTROL list), and its interrupted
17
+ * sessions. Resuming one would seal with the wrong keypair.
18
+ *
19
+ * This module carries `agent_id` into all seven and demotes `agent_name` to what it always was after
20
+ * REMOVE-001: a display label on the parent table, with exactly one source of truth.
21
+ *
22
+ * ── Why this is safe, and why it deletes nothing ────────────────────────────────────────────────
23
+ * Purely client-side. The directory has no `agent_name` column and never sees one (AC1's wire proof:
24
+ * parties are identified by pubkey + session_id; a name reaches the wire only as the self-declared
25
+ * `offered_moniker` display label, which this migration does not touch). No row content is destroyed:
26
+ * every row is COPIED into the new table before the old one is dropped. Retire-reuse orphans need no
27
+ * purge either — once joined on `agent_id`, a new same-named agent simply never matches them.
28
+ *
29
+ * ── Why one transaction ─────────────────────────────────────────────────────────────────────────
30
+ * SQLite cannot alter a PRIMARY KEY, so each table must be REBUILT (create, copy, drop, rename).
31
+ * SQLite DDL is transactional, so all seven rebuilds run inside ONE `BEGIN…COMMIT`: a crash rolls the
32
+ * entire migration back atomically and the daemon reopens on the untouched original schema. There is
33
+ * no half-migrated state to recover from, and therefore no recovery code to get wrong.
34
+ *
35
+ * ── Why the target schema is pinned literally here ──────────────────────────────────────────────
36
+ * The DDL below duplicates what `session-node-manager` / `retry-queue` create on a fresh database.
37
+ * That duplication is DELIBERATE. A migration must migrate to a FIXED target: if it read its target
38
+ * from the live schema, a future column added upstream would silently change what this migration
39
+ * produces on an old database, and the two shapes would drift apart unnoticed. The drift is instead
40
+ * caught by a test that asserts a MIGRATED database's schema is byte-identical to a FRESH one.
41
+ */
42
+ import type { Logger } from "./types.js";
43
+ import type { DaemonDatabase } from "./sqlcipher-db.js";
44
+ export interface AgentIdMigrationResult {
45
+ readonly migrated: boolean;
46
+ /** Rows copied, per rebuilt table. Absent tables are omitted. */
47
+ readonly rowCounts: Readonly<Record<string, number>>;
48
+ }
49
+ /**
50
+ * Re-key the seven child tables from `agent_name` to `agent_id`. Idempotent: a table that already has
51
+ * an `agent_id` column is skipped, so a second call is a no-op. All rebuilds share ONE transaction.
52
+ *
53
+ * Throws (leaving the database untouched) on:
54
+ * - an ambiguous `agent_name → agent_id` map (a reused retired name),
55
+ * - any row whose non-null `agent_name` does not resolve to an agent,
56
+ * - any row count that does not match pre-migration.
57
+ *
58
+ * Never half-commits, never silently drops a row, never invents an agent_id.
59
+ */
60
+ export declare function migrateSessionTablesToAgentId(db: DaemonDatabase, logger: Logger): AgentIdMigrationResult;
61
+ //# sourceMappingURL=agent-id-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-id-migration.d.ts","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAoJxD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACtD;AAyDD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,sBAAsB,CA0HxG"}
@@ -0,0 +1,331 @@
1
+ /**
2
+ * DOD-AGENT-ID-JOINKEY-1 — finish the migration REMOVE-001 started.
3
+ *
4
+ * `agent_name` was the original PRIMARY KEY of `agents`. REMOVE-001 added the stable surrogate
5
+ * `agent_id` **specifically to make names reusable** (retire keeps the rows and FREES the name), and
6
+ * migrated only the parent table. SEVEN child tables were left joining on the column that had just
7
+ * become a mutable, reusable attribute:
8
+ *
9
+ * sessions · seal_interrupted_artifacts · session_tree_leaves · transcript · message_watermarks ·
10
+ * contacts · retry_queue
11
+ *
12
+ * (`retry_queue` is the seventh — it predates REMOVE-001, so REMOVE-001 skipped seven, not six.)
13
+ *
14
+ * The hazard is not stale data. Retire an agent, create a new one with the freed name — a different
15
+ * agent_id and a different K_local keypair — and every `WHERE agent_name = ?` hands the new identity
16
+ * the dead one's transcript, its contact whitelist (an ACCESS-CONTROL list), and its interrupted
17
+ * sessions. Resuming one would seal with the wrong keypair.
18
+ *
19
+ * This module carries `agent_id` into all seven and demotes `agent_name` to what it always was after
20
+ * REMOVE-001: a display label on the parent table, with exactly one source of truth.
21
+ *
22
+ * ── Why this is safe, and why it deletes nothing ────────────────────────────────────────────────
23
+ * Purely client-side. The directory has no `agent_name` column and never sees one (AC1's wire proof:
24
+ * parties are identified by pubkey + session_id; a name reaches the wire only as the self-declared
25
+ * `offered_moniker` display label, which this migration does not touch). No row content is destroyed:
26
+ * every row is COPIED into the new table before the old one is dropped. Retire-reuse orphans need no
27
+ * purge either — once joined on `agent_id`, a new same-named agent simply never matches them.
28
+ *
29
+ * ── Why one transaction ─────────────────────────────────────────────────────────────────────────
30
+ * SQLite cannot alter a PRIMARY KEY, so each table must be REBUILT (create, copy, drop, rename).
31
+ * SQLite DDL is transactional, so all seven rebuilds run inside ONE `BEGIN…COMMIT`: a crash rolls the
32
+ * entire migration back atomically and the daemon reopens on the untouched original schema. There is
33
+ * no half-migrated state to recover from, and therefore no recovery code to get wrong.
34
+ *
35
+ * ── Why the target schema is pinned literally here ──────────────────────────────────────────────
36
+ * The DDL below duplicates what `session-node-manager` / `retry-queue` create on a fresh database.
37
+ * That duplication is DELIBERATE. A migration must migrate to a FIXED target: if it read its target
38
+ * from the live schema, a future column added upstream would silently change what this migration
39
+ * produces on an old database, and the two shapes would drift apart unnoticed. The drift is instead
40
+ * caught by a test that asserts a MIGRATED database's schema is byte-identical to a FRESH one.
41
+ */
42
+ const REKEY_TARGETS = [
43
+ {
44
+ table: "sessions",
45
+ backfill: "strict",
46
+ createSql: (t) => `
47
+ CREATE TABLE ${t} (
48
+ session_id TEXT NOT NULL,
49
+ agent_id TEXT NOT NULL,
50
+ counterparty_pubkey TEXT NOT NULL,
51
+ status TEXT NOT NULL,
52
+ created_at INTEGER NOT NULL,
53
+ updated_at INTEGER NOT NULL,
54
+ message_count INTEGER NOT NULL DEFAULT 0,
55
+ interrupted_at TEXT,
56
+ relay_peer_id TEXT,
57
+ relay_addrs TEXT,
58
+ seal_legibility TEXT,
59
+ sealed_root_hex TEXT,
60
+ counterparty_primary_pubkey TEXT,
61
+ PRIMARY KEY (agent_id, session_id)
62
+ )`,
63
+ indexSql: () => [],
64
+ },
65
+ {
66
+ table: "seal_interrupted_artifacts",
67
+ backfill: "strict",
68
+ createSql: (t) => `
69
+ CREATE TABLE ${t} (
70
+ agent_id TEXT NOT NULL,
71
+ session_id TEXT NOT NULL,
72
+ role TEXT NOT NULL,
73
+ own_leaf TEXT NOT NULL,
74
+ counterparty_leaf TEXT NOT NULL,
75
+ merkle_root TEXT NOT NULL,
76
+ nonce TEXT NOT NULL,
77
+ created_at INTEGER NOT NULL,
78
+ PRIMARY KEY (agent_id, session_id)
79
+ )`,
80
+ indexSql: () => [],
81
+ },
82
+ {
83
+ table: "session_tree_leaves",
84
+ backfill: "strict",
85
+ createSql: (t) => `
86
+ CREATE TABLE ${t} (
87
+ agent_id TEXT NOT NULL,
88
+ session_id TEXT NOT NULL,
89
+ leaf_index INTEGER NOT NULL,
90
+ leaf_kind TEXT NOT NULL,
91
+ leaf_hash_hex TEXT NOT NULL,
92
+ created_at INTEGER NOT NULL,
93
+ PRIMARY KEY (agent_id, session_id, leaf_index)
94
+ )`,
95
+ indexSql: () => [],
96
+ },
97
+ {
98
+ table: "transcript",
99
+ backfill: "strict",
100
+ createSql: (t) => `
101
+ CREATE TABLE ${t} (
102
+ agent_id TEXT NOT NULL,
103
+ session_id TEXT NOT NULL,
104
+ sequence INTEGER NOT NULL,
105
+ direction TEXT NOT NULL,
106
+ blob BLOB NOT NULL,
107
+ created_at INTEGER NOT NULL,
108
+ PRIMARY KEY (agent_id, session_id, sequence, direction)
109
+ )`,
110
+ indexSql: () => [],
111
+ },
112
+ {
113
+ table: "message_watermarks",
114
+ backfill: "strict",
115
+ createSql: (t) => `
116
+ CREATE TABLE ${t} (
117
+ agent_id TEXT NOT NULL,
118
+ session_id TEXT NOT NULL,
119
+ last_delivered_seq INTEGER NOT NULL,
120
+ PRIMARY KEY (agent_id, session_id)
121
+ )`,
122
+ indexSql: () => [],
123
+ },
124
+ {
125
+ table: "contacts",
126
+ backfill: "strict",
127
+ createSql: (t) => `
128
+ CREATE TABLE ${t} (
129
+ agent_id TEXT NOT NULL,
130
+ pubkey TEXT NOT NULL,
131
+ added_at INTEGER NOT NULL,
132
+ moniker TEXT,
133
+ PRIMARY KEY (agent_id, pubkey)
134
+ )`,
135
+ indexSql: () => [],
136
+ },
137
+ {
138
+ // The SEVENTH. DOD-LOOP-1 added `agent_name` here so two of the operator's agents could hold
139
+ // awaiting content for the SAME session_id without colliding — then left the agent OUT of the
140
+ // uniqueness constraint, which stayed `UNIQUE(session_id, nonce_hex)`. For an awaiting row,
141
+ // nonce_hex IS the content hash, so two local agents parking identical content in one session
142
+ // collided, and the collision was swallowed into memory and lost on the next restart.
143
+ //
144
+ // The correct constraint falls out of the re-key. It is expressed as a UNIQUE INDEX over
145
+ // COALESCE(agent_id, '') rather than a table constraint, because SQLite treats NULLs as DISTINCT
146
+ // in a UNIQUE constraint: with a bare UNIQUE(agent_id, session_id, nonce_hex), the legacy
147
+ // agent-less direct-retry rows (agent_id NULL) would lose the (session_id, nonce_hex) dedup they
148
+ // have always had. COALESCE folds every agent-less row into one bucket, preserving exactly the
149
+ // old semantics for them while giving each agent its own namespace.
150
+ table: "retry_queue",
151
+ backfill: "nullable",
152
+ createSql: (t) => `
153
+ CREATE TABLE ${t} (
154
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
155
+ session_id TEXT NOT NULL,
156
+ agent_id TEXT,
157
+ nonce_hex TEXT NOT NULL,
158
+ content_blob BLOB NOT NULL,
159
+ queued_at INTEGER NOT NULL,
160
+ attempts INTEGER NOT NULL DEFAULT 1,
161
+ position INTEGER NOT NULL,
162
+ awaiting_ack INTEGER NOT NULL DEFAULT 0,
163
+ content_hash_hex TEXT
164
+ )`,
165
+ indexSql: (t) => [
166
+ `CREATE INDEX IF NOT EXISTS retry_queue_by_session_position ON ${t}(session_id, position ASC)`,
167
+ `CREATE UNIQUE INDEX IF NOT EXISTS retry_queue_agent_session_nonce ON ${t}(COALESCE(agent_id, ''), session_id, nonce_hex)`,
168
+ ],
169
+ },
170
+ ];
171
+ function columnsOf(db, table) {
172
+ return db.prepare(`PRAGMA table_info(${table})`).all().map((c) => c.name);
173
+ }
174
+ function tableExists(db, table) {
175
+ return (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined);
176
+ }
177
+ /**
178
+ * The `agent_name → agent_id` map, over ALL agent rows INCLUDING retired ones (a retired agent's rows
179
+ * must backfill to its OWN id and then sit inert — never be re-attributed to the live agent that
180
+ * reused its name).
181
+ *
182
+ * If any name resolves to more than one agent_id the map is AMBIGUOUS and the migration ABORTS. It
183
+ * does not guess, does not prefer the newest, does not prefer the oldest. A wrong attribution here
184
+ * would silently bind one identity's transcript, contacts and interrupted sessions to a DIFFERENT
185
+ * keypair — which is precisely the defect this migration exists to remove, except made permanent,
186
+ * unauditable, and blessed by a migration. A loud abort costs a wipe on a single-operator machine; a
187
+ * silent guess costs correctness forever.
188
+ *
189
+ * (A timestamp-based disambiguation is available in principle — rows written after the new agent's
190
+ * creation must belong to it, since a retired agent cannot write. It is deliberately NOT used: it
191
+ * depends on all seven tables carrying a trustworthy timestamp, and cleverness buys nothing on a
192
+ * database we would happily wipe.)
193
+ */
194
+ function buildNameToAgentId(db) {
195
+ const rows = db
196
+ .prepare("SELECT agent_name, agent_id FROM agents")
197
+ .all();
198
+ const map = new Map();
199
+ const ambiguous = new Set();
200
+ for (const r of rows) {
201
+ if (map.has(r.agent_name))
202
+ ambiguous.add(r.agent_name);
203
+ map.set(r.agent_name, r.agent_id);
204
+ }
205
+ if (ambiguous.size > 0) {
206
+ throw new Error(`agent_id_backfill_ambiguous: agent name(s) [${[...ambiguous].sort().join(", ")}] resolve to more ` +
207
+ `than one agent_id (a retired agent's name was reused). The migration cannot know which identity ` +
208
+ `owns the existing session rows and refuses to guess — attributing them to the wrong keypair is ` +
209
+ `the very defect it exists to remove. Resolve by hand or start from a fresh database.`);
210
+ }
211
+ return map;
212
+ }
213
+ /** Does this table still need re-keying? True iff it exists and has no `agent_id` column. */
214
+ function needsRekey(db, table) {
215
+ if (!tableExists(db, table))
216
+ return false;
217
+ return !columnsOf(db, table).includes("agent_id");
218
+ }
219
+ /**
220
+ * Re-key the seven child tables from `agent_name` to `agent_id`. Idempotent: a table that already has
221
+ * an `agent_id` column is skipped, so a second call is a no-op. All rebuilds share ONE transaction.
222
+ *
223
+ * Throws (leaving the database untouched) on:
224
+ * - an ambiguous `agent_name → agent_id` map (a reused retired name),
225
+ * - any row whose non-null `agent_name` does not resolve to an agent,
226
+ * - any row count that does not match pre-migration.
227
+ *
228
+ * Never half-commits, never silently drops a row, never invents an agent_id.
229
+ */
230
+ export function migrateSessionTablesToAgentId(db, logger) {
231
+ const pending = REKEY_TARGETS.filter((t) => needsRekey(db, t.table));
232
+ if (pending.length === 0)
233
+ return { migrated: false, rowCounts: {} };
234
+ // Ambiguity is checked BEFORE the transaction opens: nothing has been touched when it throws. It
235
+ // is logged with the same failure event as an in-transaction abort — a refusal to migrate is a
236
+ // loud event regardless of WHERE in the process it was detected.
237
+ let nameToId;
238
+ try {
239
+ nameToId = buildNameToAgentId(db);
240
+ }
241
+ catch (err) {
242
+ logger.error("daemon.migration.agent_id_backfill.failed", {
243
+ tables: pending.map((t) => t.table),
244
+ reason: err instanceof Error ? err.message : String(err),
245
+ });
246
+ throw err;
247
+ }
248
+ const before = new Map();
249
+ for (const t of pending) {
250
+ before.set(t.table, db.prepare(`SELECT COUNT(*) AS n FROM ${t.table}`).get().n);
251
+ }
252
+ const rowCounts = {};
253
+ db.exec("BEGIN");
254
+ try {
255
+ for (const target of pending) {
256
+ const oldCols = columnsOf(db, target.table);
257
+ const tmp = `${target.table}_rekey_new`;
258
+ db.exec(target.createSql(tmp));
259
+ // Copy every column the OLD table has that the NEW table also has (minus agent_name, which is
260
+ // replaced by agent_id). A pre-DOD-LOOP-1 retry_queue lacks agent_name/awaiting_ack entirely;
261
+ // taking the intersection handles every historical shape without a per-version special case.
262
+ const newCols = columnsOf(db, tmp);
263
+ const carried = oldCols.filter((c) => c !== "agent_name" && c !== "id" && newCols.includes(c));
264
+ const hasName = oldCols.includes("agent_name");
265
+ if (hasName) {
266
+ // Resolve through a JOIN on the parent so the mapping is the database's, not this process's.
267
+ // An unresolvable row is DROPPED by the inner join and caught by the count check below —
268
+ // for `strict` tables. For `nullable` (retry_queue), a NULL name must survive, so LEFT JOIN.
269
+ const join = target.backfill === "strict" ? "JOIN" : "LEFT JOIN";
270
+ db.exec(`INSERT INTO ${tmp} (agent_id${carried.length ? ", " + carried.join(", ") : ""})
271
+ SELECT a.agent_id${carried.length ? ", " + carried.map((c) => `t.${c}`).join(", ") : ""}
272
+ FROM ${target.table} t ${join} agents a ON a.agent_name = t.agent_name`);
273
+ }
274
+ else {
275
+ // No name to resolve (pre-DOD-LOOP-1 retry_queue): agent_id stays NULL — "not agent-scoped".
276
+ if (target.backfill === "strict") {
277
+ throw new Error(`agent_id_backfill_impossible: table '${target.table}' has neither agent_name nor agent_id`);
278
+ }
279
+ db.exec(`INSERT INTO ${tmp} (${carried.join(", ")}) SELECT ${carried.map((c) => `t.${c}`).join(", ")} FROM ${target.table} t`);
280
+ }
281
+ const after = db.prepare(`SELECT COUNT(*) AS n FROM ${tmp}`).get().n;
282
+ const expected = before.get(target.table);
283
+ if (after !== expected) {
284
+ throw new Error(`agent_id_backfill_row_count_mismatch: table '${target.table}' had ${expected} row(s) before and ` +
285
+ `${after} after the copy. A row failed to resolve to an agent_id (orphaned ${target.table}.agent_name ` +
286
+ `with no matching agents row), or a name matched more than one agent. Refusing to commit.`);
287
+ }
288
+ // Completeness. `strict` → every row must carry an agent_id. `nullable` → only rows that HAD a
289
+ // name must; a row that was always agent-less legitimately stays NULL.
290
+ const unresolved = target.backfill === "strict"
291
+ ? db.prepare(`SELECT COUNT(*) AS n FROM ${tmp} WHERE agent_id IS NULL`).get().n
292
+ : hasName
293
+ ? db
294
+ .prepare(`SELECT COUNT(*) AS n FROM ${target.table} t WHERE t.agent_name IS NOT NULL
295
+ AND NOT EXISTS (SELECT 1 FROM agents a WHERE a.agent_name = t.agent_name)`)
296
+ .get().n
297
+ : 0;
298
+ if (unresolved > 0) {
299
+ throw new Error(`agent_id_backfill_incomplete: table '${target.table}' has ${unresolved} row(s) whose agent could not ` +
300
+ `be resolved to a stable agent_id. Refusing to commit a table with an unattributable row.`);
301
+ }
302
+ db.exec(`DROP TABLE ${target.table}`);
303
+ db.exec(`ALTER TABLE ${tmp} RENAME TO ${target.table}`);
304
+ for (const sql of target.indexSql(target.table))
305
+ db.exec(sql);
306
+ rowCounts[target.table] = after;
307
+ }
308
+ db.exec("COMMIT");
309
+ }
310
+ catch (err) {
311
+ // SQLite may have already aborted the transaction; a failing ROLLBACK must not mask the real error.
312
+ try {
313
+ db.exec("ROLLBACK");
314
+ }
315
+ catch {
316
+ /* the failing statement may have already aborted the txn */
317
+ }
318
+ logger.error("daemon.migration.agent_id_backfill.failed", {
319
+ tables: pending.map((t) => t.table),
320
+ reason: err instanceof Error ? err.message : String(err),
321
+ });
322
+ throw err;
323
+ }
324
+ logger.info("daemon.migration.agent_id_backfill", {
325
+ tables: Object.keys(rowCounts),
326
+ rowCounts,
327
+ agentsMapped: nameToId.size,
328
+ });
329
+ return { migrated: true, rowCounts };
330
+ }
331
+ //# sourceMappingURL=agent-id-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-id-migration.js","sourceRoot":"","sources":["../src/agent-id-migration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CELLO Daemon — the agent_settings key namespace (DOD-SETTINGS-1).
3
+ *
4
+ * A single source for every valid per-agent reachability-policy setting key, so the handler (which
5
+ * REFUSES an unknown key) and the consumers (TIER-BOUNDS-SETTINGS reads the bound overrides,
6
+ * AWAY-TIER-1 reads the away texts) can never drift. Keys are lower-snake, dotted namespaces:
7
+ *
8
+ * bounds.<tier>.max_sessions bounds.<tier>.max_bytes — per-tier bound overrides
9
+ * away.default — the agent's default away text
10
+ * away.tier.<tier> — a per-tier away text
11
+ *
12
+ * `<tier>` is a tier NAME (not the integer), for legibility in the store. BLOCKED is deliberately NOT
13
+ * settable — 0/0 is fixed (you cannot "raise" a block), and it has no away text (a blocked sender is
14
+ * refused before any reply). So the settable tiers are unknown / known / whitelisted / vip.
15
+ */
16
+ /** The tiers whose bounds and away texts are settable (BLOCKED is fixed, never overridable). */
17
+ export declare const SETTABLE_TIER_NAMES: readonly ["unknown", "known", "whitelisted", "vip"];
18
+ export type SettableTierName = (typeof SETTABLE_TIER_NAMES)[number];
19
+ /** The tier NAME for a tier integer, or null for an unknown/unsettable value (e.g. BLOCKED=0).
20
+ * Keyed off the TIER constants (review F3) so a renumbering in contacts-tier-migration can never
21
+ * silently desync the settings namespace from the grid. */
22
+ export declare function settableTierName(tier: number): SettableTierName | null;
23
+ export type BoundField = "max_sessions" | "max_bytes";
24
+ /** The setting key for a per-tier bound override. */
25
+ export declare function boundSettingKey(tier: SettableTierName, field: BoundField): string;
26
+ /** The setting key for a per-tier away text. */
27
+ export declare function awayTierSettingKey(tier: SettableTierName): string;
28
+ /** The setting key for the agent's default away text (the fallback below any per-tier text). */
29
+ export declare const AWAY_DEFAULT_KEY = "away.default";
30
+ /** True iff `key` is a known, settable reachability-policy key. An unknown key is REFUSED by the
31
+ * handler (never silently stored — a typo'd key that persisted would be a setting that never takes
32
+ * effect, invisible to the operator). */
33
+ export declare function isValidSettingKey(key: string): boolean;
34
+ /** The full list of valid keys (for surfacing in a settings-list / help). */
35
+ export declare function allSettingKeys(): string[];
36
+ /** True iff the key is a per-tier BOUND override (its value must be a positive integer). */
37
+ export declare function isBoundKey(key: string): boolean;
38
+ /**
39
+ * Validate a setting's VALUE for its key (DOD-TIER-BOUNDS-SETTINGS AC2). A bound override may only be
40
+ * a FINITE POSITIVE INTEGER — INV-TIER-BOUND: a setting can RAISE or lower a bound within finite
41
+ * limits, never REMOVE one. So `Infinity`, negatives, zero, non-integers, and non-numeric strings are
42
+ * refused (an unbounded tier is exactly what the grid forbids). Away-text values are free-form strings.
43
+ */
44
+ export declare function validateSettingValue(key: string, value: string): {
45
+ ok: true;
46
+ } | {
47
+ ok: false;
48
+ reason: string;
49
+ };
50
+ /** Max length of an away message (per-contact or per-tier/agent-default) — an answering-machine line. */
51
+ export declare const AWAY_MESSAGE_MAX_LEN = 2048;
52
+ //# sourceMappingURL=agent-settings-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-settings-keys.d.ts","sourceRoot":"","sources":["../src/agent-settings-keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,qDAAqE,CAAC;AACtG,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE;;4DAE4D;AAC5D,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAQtE;AAED,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,WAAW,CAAC;AAEtD,qDAAqD;AACrD,wBAAgB,eAAe,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,CAEjF;AAED,gDAAgD;AAChD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAEjE;AAED,gGAAgG;AAChG,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAa/C;;0CAE0C;AAC1C,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,4FAA4F;AAC5F,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAuB7G;AAED,yGAAyG;AACzG,eAAO,MAAM,oBAAoB,OAAO,CAAC"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * CELLO Daemon — the agent_settings key namespace (DOD-SETTINGS-1).
3
+ *
4
+ * A single source for every valid per-agent reachability-policy setting key, so the handler (which
5
+ * REFUSES an unknown key) and the consumers (TIER-BOUNDS-SETTINGS reads the bound overrides,
6
+ * AWAY-TIER-1 reads the away texts) can never drift. Keys are lower-snake, dotted namespaces:
7
+ *
8
+ * bounds.<tier>.max_sessions bounds.<tier>.max_bytes — per-tier bound overrides
9
+ * away.default — the agent's default away text
10
+ * away.tier.<tier> — a per-tier away text
11
+ *
12
+ * `<tier>` is a tier NAME (not the integer), for legibility in the store. BLOCKED is deliberately NOT
13
+ * settable — 0/0 is fixed (you cannot "raise" a block), and it has no away text (a blocked sender is
14
+ * refused before any reply). So the settable tiers are unknown / known / whitelisted / vip.
15
+ */
16
+ import { TIER } from "./contacts-tier-migration.js";
17
+ /** The tiers whose bounds and away texts are settable (BLOCKED is fixed, never overridable). */
18
+ export const SETTABLE_TIER_NAMES = Object.freeze(["unknown", "known", "whitelisted", "vip"]);
19
+ /** The tier NAME for a tier integer, or null for an unknown/unsettable value (e.g. BLOCKED=0).
20
+ * Keyed off the TIER constants (review F3) so a renumbering in contacts-tier-migration can never
21
+ * silently desync the settings namespace from the grid. */
22
+ export function settableTierName(tier) {
23
+ switch (tier) {
24
+ case TIER.UNKNOWN: return "unknown";
25
+ case TIER.KNOWN: return "known";
26
+ case TIER.WHITELISTED: return "whitelisted";
27
+ case TIER.VIP: return "vip";
28
+ default: return null; // BLOCKED or out-of-range — not settable
29
+ }
30
+ }
31
+ /** The setting key for a per-tier bound override. */
32
+ export function boundSettingKey(tier, field) {
33
+ return `bounds.${tier}.${field}`;
34
+ }
35
+ /** The setting key for a per-tier away text. */
36
+ export function awayTierSettingKey(tier) {
37
+ return `away.tier.${tier}`;
38
+ }
39
+ /** The setting key for the agent's default away text (the fallback below any per-tier text). */
40
+ export const AWAY_DEFAULT_KEY = "away.default";
41
+ const BOUND_FIELDS = Object.freeze(["max_sessions", "max_bytes"]);
42
+ /** Every valid setting key, precomputed. The handler validates a `set` against this exact set. */
43
+ const VALID_KEYS = new Set([
44
+ AWAY_DEFAULT_KEY,
45
+ ...SETTABLE_TIER_NAMES.flatMap((t) => [
46
+ ...BOUND_FIELDS.map((f) => boundSettingKey(t, f)),
47
+ awayTierSettingKey(t),
48
+ ]),
49
+ ]);
50
+ /** True iff `key` is a known, settable reachability-policy key. An unknown key is REFUSED by the
51
+ * handler (never silently stored — a typo'd key that persisted would be a setting that never takes
52
+ * effect, invisible to the operator). */
53
+ export function isValidSettingKey(key) {
54
+ return VALID_KEYS.has(key);
55
+ }
56
+ /** The full list of valid keys (for surfacing in a settings-list / help). */
57
+ export function allSettingKeys() {
58
+ return [...VALID_KEYS];
59
+ }
60
+ /** True iff the key is a per-tier BOUND override (its value must be a positive integer). */
61
+ export function isBoundKey(key) {
62
+ return key.startsWith("bounds.");
63
+ }
64
+ /**
65
+ * Validate a setting's VALUE for its key (DOD-TIER-BOUNDS-SETTINGS AC2). A bound override may only be
66
+ * a FINITE POSITIVE INTEGER — INV-TIER-BOUND: a setting can RAISE or lower a bound within finite
67
+ * limits, never REMOVE one. So `Infinity`, negatives, zero, non-integers, and non-numeric strings are
68
+ * refused (an unbounded tier is exactly what the grid forbids). Away-text values are free-form strings.
69
+ */
70
+ export function validateSettingValue(key, value) {
71
+ if (isBoundKey(key)) {
72
+ // `^[1-9][0-9]*$` — a positive integer with no sign, decimal, or `Infinity`/`NaN` spelling.
73
+ if (!/^[1-9][0-9]*$/.test(value)) {
74
+ return { ok: false, reason: "a bound must be a finite positive integer (no 0, negative, decimal, or Infinity)" };
75
+ }
76
+ // Number.MAX_SAFE_INTEGER guard — a value past it loses precision and is meaningless as a cap.
77
+ if (Number(value) > Number.MAX_SAFE_INTEGER) {
78
+ return { ok: false, reason: "a bound must be <= Number.MAX_SAFE_INTEGER" };
79
+ }
80
+ return { ok: true };
81
+ }
82
+ // Away-text (review F2/F3): an EMPTY / whitespace-only away text is refused — to CLEAR, use null
83
+ // (cello_contact_set_away) or omit the key; storing "" would silently blank every away reply,
84
+ // including the minimal stranger disclosure. Bounded to a sane length (an answering-machine line,
85
+ // not a document).
86
+ if (value.trim().length === 0) {
87
+ return { ok: false, reason: "an away message cannot be empty or whitespace-only (omit the key or pass null to clear)" };
88
+ }
89
+ if (value.length > AWAY_MESSAGE_MAX_LEN) {
90
+ return { ok: false, reason: `an away message must be <= ${AWAY_MESSAGE_MAX_LEN} characters` };
91
+ }
92
+ return { ok: true };
93
+ }
94
+ /** Max length of an away message (per-contact or per-tier/agent-default) — an answering-machine line. */
95
+ export const AWAY_MESSAGE_MAX_LEN = 2048;
96
+ //# sourceMappingURL=agent-settings-keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-settings-keys.js","sourceRoot":"","sources":["../src/agent-settings-keys.ts"],"names":[],"mappings":"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"}