letagents 0.12.11 → 0.12.13

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.
Files changed (49) hide show
  1. package/dist/mcp/git-remote.js +7 -7
  2. package/dist/mcp/local-state/agent-sessions.js +78 -5
  3. package/dist/mcp/local-state/local-chat.js +189 -48
  4. package/dist/mcp/local-state/storage.js +16 -9
  5. package/dist/mcp/server/daemon-tool-executor.js +92 -0
  6. package/dist/mcp/server/register-tools.js +25 -12
  7. package/dist/mcp/server/runtime/agent-sessions.js +58 -9
  8. package/dist/mcp/server/runtime/api.js +40 -4
  9. package/dist/mcp/server/runtime/daemon-tool-context.js +11 -0
  10. package/dist/mcp/server/runtime/execution-profile.js +19 -0
  11. package/dist/mcp/server/runtime/identity/directory.js +2 -2
  12. package/dist/mcp/server/runtime/messages.js +55 -0
  13. package/dist/mcp/server/runtime/presence.js +4 -2
  14. package/dist/mcp/server/runtime/room-api.js +24 -7
  15. package/dist/mcp/server/runtime/room-state.js +59 -3
  16. package/dist/mcp/server/runtime/rooms.js +67 -32
  17. package/dist/mcp/server/runtime/supervised-room-authority.js +8 -0
  18. package/dist/mcp/server/runtime/supervisor-bridge.js +702 -24
  19. package/dist/mcp/server/runtime/tool-surface-policy.js +26 -0
  20. package/dist/mcp/server/runtime/worker-bearer.js +44 -6
  21. package/dist/mcp/server/runtime-contract.js +27 -0
  22. package/dist/mcp/server/runtime.js +15 -4
  23. package/dist/mcp/server/supervised-tool-facade.js +134 -0
  24. package/dist/mcp/server/tools/agent-sessions.js +74 -7
  25. package/dist/mcp/server/tools/messages/index.js +3 -2
  26. package/dist/mcp/server/tools/messages/read-tool.js +54 -97
  27. package/dist/mcp/server/tools/messages/reasoning-tool.js +2 -0
  28. package/dist/mcp/server/tools/messages/send-tool.js +5 -0
  29. package/dist/mcp/server/tools/messages/status-tool.js +2 -0
  30. package/dist/mcp/server/tools/messages/wait-tool.js +344 -71
  31. package/dist/mcp/server/tools/onboarding/status-tool.js +9 -8
  32. package/dist/mcp/server/tools/rooms/inspection-tools.js +40 -22
  33. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +2 -1
  34. package/dist/mcp/server/tools/supervised-room-turn.js +42 -0
  35. package/dist/mcp/server/tools/tasks/board-tools.js +34 -2
  36. package/dist/mcp/server.js +14 -7
  37. package/dist/mcp/sse-client.js +163 -20
  38. package/dist/shared/activation-routing.js +187 -20
  39. package/dist/shared/agent-presence.js +6 -0
  40. package/dist/shared/desktop-release-manifest.js +63 -0
  41. package/dist/shared/desktop-release.js +60 -0
  42. package/dist/shared/scoped-ids.js +6 -0
  43. package/package.json +11 -3
  44. package/shared/message-contracts.d.mts +32 -0
  45. package/shared/message-contracts.mjs +109 -0
  46. package/shared/routing-aliases.d.mts +18 -0
  47. package/shared/routing-aliases.mjs +66 -0
  48. package/shared/sqlite-thread-routing.d.mts +72 -0
  49. package/shared/sqlite-thread-routing.mjs +1038 -0
@@ -1,3 +1,13 @@
1
+ import { normalizeRoutingHandle, normalizeRoutingSender, routingIdentityAliases, routingSenderAliasRows, routingSenderAliases, } from "../../shared/routing-aliases.mjs";
2
+ import { parsePositivePgIntegerScopedId } from "./scoped-ids.js";
3
+ /**
4
+ * Repository events can contain text written by any external contributor.
5
+ * They remain visible room activity, but their text is never an instruction
6
+ * channel for a managed local worker.
7
+ */
8
+ export function isUntrustedExternalActivationSource(source) {
9
+ return normalizeSender(source) === "github";
10
+ }
1
11
  const NON_AGENT_AT_HANDLES = new Set([
2
12
  "charset",
3
13
  "container",
@@ -33,6 +43,54 @@ export function attachAgentMessageActivation(message, identity, context = {}) {
33
43
  },
34
44
  };
35
45
  }
46
+ /**
47
+ * Send-time receipts are the activation authority. For a snapshot-bearing
48
+ * message, a receipt activates and the absence of one is the durable
49
+ * send-time "silent" — never re-promoted by re-running the router against
50
+ * later task/thread/session state, which would create a second authority.
51
+ * Only messages that predate routing snapshots keep the lazy per-reader
52
+ * decision, so legacy backlog mentions still activate rotated sessions.
53
+ */
54
+ export function attachAgentMessageActivationsFromReceipts(messages, identity, receiptsMap, snapshotNumbers, context = {}) {
55
+ if (!identity || identity.session_kind !== "worker") {
56
+ return [...messages];
57
+ }
58
+ return messages.map((message) => {
59
+ const msgIdStr = String(message.id ?? "");
60
+ const msgNum = parsePositivePgIntegerScopedId(msgIdStr, "msg");
61
+ const receipt = msgNum !== null ? receiptsMap.get(msgNum) || receiptsMap.get(msgIdStr) : null;
62
+ if (receipt) {
63
+ const reason = receipt.activation_reason;
64
+ return {
65
+ ...message,
66
+ activation: {
67
+ for_current_agent: {
68
+ decision: "activate",
69
+ reason: reason || "explicit_mention",
70
+ addressed: true,
71
+ },
72
+ },
73
+ };
74
+ }
75
+ // System failure rows are canonical silent control events. A routing
76
+ // snapshot with no receipt must not erase their diagnostic reason.
77
+ if (msgNum !== null
78
+ && snapshotNumbers.has(msgNum)
79
+ && normalizeSender(message.source) !== "managed_agent_failure") {
80
+ return {
81
+ ...message,
82
+ activation: {
83
+ for_current_agent: {
84
+ decision: "silent",
85
+ reason: "unaddressed",
86
+ addressed: false,
87
+ },
88
+ },
89
+ };
90
+ }
91
+ return attachAgentMessageActivation(message, identity, context);
92
+ });
93
+ }
36
94
  export function attachAgentMessageActivations(messages, identity, context = {}) {
37
95
  if (!identity || identity.session_kind !== "worker") {
38
96
  return [...messages];
@@ -40,17 +98,27 @@ export function attachAgentMessageActivations(messages, identity, context = {})
40
98
  return messages.map((message) => attachAgentMessageActivation(message, identity, context));
41
99
  }
42
100
  export function decideAgentMessageActivation(message, identity, context = {}) {
43
- if (normalizeSender(message.source) === "managed_agent_failure") {
101
+ if (normalizeSender(message.source) === "managed_agent_failure"
102
+ || isUntrustedExternalActivationSource(message.source)) {
44
103
  return decision("silent", "system_event");
45
104
  }
46
- if (senderMatchesIdentity(message.sender, identity)) {
105
+ const messageId = normalizedString(message.id);
106
+ const authoritativeLegacyDecision = context.authoritativeLegacyDecisions?.get(messageId);
107
+ if (authoritativeLegacyDecision)
108
+ return authoritativeLegacyDecision;
109
+ if (context.selfMessageIds !== undefined
110
+ ? context.selfMessageIds.has(messageId)
111
+ : senderMatchesIdentity(message.sender, identity)) {
47
112
  return decision("silent", "self_message");
48
113
  }
49
114
  const mentions = extractMentionHandles(message.text);
50
115
  if (mentions.some(isBroadcastHandle)) {
51
116
  return decision("activate", "broadcast");
52
117
  }
53
- if (mentions.some((mention) => identityAliases(identity).has(normalizeMentionIdentityHandle(mention)))) {
118
+ const authoritativeExplicitMentions = context.explicitMentionMessageIds;
119
+ if (authoritativeExplicitMentions !== undefined
120
+ ? authoritativeExplicitMentions.has(messageId)
121
+ : mentions.some((mention) => activationIdentityAliases(identity).has(normalizeMentionIdentityHandle(mention)))) {
54
122
  return decision("activate", "explicit_mention");
55
123
  }
56
124
  if (hasBroadcastAddress(message.text)) {
@@ -59,13 +127,22 @@ export function decideAgentMessageActivation(message, identity, context = {}) {
59
127
  if (mentions.some(isLikelyAgentMentionHandle)) {
60
128
  return decision("silent", "explicit_other_mention");
61
129
  }
62
- if (senderMatchesIdentity(message.reply_to?.sender, identity)) {
130
+ const authoritativeThreadParticipantRootIds = context.threadParticipantRootIds;
131
+ const authoritativeReplyTargets = context.replyTargetMessageIds;
132
+ const hasAuthoritativeThreadMembership = isThreadReply(message)
133
+ && authoritativeThreadParticipantRootIds !== undefined;
134
+ if (authoritativeReplyTargets !== undefined
135
+ ? authoritativeReplyTargets.has(messageId)
136
+ : !hasAuthoritativeThreadMembership && senderMatchesIdentity(message.reply_to?.sender, identity)) {
63
137
  return decision("activate", "reply_target");
64
138
  }
65
139
  if (isAgentReplyTarget(message.reply_to) && !isThreadReply(message)) {
66
140
  return decision("silent", "other_reply_target");
67
141
  }
68
- if (isThreadReply(message) && threadParticipantsIncludeIdentity(message, identity)) {
142
+ if (isThreadReply(message)
143
+ && (hasAuthoritativeThreadMembership
144
+ ? authoritativeThreadParticipantRootIds.has(threadRootId(message))
145
+ : threadParticipantsIncludeIdentity(message, identity))) {
69
146
  return decision("activate", "thread_participant");
70
147
  }
71
148
  const taskOwnerDecision = decideTaskOwnerActivation(message, identity, context);
@@ -83,9 +160,12 @@ function decision(decisionValue, reason) {
83
160
  }
84
161
  function isThreadReply(message) {
85
162
  const ownId = normalizedString(message.id);
86
- const rootId = normalizedString(message.thread_root_id) || normalizedString(message.thread?.root_message_id);
163
+ const rootId = threadRootId(message);
87
164
  return Boolean(ownId && rootId && ownId !== rootId);
88
165
  }
166
+ function threadRootId(message) {
167
+ return normalizedString(message.thread_root_id) || normalizedString(message.thread?.root_message_id);
168
+ }
89
169
  function isAgentReplyTarget(replyTo) {
90
170
  return normalizeSender(replyTo?.source) === "agent";
91
171
  }
@@ -204,24 +284,111 @@ function senderMatchesIdentity(sender, identity) {
204
284
  const normalizedSender = normalizeSender(sender);
205
285
  if (!normalizedSender)
206
286
  return false;
207
- const aliases = identityAliases(identity);
287
+ const aliases = activationIdentityAliases(identity);
208
288
  if (aliases.has(normalizedSender))
209
289
  return true;
210
290
  return String(sender || "")
211
291
  .split("|")
212
292
  .some((part) => aliases.has(normalizeSender(part)));
213
293
  }
214
- function identityAliases(identity) {
215
- const aliases = new Set();
216
- const values = [
217
- identity.actor_label,
218
- identity.display_name,
219
- identity.agent_key,
220
- identity.agent_key.split("/").pop(),
221
- ];
222
- for (const alias of aliasesForValues(values))
223
- aliases.add(alias);
224
- return aliases;
294
+ export function activationIdentityAliases(identity) {
295
+ return routingIdentityAliases(identity);
296
+ }
297
+ /** Canonical aliases materialized from a historical message sender. */
298
+ export function activationSenderAliases(sender, segmentLimit = 16) {
299
+ return routingSenderAliases(sender, segmentLimit);
300
+ }
301
+ /**
302
+ * Resolve identity-bearing addresses against the complete active room
303
+ * population. A display alias is authority only when it names one durable
304
+ * agent key globally; account/provider filtering happens after this step.
305
+ * Full historical sender labels take precedence over their pipe-delimited
306
+ * compatibility segments.
307
+ */
308
+ export function resolveGloballyAddressedAgentKeys(message, identities) {
309
+ return createGlobalAgentAddressResolver(identities)(message);
310
+ }
311
+ /**
312
+ * Build the room-wide alias authority once, then resolve a page of legacy
313
+ * messages without rebuilding every active worker alias set per message.
314
+ */
315
+ export function createGlobalAgentAddressResolver(identities) {
316
+ const keysByAlias = new Map();
317
+ for (const identity of identities) {
318
+ const key = normalizedString(identity.agent_key);
319
+ if (!key)
320
+ continue;
321
+ for (const alias of activationIdentityAliases(identity)) {
322
+ const keys = keysByAlias.get(alias) ?? new Set();
323
+ keys.add(key);
324
+ keysByAlias.set(alias, keys);
325
+ }
326
+ }
327
+ return (message) => {
328
+ const mentions = extractMentionHandles(message.text);
329
+ const broadcast = mentions.some(isBroadcastHandle) || hasBroadcastAddress(message.text);
330
+ const hasMention = mentions.some((mention) => !isBroadcastHandle(mention));
331
+ const hasAgentMention = mentions.some(isLikelyAgentMentionHandle);
332
+ const explicitMentionKeys = new Set();
333
+ for (const mention of mentions) {
334
+ if (isBroadcastHandle(mention))
335
+ continue;
336
+ const alias = normalizeMentionIdentityHandle(mention);
337
+ if (!alias)
338
+ continue;
339
+ const keys = keysByAlias.get(alias);
340
+ if (keys?.size === 1)
341
+ explicitMentionKeys.add(keys.values().next().value);
342
+ }
343
+ const replyTargetKeys = new Set();
344
+ const replyAliases = normalizedString(message.reply_to?.source) === "agent"
345
+ ? routingSenderAliasRows(message.reply_to?.sender)
346
+ : [];
347
+ const matchingKeys = (full) => {
348
+ const keys = new Set();
349
+ for (const row of replyAliases) {
350
+ if (row.isFull !== full)
351
+ continue;
352
+ for (const key of keysByAlias.get(row.alias) ?? [])
353
+ keys.add(key);
354
+ }
355
+ return keys;
356
+ };
357
+ const fullMatches = matchingKeys(true);
358
+ const replyMatches = fullMatches.size > 0 ? fullMatches : matchingKeys(false);
359
+ if (replyMatches.size === 1)
360
+ replyTargetKeys.add(replyMatches.values().next().value);
361
+ const senderKeys = new Set();
362
+ const senderAliases = routingSenderAliasRows(message.sender);
363
+ const senderMatchingKeys = (full) => {
364
+ const keys = new Set();
365
+ for (const row of senderAliases) {
366
+ if (row.isFull !== full)
367
+ continue;
368
+ for (const key of keysByAlias.get(row.alias) ?? [])
369
+ keys.add(key);
370
+ }
371
+ return keys;
372
+ };
373
+ const senderFullMatches = senderMatchingKeys(true);
374
+ const senderMatches = senderFullMatches.size > 0
375
+ ? senderFullMatches
376
+ : senderMatchingKeys(false);
377
+ if (senderMatches.size === 1)
378
+ senderKeys.add(senderMatches.values().next().value);
379
+ return {
380
+ broadcast,
381
+ hasMention,
382
+ hasAgentMention,
383
+ explicitMentionKeys,
384
+ replyTargetKeys,
385
+ senderKeys,
386
+ };
387
+ };
388
+ }
389
+ /** Shared legacy task-follow-up classifier used by API and desktop overlays. */
390
+ export function isTaskOwnerFollowUpMessageText(text) {
391
+ return isTaskOwnerFollowUp(text);
225
392
  }
226
393
  function extractMentionHandles(text) {
227
394
  const raw = typeof text === "string" ? text : "";
@@ -261,10 +428,10 @@ function normalizedString(value) {
261
428
  return typeof value === "string" ? value.trim() : "";
262
429
  }
263
430
  function normalizeSender(value) {
264
- return normalizedString(value).toLowerCase().replace(/\s+/g, " ");
431
+ return normalizeRoutingSender(value);
265
432
  }
266
433
  function normalizeHandle(value) {
267
- return normalizedString(value).toLowerCase().replace(/[^a-z0-9_.:/-]+/g, "");
434
+ return normalizeRoutingHandle(value);
268
435
  }
269
436
  function normalizeMentionIdentityHandle(value) {
270
437
  const normalized = normalizeHandle(value);
@@ -21,6 +21,12 @@ export const ROOM_AGENT_SESSION_KINDS = [
21
21
  "controller",
22
22
  "worker",
23
23
  ];
24
+ export function isRoomAgentDeliveryCredentialExpired(fence, now = Date.now()) {
25
+ if (fence?.kind !== "bearer" || !fence.expires_at)
26
+ return false;
27
+ const expiresAt = Date.parse(fence.expires_at);
28
+ return Number.isFinite(expiresAt) && expiresAt <= now;
29
+ }
24
30
  export function normalizeRoomAgentSessionKind(value) {
25
31
  return String(value || "").trim().toLowerCase() === "worker" ? "worker" : "controller";
26
32
  }
@@ -0,0 +1,63 @@
1
+ export const MAC_DESKTOP_PUBLIC_BASE_URL = "https://downloads.letagents.chat";
2
+ function record(value, label) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4
+ throw new Error(`${label} must be an object.`);
5
+ }
6
+ return value;
7
+ }
8
+ function numericVersion(value) {
9
+ if (!/^\d+\.\d+\.\d+$/.test(value)) {
10
+ throw new Error("Desktop release manifest version must be numeric x.y.z.");
11
+ }
12
+ return value.split(".").map(Number);
13
+ }
14
+ function compareVersions(left, right) {
15
+ const leftParts = numericVersion(left);
16
+ const rightParts = numericVersion(right);
17
+ for (let index = 0; index < leftParts.length; index += 1) {
18
+ if (leftParts[index] !== rightParts[index])
19
+ return leftParts[index] - rightParts[index];
20
+ }
21
+ return 0;
22
+ }
23
+ export function parseMacDesktopPublicReleaseManifest(value, minimumVersion) {
24
+ const manifest = record(value, "Desktop release manifest");
25
+ const version = manifest.version;
26
+ if (manifest.schemaVersion !== 1 || manifest.channel !== "beta") {
27
+ throw new Error("Desktop release manifest has an unsupported contract.");
28
+ }
29
+ if (typeof version !== "string") {
30
+ throw new Error("Desktop release manifest version must be numeric x.y.z.");
31
+ }
32
+ numericVersion(version);
33
+ if (compareVersions(version, minimumVersion) < 0) {
34
+ throw new Error(`Desktop release manifest ${version} is older than ${minimumVersion}.`);
35
+ }
36
+ const rawAssets = record(manifest.assets, "Desktop release assets");
37
+ const checksumsUrl = `${MAC_DESKTOP_PUBLIC_BASE_URL}/desktop/v${version}/checksums.txt`;
38
+ if (manifest.checksumsUrl !== checksumsUrl) {
39
+ throw new Error("Desktop release manifest does not use the immutable public checksum URL.");
40
+ }
41
+ const assets = {};
42
+ for (const architecture of ["arm64", "x64"]) {
43
+ const rawAsset = record(rawAssets[architecture], `${architecture} desktop release asset`);
44
+ const fileName = `LetAgents-${version}-darwin-${architecture}.dmg`;
45
+ const publicUrl = `${MAC_DESKTOP_PUBLIC_BASE_URL}/desktop/v${version}/${fileName}`;
46
+ if (rawAsset.fileName !== fileName || rawAsset.publicUrl !== publicUrl) {
47
+ throw new Error(`${architecture} desktop release asset does not use the immutable public URL.`);
48
+ }
49
+ if (!Number.isSafeInteger(rawAsset.bytes) || Number(rawAsset.bytes) <= 0) {
50
+ throw new Error(`${architecture} desktop release asset must have a positive byte size.`);
51
+ }
52
+ if (typeof rawAsset.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(rawAsset.sha256)) {
53
+ throw new Error(`${architecture} desktop release asset must have a lowercase SHA-256 digest.`);
54
+ }
55
+ assets[architecture] = {
56
+ fileName,
57
+ publicUrl,
58
+ bytes: Number(rawAsset.bytes),
59
+ sha256: rawAsset.sha256,
60
+ };
61
+ }
62
+ return { schemaVersion: 1, channel: "beta", version, checksumsUrl, assets };
63
+ }
@@ -0,0 +1,60 @@
1
+ const MAC_DESKTOP_BETA_VERSION = "0.1.5";
2
+ const MAC_DESKTOP_PUBLIC_BASE_URL = "https://downloads.letagents.chat/desktop";
3
+ function macDesktopAsset(architecture, sha256) {
4
+ const fileName = `LetAgents-${MAC_DESKTOP_BETA_VERSION}-darwin-${architecture}.dmg`;
5
+ return {
6
+ fileName,
7
+ publicUrl: `${MAC_DESKTOP_PUBLIC_BASE_URL}/v${MAC_DESKTOP_BETA_VERSION}/${fileName}`,
8
+ sha256,
9
+ };
10
+ }
11
+ export const MAC_DESKTOP_BETA_RELEASE = {
12
+ version: MAC_DESKTOP_BETA_VERSION,
13
+ tag: `desktop-v${MAC_DESKTOP_BETA_VERSION}`,
14
+ assets: {
15
+ arm64: macDesktopAsset("arm64", "704355796cee5214a9cfbb79105a035e0e00dc3a3aa57be7d389b97cf35fc804"),
16
+ x64: macDesktopAsset("x64", "1e4b0b8b2c42ad8eaaf652fc887ab089445c8dd83233d763a6acdaa6617c2471"),
17
+ },
18
+ };
19
+ export const MAC_DESKTOP_BETA_CHECKSUM_RELEASES = [
20
+ MAC_DESKTOP_BETA_RELEASE,
21
+ {
22
+ version: "0.1.4",
23
+ assets: {
24
+ arm64: {
25
+ fileName: "LetAgents-0.1.4-darwin-arm64.dmg",
26
+ sha256: "27abe236232d33db10ed4533f4a7443a66f93568e9fa73a2ca472b6467fcf1cb",
27
+ },
28
+ x64: {
29
+ fileName: "LetAgents-0.1.4-darwin-x64.dmg",
30
+ sha256: "4c807ad0c799b4e46ab81d1098e5b65934d6b0b49f0a99ced2713897e1c2bc35",
31
+ },
32
+ },
33
+ },
34
+ {
35
+ version: "0.1.3",
36
+ assets: {
37
+ arm64: {
38
+ fileName: "LetAgents-0.1.3-darwin-arm64.dmg",
39
+ sha256: "6010454bc7375a38571d707f90c077207a7d2b49b01a1db1655b03f4def9b502",
40
+ },
41
+ x64: {
42
+ fileName: "LetAgents-0.1.3-darwin-x64.dmg",
43
+ sha256: "b7574e17ef87aebf418926478de10d9937fd1a96ca0c7a53b826a109302c5560",
44
+ },
45
+ },
46
+ },
47
+ {
48
+ version: "0.1.2",
49
+ assets: {
50
+ arm64: {
51
+ fileName: "LetAgents-0.1.2-darwin-arm64.dmg",
52
+ sha256: "e5355deced8383bc7d024ec60b109a38dde69dfeb6b6339352e1f5bc5c53bd43",
53
+ },
54
+ x64: {
55
+ fileName: "LetAgents-0.1.2-darwin-x64.dmg",
56
+ sha256: "67d2896b806695dae8c0224b3bc2780aee6902e897569c983ecc1bdb0330b6b0",
57
+ },
58
+ },
59
+ },
60
+ ];
@@ -0,0 +1,6 @@
1
+ import { POSTGRES_INTEGER_MAX, parsePositivePgIntegerScopedId as parseSharedPositivePgIntegerScopedId, } from "../../shared/message-contracts.mjs";
2
+ export { POSTGRES_INTEGER_MAX };
3
+ /** Parse the canonical `<prefix>_<positive PostgreSQL integer>` wire form. */
4
+ export function parsePositivePgIntegerScopedId(value, prefix) {
5
+ return parseSharedPositivePgIntegerScopedId(value, prefix);
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "letagents",
3
- "version": "0.12.11",
3
+ "version": "0.12.13",
4
4
  "description": "Let Agents Chat — MCP server for AI agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/mcp/server.js",
@@ -11,6 +11,7 @@
11
11
  "dist/mcp/**",
12
12
  "dist/api/board-intent-payloads.js",
13
13
  "dist/shared/**",
14
+ "shared/**",
14
15
  "README.md"
15
16
  ],
16
17
  "scripts": {
@@ -24,6 +25,9 @@
24
25
  "test:orchestrator": "node --import tsx --test src/orchestrator/__tests__/*.test.ts",
25
26
  "test:web": "node --import tsx --test src/web/tests/*.test.ts src/web/src/composables/*.test.ts",
26
27
  "test:dependency-age": "node --test scripts/verify-dependency-age.test.mjs",
28
+ "test:docker-context": "node --test scripts/verify-docker-context.test.mjs",
29
+ "test:workflow-supply-chain": "node --test scripts/verify-workflow-supply-chain.test.mjs",
30
+ "test:notifications": "node --import tsx --test src/api/notifications/__tests__/*.test.ts",
27
31
  "verify:dependency-age": "node scripts/verify-dependency-age.mjs",
28
32
  "db:generate": "drizzle-kit generate",
29
33
  "db:migrate": "tsx src/api/migrate.ts",
@@ -31,7 +35,7 @@
31
35
  "prepublishOnly": "npm run build",
32
36
  "dev:api": "tsx src/api/server.ts",
33
37
  "dev:mcp": "tsx src/mcp/server.ts",
34
- "dev:desktop": "npm --prefix apps/desktop run dev"
38
+ "dev:desktop": "node scripts/ensure-desktop-dependencies.mjs && npm --prefix apps/desktop run dev"
35
39
  },
36
40
  "keywords": [
37
41
  "mcp",
@@ -62,8 +66,12 @@
62
66
  "ws": "8.21.0"
63
67
  },
64
68
  "overrides": {
69
+ "@hono/node-server": "2.0.10",
65
70
  "@esbuild-kit/core-utils": {
66
71
  "esbuild": "^0.25.12"
67
- }
72
+ },
73
+ "fast-uri": "3.1.5",
74
+ "hono": "4.12.34",
75
+ "ip-address": "10.3.1"
68
76
  }
69
77
  }
@@ -0,0 +1,32 @@
1
+ export const POSTGRES_INTEGER_MAX: number;
2
+ export const MESSAGE_SENDER_MAX_CODE_POINTS: number;
3
+ export const MESSAGE_SENDER_MAX_UTF8_BYTES: number;
4
+ export function parsePositivePgIntegerScopedId(value: unknown, prefix: string): number | null;
5
+ export function isMessageSenderWithinBounds(sender: unknown): sender is string;
6
+ export type ParsedAccountAgentRouting =
7
+ | { version: 1; authority: "invalid" }
8
+ | {
9
+ version: 1;
10
+ authority: "receipts";
11
+ recipientAgentKeys: string[];
12
+ recipientSessions: Array<{
13
+ agentKey: string;
14
+ agentSessionId: string;
15
+ successorAgentSessionId?: string;
16
+ }>;
17
+ controlAuthorized: boolean;
18
+ }
19
+ | {
20
+ version: 1;
21
+ authority: "legacy";
22
+ recipientAgentKeys: string[];
23
+ recipientSessions: Array<{
24
+ agentKey: string;
25
+ agentSessionId: string;
26
+ activationReason: string;
27
+ }>;
28
+ controlAuthorized: boolean;
29
+ };
30
+ export function parseAccountAgentRoutingEnvelope(
31
+ routing: unknown,
32
+ ): ParsedAccountAgentRouting | undefined;
@@ -0,0 +1,109 @@
1
+ import { normalizeRoutingSender } from "./routing-aliases.mjs";
2
+
3
+ export const POSTGRES_INTEGER_MAX = 2_147_483_647;
4
+ export const MESSAGE_SENDER_MAX_CODE_POINTS = 512;
5
+ export const MESSAGE_SENDER_MAX_UTF8_BYTES = 2_048;
6
+
7
+ /** Parse the canonical `<prefix>_<positive PostgreSQL integer>` wire form. */
8
+ export function parsePositivePgIntegerScopedId(value, prefix) {
9
+ if (typeof value !== "string" || typeof prefix !== "string" || !prefix) return null;
10
+ const marker = `${prefix}_`;
11
+ if (!value.startsWith(marker)) return null;
12
+ const decimal = value.slice(marker.length);
13
+ if (!/^[1-9]\d*$/.test(decimal)) return null;
14
+ const parsed = Number(decimal);
15
+ return Number.isSafeInteger(parsed) && parsed <= POSTGRES_INTEGER_MAX ? parsed : null;
16
+ }
17
+
18
+ /** Bound sender labels before routing projections duplicate their raw value. */
19
+ export function isMessageSenderWithinBounds(sender) {
20
+ if (typeof sender !== "string") return false;
21
+ let codePoints = 0;
22
+ for (const _character of sender) {
23
+ codePoints += 1;
24
+ if (codePoints > MESSAGE_SENDER_MAX_CODE_POINTS) return false;
25
+ }
26
+ return new TextEncoder().encode(sender).byteLength <= MESSAGE_SENDER_MAX_UTF8_BYTES;
27
+ }
28
+
29
+ /**
30
+ * Parse the account-scoped routing envelope shared by Desktop and MCP.
31
+ * Undefined means an older/local message supplied no authority. Any present
32
+ * malformed shape returns explicit invalid authority and must never fall back
33
+ * to mutable display aliases.
34
+ */
35
+ export function parseAccountAgentRoutingEnvelope(routing) {
36
+ if (routing === undefined) return undefined;
37
+ if (!routing || typeof routing !== "object" || routing.version !== 1) {
38
+ return { version: 1, authority: "invalid" };
39
+ }
40
+ if (routing.authority !== "receipts" && routing.authority !== "legacy") {
41
+ return { version: 1, authority: "invalid" };
42
+ }
43
+ if (
44
+ !Array.isArray(routing.recipient_agent_keys)
45
+ || !routing.recipient_agent_keys.every((value) => typeof value === "string")
46
+ || !Array.isArray(routing.recipient_agent_sessions)
47
+ ) {
48
+ return { version: 1, authority: "invalid" };
49
+ }
50
+ const recipientAgentKeys = routing.recipient_agent_keys.map(normalizeRoutingSender);
51
+ if (recipientAgentKeys.some((value) => !value)) {
52
+ return { version: 1, authority: "invalid" };
53
+ }
54
+ const uniqueKeys = new Set(recipientAgentKeys);
55
+ if (uniqueKeys.size !== recipientAgentKeys.length) {
56
+ return { version: 1, authority: "invalid" };
57
+ }
58
+ const targetKeys = new Set();
59
+ const recipientSessions = [];
60
+ for (const value of routing.recipient_agent_sessions) {
61
+ if (!value || typeof value !== "object") {
62
+ return { version: 1, authority: "invalid" };
63
+ }
64
+ const agentKey = typeof value.agent_key === "string"
65
+ ? normalizeRoutingSender(value.agent_key)
66
+ : "";
67
+ const agentSessionId = typeof value.agent_session_id === "string"
68
+ ? value.agent_session_id.trim()
69
+ : "";
70
+ if (!agentKey || !agentSessionId || targetKeys.has(agentKey)) {
71
+ return { version: 1, authority: "invalid" };
72
+ }
73
+ targetKeys.add(agentKey);
74
+ if (routing.authority === "receipts") {
75
+ const successorAgentSessionId = value.successor_agent_session_id === undefined
76
+ ? undefined
77
+ : typeof value.successor_agent_session_id === "string"
78
+ ? value.successor_agent_session_id.trim()
79
+ : "";
80
+ if (successorAgentSessionId !== undefined && !successorAgentSessionId) {
81
+ return { version: 1, authority: "invalid" };
82
+ }
83
+ recipientSessions.push({
84
+ agentKey,
85
+ agentSessionId,
86
+ ...(successorAgentSessionId ? { successorAgentSessionId } : {}),
87
+ });
88
+ } else {
89
+ const activationReason = typeof value.activation_reason === "string"
90
+ ? value.activation_reason.trim()
91
+ : "";
92
+ if (!activationReason) return { version: 1, authority: "invalid" };
93
+ recipientSessions.push({ agentKey, agentSessionId, activationReason });
94
+ }
95
+ }
96
+ if (
97
+ targetKeys.size !== uniqueKeys.size
98
+ || [...uniqueKeys].some((key) => !targetKeys.has(key))
99
+ ) {
100
+ return { version: 1, authority: "invalid" };
101
+ }
102
+ return {
103
+ version: 1,
104
+ authority: routing.authority,
105
+ recipientAgentKeys: [...uniqueKeys],
106
+ recipientSessions,
107
+ controlAuthorized: routing.control_authorized === true,
108
+ };
109
+ }
@@ -0,0 +1,18 @@
1
+ export type RoutingIdentityLike = {
2
+ actorLabel?: string | null;
3
+ actor_label?: string | null;
4
+ displayName?: string | null;
5
+ display_name?: string | null;
6
+ agentKey?: string | null;
7
+ agent_key?: string | null;
8
+ };
9
+
10
+ export function normalizeRoutingSender(value: unknown): string;
11
+ export function normalizeRoutingHandle(value: unknown): string;
12
+ export function routingIdentityAliases(identity: RoutingIdentityLike): Set<string>;
13
+ export function routingSenderAliasRows(
14
+ sender: unknown,
15
+ segmentLimit?: number,
16
+ ): Array<{ alias: string; isFull: boolean }>;
17
+ export function routingSenderAliases(sender: unknown, segmentLimit?: number): Set<string>;
18
+ export function routingAliasHash(alias: string): string;