@securecoms/agent-bridge 0.23.0 → 0.25.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 +83 -2
- package/connectors/hermes/adapter.py +30 -0
- package/connectors/hermes/tests/test_adapter_wiring.py +23 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +2 -1
- package/connectors/openclaw/openclaw-entry.js +30 -0
- package/connectors/openclaw/openclaw-entry.smoke.test.ts +26 -0
- package/dist/bin.js +23447 -3097
- package/dist/bridge-agent.d.ts +630 -21
- package/dist/coordination.d.ts +24 -2
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +4 -4
- package/dist/index.js +23190 -2839
- package/dist/reply-intent.d.ts +68 -0
- package/dist/sodium-ready.d.ts +11 -5
- package/dist/version.d.ts +1 -1
- package/dist/voice/fast-client.d.ts +12 -2
- package/dist/ws-server.d.ts +33 -4
- package/package.json +12 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** The body of one reply part, as the intent stores it (sealed at rest). */
|
|
2
|
+
export type ReplyBody = {
|
|
3
|
+
kind: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "a2a";
|
|
7
|
+
a2a: {
|
|
8
|
+
a2a_version: string;
|
|
9
|
+
kind: "message" | "task" | "artifact";
|
|
10
|
+
object: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Where an intent lives: its scope (what it answers) and its part within it. */
|
|
14
|
+
export interface ReplyIntentKey {
|
|
15
|
+
scope: string;
|
|
16
|
+
part: string;
|
|
17
|
+
inReplyTo: string | null;
|
|
18
|
+
}
|
|
19
|
+
/** Longest idempotency_key accepted (mirrors the in_reply_to bound). */
|
|
20
|
+
export declare const MAX_IDEMPOTENCY_KEY_LENGTH = 200;
|
|
21
|
+
/** The HMAC key for reply ids and body MACs, derived from the identity seed. */
|
|
22
|
+
export declare function deriveReplyIdKey(identitySeed: Uint8Array): Buffer;
|
|
23
|
+
/**
|
|
24
|
+
* The first 16 bytes of `digest` as an RFC 4122-shaped UUID (version nibble 4,
|
|
25
|
+
* variant 10). The backend parses message_id with `uuid.UUID()` and the
|
|
26
|
+
* envelope schema only requires `format: uuid`; this is the encoding the voice
|
|
27
|
+
* fast path's `fastReplyId` already ships with.
|
|
28
|
+
*/
|
|
29
|
+
export declare function uuidFromDigest(digest: Uint8Array): string;
|
|
30
|
+
/**
|
|
31
|
+
* The deterministic message_id of one reply part. JSON array encoding keeps the
|
|
32
|
+
* input injective (no separator can be smuggled through a free-text key).
|
|
33
|
+
*/
|
|
34
|
+
export declare function replyMessageId(key: Uint8Array, identityId: string, channelId: string, intent: Pick<ReplyIntentKey, "scope" | "part">): string;
|
|
35
|
+
/** Canonical bytes of a reply body (what is sealed and what is MACed). */
|
|
36
|
+
export declare function encodeReplyBody(body: ReplyBody): Uint8Array;
|
|
37
|
+
export declare function decodeReplyBody(bytes: Uint8Array): ReplyBody;
|
|
38
|
+
/**
|
|
39
|
+
* Keyed MAC binding a body to its reply id and channel. Compares retries
|
|
40
|
+
* without storing a plain digest, and (PR #716 review LOW-6) makes a body moved
|
|
41
|
+
* to another row, or a row whose channel was edited, fail verification.
|
|
42
|
+
*/
|
|
43
|
+
export declare function replyBodyMac(key: Uint8Array, replyId: string, channelId: string, body: Uint8Array): string;
|
|
44
|
+
/** The ordinal of an unkeyed reply part (`p:<n>`), or null for a keyed part. */
|
|
45
|
+
export declare function partOrdinal(part: string): number | null;
|
|
46
|
+
/**
|
|
47
|
+
* The intent key for a send. `in_reply_to` wins the scope; an explicit
|
|
48
|
+
* idempotency_key names the part (else the per-delivery ordinal does). An
|
|
49
|
+
* idempotency_key alone is its own scope with a single part.
|
|
50
|
+
*/
|
|
51
|
+
export declare function replyIntentKey(opts: {
|
|
52
|
+
inReplyTo?: string;
|
|
53
|
+
idempotencyKey?: string;
|
|
54
|
+
ordinal: () => number;
|
|
55
|
+
}): ReplyIntentKey | null;
|
|
56
|
+
/** A caller reused an explicit idempotency_key for a different body. */
|
|
57
|
+
export declare class IdempotencyConflictError extends Error {
|
|
58
|
+
constructor();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* PR #716 review LOW-6: a stored reply intent failed verification (its id does
|
|
62
|
+
* not derive from its own identity, channel, scope and part, its body does not
|
|
63
|
+
* match its MAC, or its cached envelope names another id or channel). It is
|
|
64
|
+
* dropped and never published.
|
|
65
|
+
*/
|
|
66
|
+
export declare class ReplyIntentIntegrityError extends Error {
|
|
67
|
+
constructor();
|
|
68
|
+
}
|
package/dist/sodium-ready.d.ts
CHANGED
|
@@ -33,11 +33,17 @@
|
|
|
33
33
|
* second warm-up, not a shortcut.
|
|
34
34
|
*
|
|
35
35
|
* ⛔ 2. DO NOT IMPORT `libsodium-wrappers-sumo` DIRECTLY HERE. ⇒ THIS ONE STILL
|
|
36
|
-
* BINDS ON THIS FILE
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* BINDS ON THIS FILE. Before #895 that specifier was absent from the EXTERNAL
|
|
37
|
+
* list in `esbuild.config.mjs`, so esbuild would have bundled a SECOND
|
|
38
|
+
* libsodium instance out of this file: the await would warm a heap that
|
|
39
|
+
* nothing reads, and the fix would look correct and do nothing. #895 inlines
|
|
40
|
+
* the patched `@agentvault/crypto` and makes `libsodium-wrappers-sumo` EXTERNAL
|
|
41
|
+
* precisely so the process keeps ONE instance, shared by the inlined crypto
|
|
42
|
+
* and anything else. ⛔ That singleton is now carried by the EXTERNAL entry;
|
|
43
|
+
* removing it from that list re-creates the split heap. `scripts/pack-smoke.sh`
|
|
44
|
+
* G-5b proves it on the installed tarball: the warm-up below, which only
|
|
45
|
+
* touches the INLINED crypto, must warm the libsodium the installed tree
|
|
46
|
+
* resolves.
|
|
41
47
|
* ⚠️ The same inlining is why the delegate below is not a no-op in the shipped
|
|
42
48
|
* bundle: the SDK barrier is inlined into `dist/bin.js` exactly once, and this
|
|
43
49
|
* delegate points at that one copy.
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const BRIDGE_VERSION = "0.
|
|
1
|
+
export declare const BRIDGE_VERSION = "0.25.0";
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import type { FastPathConfig } from "./persona";
|
|
2
2
|
import type { ChatMessage } from "./fast-prompt";
|
|
3
|
+
/**
|
|
4
|
+
* SEC-06 (#12): the response body cap. A fast completion is at most 400
|
|
5
|
+
* tokens (a few KB of JSON), and the backend proxy enforces the same 64 KiB.
|
|
6
|
+
*/
|
|
7
|
+
export declare const FAST_LLM_MAX_RESPONSE_BYTES: number;
|
|
3
8
|
export declare class FastLlmError extends Error {
|
|
4
|
-
readonly kind: "timeout" | "http" | "shape" | "network";
|
|
5
|
-
constructor(message: string, kind: "timeout" | "http" | "shape" | "network");
|
|
9
|
+
readonly kind: "timeout" | "http" | "shape" | "network" | "size";
|
|
10
|
+
constructor(message: string, kind: "timeout" | "http" | "shape" | "network" | "size");
|
|
6
11
|
}
|
|
7
12
|
export declare class FastLlmClient {
|
|
8
13
|
private readonly cfg;
|
|
9
14
|
private readonly fetchImpl;
|
|
10
15
|
private readonly key;
|
|
11
16
|
constructor(cfg: FastPathConfig, fetchImpl: typeof fetch);
|
|
17
|
+
/**
|
|
18
|
+
* One deadline (`cfg.timeoutMs`) covers the whole call: connect, headers
|
|
19
|
+
* AND the body. The timer stays armed until the body is read, so a peer that
|
|
20
|
+
* sends headers and then stalls or trickles cannot hold the voice turn.
|
|
21
|
+
*/
|
|
12
22
|
complete(messages: ChatMessage[]): Promise<string>;
|
|
13
23
|
}
|
package/dist/ws-server.d.ts
CHANGED
|
@@ -16,12 +16,19 @@
|
|
|
16
16
|
* { "type":"error", error, message, detail? } // canonical codes (see errors.ts):
|
|
17
17
|
* // channel_pending | rate_limited |
|
|
18
18
|
* // invalid_request | agent_paused |
|
|
19
|
-
* // send_forbidden (
|
|
19
|
+
* // send_forbidden (those two are
|
|
20
20
|
* // TERMINAL: the in_reply_to message
|
|
21
|
-
* // is acked by the daemon, no retry)
|
|
21
|
+
* // is acked by the daemon, no retry) |
|
|
22
|
+
* // group_superseded (NOT terminal:
|
|
23
|
+
* // the reply is republished after
|
|
24
|
+
* // the agent rejoins)
|
|
22
25
|
* Agent → daemon:
|
|
23
26
|
* { "type":"send", channel_id, text?, a2a?, idempotency_key?,
|
|
24
|
-
* in_reply_to? } // exactly one of text | a2a
|
|
27
|
+
* in_reply_to? } // exactly one of text | a2a. SEC-06 #18: in_reply_to
|
|
28
|
+
* // and/or idempotency_key make the send idempotent — a
|
|
29
|
+
* // deterministic keyed message_id and a durable reply
|
|
30
|
+
* // intent written before the publish, so a retry or a
|
|
31
|
+
* // redelivery republishes the SAME reply
|
|
25
32
|
* { "type":"status", channel_id, state, detail? }
|
|
26
33
|
* { "type":"ack", message_id } // confirms a delivered `message` was consumed
|
|
27
34
|
* { "type":"nack", message_id, reason? } // final-wave I1(b): THIS message's turn
|
|
@@ -38,7 +45,9 @@
|
|
|
38
45
|
* Inbound delivery is at-least-once. A delivered `message` is held in-flight
|
|
39
46
|
* until the agent sends a matching `ack`; if the socket drops first, the frame
|
|
40
47
|
* is re-buffered and redelivered to the next consumer. The agent dedups by
|
|
41
|
-
* message_id
|
|
48
|
+
* message_id. SEC-06 #18: a redelivered message whose reply already has a
|
|
49
|
+
* durable intent is NOT handed to the agent again; the daemon republishes that
|
|
50
|
+
* reply itself (same message_id), so the model does not run twice.
|
|
42
51
|
*
|
|
43
52
|
* Bounded work (Phase 3):
|
|
44
53
|
* - F2 ordering: at most ONE message per channel is in flight on a socket;
|
|
@@ -55,6 +64,9 @@
|
|
|
55
64
|
* behind a 2^attempts × 5 s backoff. A nack fails only its own message.
|
|
56
65
|
* Messages never handed over (mid-flush close, a failed frame write) go
|
|
57
66
|
* back without that penalty.
|
|
67
|
+
* - #842: a delivery still un-acked after INFLIGHT_STALE_AFTER_MS is LOGGED
|
|
68
|
+
* once (`runtime_inbox.inflight_stale`) and otherwise left alone. There
|
|
69
|
+
* is deliberately no lease: see INFLIGHT_STALE_AFTER_MS.
|
|
58
70
|
*
|
|
59
71
|
* Single consumer: the daemon serves one agent per process. A new `/v1/ws`
|
|
60
72
|
* connection replaces the prior one (newest wins, mirroring the SSE path); the
|
|
@@ -68,7 +80,24 @@ import type { BridgeAgent } from "./bridge-agent.js";
|
|
|
68
80
|
export interface AgentWebSocketOptions {
|
|
69
81
|
token: string;
|
|
70
82
|
allowOrigins: string[];
|
|
83
|
+
/** Test seam: the stale-in-flight sweep period (default INFLIGHT_STALE_SWEEP_MS). */
|
|
84
|
+
inflightStaleSweepMs?: number;
|
|
85
|
+
/** Test seam: the age past which an un-acked delivery is logged (default INFLIGHT_STALE_AFTER_MS). */
|
|
86
|
+
inflightStaleAfterMs?: number;
|
|
87
|
+
/** Test seam: the clock the delivery ages are measured on. */
|
|
88
|
+
now?: () => number;
|
|
71
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* FB-P1-024 / #842 ruling §3: a stale-in-flight OBSERVATION, not a lease.
|
|
92
|
+
* The bridge cannot tell a message queued inside the consumer from one whose
|
|
93
|
+
* turn is running, so it must not requeue or cancel on age: a lease short
|
|
94
|
+
* enough to matter would re-run live turns (duplicate replies and side
|
|
95
|
+
* effects). It only logs `runtime_inbox.inflight_stale`, once per delivery,
|
|
96
|
+
* content-free, and takes NO action. The post-launch fix is protocol v2
|
|
97
|
+
* (`started` / `cancel` negotiated in `hello`), not a shorter threshold here.
|
|
98
|
+
*/
|
|
99
|
+
export declare const INFLIGHT_STALE_SWEEP_MS = 60000;
|
|
100
|
+
export declare const INFLIGHT_STALE_AFTER_MS: number;
|
|
72
101
|
export declare function attachAgentWebSocket(server: HttpServer, agent: BridgeAgent, opts: AgentWebSocketOptions): {
|
|
73
102
|
close(): void;
|
|
74
103
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@securecoms/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.5.0"
|
|
7
|
+
},
|
|
5
8
|
"main": "dist/index.js",
|
|
6
9
|
"types": "dist/index.d.ts",
|
|
7
10
|
"bin": {
|
|
@@ -34,17 +37,24 @@
|
|
|
34
37
|
"test:watch": "vitest"
|
|
35
38
|
},
|
|
36
39
|
"dependencies": {
|
|
37
|
-
"@
|
|
40
|
+
"@hpke/chacha20poly1305": "^1.8.0",
|
|
41
|
+
"@hpke/core": "^1.8.0",
|
|
42
|
+
"@hpke/dhkem-x448": "^1.8.0",
|
|
43
|
+
"@hpke/hybridkem-x-wing": "^0.7.0",
|
|
44
|
+
"@hpke/ml-kem": "^0.3.0",
|
|
38
45
|
"@noble/ciphers": "^1.3.0",
|
|
39
46
|
"@noble/curves": "^1.9.7",
|
|
40
47
|
"@noble/hashes": "^1.4.0",
|
|
48
|
+
"@noble/post-quantum": "^0.6.0",
|
|
41
49
|
"ajv": "^8.18.0",
|
|
42
50
|
"ajv-formats": "^3.0.1",
|
|
51
|
+
"libsodium-wrappers-sumo": "0.7.15",
|
|
43
52
|
"ws": "^8.21.0",
|
|
44
53
|
"zod": "^3.23.0"
|
|
45
54
|
},
|
|
46
55
|
"devDependencies": {
|
|
47
56
|
"@agentvault/connect-core": "*",
|
|
57
|
+
"@agentvault/crypto": "0.4.1",
|
|
48
58
|
"@types/node": "^22.0.0",
|
|
49
59
|
"@types/ws": "^8.18.1",
|
|
50
60
|
"esbuild": "0.24.0",
|