@cello-protocol/daemon 0.0.45 → 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,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":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,8BAA8B,CAAC;AAEpD,gGAAgG;AAChG,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,CAAU,CAAC,CAAC;AAGtG;;4DAE4D;AAC5D,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,SAAS,CAAC;QACpC,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,OAAO,CAAC;QAChC,KAAK,IAAI,CAAC,WAAW,CAAC,CAAC,OAAO,aAAa,CAAC;QAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,KAAK,CAAC;QAC5B,OAAO,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,yCAAyC;IACjE,CAAC;AACH,CAAC;AAID,qDAAqD;AACrD,MAAM,UAAU,eAAe,CAAC,IAAsB,EAAE,KAAiB;IACvE,OAAO,UAAU,IAAI,IAAI,KAAK,EAAE,CAAC;AACnC,CAAC;AAED,gDAAgD;AAChD,MAAM,UAAU,kBAAkB,CAAC,IAAsB;IACvD,OAAO,aAAa,IAAI,EAAE,CAAC;AAC7B,CAAC;AAED,gGAAgG;AAChG,MAAM,CAAC,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAE/C,MAAM,YAAY,GAA0B,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC,CAAC;AAEzF,kGAAkG;AAClG,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAS;IACtD,gBAAgB;IAChB,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACpC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACjD,kBAAkB,CAAC,CAAC,CAAC;KACtB,CAAC;CACH,CAAC,CAAC;AAEH;;0CAE0C;AAC1C,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,OAAO,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,cAAc;IAC5B,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC;AACzB,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,OAAO,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,KAAa;IAC7D,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACpB,4FAA4F;QAC5F,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kFAAkF,EAAE,CAAC;QACnH,CAAC;QACD,+FAA+F;QAC/F,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,4CAA4C,EAAE,CAAC;QAC7E,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACtB,CAAC;IACD,iGAAiG;IACjG,8FAA8F;IAC9F,kGAAkG;IAClG,mBAAmB;IACnB,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yFAAyF,EAAE,CAAC;IAC1H,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QACxC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,8BAA8B,oBAAoB,aAAa,EAAE,CAAC;IAChG,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * CELLO Daemon — contacts tier metadata (DOD-TIER-1, address-book Step 1).
3
+ *
4
+ * `contacts` was re-keyed to the stable `agent_id` in daemon@0.0.45 (DOD-AGENT-ID-JOINKEY-1). This
5
+ * module lands the address-book's per-contact metadata ON that key: a reachability `tier`, the
6
+ * `provenance` of the relationship, the last self-declared name a peer offered (for Option-C rename
7
+ * detection, Step 3), and a per-contact `away_message` (Step 4). All four are pure ADD COLUMN — no
8
+ * table rebuild, no PK change — so, unlike the join-key migration, this one is simple and idempotent
9
+ * and never has to appear in that migration's pinned DDL. It runs AFTER `migrateSessionTablesToAgentId`.
10
+ *
11
+ * Design source: docs/planning/user-stories/m8c/2026-07-10_contact-address-book-design.md (§1).
12
+ */
13
+ import type { DaemonDatabase } from "./sqlcipher-db.js";
14
+ import type { Logger } from "./types.js";
15
+ /**
16
+ * The five reachability tiers, ordered so `>=` is meaningful (blocked < unknown < known < whitelisted
17
+ * < vip). Stored as the INTEGER `contacts.tier`. This const map is the SINGLE source of the numbers;
18
+ * no bare tier integer may appear at a call site (DOD-TIER-1 AC2).
19
+ *
20
+ * BLOCKED is a real, meaningful ZERO — see `normalizeTier` for why that matters.
21
+ */
22
+ export declare const TIER: Readonly<{
23
+ readonly BLOCKED: 0;
24
+ readonly UNKNOWN: 1;
25
+ readonly KNOWN: 2;
26
+ readonly WHITELISTED: 3;
27
+ readonly VIP: 4;
28
+ }>;
29
+ export type TierName = keyof typeof TIER;
30
+ export type TierValue = (typeof TIER)[TierName];
31
+ /** True iff `n` is exactly one of the five defined tier integers (0..4). The validation gate for
32
+ * `cello_contact_set_tier` (Step 3) — an unknown value is refused, never coerced. */
33
+ export declare function isKnownTierValue(n: number): boolean;
34
+ /**
35
+ * The read-side default, and the single most safety-critical function in this unit.
36
+ *
37
+ * A contact's effective tier is UNKNOWN when there is NO row (undefined) OR when the row's `tier`
38
+ * column is NULL (a row that predates a tier being set, or a stray never-stamped row). Both collapse
39
+ * to UNKNOWN — the tighter default: a caller must never accidentally treat an unresolved contact as
40
+ * reachable.
41
+ *
42
+ * It is written with EXPLICIT null/undefined checks, never `tier || UNKNOWN`, for two reasons that
43
+ * are both live bugs otherwise:
44
+ * - `0 || 1 === 1`: a `|| UNKNOWN` would swallow BLOCKED(0) and silently un-block a blocked contact.
45
+ * - `null >= 0 === true`: a NULL reaching a `>=` bound check reads as "not blocked", and a NULL
46
+ * reaching a `grid[tier]` lookup is `grid[null]` → undefined → crash. Normalizing NULL→UNKNOWN
47
+ * here keeps every downstream comparison and lookup total.
48
+ */
49
+ export declare function normalizeTier(tier: number | null | undefined): number;
50
+ /** DOD-TIER-2: the abuse bounds for one tier. INV-TIER-BOUND — every field is FINITE (no tier is
51
+ * unbounded; `vip` is a large but real number, never Infinity). Step 4 lets settings OVERRIDE these
52
+ * defaults, but a setting may only raise/lower within finite bounds, never remove one. */
53
+ export interface TierBound {
54
+ /** Max concurrent (active|interrupted) sessions this agent will hold from ONE sender at this tier. */
55
+ readonly maxSessionsPerSender: number;
56
+ /** Max cumulative RECEIVED bytes for a single session at this tier (anti-drip-feed). */
57
+ readonly maxBytesPerSession: number;
58
+ }
59
+ /**
60
+ * The hardcoded default bounds grid (DOD-TIER-2). One named map — the SINGLE source; call sites
61
+ * reference it via `tierBoundsFor`, never inline a number. Step 4 (DOD-TIER-BOUNDS-SETTINGS) makes
62
+ * these overridable per agent; until then they are the policy.
63
+ *
64
+ * INV-TIER-BOUND: a HIGHER tier only RAISES a bound — no tier removes one. `vip` is 50 sessions / 2 GiB,
65
+ * deliberately finite. `blocked` is 0/0, which is what makes DOD-TIER-3 fall out for free: a 0 session
66
+ * cap refuses a blocked sender through the SAME per-sender-cap path an over-cap unknown takes, with no
67
+ * separate branch and therefore no distinguishing oracle.
68
+ */
69
+ export declare const DEFAULT_TIER_BOUNDS: Readonly<Record<number, TierBound>>;
70
+ /** The bounds for a tier, TOTAL: any input is normalized to a defined tier first, so the return is
71
+ * always a real `TierBound` — a corrupt/out-of-range tier gets UNKNOWN's bounds, never `undefined`
72
+ * (the `grid[99]` crash the whole tier design guards against). */
73
+ export declare function tierBoundsFor(tier: number | null | undefined): TierBound;
74
+ /**
75
+ * Idempotently add the tier-metadata columns to `contacts` and grandfather existing contacts.
76
+ *
77
+ * SQLite has no `ADD COLUMN IF NOT EXISTS`, so each ALTER is PRAGMA-guarded. Crucially the columns
78
+ * are added with NO DEFAULT: a `DEFAULT` would give every existing row a value the instant the column
79
+ * is created, so the grandfather backfill (`UPDATE … WHERE tier IS NULL`) would then match NOTHING —
80
+ * every auto-accepting contact would silently drop to UNKNOWN. No default → existing rows read NULL →
81
+ * the explicit backfill promotes them to WHITELISTED, preserving today's binary-whitelist behaviour
82
+ * (design §1). Silently revoking access people already rely on is a worse failure than grandfathering
83
+ * a permissive default for a handful of contacts they can list and demote at leisure.
84
+ *
85
+ * The grandfather is ONE-TIME: it runs only in the invocation that first adds the `tier` column. A
86
+ * NULL that appears later (a stray un-stamped row) is NOT promoted — it is read as UNKNOWN by
87
+ * `normalizeTier`, the tighter default. The promotion is tied to the column's birth, not to NULL-ness.
88
+ */
89
+ export declare function migrateContactsAddTierMetadata(db: DaemonDatabase, logger: Logger): void;
90
+ //# sourceMappingURL=contacts-tier-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contacts-tier-migration.d.ts","sourceRoot":"","sources":["../src/contacts-tier-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,IAAI;;;;;;EAMN,CAAC;AAEZ,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,IAAI,CAAC;AACzC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC;AAIhD;sFACsF;AACtF,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAQrE;AAED;;2FAE2F;AAC3F,MAAM,WAAW,SAAS;IACxB,sGAAsG;IACtG,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,wFAAwF;IACxF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAMlE,CAAC;AAEH;;mEAEmE;AACnE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,CAExE;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,8BAA8B,CAAC,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CA8CvF"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * CELLO Daemon — contacts tier metadata (DOD-TIER-1, address-book Step 1).
3
+ *
4
+ * `contacts` was re-keyed to the stable `agent_id` in daemon@0.0.45 (DOD-AGENT-ID-JOINKEY-1). This
5
+ * module lands the address-book's per-contact metadata ON that key: a reachability `tier`, the
6
+ * `provenance` of the relationship, the last self-declared name a peer offered (for Option-C rename
7
+ * detection, Step 3), and a per-contact `away_message` (Step 4). All four are pure ADD COLUMN — no
8
+ * table rebuild, no PK change — so, unlike the join-key migration, this one is simple and idempotent
9
+ * and never has to appear in that migration's pinned DDL. It runs AFTER `migrateSessionTablesToAgentId`.
10
+ *
11
+ * Design source: docs/planning/user-stories/m8c/2026-07-10_contact-address-book-design.md (§1).
12
+ */
13
+ /**
14
+ * The five reachability tiers, ordered so `>=` is meaningful (blocked < unknown < known < whitelisted
15
+ * < vip). Stored as the INTEGER `contacts.tier`. This const map is the SINGLE source of the numbers;
16
+ * no bare tier integer may appear at a call site (DOD-TIER-1 AC2).
17
+ *
18
+ * BLOCKED is a real, meaningful ZERO — see `normalizeTier` for why that matters.
19
+ */
20
+ export const TIER = Object.freeze({
21
+ BLOCKED: 0,
22
+ UNKNOWN: 1,
23
+ KNOWN: 2,
24
+ WHITELISTED: 3,
25
+ VIP: 4,
26
+ });
27
+ const TIER_VALUES = Object.freeze(Object.values(TIER));
28
+ /** True iff `n` is exactly one of the five defined tier integers (0..4). The validation gate for
29
+ * `cello_contact_set_tier` (Step 3) — an unknown value is refused, never coerced. */
30
+ export function isKnownTierValue(n) {
31
+ return Number.isInteger(n) && TIER_VALUES.includes(n);
32
+ }
33
+ /**
34
+ * The read-side default, and the single most safety-critical function in this unit.
35
+ *
36
+ * A contact's effective tier is UNKNOWN when there is NO row (undefined) OR when the row's `tier`
37
+ * column is NULL (a row that predates a tier being set, or a stray never-stamped row). Both collapse
38
+ * to UNKNOWN — the tighter default: a caller must never accidentally treat an unresolved contact as
39
+ * reachable.
40
+ *
41
+ * It is written with EXPLICIT null/undefined checks, never `tier || UNKNOWN`, for two reasons that
42
+ * are both live bugs otherwise:
43
+ * - `0 || 1 === 1`: a `|| UNKNOWN` would swallow BLOCKED(0) and silently un-block a blocked contact.
44
+ * - `null >= 0 === true`: a NULL reaching a `>=` bound check reads as "not blocked", and a NULL
45
+ * reaching a `grid[tier]` lookup is `grid[null]` → undefined → crash. Normalizing NULL→UNKNOWN
46
+ * here keeps every downstream comparison and lookup total.
47
+ */
48
+ export function normalizeTier(tier) {
49
+ if (tier === null || tier === undefined)
50
+ return TIER.UNKNOWN;
51
+ // TOTAL over ALL inputs: a stored value that is not one of the five defined tiers (a corrupt or
52
+ // future-version row) also collapses to UNKNOWN, so `getTier` is guaranteed to return a value in
53
+ // 0..4 and every downstream `grid[tier]` lookup / `>=` comparison is total. Without this, a corrupt
54
+ // `tier = 99` would sail through and become the `grid[99] → undefined` crash the bound grid fears.
55
+ if (!isKnownTierValue(tier))
56
+ return TIER.UNKNOWN;
57
+ return tier;
58
+ }
59
+ /**
60
+ * The hardcoded default bounds grid (DOD-TIER-2). One named map — the SINGLE source; call sites
61
+ * reference it via `tierBoundsFor`, never inline a number. Step 4 (DOD-TIER-BOUNDS-SETTINGS) makes
62
+ * these overridable per agent; until then they are the policy.
63
+ *
64
+ * INV-TIER-BOUND: a HIGHER tier only RAISES a bound — no tier removes one. `vip` is 50 sessions / 2 GiB,
65
+ * deliberately finite. `blocked` is 0/0, which is what makes DOD-TIER-3 fall out for free: a 0 session
66
+ * cap refuses a blocked sender through the SAME per-sender-cap path an over-cap unknown takes, with no
67
+ * separate branch and therefore no distinguishing oracle.
68
+ */
69
+ export const DEFAULT_TIER_BOUNDS = Object.freeze({
70
+ [TIER.BLOCKED]: { maxSessionsPerSender: 0, maxBytesPerSession: 0 },
71
+ [TIER.UNKNOWN]: { maxSessionsPerSender: 3, maxBytesPerSession: 25 * 1024 * 1024 },
72
+ [TIER.KNOWN]: { maxSessionsPerSender: 5, maxBytesPerSession: 100 * 1024 * 1024 },
73
+ [TIER.WHITELISTED]: { maxSessionsPerSender: 20, maxBytesPerSession: 500 * 1024 * 1024 },
74
+ [TIER.VIP]: { maxSessionsPerSender: 50, maxBytesPerSession: 2 * 1024 * 1024 * 1024 },
75
+ });
76
+ /** The bounds for a tier, TOTAL: any input is normalized to a defined tier first, so the return is
77
+ * always a real `TierBound` — a corrupt/out-of-range tier gets UNKNOWN's bounds, never `undefined`
78
+ * (the `grid[99]` crash the whole tier design guards against). */
79
+ export function tierBoundsFor(tier) {
80
+ return DEFAULT_TIER_BOUNDS[normalizeTier(tier)];
81
+ }
82
+ const TIER_METADATA_COLUMNS = [
83
+ { name: "tier", ddl: "tier INTEGER" },
84
+ { name: "provenance", ddl: "provenance TEXT" },
85
+ { name: "last_offered_moniker", ddl: "last_offered_moniker TEXT" },
86
+ { name: "away_message", ddl: "away_message TEXT" },
87
+ ];
88
+ /**
89
+ * Idempotently add the tier-metadata columns to `contacts` and grandfather existing contacts.
90
+ *
91
+ * SQLite has no `ADD COLUMN IF NOT EXISTS`, so each ALTER is PRAGMA-guarded. Crucially the columns
92
+ * are added with NO DEFAULT: a `DEFAULT` would give every existing row a value the instant the column
93
+ * is created, so the grandfather backfill (`UPDATE … WHERE tier IS NULL`) would then match NOTHING —
94
+ * every auto-accepting contact would silently drop to UNKNOWN. No default → existing rows read NULL →
95
+ * the explicit backfill promotes them to WHITELISTED, preserving today's binary-whitelist behaviour
96
+ * (design §1). Silently revoking access people already rely on is a worse failure than grandfathering
97
+ * a permissive default for a handful of contacts they can list and demote at leisure.
98
+ *
99
+ * The grandfather is ONE-TIME: it runs only in the invocation that first adds the `tier` column. A
100
+ * NULL that appears later (a stray un-stamped row) is NOT promoted — it is read as UNKNOWN by
101
+ * `normalizeTier`, the tighter default. The promotion is tied to the column's birth, not to NULL-ness.
102
+ */
103
+ export function migrateContactsAddTierMetadata(db, logger) {
104
+ const existing = new Set(db.prepare("PRAGMA table_info(contacts)").all().map((c) => c.name));
105
+ const toAdd = TIER_METADATA_COLUMNS.filter((c) => !existing.has(c.name));
106
+ if (toAdd.length === 0)
107
+ return; // idempotent: every column already present → nothing to do
108
+ // The ADD COLUMNs and the grandfather backfill MUST commit atomically, in ONE transaction (SQLite
109
+ // DDL is transactional — the same reason the join-key migration uses one BEGIN…COMMIT). If a crash
110
+ // landed between the `tier` ALTER committing and the backfill, the next init would see the column
111
+ // exists, skip the one-time grandfather FOREVER, and silently demote every legacy contact to
112
+ // UNKNOWN — the exact "silently revoking access people already rely on" failure this file exists to
113
+ // avoid, with no log and no error. Wrapping both makes "column added" and "existing rows
114
+ // grandfathered" inseparable: a crash rolls back BOTH, and the clean re-run does the whole thing —
115
+ // which is what makes column-existence a VALID durability marker for the one-time gate.
116
+ const addsTier = toAdd.some((c) => c.name === "tier");
117
+ let grandfathered = 0;
118
+ db.exec("BEGIN");
119
+ try {
120
+ for (const col of toAdd)
121
+ db.exec(`ALTER TABLE contacts ADD COLUMN ${col.ddl}`);
122
+ // Grandfather EXISTING contacts to WHITELISTED — but ONLY on the migration that first adds `tier`
123
+ // (a one-time, column-birth event). A NULL written AFTER the column exists is never promoted here;
124
+ // it is a read-time UNKNOWN via normalizeTier, the tighter default.
125
+ if (addsTier) {
126
+ grandfathered = Number(db.prepare("UPDATE contacts SET tier = ? WHERE tier IS NULL").run(TIER.WHITELISTED).changes);
127
+ }
128
+ db.exec("COMMIT");
129
+ }
130
+ catch (err) {
131
+ // SQLite may have already aborted the transaction; a failing ROLLBACK must not mask the real error.
132
+ try {
133
+ db.exec("ROLLBACK");
134
+ }
135
+ catch {
136
+ /* the failing statement may have already aborted the txn */
137
+ }
138
+ logger.error("contacts.tier.migration.failed", {
139
+ columns: toAdd.map((c) => c.name),
140
+ reason: err instanceof Error ? err.message : String(err),
141
+ });
142
+ throw err;
143
+ }
144
+ logger.info("contacts.tier.columns.added", { columns: toAdd.map((c) => c.name) });
145
+ if (grandfathered > 0) {
146
+ logger.info("contacts.tier.grandfathered", { count: grandfathered, tier: TIER.WHITELISTED });
147
+ }
148
+ }
149
+ //# sourceMappingURL=contacts-tier-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contacts-tier-migration.js","sourceRoot":"","sources":["../src/contacts-tier-migration.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAmvL7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAU/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAQ/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA64L7E"}