@securecoms/agent-bridge 0.21.0 → 0.24.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/README.md +91 -3
- package/connectors/hermes/adapter.py +63 -7
- package/connectors/hermes/tests/test_adapter_wiring.py +23 -0
- package/connectors/hermes/tests/test_logging_privacy.py +61 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +561 -0
- package/connectors/openclaw/openclaw-entry.js +270 -47
- package/connectors/openclaw/openclaw-entry.smoke.test.ts +26 -0
- package/dist/bin.d.ts +3 -7
- package/dist/bin.js +7626 -954
- package/dist/bridge-agent.d.ts +846 -30
- package/dist/coordination.d.ts +112 -5
- package/dist/decrypt-error-log-limiter.d.ts +6 -30
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +24 -5
- package/dist/index.js +7608 -927
- package/dist/reply-intent.d.ts +68 -0
- package/dist/service-log.d.ts +7 -0
- package/dist/summarize.d.ts +3 -2
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-client.d.ts +12 -2
- package/dist/ws-server.d.ts +37 -3
- package/package.json +4 -1
package/dist/coordination.d.ts
CHANGED
|
@@ -1,24 +1,131 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Extract @mention names from plaintext. Returns lowercased names.
|
|
3
|
+
*
|
|
4
|
+
* A token starts with a word character and may contain `.` and `-`, so an agent
|
|
5
|
+
* handle (`@sc-agent-01`) and a full identity UUID mention stay whole (the gen2
|
|
6
|
+
* `@(\w[\w]*)` cut both at the first hyphen, so neither could ever address its
|
|
7
|
+
* agent). Trailing `.`/`-` is sentence punctuation and is trimmed
|
|
8
|
+
* (`thanks @Stephen.` → "stephen").
|
|
9
|
+
*/
|
|
2
10
|
export declare function parseMentions(text: string): string[];
|
|
3
11
|
/**
|
|
4
12
|
* Lower-cased set of names that count as "this agent" in a room: framework
|
|
5
13
|
* agentName + its first word, accountId + its first word, and (when known) the
|
|
6
14
|
* AgentVault room display_name + its first word. First word = split on
|
|
7
|
-
* whitespace or "("
|
|
15
|
+
* whitespace or "("; it also counts with trailing punctuation removed
|
|
16
|
+
* ("Stephen, PhD" → "stephen,", "stephen"), matching {@link parseMentions}.
|
|
17
|
+
*
|
|
18
|
+
* `roomDisplayName` is THIS AGENT's own display_name as a member of the room
|
|
19
|
+
* (its roster row), never the channel's name: a channel called
|
|
20
|
+
* "Hermesav, Cortina011" must not make `@hermesav` address this agent.
|
|
8
21
|
*/
|
|
9
22
|
export declare function ownRoomNames(agentName: string, accountId: string, roomDisplayName?: string): Set<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Phase 3 F3 — default per-room agent-to-agent hop limit: at most this many
|
|
25
|
+
* CONSECUTIVE agent-authored messages (since the last human message) in one
|
|
26
|
+
* channel. Two agents that keep @mentioning each other stop after this many
|
|
27
|
+
* turns instead of spending the owners' BYOK budget forever.
|
|
28
|
+
*/
|
|
29
|
+
export declare const DEFAULT_MAX_AGENT_HOPS = 3;
|
|
30
|
+
/**
|
|
31
|
+
* Whether an agent-authored message may be answered. `agentHops` counts the
|
|
32
|
+
* consecutive agent-authored messages in the channel INCLUDING this one; our
|
|
33
|
+
* reply would be one more agent turn, so it is allowed only while
|
|
34
|
+
* `agentHops < maxAgentHops`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function agentHopAllowed(agentHops: number, maxAgentHops?: number): boolean;
|
|
10
37
|
/**
|
|
11
38
|
* Whether this agent should process a room message based on its @mentions.
|
|
12
39
|
*
|
|
13
|
-
*
|
|
14
|
-
* NOT gate on senderIsAgent — every message is processed unless it @mentions a
|
|
15
|
-
* *different* agent specifically:
|
|
40
|
+
* Human (or unknown) sender — the gen2 floor, unchanged:
|
|
16
41
|
* - no mentions → true (broadcast)
|
|
17
42
|
* - @all / @everyone → true
|
|
18
43
|
* - this agent is mentioned → true
|
|
19
44
|
* - else (a different agent is mentioned) → false
|
|
45
|
+
*
|
|
46
|
+
* Agent sender (Phase 3 F3 loop guard): processed ONLY when that agent
|
|
47
|
+
* @mentions THIS agent by name (a broadcast, @all or @everyone from an agent
|
|
48
|
+
* is not an address) AND the per-room hop limit is not exhausted
|
|
49
|
+
* (`agentHops`, see {@link agentHopAllowed}). An unknown sender type keeps the
|
|
50
|
+
* fail-open human semantics so an unclassifiable message is never dropped.
|
|
20
51
|
*/
|
|
21
52
|
export declare function shouldProcessRoomMessage(plaintext: string, agentName: string, accountId: string, opts?: {
|
|
22
53
|
senderIsAgent?: boolean;
|
|
23
54
|
roomDisplayName?: string;
|
|
55
|
+
/** Consecutive agent-authored messages in this room, including this one. */
|
|
56
|
+
agentHops?: number;
|
|
57
|
+
maxAgentHops?: number;
|
|
58
|
+
}): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Final-wave I2: a channel's agent-hop count resets once this long has passed
|
|
61
|
+
* since the last agent-authored message in it. Without it, two agents in a DM
|
|
62
|
+
* (where no human ever speaks) went permanently deaf to each other after
|
|
63
|
+
* DEFAULT_MAX_AGENT_HOPS turns. A live loop is seconds apart, so the cap still
|
|
64
|
+
* stops it; a conversation resumed ten quiet minutes later starts a new run.
|
|
65
|
+
*/
|
|
66
|
+
export declare const AGENT_HOP_QUIET_MS: number;
|
|
67
|
+
/**
|
|
68
|
+
* Per-channel count of consecutive agent-authored messages since the last
|
|
69
|
+
* human message (or the last quiet window, {@link AGENT_HOP_QUIET_MS}). Fed
|
|
70
|
+
* with EVERY inbound text message of a channel (whether or not this agent
|
|
71
|
+
* processes it) plus this agent's own SUBSTANTIVE sends, so two agents in one
|
|
72
|
+
* room keep the same count from their own vantage points.
|
|
73
|
+
*
|
|
74
|
+
* In memory and bounded (oldest channel evicted first): a restart resets the
|
|
75
|
+
* counts, which can at worst allow one more bounded run of hops.
|
|
76
|
+
*
|
|
77
|
+
* Idempotent per message: delivery is at-least-once (a failed ack redelivers
|
|
78
|
+
* the row, and custody serves it again), so the same message can reach
|
|
79
|
+
* {@link noteInbound} more than once. Each (channel, message id) is counted
|
|
80
|
+
* once, and a repeat returns the hop count its first sighting got, so the
|
|
81
|
+
* repeat gets the same verdict and leaves the run untouched. Counting a repeat
|
|
82
|
+
* inflated the run until the NEXT agent message was suppressed and acked, a
|
|
83
|
+
* silent loss. The key is sound because a message id is globally unique at the
|
|
84
|
+
* backend (`mls_messages_pkey`): a second, different message under a reused id
|
|
85
|
+
* is refused (`message_id_conflict`) and never fanned out, so a peer cannot
|
|
86
|
+
* use it to slip past the cap.
|
|
87
|
+
*/
|
|
88
|
+
export declare class AgentHopTracker {
|
|
89
|
+
private readonly maxChannels;
|
|
90
|
+
private readonly hops;
|
|
91
|
+
/** `${channelId}\u0000${messageId}` → the hop count its first sighting got. Map order = age. */
|
|
92
|
+
private readonly seen;
|
|
93
|
+
private readonly now;
|
|
94
|
+
private readonly quietMs;
|
|
95
|
+
private readonly maxSeenIds;
|
|
96
|
+
constructor(maxChannels?: number, opts?: {
|
|
97
|
+
now?: () => number;
|
|
98
|
+
quietMs?: number;
|
|
99
|
+
maxSeenIds?: number;
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Record an inbound message; returns the consecutive agent hops including it.
|
|
103
|
+
* `messageId` makes the call idempotent (see the class comment). Without one
|
|
104
|
+
* the message is counted every time, the conservative direction for a loop
|
|
105
|
+
* guard.
|
|
106
|
+
*/
|
|
107
|
+
noteInbound(channelId: string, senderIsAgent: boolean | undefined, messageId?: string): number;
|
|
108
|
+
/** Test seam: how many message ids are remembered. */
|
|
109
|
+
seenIdCount(): number;
|
|
110
|
+
/**
|
|
111
|
+
* Record one of this agent's own SUBSTANTIVE sends (an agent turn in the
|
|
112
|
+
* channel). Callers do not report status frames, daily-limit or dead-letter
|
|
113
|
+
* notices, or continuation parts of one multi-part reply.
|
|
114
|
+
*/
|
|
115
|
+
noteOwnSend(channelId: string): void;
|
|
116
|
+
size(): number;
|
|
117
|
+
private bump;
|
|
118
|
+
private set;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Final-wave I2: whether a runtime `send` is a SUBSTANTIVE agent turn for the
|
|
122
|
+
* hop count. Not one: a notice (`notice: true` — e.g. the daily-limit reply)
|
|
123
|
+
* or a continuation part of a multi-part reply (its in_reply_to already has a
|
|
124
|
+
* durable reply receipt, i.e. an earlier part was published).
|
|
125
|
+
*/
|
|
126
|
+
export declare function isAgentTurn(receipts: {
|
|
127
|
+
runtimeReplyReceipt(inReplyTo: string): unknown;
|
|
128
|
+
}, frame: {
|
|
129
|
+
notice?: unknown;
|
|
130
|
+
in_reply_to?: unknown;
|
|
24
131
|
}): boolean;
|
|
@@ -1,25 +1,19 @@
|
|
|
1
|
+
import { type ServiceLog } from "./service-log.js";
|
|
1
2
|
export interface DecryptErrorInfo {
|
|
2
|
-
/** MLS epoch the failing row was at,
|
|
3
|
+
/** MLS epoch the failing row was at, when it is a finite number. */
|
|
3
4
|
epoch?: number | string;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
/** The decrypt error's message (already stringified by the caller). */
|
|
7
|
-
message: string;
|
|
5
|
+
/** Original thrown value; reduced to a bounded class by the service logger. */
|
|
6
|
+
error: unknown;
|
|
8
7
|
}
|
|
9
8
|
export interface DecryptErrorLogLimiterOpts {
|
|
10
9
|
/** Suppression window in ms. Default 60_000 (60s). */
|
|
11
10
|
windowMs?: number;
|
|
12
11
|
/** Clock seam for tests. Defaults to Date.now. */
|
|
13
12
|
now?: () => number;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
log?: (line: string) => void;
|
|
13
|
+
/** Structured service-log seam for tests. */
|
|
14
|
+
log?: ServiceLog;
|
|
17
15
|
}
|
|
18
|
-
/** Default suppression window: 60 seconds. */
|
|
19
16
|
export declare const DEFAULT_DECRYPT_ERROR_LOG_WINDOW_MS = 60000;
|
|
20
|
-
/** Hard cap on distinct error reasons surfaced per channel per window. A wedged
|
|
21
|
-
* channel has a small handful of genuine reasons; beyond this the extra volume
|
|
22
|
-
* is noise (or an adversarial/buggy server cycling messages) and is suppressed. */
|
|
23
17
|
export declare const MAX_DISTINCT_PER_WINDOW = 4;
|
|
24
18
|
export declare class DecryptErrorLogLimiter {
|
|
25
19
|
private readonly windowMs;
|
|
@@ -27,25 +21,7 @@ export declare class DecryptErrorLogLimiter {
|
|
|
27
21
|
private readonly log;
|
|
28
22
|
private readonly windows;
|
|
29
23
|
constructor(opts?: DecryptErrorLogLimiterOpts);
|
|
30
|
-
/**
|
|
31
|
-
* Record one decrypt error for `channelId`. Logs immediately if this is
|
|
32
|
-
* the channel's first error since its window opened (or its prior window
|
|
33
|
-
* had already elapsed, in which case a summary for that just-closed
|
|
34
|
-
* window is flushed first); otherwise the error is counted silently
|
|
35
|
-
* toward the window's eventual summary line.
|
|
36
|
-
*/
|
|
37
24
|
record(channelId: string, info: DecryptErrorInfo): void;
|
|
38
|
-
/**
|
|
39
|
-
* Clear a channel's window (e.g. once the caller learns the channel
|
|
40
|
-
* cleared/healed) so its NEXT error logs immediately as a fresh "first"
|
|
41
|
-
* rather than being folded into a stale window's summary. There is
|
|
42
|
-
* currently no onDecryptSuccess-style hook from the SDK to wire this to
|
|
43
|
-
* automatically (see bridge-agent.ts's onDecryptError wiring comment) —
|
|
44
|
-
* exposed here so a future success signal (or a caller with its own
|
|
45
|
-
* channel-cleared signal) can reset explicitly. In its absence, the
|
|
46
|
-
* window simply rolls over on interval elapse, which is an accepted
|
|
47
|
-
* simplification (see report).
|
|
48
|
-
*/
|
|
49
25
|
reset(channelId: string): void;
|
|
50
26
|
private logFirst;
|
|
51
27
|
private flushSummary;
|
package/dist/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* WebSocket returns `{type:"error", error, message, detail?}`. A client gets the
|
|
7
7
|
* same `error` code regardless of which transport it used. (A2A §9.5/§10.6 pattern.)
|
|
8
8
|
*/
|
|
9
|
-
export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "unknown_action_run" | "vault_integrity_failed" | "internal";
|
|
9
|
+
export type BridgeErrorCode = "unauthorized" | "forbidden_origin" | "not_found" | "invalid_request" | "invalid_json" | "body_too_large" | "channel_pending" | "rate_limited" | "agent_paused" | "send_forbidden" | "group_superseded" | "unknown_action_run" | "vault_integrity_failed" | "internal";
|
|
10
10
|
export interface BridgeErrorSpec {
|
|
11
11
|
/** HTTP status this code maps to (the WS binding ignores it). */
|
|
12
12
|
http: number;
|
package/dist/http-server.d.ts
CHANGED
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { BridgeAgent, BridgeMessage } from "./bridge-agent.js";
|
|
4
|
+
/**
|
|
5
|
+
* #335 handling decision: the throwing-write swallow stays (a dying socket
|
|
6
|
+
* must cost only that connection's remainder — the caller requeues it and
|
|
7
|
+
* only acks what was written — never crash the flush loop), but it is no
|
|
8
|
+
* longer invisible: the optional `onWriteError` hook reports the thrown
|
|
9
|
+
* value so the caller can service-log its sanitized class. The hook is
|
|
10
|
+
* telemetry-only by contract: a throwing hook is contained here, so it can
|
|
11
|
+
* never change the returned remainder or turn a skipped write into a crash.
|
|
12
|
+
* (Relay precedent: connect-room-relay src/http.ts `writeSse`, PR #654.)
|
|
13
|
+
*/
|
|
4
14
|
export declare function writeSse(res: {
|
|
5
15
|
destroyed?: boolean;
|
|
6
16
|
writableEnded?: boolean;
|
|
7
17
|
write: (s: string) => boolean;
|
|
8
|
-
}, msgs: BridgeMessage[]): BridgeMessage[];
|
|
18
|
+
}, msgs: BridgeMessage[], onWriteError?: (error: unknown) => void): BridgeMessage[];
|
|
19
|
+
/**
|
|
20
|
+
* Once-per-connection write-failure reporter (#335). A throwing `res.write`
|
|
21
|
+
* means the socket is dying — teardown follows via the request's `'close'` —
|
|
22
|
+
* so one WARN per connection is the whole story; repeats from a re-entered
|
|
23
|
+
* flush on the same corpse stay silent. Logs the sanitized `error_class`
|
|
24
|
+
* only (ServiceLogFields is number/boolean-typed by design — message text,
|
|
25
|
+
* channel ids, and exception prose cannot reach the log line).
|
|
26
|
+
*/
|
|
27
|
+
export declare function sseWriteFailureReporter(): (error: unknown) => void;
|
|
9
28
|
export interface HttpServerOptions {
|
|
10
29
|
token: string;
|
|
11
30
|
allowOrigins: string[];
|
|
@@ -34,32 +53,32 @@ export declare const memoryRecordSchema: z.ZodEffects<z.ZodObject<{
|
|
|
34
53
|
message_id: z.ZodOptional<z.ZodString>;
|
|
35
54
|
idempotency_key: z.ZodOptional<z.ZodString>;
|
|
36
55
|
}, "strip", z.ZodTypeAny, {
|
|
37
|
-
role: "agent" | "peer";
|
|
38
56
|
text: string;
|
|
57
|
+
role: "agent" | "peer";
|
|
39
58
|
conversation_id: string;
|
|
40
59
|
modality: "voice" | "text";
|
|
41
60
|
message_id?: string | undefined;
|
|
42
61
|
idempotency_key?: string | undefined;
|
|
43
62
|
peer_id?: string | undefined;
|
|
44
63
|
}, {
|
|
45
|
-
role: "agent" | "peer";
|
|
46
64
|
text: string;
|
|
65
|
+
role: "agent" | "peer";
|
|
47
66
|
conversation_id: string;
|
|
48
67
|
modality: "voice" | "text";
|
|
49
68
|
message_id?: string | undefined;
|
|
50
69
|
idempotency_key?: string | undefined;
|
|
51
70
|
peer_id?: string | undefined;
|
|
52
71
|
}>, {
|
|
53
|
-
role: "agent" | "peer";
|
|
54
72
|
text: string;
|
|
73
|
+
role: "agent" | "peer";
|
|
55
74
|
conversation_id: string;
|
|
56
75
|
modality: "voice" | "text";
|
|
57
76
|
message_id?: string | undefined;
|
|
58
77
|
idempotency_key?: string | undefined;
|
|
59
78
|
peer_id?: string | undefined;
|
|
60
79
|
}, {
|
|
61
|
-
role: "agent" | "peer";
|
|
62
80
|
text: string;
|
|
81
|
+
role: "agent" | "peer";
|
|
63
82
|
conversation_id: string;
|
|
64
83
|
modality: "voice" | "text";
|
|
65
84
|
message_id?: string | undefined;
|