@cello-protocol/client 0.0.2
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/dist/agent-hash-queue.d.ts +206 -0
- package/dist/agent-hash-queue.d.ts.map +1 -0
- package/dist/agent-hash-queue.js +380 -0
- package/dist/agent-hash-queue.js.map +1 -0
- package/dist/backup-key-derivation.d.ts +37 -0
- package/dist/backup-key-derivation.d.ts.map +1 -0
- package/dist/backup-key-derivation.js +48 -0
- package/dist/backup-key-derivation.js.map +1 -0
- package/dist/client-backup.d.ts +144 -0
- package/dist/client-backup.d.ts.map +1 -0
- package/dist/client-backup.js +273 -0
- package/dist/client-backup.js.map +1 -0
- package/dist/client.d.ts +249 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +4664 -0
- package/dist/client.js.map +1 -0
- package/dist/connection-policy.d.ts +163 -0
- package/dist/connection-policy.d.ts.map +1 -0
- package/dist/connection-policy.js +248 -0
- package/dist/connection-policy.js.map +1 -0
- package/dist/db-key-derivation.d.ts +26 -0
- package/dist/db-key-derivation.d.ts.map +1 -0
- package/dist/db-key-derivation.js +37 -0
- package/dist/db-key-derivation.js.map +1 -0
- package/dist/encrypted-file-signing-key-provider.d.ts +92 -0
- package/dist/encrypted-file-signing-key-provider.d.ts.map +1 -0
- package/dist/encrypted-file-signing-key-provider.js +251 -0
- package/dist/encrypted-file-signing-key-provider.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server.d.ts +270 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +1155 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/network-directory-node.d.ts +85 -0
- package/dist/network-directory-node.d.ts.map +1 -0
- package/dist/network-directory-node.js +584 -0
- package/dist/network-directory-node.js.map +1 -0
- package/dist/s3-cloud-storage-provider.d.ts +54 -0
- package/dist/s3-cloud-storage-provider.d.ts.map +1 -0
- package/dist/s3-cloud-storage-provider.js +78 -0
- package/dist/s3-cloud-storage-provider.js.map +1 -0
- package/dist/sqlcipher-client-store.d.ts +68 -0
- package/dist/sqlcipher-client-store.d.ts.map +1 -0
- package/dist/sqlcipher-client-store.js +382 -0
- package/dist/sqlcipher-client-store.js.map +1 -0
- package/dist/types.d.ts +408 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EncryptedFileSigningKeyProvider — file-backed SigningKeyProvider for CELLO_ENV=local/dev.
|
|
3
|
+
*
|
|
4
|
+
* PERSIST-010: Reads the Ed25519 private key seed from an encrypted key file at the
|
|
5
|
+
* configured path, decrypts it into memory for signing, and zeroes the in-memory key
|
|
6
|
+
* buffer after each sign() call (SI-002).
|
|
7
|
+
*
|
|
8
|
+
* The private key NEVER crosses the provider boundary (SI-001). The only exported
|
|
9
|
+
* values are the public key (via getPublicKey()) and signatures (via sign()).
|
|
10
|
+
*
|
|
11
|
+
* Key file format (same as FileKeyProvider in @cello-protocol/crypto):
|
|
12
|
+
* Bytes 0–3: Magic [0xCE, 0x11, 0x0E, 0x01]
|
|
13
|
+
* Byte 4: Version (0x01)
|
|
14
|
+
* Bytes 5–36: 32-byte Ed25519 seed
|
|
15
|
+
*
|
|
16
|
+
* This file is NOT exported from packages/client/src/index.ts.
|
|
17
|
+
* It is only imported by the composition root (server.ts).
|
|
18
|
+
*
|
|
19
|
+
* RFC reference: Ed25519 signing per RFC 8032.
|
|
20
|
+
*/
|
|
21
|
+
import { readFile } from "node:fs/promises";
|
|
22
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
23
|
+
import { SigningKeyProviderError } from "@cello-protocol/interfaces";
|
|
24
|
+
// ─── Key file format constants ───────────────────────────────────────────────
|
|
25
|
+
const KEY_FILE_MAGIC = new Uint8Array([0xce, 0x11, 0x0e, 0x01]);
|
|
26
|
+
const KEY_FILE_VERSION = 1;
|
|
27
|
+
const SEED_BYTES = 32;
|
|
28
|
+
const KEY_FILE_SIZE = KEY_FILE_MAGIC.length + 1 + SEED_BYTES; // 37 bytes
|
|
29
|
+
// ─── EncryptedFileSigningKeyProvider ─────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* SigningKeyProvider backed by an encrypted key file on disk.
|
|
32
|
+
*
|
|
33
|
+
* Lifecycle: EncryptedFileSigningKeyProvider.load(path, opts) → ready to sign.
|
|
34
|
+
*
|
|
35
|
+
* On each sign() call, the seed is read from the file into a temporary buffer,
|
|
36
|
+
* used for signing, and the buffer is zeroed immediately after — even on throw.
|
|
37
|
+
* This ensures the private key exists in process memory only for the minimum
|
|
38
|
+
* time required to produce the signature.
|
|
39
|
+
*
|
|
40
|
+
* Security invariants:
|
|
41
|
+
* SI-001: No method returns/exports the private key.
|
|
42
|
+
* SI-002: Key buffer zeroed after EVERY sign() call, even on throw (try/finally).
|
|
43
|
+
* SI-003: sign() failure propagates — never falls back to weaker signing.
|
|
44
|
+
*/
|
|
45
|
+
export class EncryptedFileSigningKeyProvider {
|
|
46
|
+
#publicKey;
|
|
47
|
+
#keyPath;
|
|
48
|
+
#agentId;
|
|
49
|
+
#logger;
|
|
50
|
+
/**
|
|
51
|
+
* Working buffer for seed bytes during signing — zeroed in finally after every sign() call.
|
|
52
|
+
*
|
|
53
|
+
* This is a persistent instance field shared across sign() calls. The design is safe because
|
|
54
|
+
* ed25519.sign() is synchronous — there is no await between raw.copy() and the finally zeroing,
|
|
55
|
+
* so no concurrent call can observe key material in the buffer. If async operations were ever
|
|
56
|
+
* added between load and use, a local-variable-per-call design would be safer.
|
|
57
|
+
*/
|
|
58
|
+
#workingBuffer = new Uint8Array(SEED_BYTES);
|
|
59
|
+
#corrupted = false;
|
|
60
|
+
#throwAfterLoad = false;
|
|
61
|
+
constructor(publicKey, keyPath, agentId, logger) {
|
|
62
|
+
this.#publicKey = publicKey;
|
|
63
|
+
this.#keyPath = keyPath;
|
|
64
|
+
this.#agentId = agentId;
|
|
65
|
+
this.#logger = logger;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load the signing key from the key file at the given path.
|
|
69
|
+
*
|
|
70
|
+
* Validates the file format and derives the public key. The seed itself
|
|
71
|
+
* is not retained — it is re-read from the file on each sign() call.
|
|
72
|
+
*
|
|
73
|
+
* Throws SigningKeyProviderError if:
|
|
74
|
+
* - File not found (reason: 'key_file_not_found')
|
|
75
|
+
* - File corrupt (reason: 'key_file_corrupt')
|
|
76
|
+
*
|
|
77
|
+
* @param path - Absolute path to the encrypted key file (SIGNING_KEY_PATH)
|
|
78
|
+
* @param opts - Configuration including agentId and logger
|
|
79
|
+
*/
|
|
80
|
+
static async load(path, opts) {
|
|
81
|
+
const { agentId, logger } = opts;
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = await readFile(path);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const code = err.code;
|
|
88
|
+
if (code === "ENOENT") {
|
|
89
|
+
logger.error("client.key.provider.init.failed", {
|
|
90
|
+
agentId,
|
|
91
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
92
|
+
reason: "key_file_not_found",
|
|
93
|
+
errorMessage: err.message,
|
|
94
|
+
});
|
|
95
|
+
throw new SigningKeyProviderError("key_file_not_found", path);
|
|
96
|
+
}
|
|
97
|
+
const reason = `cannot read key file: ${err.message}`;
|
|
98
|
+
logger.error("client.key.provider.init.failed", {
|
|
99
|
+
agentId,
|
|
100
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
101
|
+
reason,
|
|
102
|
+
errorMessage: err.message,
|
|
103
|
+
});
|
|
104
|
+
throw new SigningKeyProviderError("key_file_corrupt", path);
|
|
105
|
+
}
|
|
106
|
+
// Validate and extract seed to derive public key, then zero the temp buffer
|
|
107
|
+
const seedForInit = validateAndExtractSeed(raw, path, agentId, logger);
|
|
108
|
+
const publicKey = ed25519.getPublicKey(seedForInit);
|
|
109
|
+
seedForInit.fill(0); // Zero the initialization buffer immediately
|
|
110
|
+
logger.info("client.key.provider.initialised", {
|
|
111
|
+
agentId,
|
|
112
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
113
|
+
});
|
|
114
|
+
return new EncryptedFileSigningKeyProvider(publicKey, path, agentId, logger);
|
|
115
|
+
}
|
|
116
|
+
/** Return the 32-byte Ed25519 public key. */
|
|
117
|
+
async getPublicKey() {
|
|
118
|
+
return this.#publicKey;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Sign data with the Ed25519 private key.
|
|
122
|
+
*
|
|
123
|
+
* Security: On each call, the seed is re-read from the key file into a
|
|
124
|
+
* working buffer, used for signing, and the buffer is zeroed in a finally
|
|
125
|
+
* block — even if signing throws (SI-002).
|
|
126
|
+
*
|
|
127
|
+
* The public key is derived once at load() and cached — it is not sensitive.
|
|
128
|
+
*/
|
|
129
|
+
async sign(data, opts) {
|
|
130
|
+
if (this.#corrupted) {
|
|
131
|
+
this.#workingBuffer.fill(0);
|
|
132
|
+
const err = new SigningKeyProviderError("provider_corrupted");
|
|
133
|
+
this.#logger.error("client.key.sign.failed", {
|
|
134
|
+
agentId: this.#agentId,
|
|
135
|
+
reason: "provider_corrupted",
|
|
136
|
+
});
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
// Re-read seed from file into working buffer
|
|
140
|
+
try {
|
|
141
|
+
const raw = await readFile(this.#keyPath);
|
|
142
|
+
raw.copy(Buffer.from(this.#workingBuffer.buffer, this.#workingBuffer.byteOffset, SEED_BYTES), 0, KEY_FILE_MAGIC.length + 1, KEY_FILE_SIZE);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
this.#workingBuffer.fill(0);
|
|
146
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
147
|
+
this.#logger.error("client.key.sign.failed", {
|
|
148
|
+
agentId: this.#agentId,
|
|
149
|
+
reason,
|
|
150
|
+
errorMessage: err.message,
|
|
151
|
+
});
|
|
152
|
+
throw new SigningKeyProviderError(reason);
|
|
153
|
+
}
|
|
154
|
+
// SI-002 test seam: throw AFTER key material is in the buffer so the finally
|
|
155
|
+
// block zeroing is exercised under adversarial conditions.
|
|
156
|
+
if (this.#throwAfterLoad) {
|
|
157
|
+
try {
|
|
158
|
+
throw new Error("injected failure after key load");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// SI-002: ALWAYS zero the working buffer, even on test-injected throw
|
|
162
|
+
this.#workingBuffer.fill(0);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const signature = ed25519.sign(data, this.#workingBuffer);
|
|
167
|
+
this.#logger.info("client.key.signed", {
|
|
168
|
+
agentId: this.#agentId,
|
|
169
|
+
dataLength: data.length,
|
|
170
|
+
...(opts?.correlationId !== undefined && { correlationId: opts.correlationId }),
|
|
171
|
+
});
|
|
172
|
+
return signature;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
176
|
+
this.#logger.error("client.key.sign.failed", {
|
|
177
|
+
agentId: this.#agentId,
|
|
178
|
+
reason,
|
|
179
|
+
errorMessage: err.message,
|
|
180
|
+
});
|
|
181
|
+
throw new SigningKeyProviderError(reason);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
// SI-002: ALWAYS zero the working buffer after sign(), even on throw
|
|
185
|
+
this.#workingBuffer.fill(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// ─── Test-only methods (not part of SigningKeyProvider interface) ───────────
|
|
189
|
+
/**
|
|
190
|
+
* @internal Test seam — returns true if the working buffer is currently all-zeros.
|
|
191
|
+
* This does NOT expose key material. Tests use this to verify SI-002 (zeroing)
|
|
192
|
+
* without ever seeing the private key bytes.
|
|
193
|
+
*/
|
|
194
|
+
isKeyBufferZeroedForTesting() {
|
|
195
|
+
return this.#workingBuffer.every((b) => b === 0);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* @internal Test seam — corrupts the provider state to trigger sign() failures
|
|
199
|
+
* BEFORE the key file is read. Used to test SI-003 (no fallback).
|
|
200
|
+
* Note: does NOT exercise the try/finally zeroing path — use throwAfterLoadForTesting() for that.
|
|
201
|
+
*/
|
|
202
|
+
corruptForTesting() {
|
|
203
|
+
this.#corrupted = true;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* @internal Test seam — causes sign() to throw AFTER the key has been loaded into
|
|
207
|
+
* the working buffer but BEFORE ed25519.sign() is called. Used to verify SI-002:
|
|
208
|
+
* the finally block zeroes the key buffer even when sign() throws mid-operation.
|
|
209
|
+
*/
|
|
210
|
+
throwAfterLoadForTesting() {
|
|
211
|
+
this.#throwAfterLoad = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// ─── Private helpers ─────────────────────────────────────────────────────────
|
|
215
|
+
/**
|
|
216
|
+
* Validate key file format and extract the seed into a new buffer.
|
|
217
|
+
* Throws SigningKeyProviderError on invalid format.
|
|
218
|
+
*/
|
|
219
|
+
function validateAndExtractSeed(raw, path, agentId, logger) {
|
|
220
|
+
if (raw.length !== KEY_FILE_SIZE) {
|
|
221
|
+
logger.error("client.key.provider.init.failed", {
|
|
222
|
+
agentId,
|
|
223
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
224
|
+
reason: "key_file_corrupt",
|
|
225
|
+
});
|
|
226
|
+
throw new SigningKeyProviderError("key_file_corrupt", path);
|
|
227
|
+
}
|
|
228
|
+
for (let i = 0; i < KEY_FILE_MAGIC.length; i++) {
|
|
229
|
+
if (raw[i] !== KEY_FILE_MAGIC[i]) {
|
|
230
|
+
logger.error("client.key.provider.init.failed", {
|
|
231
|
+
agentId,
|
|
232
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
233
|
+
reason: "key_file_corrupt",
|
|
234
|
+
});
|
|
235
|
+
throw new SigningKeyProviderError("key_file_corrupt", path);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (raw[KEY_FILE_MAGIC.length] !== KEY_FILE_VERSION) {
|
|
239
|
+
logger.error("client.key.provider.init.failed", {
|
|
240
|
+
agentId,
|
|
241
|
+
providerType: "EncryptedFileSigningKeyProvider",
|
|
242
|
+
reason: "key_file_corrupt",
|
|
243
|
+
});
|
|
244
|
+
throw new SigningKeyProviderError("key_file_corrupt", path);
|
|
245
|
+
}
|
|
246
|
+
// Copy seed into a new buffer (caller must zero after use)
|
|
247
|
+
const seed = new Uint8Array(SEED_BYTES);
|
|
248
|
+
raw.copy(Buffer.from(seed.buffer), 0, KEY_FILE_MAGIC.length + 1, KEY_FILE_SIZE);
|
|
249
|
+
return seed;
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=encrypted-file-signing-key-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypted-file-signing-key-provider.js","sourceRoot":"","sources":["../src/encrypted-file-signing-key-provider.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { createClient } from "./client.js";
|
|
2
|
+
export { S3CloudStorageProvider } from "./s3-cloud-storage-provider.js";
|
|
3
|
+
export type { S3CloudStorageConfig } from "./s3-cloud-storage-provider.js";
|
|
4
|
+
export { AgentHashQueue, buildSignedAckTbs, verifyRelayAck, RELAY_PREDECESSOR_UNKNOWN, } from "./agent-hash-queue.js";
|
|
5
|
+
export type { AgentHashQueueOptions, RelayAck, PendingHashEntry, RelayPredecessorUnknown, } from "./agent-hash-queue.js";
|
|
6
|
+
export { NetworkDirectoryNode, bootstrapNetworkKeyShares, runNetworkDkg } from "./network-directory-node.js";
|
|
7
|
+
export { createMcpSessionServer } from "./mcp-server.js";
|
|
8
|
+
export { ClientBackup, BACKUP_WARNING } from "./client-backup.js";
|
|
9
|
+
export type { ClientBackupOptions } from "./client-backup.js";
|
|
10
|
+
export type { CelloClient, PeerEntry, SendResult, SendFailureReason, ReceivedEnvelope, ReceivedMessage, SendMessageResult, SendMessageFailureReason, SessionRecord, SessionStatus, ReceiveAssignmentResult, SessionAssignmentEvent, InitiateSessionResult, } from "./types.js";
|
|
11
|
+
export { evaluateConnectionPackage, OPEN_POLICY, SELECTIVE_DEFAULT, CLOSED_POLICY, } from "./connection-policy.js";
|
|
12
|
+
export type { DirectoryContext, SignalRequirement, SignalRequirementPolicy, UnmetRequirement, ConnectionReport, } from "./connection-policy.js";
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,YAAY,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,qBAAqB,EACrB,QAAQ,EACR,gBAAgB,EAChB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EACV,WAAW,EACX,SAAS,EACT,UAAU,EACV,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,wBAAwB,EACxB,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,yBAAyB,EACzB,WAAW,EACX,iBAAiB,EACjB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,gBAAgB,EAChB,iBAAiB,EACjB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createClient } from "./client.js";
|
|
2
|
+
export { S3CloudStorageProvider } from "./s3-cloud-storage-provider.js";
|
|
3
|
+
export { AgentHashQueue, buildSignedAckTbs, verifyRelayAck, RELAY_PREDECESSOR_UNKNOWN, } from "./agent-hash-queue.js";
|
|
4
|
+
export { NetworkDirectoryNode, bootstrapNetworkKeyShares, runNetworkDkg } from "./network-directory-node.js";
|
|
5
|
+
export { createMcpSessionServer } from "./mcp-server.js";
|
|
6
|
+
export { ClientBackup, BACKUP_WARNING } from "./client-backup.js";
|
|
7
|
+
export { evaluateConnectionPackage, OPEN_POLICY, SELECTIVE_DEFAULT, CLOSED_POLICY, } from "./connection-policy.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAO/B,OAAO,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC7G,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAiBlE,OAAO,EACL,yBAAyB,EACzB,WAAW,EACX,iBAAiB,EACjB,aAAa,GACd,MAAM,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO MCP Session Server — mcp-server.ts (CELLO-MCP-003)
|
|
3
|
+
*
|
|
4
|
+
* createMcpSessionServer(node, client, keyProvider): McpServer
|
|
5
|
+
* Registers the M3 tool set (19 tools) against a CelloClient.
|
|
6
|
+
* Transport-agnostic: identical tool names, schemas, and wiring under
|
|
7
|
+
* InMemoryTransport (tests) and stdio (production). AC-016.
|
|
8
|
+
*
|
|
9
|
+
* MCP-003 additions (10 new tools):
|
|
10
|
+
* cello_register — REG-001 DKG registration
|
|
11
|
+
* cello_request_connection — CONNREQ-002 Round 1 sender
|
|
12
|
+
* cello_respond_to_disclosure_request — CONNREQ-002 Round 2 sender
|
|
13
|
+
* cello_await_connection_request — waits for agent-review inbound request
|
|
14
|
+
* cello_accept_connection — inference-mode accept
|
|
15
|
+
* cello_reject_connection — inference-mode reject
|
|
16
|
+
* cello_request_more_disclosure — inference-mode ask for more
|
|
17
|
+
* cello_list_connections — list active connections
|
|
18
|
+
* cello_set_policy — configure policy engine
|
|
19
|
+
* cello_get_policy — read current policy
|
|
20
|
+
*
|
|
21
|
+
* MCP-002 modifications:
|
|
22
|
+
* cello_initiate_session: checks hasConnection() first
|
|
23
|
+
* cello_status: gains registered, agent_id, connection_count, policy_mode, policy_review_mode
|
|
24
|
+
*
|
|
25
|
+
* PSEUDOCODE (Phase P — MCP-003):
|
|
26
|
+
*
|
|
27
|
+
* cello_register({ phone_stub }):
|
|
28
|
+
* 1. result = await client.register(phone_stub)
|
|
29
|
+
* 2. if 'error' in result: return { error: { reason: result.error } }
|
|
30
|
+
* 3. return { registered: true, agent_id, primary_pubkey, ml_dsa_pubkey }
|
|
31
|
+
* SI-001: never include ml_dsa secret
|
|
32
|
+
*
|
|
33
|
+
* cello_request_connection({ target_pubkey, include_endorsements?, include_attestations? }):
|
|
34
|
+
* 1. Build a minimal ConnectionPackage (empty endorsements/attestations unless requested)
|
|
35
|
+
* 2. result = await client.cello_request_connection({ target_pubkey, package_cbor })
|
|
36
|
+
* 3. Map result to MCP response shape
|
|
37
|
+
*
|
|
38
|
+
* cello_respond_to_disclosure_request({ connection_request_id, ... }):
|
|
39
|
+
* 1. result = await client.cello_respond_to_disclosure_request({ connection_request_id, package_cbor })
|
|
40
|
+
* 2. Map result
|
|
41
|
+
*
|
|
42
|
+
* cello_await_connection_request({ timeout_ms? }):
|
|
43
|
+
* 1. result = await client.awaitConnectionRequest(timeout_ms ?? 30_000)
|
|
44
|
+
* 2. if timeout: return { type: 'timeout' }
|
|
45
|
+
* 3. Return { type: 'pending_review', connection_request_id, from_pubkey, report }
|
|
46
|
+
* SI-003: report is ConnectionReport — no raw signatures, no full pubkeys
|
|
47
|
+
*
|
|
48
|
+
* cello_accept_connection({ connection_request_id }):
|
|
49
|
+
* 1. result = await client.acceptConnection(connection_request_id)
|
|
50
|
+
* 2. Map to { accepted: true, connection_id } or { error: { reason } }
|
|
51
|
+
*
|
|
52
|
+
* cello_reject_connection({ connection_request_id, reason? }):
|
|
53
|
+
* 1. result = await client.rejectConnection(connection_request_id, reason)
|
|
54
|
+
* 2. Map to { rejected: true } or { error: { reason } }
|
|
55
|
+
*
|
|
56
|
+
* cello_request_more_disclosure({ connection_request_id, requested_items }):
|
|
57
|
+
* 1. result = await client.requestMoreDisclosure(connection_request_id, requested_items)
|
|
58
|
+
* 2. Map to { request_sent: true } or { error: { reason } }
|
|
59
|
+
*
|
|
60
|
+
* cello_list_connections():
|
|
61
|
+
* 1. connections = client.listConnections()
|
|
62
|
+
* 2. Map each record to { connection_id, counterparty_pubkey, counterparty_primary_pubkey,
|
|
63
|
+
* established_at, status: 'active' }
|
|
64
|
+
*
|
|
65
|
+
* cello_set_policy({ mode, review_mode, requirements? }):
|
|
66
|
+
* 1. policy = { mode, review_mode, requirements: requirements ?? [] }
|
|
67
|
+
* 2. client.setPolicy(policy)
|
|
68
|
+
* 3. return { policy_set: true, mode, review_mode, requirement_count }
|
|
69
|
+
*
|
|
70
|
+
* cello_get_policy():
|
|
71
|
+
* 1. policy = client.getPolicy()
|
|
72
|
+
* 2. return { mode, review_mode, requirements }
|
|
73
|
+
*
|
|
74
|
+
* Modified: cello_initiate_session({ target_pubkey }):
|
|
75
|
+
* 1. NEW: check client.hasConnection(target_pubkey)
|
|
76
|
+
* if !connection → return { ok: false, reason: 'no_connection' }
|
|
77
|
+
* (existing transport guard + directory signaling unchanged)
|
|
78
|
+
*
|
|
79
|
+
* Modified: cello_status():
|
|
80
|
+
* + registered: bool
|
|
81
|
+
* + agent_id: hex | null
|
|
82
|
+
* + connection_count: number
|
|
83
|
+
* + policy_mode: string
|
|
84
|
+
* + policy_review_mode: string
|
|
85
|
+
*
|
|
86
|
+
* PSEUDOCODE (Phase P):
|
|
87
|
+
*
|
|
88
|
+
* State held by the MCP server instance:
|
|
89
|
+
* startedAt: number = Date.now()
|
|
90
|
+
* inboundSessionQueue: Uint8Array[] — FIFO queue of inbound session_id bytes
|
|
91
|
+
*
|
|
92
|
+
* Server setup:
|
|
93
|
+
* client.onSessionAssignment((sessionIdBytes) => {
|
|
94
|
+
* inboundSessionQueue.push(sessionIdBytes)
|
|
95
|
+
* })
|
|
96
|
+
*
|
|
97
|
+
* Helper: transportStarted():
|
|
98
|
+
* return node.listenAddresses().length > 0
|
|
99
|
+
*
|
|
100
|
+
* Helper: directoryReachable():
|
|
101
|
+
* // In M1, we determine directory reachability from whether we have active sessions
|
|
102
|
+
* // with a directory_endpoint set. Best-effort check from session records.
|
|
103
|
+
* return any session in client.listSessions() has a directory_endpoint with a peer_id
|
|
104
|
+
*
|
|
105
|
+
* Helper: toHex(bytes):
|
|
106
|
+
* return Buffer.from(bytes).toString('hex')
|
|
107
|
+
*
|
|
108
|
+
* Helper: fromHex(str):
|
|
109
|
+
* return Buffer.from(str, 'hex')
|
|
110
|
+
*
|
|
111
|
+
* ─── tool: cello_initiate_session({ target_pubkey }) ─────────────────────────────
|
|
112
|
+
* 1. Guard: if !transportStarted() → return transport_not_started error
|
|
113
|
+
* 2. SESSION-002 session_request flow:
|
|
114
|
+
* a. Look up target_pubkey in client's sessions (M1 stub: directory signaling is
|
|
115
|
+
* handled via receiveSessionAssignment. The session_request must be sent through
|
|
116
|
+
* the directory signaling stream. In M1 this is driven externally by the test
|
|
117
|
+
* harness calling receiveSessionAssignment on both clients. Here we emit the
|
|
118
|
+
* request via the stored directory stream if available, then poll listSessions()
|
|
119
|
+
* for the resulting session_id.)
|
|
120
|
+
* b. Actually: The NODE-001 signaling flow is: client initiates a session_request
|
|
121
|
+
* to the directory over the persistent signaling stream, the directory creates a
|
|
122
|
+
* SessionAssignment and pushes it to both clients. In M1, the directory is a
|
|
123
|
+
* separate process that receives the request. For the MCP tool surface:
|
|
124
|
+
* - The client must have a way to send a session_request to the directory.
|
|
125
|
+
* - CelloClientImpl already holds directoryStreams per session. But those are
|
|
126
|
+
* post-assignment. The pre-session directory signaling stream is separate.
|
|
127
|
+
* c. Per CONTEXT.md: session establishment — directory issues signed SessionAssignment.
|
|
128
|
+
* The client sends session_request to directory's /cello/signaling/1.0.0 stream.
|
|
129
|
+
* d. Implementation: initiate a directory signaling stream at the MCP server level
|
|
130
|
+
* (separate from the per-session streams in CelloClientImpl). The MCP server
|
|
131
|
+
* must know the directory endpoint. In M1 tests, the directory endpoint is
|
|
132
|
+
* obtained from an existing session's record OR passed at construction time.
|
|
133
|
+
*
|
|
134
|
+
* NOTE: In M1, cello_initiate_session is driven by the test harness calling
|
|
135
|
+
* receiveSessionAssignment on both clients (the directory side is real in e2e tests).
|
|
136
|
+
* The MCP tool implements the client-side: send the session_request frame to the
|
|
137
|
+
* directory signaling stream and poll until session_assignment arrives.
|
|
138
|
+
*
|
|
139
|
+
* For the actual M1 implementation:
|
|
140
|
+
* 1. Connect to directory /cello/signaling/1.0.0 (using stored endpoint from existing session,
|
|
141
|
+
* or a pre-configured directory multiaddr)
|
|
142
|
+
* 2. Auth challenge-response
|
|
143
|
+
* 3. Send { type: "session_request", target_pubkey: fromHex(target_pubkey) }
|
|
144
|
+
* 4. Poll listSessions() every 100ms until a new session with counterparty_pubkey == target_pubkey
|
|
145
|
+
* appears, or timeout (10s)
|
|
146
|
+
* 5. Return session details from the new SessionRecord
|
|
147
|
+
*
|
|
148
|
+
* ─── tool: cello_await_session({ timeout_ms }) ────────────────────────────────────
|
|
149
|
+
* 1. deadline = Date.now() + timeout_ms
|
|
150
|
+
* 2. Poll every 20ms until deadline:
|
|
151
|
+
* a. if inboundSessionQueue.length > 0:
|
|
152
|
+
* sessionId = inboundSessionQueue.shift()
|
|
153
|
+
* sessionIdHex = toHex(sessionId)
|
|
154
|
+
* record = client.listSessions().find(s => toHex(s.session_id) === sessionIdHex)
|
|
155
|
+
* if record: return { type: 'new_session', session_id: sessionIdHex,
|
|
156
|
+
* counterparty_pubkey: toHex(record.counterparty_pubkey),
|
|
157
|
+
* genesis_prev_root: toHex(record.genesis_prev_root) }
|
|
158
|
+
* 3. return { type: 'timeout' }
|
|
159
|
+
*
|
|
160
|
+
* ─── tool: cello_send({ session_id, content }) ────────────────────────────────────
|
|
161
|
+
* 1. Guard: transport_not_started
|
|
162
|
+
* 2. result = await client.sendMessage(session_id, TextEncoder.encode(content))
|
|
163
|
+
* 3. if result.ok:
|
|
164
|
+
* // Retrieve leaf_hash from the session record's most recent leaf
|
|
165
|
+
* record = client.listSessions().find(s => toHex(s.session_id) === session_id)
|
|
166
|
+
* leafHash = computeLeafHash(record.local_tree_leaves[last])
|
|
167
|
+
* return { delivered: true, leaf_hash: toHex(leafHash) }
|
|
168
|
+
* else:
|
|
169
|
+
* return { delivered: false, reason: result.reason }
|
|
170
|
+
*
|
|
171
|
+
* ─── tool: cello_receive_session({ session_id, timeout_ms }) — session-locked ────────
|
|
172
|
+
* 1. Guard: transport_not_started
|
|
173
|
+
* 2. deadline = Date.now() + timeout_ms
|
|
174
|
+
* 3. Poll every 20ms until deadline:
|
|
175
|
+
* a. msg = client.receiveMessage(session_id)
|
|
176
|
+
* b. if msg:
|
|
177
|
+
* return { type: 'message', content: TextDecoder.decode(msg.content),
|
|
178
|
+
* sender_pubkey: toHex(msg.senderPubkey),
|
|
179
|
+
* sequence_number: msg.sequenceNumber,
|
|
180
|
+
* leaf_hash: toHex(msg.leafHash) }
|
|
181
|
+
* 4. return { type: 'timeout' }
|
|
182
|
+
*
|
|
183
|
+
* ─── tool: cello_receive({ timeout_ms }) — any-session default ───────────────────────
|
|
184
|
+
* 1. Guard: transport_not_started
|
|
185
|
+
* 2. result = await client.receiveMessageAsync(timeout_ms)
|
|
186
|
+
* 3. if timeout: return { type: 'timeout' }
|
|
187
|
+
* 4. return { type: 'message', session_id, content, sender_pubkey, sequence_number, leaf_hash }
|
|
188
|
+
*
|
|
189
|
+
* ─── tool: cello_close_session({ session_id }) ────────────────────────────────────
|
|
190
|
+
* 1. Guard: transport_not_started
|
|
191
|
+
* 2. result = await client.initiateSessionSeal(session_id)
|
|
192
|
+
* if !result.ok: return { status: 'seal_rejected', sealed_root: null,
|
|
193
|
+
* close_timestamp: Date.now(), reason: result.reason, mmr_peak: null }
|
|
194
|
+
* 3. Poll listSessions() every 100ms for up to 30s:
|
|
195
|
+
* a. record = sessions.find(s => toHex(s.session_id) === session_id)
|
|
196
|
+
* b. if record.status === 'sealed':
|
|
197
|
+
* return { status: 'sealed', sealed_root: toHex(record.sealed_root),
|
|
198
|
+
* close_timestamp: Date.now(), reason: null, mmr_peak: null }
|
|
199
|
+
* c. if record.status === 'seal_rejected':
|
|
200
|
+
* return { status: 'seal_rejected', sealed_root: null,
|
|
201
|
+
* close_timestamp: Date.now(), reason: 'directory_rejected', mmr_peak: null }
|
|
202
|
+
* 4. return { status: 'seal_deferred', sealed_root: null,
|
|
203
|
+
* close_timestamp: Date.now(), reason: 'directory_unreachable', mmr_peak: null }
|
|
204
|
+
*
|
|
205
|
+
* ─── tool: cello_list_sessions() ──────────────────────────────────────────────────
|
|
206
|
+
* 1. records = client.listSessions()
|
|
207
|
+
* 2. For each record:
|
|
208
|
+
* emit { session_id: hex, counterparty_pubkey: hex, counterparty_peer_id: string,
|
|
209
|
+
* relay_endpoint: { peer_id: hex, multiaddrs }, status, last_seen_seq,
|
|
210
|
+
* leaf_count: record.local_tree_leaves.length }
|
|
211
|
+
*
|
|
212
|
+
* ─── tool: cello_status() ─────────────────────────────────────────────────────────
|
|
213
|
+
* No transport_not_started guard (same as MCP-001).
|
|
214
|
+
* 1. ownPubkey = toHex(await keyProvider.getPublicKey())
|
|
215
|
+
* 2. activeSessions = client.listSessions().filter(s => s.status === 'active')
|
|
216
|
+
* 3. return {
|
|
217
|
+
* transport_started: transportStarted(),
|
|
218
|
+
* own_pubkey: ownPubkey,
|
|
219
|
+
* listen_addresses: node.listenAddresses(),
|
|
220
|
+
* connected_peer_count: node.getConnections().length,
|
|
221
|
+
* uptime_seconds: Math.floor((Date.now() - startedAt) / 1000),
|
|
222
|
+
* active_session_count: activeSessions.length,
|
|
223
|
+
* directory_reachable: directoryReachable()
|
|
224
|
+
* }
|
|
225
|
+
*
|
|
226
|
+
* ─── tool: cello_get_sealed_receipt({ session_id }) ──────────────────────────────
|
|
227
|
+
* SI-001: MUST NOT return for seal_rejected sessions.
|
|
228
|
+
* SI-002: MUST NOT return private key material.
|
|
229
|
+
* 1. record = client.listSessions().find(s => toHex(s.session_id) === session_id)
|
|
230
|
+
* 2. if !record: return { error: { reason: 'session_not_found', session_id } }
|
|
231
|
+
* 3. if record.status !== 'sealed': return { error: { reason: 'session_not_sealed', session_id } }
|
|
232
|
+
* 4. pubA = record (lower hex participant), pubB = higher hex participant (from genesis_prev_root ordering)
|
|
233
|
+
* Actually: participants are [own_pubkey, counterparty_pubkey] sorted as stored. The spec says
|
|
234
|
+
* [A_pubkey, B_pubkey] which is the order they appear in the session (A=initiator, B=responder).
|
|
235
|
+
* Since we don't know which role we played, emit [own, counterparty] in canonical order.
|
|
236
|
+
* 5. return { session_id, sealed_root: hex, participants: [hex, hex],
|
|
237
|
+
* close_timestamp: <from seal>, attestation_self: 'PENDING',
|
|
238
|
+
* attestation_counterparty: 'PENDING',
|
|
239
|
+
* leaf_count: record.local_tree_leaves.length,
|
|
240
|
+
* directory_signature: hex }
|
|
241
|
+
*
|
|
242
|
+
* ─── tool: cello_get_inclusion_proof({ session_id, leaf_index }) ──────────────────
|
|
243
|
+
* SI-001: MUST NOT return proof for seal_rejected sessions.
|
|
244
|
+
* SI-003: returned sealed_root MUST equal inclusionProof reconstruction root.
|
|
245
|
+
* 1. record = client.listSessions().find(s => toHex(s.session_id) === session_id)
|
|
246
|
+
* 2. if !record || record.status !== 'sealed':
|
|
247
|
+
* return { error: { reason: 'session_not_sealed' } }
|
|
248
|
+
* 3. treeSize = record.local_tree_leaves.length
|
|
249
|
+
* 4. if leaf_index < 0 || leaf_index >= treeSize:
|
|
250
|
+
* return { error: { reason: 'leaf_index_out_of_range', leaf_index, tree_size: treeSize } }
|
|
251
|
+
* 5. Build tree from record.local_tree_leaves using buildMerkleTree(inputs: LeafInput[])
|
|
252
|
+
* 6. leafHash = tree.levelHashes[0][leaf_index]
|
|
253
|
+
* 7. proof = inclusionProof(tree, leaf_index)
|
|
254
|
+
* 8. root = merkleRoot(tree)
|
|
255
|
+
* 9. SI-003: assert root equals record.sealed_root (consistent snapshot)
|
|
256
|
+
* NOTE: sealed_root from directory is based on the canonical leaf sequence. The local
|
|
257
|
+
* tree should match if seal completed. If they don't match, return internal_error.
|
|
258
|
+
* 10. return { leaf_hash: hex, leaf_index, tree_size: treeSize, proof: [hex], sealed_root: hex }
|
|
259
|
+
*/
|
|
260
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
261
|
+
import type { CelloClient } from "./types.js";
|
|
262
|
+
import type { CelloNode } from "@cello-protocol/transport";
|
|
263
|
+
import type { KeyProvider } from "@cello-protocol/crypto";
|
|
264
|
+
import type { CheckpointStatusProvider } from "@cello-protocol/interfaces";
|
|
265
|
+
import type { ClientBackup } from "./client-backup.js";
|
|
266
|
+
export declare function createMcpSessionServer(node: CelloNode, client: CelloClient, keyProvider: KeyProvider, opts?: {
|
|
267
|
+
checkpointStatusProvider?: CheckpointStatusProvider;
|
|
268
|
+
clientBackup?: ClientBackup;
|
|
269
|
+
}): McpServer;
|
|
270
|
+
//# sourceMappingURL=mcp-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,WAAW,EAA0B,MAAM,YAAY,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAI1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAuBvD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,WAAW,EACxB,IAAI,CAAC,EAAE;IAAE,wBAAwB,CAAC,EAAE,wBAAwB,CAAC;IAAC,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GAC1F,SAAS,CA+jCX"}
|