@securecoms/agent-bridge 0.31.0 → 0.33.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.
@@ -46,7 +46,8 @@ export declare const LOCAL_REPORT_FILE = "local-config.json";
46
46
  * and it never affects which file the runtime actually reads.
47
47
  */
48
48
  export interface LocalReport {
49
- persona: Record<string, string>;
49
+ /** F-0 (spec D-3): name and UTF-8 size of each non-blank persona file. The text never leaves readLocalReport. */
50
+ local_persona_files: LocalPersonaFileReport[];
50
51
  has_search_key: boolean;
51
52
  has_llm_key: boolean;
52
53
  /** L-2 (spec §7.9): a readable key for an extract-capable provider. Only a real true counts; absent is false. */
@@ -64,6 +65,11 @@ export interface LocalReport {
64
65
  * so no path (the owner's `allowed_secret_paths` sits in the same file) can
65
66
  * ride along to the backend.
66
67
  */
68
+ export declare const LOCAL_PERSONA_FILE_NAMES: readonly ["IDENTITY.md", "SOUL.md", "AGENTS.md"];
69
+ export interface LocalPersonaFileReport {
70
+ name: (typeof LOCAL_PERSONA_FILE_NAMES)[number];
71
+ bytes: number;
72
+ }
67
73
  export interface SecretPathsReport {
68
74
  mode: "report" | "enforce";
69
75
  inside: number;
@@ -80,11 +86,11 @@ export interface SecretPathsReport {
80
86
  export type LegacyMemoryKind = "global-memory" | "conversation-history" | "captured-memory";
81
87
  export declare const LEGACY_MEMORY_FACT_MAX_BYTES = 1024;
82
88
  export declare const LEGACY_MEMORY_FACT_DECISION_MAX = 500;
89
+ /** What leaves the machine for one legacy source: metadata only (F-0 G-5). Never `content`, never `path`. */
83
90
  export interface LegacyMemoryReport {
84
91
  id: string;
85
92
  kind: LegacyMemoryKind;
86
93
  channel_id?: string;
87
- content: string | null;
88
94
  bytes: number;
89
95
  exportable: boolean;
90
96
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const BRIDGE_VERSION = "0.31.0";
1
+ export declare const BRIDGE_VERSION = "0.33.0";
@@ -28,6 +28,10 @@ export interface FastPathConfig {
28
28
  maxTokens: number;
29
29
  /** Extra request headers. Set only by settingsToVoiceConfig (the SecureComs proxy); never by parseVoiceConfig. */
30
30
  headers?: Readonly<Record<string, string>>;
31
+ /** E.2 (spec §6, OQ-4): the credential file's explicit opt-in to call this provider directly from the agent's
32
+ * machine. Set only by parseVoiceConfig, only for `route: "direct"`; a backend settings fetch then never replaces
33
+ * this config with the SecureComs proxy's. */
34
+ route?: "direct";
31
35
  }
32
36
  export interface VoiceConfig {
33
37
  persona: VoicePersona;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * ADR-0024 step 2a, plan Task 19 (spec §9, B.8): the lookup and binding rule for the hidden per-conversation voice
3
+ * channel V (channel_kind "voice", immutable parent_channel_id = P).
4
+ *
5
+ * Pure functions plus one small cache class; BridgeAgent holds only the wiring (network reads, held MLS state, the
6
+ * ingest cycle). Nothing here logs, reads message content or touches the network.
7
+ *
8
+ * Lookup. V is hidden from the enclave listing, so it reaches the bridge as an unknown channel (a Welcome or a row).
9
+ * The bridge resolves it with GET /api/v1/channels/{V}, which returns channel_kind "voice" and parent_channel_id to a
10
+ * P member and the same 404 to an absent channel and to an unauthorized caller. Only a 200 voice row with a parent is
11
+ * cached forever (the parent link is immutable, spec §1). This channel's own 200 row with another kind is cached as
12
+ * "not a V" (channel_kind is immutable); any other 200, and a voice claim without a usable parent, is an error. A 404 or an error is retried with a bounded back-off: 30 s, doubling, capped at 10 min, never given up
13
+ * (a 404 can become a 200 once the bridge's own Welcome lands).
14
+ *
15
+ * Binding. The bridge binds V to P only when, in its OWN ratchet trees at their current epochs, every leaf identity
16
+ * of V is a leaf identity of P, or exactly one identity R that is not a leaf of P and that V's member list marks
17
+ * is_relay. Any other leaf, a second non-P identity (even a second relay), a non-basic credential, or a tree the
18
+ * bridge does not hold refuses. A P member not yet in V is allowed.
19
+ */
20
+ /** One non-blank leaf, as `MlsGroupKeyProvider.leafTreeSummary` reports it (identity null for a non-basic credential). */
21
+ export interface VoiceLeaf {
22
+ readonly basic: boolean;
23
+ readonly identity: string | null;
24
+ }
25
+ /** A local ratchet tree summary: `ownLeafIndex` null means the bridge holds no group for the channel. */
26
+ export interface VoiceTreeSummary {
27
+ readonly ownLeafIndex: number | null;
28
+ readonly leaves: readonly VoiceLeaf[];
29
+ }
30
+ export type VoiceBindingRefusal =
31
+ /** The bridge holds no V group (an empty tree is not a vacuous subset of P). */
32
+ "v_not_held"
33
+ /** The bridge holds no P group. */
34
+ | "p_not_held"
35
+ /** A leaf of V (or P) carries a non-basic credential, whose identity cannot be compared. */
36
+ | "non_basic_leaf"
37
+ /** Exactly one non-P identity, and V's member list does not mark it is_relay. */
38
+ | "foreign_leaf"
39
+ /** Two or more non-P identities, whatever their relay marks. */
40
+ | "two_non_parent"
41
+ /** Task 20 ruling 2: two or more V leaf identities that V's member list marks is_relay (P leaves or not). */
42
+ | "two_relays"
43
+ /** Exactly one non-P identity, and V's member list could not be read. */
44
+ | "relay_unknown"
45
+ /** The trees moved while they were read; re-evaluated next time (fail closed meanwhile). */
46
+ | "moved"
47
+ /** parent_channel_id names a known V, or a channel no listing has classified (fix M-4). */
48
+ | "parent_not_conversation";
49
+ export type VoiceBindingVerdict = {
50
+ readonly bound: true;
51
+ } | {
52
+ readonly bound: false;
53
+ readonly reason: VoiceBindingRefusal;
54
+ };
55
+ /**
56
+ * The binding rule (spec §9). `isVRelay` reads V's member list (server-set `is_relay`); `undefined` means the list is
57
+ * unavailable, which refuses only when a relay candidate actually needs it.
58
+ */
59
+ export declare function evaluateVoiceBinding(v: VoiceTreeSummary, p: VoiceTreeSummary, isVRelay: ((identityId: string) => boolean) | undefined): VoiceBindingVerdict;
60
+ /**
61
+ * Task 20 ruling 2: R, the relay identity a bound V admits: the unique V leaf identity V's fresh member list marks
62
+ * is_relay, whether or not it is also a P leaf. Undefined when there is none, more than one, or the list is unreadable
63
+ * (no relay frame is accepted then). Call it only for a V that {@link evaluateVoiceBinding} bound on the same inputs.
64
+ */
65
+ export declare function voiceRelayIdentity(v: VoiceTreeSummary, isVRelay: ((identityId: string) => boolean) | undefined): string | undefined;
66
+ /** What one get_channel(V) answer means. */
67
+ export type VoiceResolution = {
68
+ readonly kind: "voice";
69
+ readonly parentChannelId: string;
70
+ } | {
71
+ readonly kind: "not_voice";
72
+ }
73
+ /** 404: absent, or not visible to this caller (the route answers both the same). */
74
+ | {
75
+ readonly kind: "absent";
76
+ }
77
+ /** Any other status, a network error, or a voice row without a usable parent. */
78
+ | {
79
+ readonly kind: "error";
80
+ };
81
+ /** Classify a get_channel response. `status` is "threw" for a network error or deadline. */
82
+ export declare function parseVoiceResolution(channelId: string, status: number | "threw", body: unknown): VoiceResolution;
83
+ /** First retry after a failed lookup; doubles per consecutive failure up to {@link VOICE_RESOLVE_RETRY_MAX_MS}. */
84
+ export declare const VOICE_RESOLVE_RETRY_BASE_MS = 30000;
85
+ export declare const VOICE_RESOLVE_RETRY_MAX_MS: number;
86
+ /** Final review M-f: a 404 is retried within this when the bridge holds an MLS group for the channel. */
87
+ export declare const VOICE_RESOLVE_ABSENT_HELD_MAX_MS = 60000;
88
+ /** How the receive cycle treats one channel's frames. */
89
+ export type VoiceChannelGate =
90
+ /** Not a V: today's path. */
91
+ "normal"
92
+ /** A resolved V, or a channel get_channel answered 404: never a normal channel; acked, not processed. */
93
+ | "drop"
94
+ /** Unclassified (lookup errored, or not yet tried): not processed and not acked, so it is re-served. */
95
+ | "hold";
96
+ /** A held MLS position: the group and epoch of one channel's manager. */
97
+ export interface VoicePosition {
98
+ readonly groupId: string;
99
+ readonly epoch: number;
100
+ }
101
+ /**
102
+ * The positions a verdict was computed at (fix I-1, Task 20 R1). `v` is V's held (group, epoch), compared with each
103
+ * frame's own `openedAt` (the epoch whose secret opened it, from the receive path); null when V was not held. `p` is
104
+ * P's held (group, epoch) as one string, compared with the frame's conservative pre-batch P stamp (no cross-group order
105
+ * is known, so any P commit in the frame's batch refuses it).
106
+ */
107
+ export interface VoiceVerdictPositions {
108
+ readonly v: VoicePosition | null;
109
+ readonly p: string;
110
+ }
111
+ /** What the accept filter needs about a bound V, captured with its verdict (Task 20). */
112
+ export interface VoiceFrameContext {
113
+ readonly parent: string;
114
+ /** R: the relay identity this binding admitted (ruling 2), if any. */
115
+ readonly relay: string | undefined;
116
+ /** P's leaf identities at the verdict's P position. */
117
+ readonly parentMembers: ReadonlySet<string>;
118
+ }
119
+ /**
120
+ * Receipt records kept at most. Task 20 R2: nothing is ever evicted (an evicted record let a re-served frame take a
121
+ * fresh stamp). At the cap a new arrival is simply not recorded, and a frame without a record is refused.
122
+ */
123
+ export declare const VOICE_RECEIPT_CAP = 10000;
124
+ /**
125
+ * Final review M-d: a receipt record older than this is pruned when the map is full. Safe since R2's earlier-cycle
126
+ * guard: a frame first received in an earlier cycle never takes a fresh stamp, record or not. Above the lookup
127
+ * back-off cap, so a frame held across a lookup outage keeps its record while it can still be re-served.
128
+ */
129
+ export declare const VOICE_RECEIPT_MAX_AGE_MS: number;
130
+ /** P's held position as the receipt stamp's comparable string. */
131
+ export declare function voiceParentStamp(p: {
132
+ groupId: string | null;
133
+ epoch: number | null;
134
+ }): string;
135
+ /**
136
+ * The V cache: successful resolutions (forever), failures (bounded back-off), and each V's current binding verdict.
137
+ * Time is passed in, so the class is deterministic.
138
+ */
139
+ export declare class VoiceChannelRegistry {
140
+ private readonly parents;
141
+ private readonly notVoice;
142
+ private readonly failures;
143
+ private readonly bindings;
144
+ private readonly generations;
145
+ /** Fix I-1 / Task 20 R1: message id -> P's held position before the batch that first received it; null = not knowable. */
146
+ private readonly receipts;
147
+ /** Final review M-d: arrivals not recorded because the map was full of young records. */
148
+ private capReached;
149
+ /** True when a lookup should run now: never once resolved; after a failure, only past its back-off. */
150
+ resolveDue(channelId: string, now: number, opts?: {
151
+ heldGroup?: boolean;
152
+ }): boolean;
153
+ record(channelId: string, r: VoiceResolution, now: number): void;
154
+ /** True once get_channel has answered for this channel (voice or not). */
155
+ isResolved(channelId: string): boolean;
156
+ /** Final review M-a: true once get_channel answered this channel's own row with a kind other than voice. */
157
+ isNotVoice(channelId: string): boolean;
158
+ /** Final review (R∈P): bound Vs whose admitted R is also a P leaf (the pre-stamp shape; a sunset signal). */
159
+ relayInParentCount(): number;
160
+ /** True for a resolved V, bound or not. */
161
+ isVoice(channelId: string): boolean;
162
+ /** P of a resolved V, bound or not; undefined otherwise. Never use this to route: see {@link boundParentOf}. */
163
+ parentOf(channelId: string): string | undefined;
164
+ /** Every resolved V with its P. */
165
+ voiceChannels(): Array<[string, string]>;
166
+ /** The gate for a channel the bridge's listing has never classified (`unlisted`), or for a resolved V. */
167
+ gate(channelId: string, unlisted: boolean): VoiceChannelGate;
168
+ bindingKey(channelId: string): string | undefined;
169
+ setBinding(channelId: string, key: string, verdict: VoiceBindingVerdict, positions?: VoiceVerdictPositions | null, admitted?: {
170
+ relay?: string;
171
+ parentMembers?: ReadonlySet<string>;
172
+ }): void;
173
+ /** Fix M-1: each evaluation of a V takes a generation; only the newest started may store its verdict. */
174
+ beginEvaluation(channelId: string): number;
175
+ isLatestEvaluation(channelId: string, generation: number): boolean;
176
+ /**
177
+ * Fix I-1: records a frame's receipt stamp the FIRST time it is seen; a re-served frame keeps its first record.
178
+ * Task 20 R2: at the cap nothing is evicted and the new arrival is not recorded (refused: no record, no parent).
179
+ */
180
+ noteReceipt(messageId: string, pStamp: string | null, now?: number): void;
181
+ /** Final review M-d: arrivals refused because the receipt map was full (a number only). */
182
+ receiptCapReached(): number;
183
+ /** Test visibility: receipt records held. */
184
+ receiptCount(): number;
185
+ forgetReceipt(messageId: string): void;
186
+ /**
187
+ * Fix I-1: P for one frame on V only when V is bound now AND both groups' held positions are exactly those at the
188
+ * frame's receipt. A frame received at other positions (or with none recorded) gets undefined: dropped, never held.
189
+ */
190
+ boundParentForFrame(channelId: string, messageId: string, openedAt?: VoicePosition | null): string | undefined;
191
+ /**
192
+ * Task 20: the per-frame verdict with what the accept filter needs. Bound now AND the frame's pre-batch P stamp equals
193
+ * the verdict's P position AND (R1) the frame's own opening position equals the verdict's V position. A frame with
194
+ * no record, a null record, or no `openedAt` is refused.
195
+ */
196
+ frameContext(channelId: string, messageId: string, openedAt?: VoicePosition | null): VoiceFrameContext | undefined;
197
+ /** The current verdict, or undefined when never evaluated. */
198
+ binding(channelId: string): VoiceBindingVerdict | undefined;
199
+ /** P only while V is a resolved V whose last verdict is bound; undefined otherwise. */
200
+ boundParentOf(channelId: string): string | undefined;
201
+ }
@@ -0,0 +1,22 @@
1
+ export type VoiceFrameClass =
2
+ /** A text frame from R: a model turn (voice-scoped by VS-3). */
3
+ "relay_turn"
4
+ /** The call-start greeting marker from a P member (VS-4). */
5
+ | "greeting"
6
+ /** A call transcript: acked, not a model turn. */
7
+ | "call_transcript"
8
+ /** Anything else: dropped unprocessed, never recorded. */
9
+ | "drop";
10
+ export interface VoiceFrameInput {
11
+ /** The envelope's sender identity. */
12
+ readonly senderId: string;
13
+ /** This agent's own identity (its own frames are never a turn). */
14
+ readonly ownIdentityId: string;
15
+ /** R, or undefined when the binding admitted no relay. */
16
+ readonly relay: string | undefined;
17
+ /** P's leaf identities at the verdict's position. */
18
+ readonly parentMembers: ReadonlySet<string>;
19
+ /** The decrypted plaintext. */
20
+ readonly plaintext: Uint8Array;
21
+ }
22
+ export declare function classifyVoiceFrame(f: VoiceFrameInput): VoiceFrameClass;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securecoms/agent-bridge",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"