@securecoms/agent-bridge 0.26.1 → 0.26.3

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.
@@ -51,10 +51,32 @@ export type BridgeToolGrant = {
51
51
  };
52
52
  /**
53
53
  * Immutable authorization context derived from authenticated backend roster
54
- * data. The E2E plaintext never contributes to these claims. Tool grants are
55
- * intentionally empty until the channel-grant persistence/API lands; this
56
- * makes the new contract fail closed during staged deployment.
54
+ * data. The E2E plaintext never contributes to these claims, with ONE bounded
55
+ * exception: a relayed voice turn's `speaker_identity_id`, honoured only when
56
+ * the envelope's authenticated sender is the channel's server-marked relay.
57
+ *
58
+ * Tool grants (architect ruling, 2026-10-02): until channel-grant persistence
59
+ * lands, the ONLY non-empty grant list is OWNER_DM_DEFAULT_READ_GRANTS
60
+ * (`current_time` + `web_search`, read mode), and only on a DM whose sender is
61
+ * the pinned structural owner (or a relayed voice turn whose speaker is). Every
62
+ * other audience, an unpinned or anomalous owner, and the kill switch
63
+ * `SECURECOMS_OWNER_DM_DEFAULT_GRANTS` (anything but unset/`on`) get `[]`.
64
+ * SEC-06 is LIVE for these two tools: docs/security/sec-06-runtime-bounds-audit.md.
65
+ */
66
+ /**
67
+ * Architect ruling 2026-10-02: the default read grants an owner DM carries.
68
+ * A frozen constant in the daemon, NOT runtime config: a runtime cannot widen
69
+ * it, and the runtime's own tool allow-list still intersects it (a tool the
70
+ * runtime does not load is never offered). `fetch_url`, `echo`, `sql_query`
71
+ * and `http_request` are deliberately absent.
72
+ */
73
+ export declare const OWNER_DM_DEFAULT_READ_GRANTS: readonly BridgeToolGrant[];
74
+ /**
75
+ * Kill switch for OWNER_DM_DEFAULT_READ_GRANTS, read once at BridgeAgent
76
+ * construction (daemon startup). Unset or exactly `on` keeps the grants;
77
+ * any other value (`off`, `0`, `false`, empty) sends `[]` on every turn.
57
78
  */
79
+ export declare function ownerDmDefaultGrantsEnabled(env?: NodeJS.ProcessEnv): boolean;
58
80
  export interface BridgeAuthorization {
59
81
  readonly version: 1;
60
82
  readonly audience: AgentAudience;
@@ -272,6 +294,13 @@ export interface BridgeAgentDeps {
272
294
  fastLlm?: {
273
295
  complete(messages: ChatMessage[]): Promise<string>;
274
296
  };
297
+ /**
298
+ * Voice repeats (a): how long a relayed transcribed voice turn waits for
299
+ * the rest of a VAD-split utterance, measured from its first fragment
300
+ * (default: env `SECURECOMS_VOICE_COALESCE_MS`, else 0 = disabled).
301
+ * Injected so tests do not wait the full window.
302
+ */
303
+ voiceCoalesceWindowMs?: number;
275
304
  /**
276
305
  * B7 (FileSafe vault): persistence for the vault kekset's owner-pin TOFU
277
306
  * state. Defaults to a {@link FileOwnerAuthorityStore} at
@@ -280,6 +309,22 @@ export interface BridgeAgentDeps {
280
309
  */
281
310
  vaultOwnerStore?: OwnerAuthorityStore;
282
311
  }
312
+ /**
313
+ * Voice repeats (a), owner report 2026-10-02: one spoken question reached the
314
+ * bridge as two relayed turns ~2 s apart (the relay's VAD split it), which
315
+ * produced a fast reply to the fragment, an ack, and a runtime answer.
316
+ * Relayed transcribed voice turns from the same speaker on the same channel
317
+ * that arrive within this window of the FIRST fragment are joined into one
318
+ * turn before the fast path plans it. The cost is that every relayed voice
319
+ * turn waits this long before it is answered, so it is OFF by default (0):
320
+ * the observed ~2 s split would not fit a window short enough for live
321
+ * voice. Enable with env `SECURECOMS_VOICE_COALESCE_MS` (read at startup).
322
+ */
323
+ export declare const VOICE_COALESCE_WINDOW_MS = 0;
324
+ /** Upper bound for the window: a typo must not park every voice turn for minutes. */
325
+ export declare const VOICE_COALESCE_WINDOW_MAX_MS = 5000;
326
+ /** `SECURECOMS_VOICE_COALESCE_MS` as integer ms, clamped to VOICE_COALESCE_WINDOW_MAX_MS; anything else is 0 (disabled). */
327
+ export declare function voiceCoalesceWindowFromEnv(env?: NodeJS.ProcessEnv): number;
283
328
  /** Default inbound-buffer cap (messages) when deps.maxBuffer is unset. */
284
329
  export declare const DEFAULT_MAX_BUFFER = 1000;
285
330
  /**
@@ -738,6 +783,19 @@ export declare class BridgeAgent {
738
783
  /** Room send-floor tokens reserved by queued fast replies, per channel (F3). */
739
784
  private readonly fastReplyReserved;
740
785
  private static readonly FAST_REPLY_MAX_JOBS;
786
+ /** Voice repeats (a): see {@link VOICE_COALESCE_WINDOW_MS}. */
787
+ private readonly voiceCoalesceWindowMs;
788
+ /** H1: fast-path reply ids being published right now (recorded as voice memory). */
789
+ private readonly spokenReplyIds;
790
+ /** Open coalescing windows, keyed by channel + speaker. */
791
+ private readonly voiceCoalescing;
792
+ /** Ids of every coalesced fragment (capped FIFO): a redelivered row is acked, never a second turn. */
793
+ private readonly voiceCoalescedIds;
794
+ /** Merged turn id -> the absorbed fragments' held frames, retired with it. */
795
+ private readonly voiceCoalescedTails;
796
+ /** (4) recv.lag: application queue ids already measured (capped FIFO), so a re-pulled row is not re-counted. */
797
+ private readonly recvLagSeen;
798
+ private static readonly RECV_LAG_SEEN_CAP;
741
799
  /** Fast-path turns handed to the runtime behind a spoken ack (LLM [[DEFER]], pre-check, or ack_only). */
742
800
  private fastPathDeferred;
743
801
  /** Of those, `full`-mode turns the deterministic pre-check deferred without calling the fast model. */
@@ -830,6 +888,8 @@ export declare class BridgeAgent {
830
888
  * back to the delivery-row-driven Task 11 behaviour only.
831
889
  */
832
890
  private readonly selfHealSweepEnabled;
891
+ /** Read once at startup; see ownerDmDefaultGrantsEnabled. */
892
+ private readonly ownerDmDefaultGrants;
833
893
  /** Task 12: proactive sweep cadence (in addition to the once-at-boot fire). */
834
894
  private static readonly SELF_HEAL_SWEEP_INTERVAL_MS;
835
895
  /**
@@ -1658,6 +1718,7 @@ export declare class BridgeAgent {
1658
1718
  * Owner report 2026-10-02 (calls never used tools) motivates "ack".
1659
1719
  */
1660
1720
  private planFastPath;
1721
+ private noteVoiceCoalesced;
1661
1722
  private markFastPathSeen;
1662
1723
  /**
1663
1724
  * Off-cycle per-channel queue: one promise chain per channel, so within a
@@ -1668,6 +1729,15 @@ export declare class BridgeAgent {
1668
1729
  * reserve a room send-floor token until they finish. A job never throws.
1669
1730
  */
1670
1731
  private enqueueFastReply;
1732
+ /** Count a fast reply against FAST_REPLY_MAX_JOBS and reserve its room send-floor token; returns the release. */
1733
+ private reserveFastReply;
1734
+ /**
1735
+ * Speak a planned fast turn ("ack" or "fast") and settle its held frame:
1736
+ * retired when the fast model answered alone, otherwise released to the
1737
+ * runtime (with the "ack already spoken" guidance only when an ack or a
1738
+ * deferring fast reply was actually published — F3).
1739
+ */
1740
+ private speakAndSettle;
1671
1741
  /**
1672
1742
  * Stage a runtime frame without overtaking a turn of the same channel that
1673
1743
  * is still queued behind the fast path (F2): while the channel has a queue,
@@ -1676,6 +1746,8 @@ export declare class BridgeAgent {
1676
1746
  private stageRuntimeMessageInOrder;
1677
1747
  /** Test seam: resolves once every queued fast reply has settled. */
1678
1748
  settleFastReplies(): Promise<void>;
1749
+ /** Send a line the voice fast path speaks: its memory record is "voice" (H1). */
1750
+ private sendSpoken;
1679
1751
  /** Speak a fixed acknowledgement line; failures are counted, never thrown. */
1680
1752
  private sendFastLine;
1681
1753
  /**
@@ -1705,6 +1777,21 @@ export declare class BridgeAgent {
1705
1777
  private stageHeldRuntimeMessage;
1706
1778
  /** Settle a held frame: the fast path answered alone (retire it) or the runtime answers next (release it, in channel order). */
1707
1779
  private settleHeldRuntimeMessage;
1780
+ /**
1781
+ * Voice repeats (b): a held frame's memory was read when its turn ARRIVED,
1782
+ * before the replies queued ahead of it (an earlier turn's fast reply, this
1783
+ * turn's own ack) were spoken. The runtime replaces its history with the
1784
+ * frame's memory, so with the arrival snapshot it answered an
1785
+ * already-answered turn again. At release the context is re-read and the
1786
+ * AGENT turns it holds beyond the snapshot (afterPublish ->
1787
+ * recordOutboundMemory) are appended to the snapshot. Peer turns recorded
1788
+ * since (this turn itself, later turns) are not, so the frame stays
1789
+ * prior-turn-only for what the runtime is about to answer. New turns are
1790
+ * found by recorded time, never by `seq` (the OpenClaw provider renumbers
1791
+ * seq per read). A failed re-read keeps the arrival snapshot.
1792
+ */
1793
+ private memoryAtRelease;
1794
+ private static readonly TURN_MEMORY_LIMIT;
1708
1795
  /**
1709
1796
  * List channels in one enclave (default: the daemon's own home enclave).
1710
1797
  * Task 3: called per-*known* enclave by refreshChannelMeta so a
@@ -1867,8 +1954,37 @@ export declare class BridgeAgent {
1867
1954
  * Missing or stale metadata is UNKNOWN with zero grants, never a permissive
1868
1955
  * guess. The returned object and grant array are frozen before they cross the
1869
1956
  * daemon boundary so one consumer cannot widen a later execution check.
1957
+ *
1958
+ * Grants (architect ruling 2026-10-02): OWNER_DM_DEFAULT_READ_GRANTS only when
1959
+ * the kill switch is on, the channel is a DM, and EITHER
1960
+ * (a) audience is owner_dm AND the sender is the pinned structural owner
1961
+ * (`channelOwner`, never "anomalous") — a lying `is_agent_owner` flag
1962
+ * alone gets nothing; OR
1963
+ * (b) the sender is this channel's server-marked relay (`is_relay`) and the
1964
+ * relayed `speaker_identity_id` (passed by the caller only for relay
1965
+ * turns) is that same pinned owner, whose roster row is the agent's
1966
+ * owner. A speaker claim from any non-relay sender gets nothing.
1967
+ * Everything else gets the frozen empty list.
1870
1968
  */
1871
1969
  private authorizationFor;
1970
+ /**
1971
+ * One content-free INFO line per authorization decision (every call site of
1972
+ * authorizationFor stages exactly one runtime turn). It says WHY a turn got
1973
+ * the grants it got, without any identifier, name or text: every field is a
1974
+ * boolean, a count, or a closed-set label.
1975
+ *
1976
+ * owner_pin pinned | anomalous | missing (channelOwner cache)
1977
+ * owner_row_is_agent_owner the pinned owner's roster row; `absent` when
1978
+ * there is no pin or the row is not in the roster
1979
+ * sender_is_owner sender identity == pinned owner identity
1980
+ * relay sender's roster row has is_relay
1981
+ * relay_speaker_is_owner `na` unless a relay speaker was passed
1982
+ * kill_switch SECURECOMS_OWNER_DM_DEFAULT_GRANTS as read at
1983
+ * startup: `on` = default grants enabled
1984
+ * meta_fresh / roster_fresh whether the channel meta / roster caches were
1985
+ * fresh (either stale forces audience=unknown)
1986
+ */
1987
+ private logAuthzDecision;
1872
1988
  /**
1873
1989
  * Task 8 (owner-attested Reset Secure Session): STRUCTURAL owner selection —
1874
1990
  * NEVER read the server's `role` field to pick the owner. In a DM the owner
@@ -2015,6 +2131,15 @@ export declare class BridgeAgent {
2015
2131
  */
2016
2132
  private ingestOnce;
2017
2133
  private startIngestCycle;
2134
+ /**
2135
+ * (4) recv.lag = server message created_at -> this pull's completion, per
2136
+ * application row, counted once per queue id. It makes the daemon-side
2137
+ * receive delay measurable (recv.pull alone times only the HTTP call).
2138
+ * Cross-clock: daemon-vs-server skew lands in the sample; a negative lag
2139
+ * is clamped to 0. The server serialises a naive UTC datetime (no offset),
2140
+ * which Date.parse would read as LOCAL time, so a bare timestamp is UTC.
2141
+ */
2142
+ private recordRecvLag;
2018
2143
  private ingestCycle;
2019
2144
  drain(channelId: string | undefined, waitSeconds: number): Promise<BridgeMessage[]>;
2020
2145
  /** Notify-only SSE signal: fired after a buffer push. Listener pulls via takeBuffered. */