@agent-team-foundation/first-tree-hub 0.9.7 → 0.9.9

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.
@@ -0,0 +1,40 @@
1
+ -- Multi-tenancy hardening:
2
+ -- 1. Drop dead column `agents.cloud_user_id` (unused since introduction in
3
+ -- 0010; never written by any code path).
4
+ -- 2. Bind every client to exactly one organization via `clients.organization_id`.
5
+ --
6
+ -- A client is bound to one org for its lifetime — Rule R-RUN and the
7
+ -- `client:register` handshake reject cross-org reuse of a clientId. See
8
+ -- docs/multi-tenancy-hardening-design.md.
9
+ --
10
+ -- Backfill strategy (guarded for safety across environments):
11
+ -- * Current production: exactly one org → UPDATE fills every row.
12
+ -- * Fresh installs / empty DB: clients table is empty → UPDATE is a no-op,
13
+ -- SET NOT NULL succeeds on the empty table.
14
+ -- * Any environment reaching this migration with multi-org data but
15
+ -- unpopulated clients.organization_id: the guard skips the UPDATE, and
16
+ -- SET NOT NULL fails loudly rather than misassigning rows to an
17
+ -- arbitrary org. Operator must backfill manually, then re-run.
18
+
19
+ ALTER TABLE "agents" DROP COLUMN "cloud_user_id";
20
+
21
+ --> statement-breakpoint
22
+ ALTER TABLE "clients" ADD COLUMN "organization_id" text;
23
+
24
+ --> statement-breakpoint
25
+ ALTER TABLE "clients"
26
+ ADD CONSTRAINT "clients_organization_id_organizations_id_fk"
27
+ FOREIGN KEY ("organization_id") REFERENCES "organizations"("id")
28
+ ON DELETE no action ON UPDATE no action;
29
+
30
+ --> statement-breakpoint
31
+ UPDATE "clients"
32
+ SET "organization_id" = (SELECT "id" FROM "organizations" LIMIT 1)
33
+ WHERE "organization_id" IS NULL
34
+ AND (SELECT count(*) FROM "organizations") = 1;
35
+
36
+ --> statement-breakpoint
37
+ ALTER TABLE "clients" ALTER COLUMN "organization_id" SET NOT NULL;
38
+
39
+ --> statement-breakpoint
40
+ CREATE INDEX IF NOT EXISTS "idx_clients_org" ON "clients" ("organization_id");
@@ -162,6 +162,13 @@
162
162
  "when": 1777161600000,
163
163
  "tag": "0022_session_events",
164
164
  "breakpoints": true
165
+ },
166
+ {
167
+ "idx": 23,
168
+ "version": "7",
169
+ "when": 1777248000000,
170
+ "tag": "0023_clients_org_scoping",
171
+ "breakpoints": true
165
172
  }
166
173
  ]
167
174
  }
@@ -1,6 +1,28 @@
1
1
  import { d as __exportAll } from "./esm-CYu4tXXn.mjs";
2
2
  import { z } from "zod";
3
3
  //#region ../shared/dist/index.mjs
4
+ const MENTION_REGEX = /(?<![A-Za-z0-9_.@-])@([A-Za-z0-9_-]{1,64})\b/g;
5
+ function stripCode(content) {
6
+ return content.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "").replace(/`[^`\n]+`/g, "");
7
+ }
8
+ /**
9
+ * Resolve `@<name>` mentions in `content` to a list of participant agentIds.
10
+ * Names match case-insensitively; unknown `@tokens` are dropped.
11
+ */
12
+ function extractMentions(content, participants) {
13
+ const stripped = stripCode(content);
14
+ const nameMap = /* @__PURE__ */ new Map();
15
+ for (const p of participants) if (p.name) nameMap.set(p.name.toLowerCase(), p.agentId);
16
+ if (nameMap.size === 0) return [];
17
+ const hits = /* @__PURE__ */ new Set();
18
+ for (const m of stripped.matchAll(MENTION_REGEX)) {
19
+ const token = m[1];
20
+ if (!token) continue;
21
+ const id = nameMap.get(token.toLowerCase());
22
+ if (id) hits.add(id);
23
+ }
24
+ return [...hits];
25
+ }
4
26
  const adapterPlatformSchema = z.enum([
5
27
  "feishu",
6
28
  "slack",
@@ -187,7 +209,6 @@ z.object({
187
209
  inboxId: z.string(),
188
210
  status: z.string(),
189
211
  source: z.string().nullable().optional(),
190
- cloudUserId: z.string().nullable().optional(),
191
212
  visibility: agentVisibilitySchema,
192
213
  metadata: z.record(z.string(), z.unknown()),
193
214
  managerId: z.string().nullable(),
@@ -404,6 +425,11 @@ const chatParticipantSchema = z.object({
404
425
  mode: z.string(),
405
426
  joinedAt: z.string()
406
427
  });
428
+ chatParticipantSchema.extend({
429
+ name: z.string().nullable(),
430
+ displayName: z.string().nullable(),
431
+ type: z.string()
432
+ });
407
433
  z.object({
408
434
  id: z.string(),
409
435
  organizationId: z.string(),
@@ -443,6 +469,41 @@ const paginationQuerySchema = z.object({
443
469
  limit: z.coerce.number().int().min(1).max(100).default(20),
444
470
  cursor: z.string().optional()
445
471
  });
472
+ const supportedImageMimeSchema = z.enum([
473
+ "image/png",
474
+ "image/jpeg",
475
+ "image/gif",
476
+ "image/webp"
477
+ ]);
478
+ /**
479
+ * Legacy inbound shape: an image message with base64 bytes inlined into
480
+ * `messages.content`. Web still uploads in this shape so no new endpoint is
481
+ * needed; the server extracts the bytes, broadcasts them as an `image_payload`
482
+ * WS frame, then rewrites `content` to {@link imageRefContentSchema} before
483
+ * the DB insert.
484
+ */
485
+ const imageInlineContentSchema = z.object({
486
+ data: z.string().min(1),
487
+ mimeType: supportedImageMimeSchema,
488
+ filename: z.string().min(1),
489
+ size: z.number().int().nonnegative().optional(),
490
+ imageId: z.string().uuid().optional()
491
+ });
492
+ z.object({
493
+ imageId: z.string().uuid(),
494
+ mimeType: supportedImageMimeSchema,
495
+ filename: z.string().min(1),
496
+ size: z.number().int().nonnegative().optional()
497
+ });
498
+ z.object({
499
+ type: z.literal("image_payload"),
500
+ imageId: z.string().uuid(),
501
+ chatId: z.string(),
502
+ base64: z.string().min(1),
503
+ mimeType: supportedImageMimeSchema,
504
+ filename: z.string().min(1),
505
+ size: z.number().int().nonnegative().optional()
506
+ });
446
507
  const messageSourceSchema = z.enum([
447
508
  "hub_ui",
448
509
  "cli",
@@ -475,17 +536,7 @@ const sendToAgentSchema = z.object({
475
536
  replyToChat: z.string().optional(),
476
537
  source: messageSourceSchema.optional()
477
538
  });
478
- /**
479
- * Wire format for messages routed FROM the Hub TO a client runtime.
480
- *
481
- * Adds `configVersion` so the client can compare against its locally cached
482
- * agent runtime config and refresh before delivering the message to the SDK.
483
- *
484
- * Step 3: this is the single shape used by `buildClientMessagePayload` —
485
- * never serialise a raw `messageSchema` row to a client; always go through
486
- * the dispatcher.
487
- */
488
- const clientMessageSchema = z.object({
539
+ const messageSchema = z.object({
489
540
  id: z.string(),
490
541
  chatId: z.string(),
491
542
  senderId: z.string(),
@@ -497,7 +548,46 @@ const clientMessageSchema = z.object({
497
548
  inReplyTo: z.string().nullable(),
498
549
  source: messageSourceSchema.nullable(),
499
550
  createdAt: z.string()
500
- }).extend({ configVersion: z.number().int().positive() });
551
+ });
552
+ /**
553
+ * Snapshot of the `in_reply_to` target that the server materialises at
554
+ * dispatch time so the receiving runtime can decide whether this is an
555
+ * echo it should suppress (see proposal hub-agent-messaging-reply-and-mentions).
556
+ *
557
+ * `chatId` is the original message's `chat_id`; `replyToChat` is the chat
558
+ * its sender expected replies to flow back to (often a different chat).
559
+ * `null` when the message is not a reply, or the original could not be
560
+ * resolved (e.g. deleted).
561
+ */
562
+ const inReplyToSnapshotSchema = z.object({
563
+ senderId: z.string(),
564
+ chatId: z.string(),
565
+ replyToChat: z.string().nullable()
566
+ }).nullable();
567
+ /** Per-chat participation mode exposed to the recipient runtime. */
568
+ const participantModeSchema = z.enum(["full", "mention_only"]);
569
+ /**
570
+ * Wire format for messages routed FROM the Hub TO a client runtime.
571
+ *
572
+ * Adds `configVersion` so the client can compare against its locally cached
573
+ * agent runtime config and refresh before delivering the message to the SDK.
574
+ *
575
+ * Step 3: this is the single shape used by `buildClientMessagePayload` —
576
+ * never serialise a raw `messageSchema` row to a client; always go through
577
+ * the dispatcher.
578
+ *
579
+ * `recipientMode` is the receiving agent's own mode in the entry's chat —
580
+ * `mention_only` participants must only start a session when they appear in
581
+ * `metadata.mentions` (see session-manager.ts).
582
+ *
583
+ * `inReplyToSnapshot` is populated when `inReplyTo` resolves to an existing
584
+ * message; runtime uses it to suppress self-reply echo on direct chats.
585
+ */
586
+ const clientMessageSchema = messageSchema.extend({
587
+ configVersion: z.number().int().positive(),
588
+ recipientMode: participantModeSchema.default("full"),
589
+ inReplyToSnapshot: inReplyToSnapshotSchema.default(null)
590
+ });
501
591
  z.enum([
502
592
  "pending",
503
593
  "delivered",
@@ -959,4 +1049,4 @@ async function bindFeishuUser(serverUrl, accessToken, agentId, humanAgentId, fei
959
1049
  }
960
1050
  }
961
1051
  //#endregion
962
- export { updateAdapterConfigSchema as $, createMemberSchema as A, notificationQuerySchema as B, agentTypeSchema as C, createAdapterMappingSchema as D, createAdapterConfigSchema as E, inboxPollQuerySchema as F, sendMessageSchema as G, refreshTokenSchema as H, isRedactedEnvValue as I, sessionEventMessageSchema as J, sendToAgentSchema as K, linkTaskChatSchema as L, createTaskSchema as M, delegateFeishuUserSchema as N, createAgentSchema as O, dryRunAgentRuntimeConfigSchema as P, taskListQuerySchema as Q, loginSchema as R, agentRuntimeConfigPayloadSchema as S, connectTokenExchangeSchema as T, runtimeStateMessageSchema as U, paginationQuerySchema as V, selfServiceFeishuBotSchema as W, sessionReconcileRequestSchema as X, sessionEventSchema as Y, sessionStateMessageSchema as Z, addParticipantSchema as _, AGENT_SELECTOR_HEADER as a, updateSystemConfigSchema as at, agentBindRequestSchema as b, AGENT_TYPES as c, SYSTEM_CONFIG_DEFAULTS as d, updateAgentRuntimeConfigSchema as et, TASK_CREATOR_TYPES as f, WS_AUTH_FRAME_TIMEOUT_MS as g, TASK_TERMINAL_STATUSES as h, AGENT_BIND_REJECT_REASONS as i, updateOrganizationSchema as it, createOrganizationSchema as j, createChatSchema as k, AGENT_VISIBILITY as l, TASK_STATUSES as m, bindFeishuUser as n, updateChatSchema as nt, AGENT_SOURCES as o, updateTaskStatusSchema as ot, TASK_HEALTH_SIGNALS as p, sessionCompletionMessageSchema as q, feishu_exports as r, updateMemberSchema as rt, AGENT_STATUSES as s, wsAuthFrameSchema as st, bindFeishuBot as t, updateAgentSchema as tt, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as u, adminCreateTaskSchema as v, clientRegisterSchema as w, agentPinnedMessageSchema as x, adminUpdateTaskSchema as y, messageSourceSchema as z };
1052
+ export { sessionStateMessageSchema as $, createMemberSchema as A, loginSchema as B, agentTypeSchema as C, createAdapterMappingSchema as D, createAdapterConfigSchema as E, extractMentions as F, runtimeStateMessageSchema as G, notificationQuerySchema as H, imageInlineContentSchema as I, sendToAgentSchema as J, selfServiceFeishuBotSchema as K, inboxPollQuerySchema as L, createTaskSchema as M, delegateFeishuUserSchema as N, createAgentSchema as O, dryRunAgentRuntimeConfigSchema as P, sessionReconcileRequestSchema as Q, isRedactedEnvValue as R, agentRuntimeConfigPayloadSchema as S, connectTokenExchangeSchema as T, paginationQuerySchema as U, messageSourceSchema as V, refreshTokenSchema as W, sessionEventMessageSchema as X, sessionCompletionMessageSchema as Y, sessionEventSchema as Z, addParticipantSchema as _, AGENT_SELECTOR_HEADER as a, updateMemberSchema as at, agentBindRequestSchema as b, AGENT_TYPES as c, updateTaskStatusSchema as ct, SYSTEM_CONFIG_DEFAULTS as d, taskListQuerySchema as et, TASK_CREATOR_TYPES as f, WS_AUTH_FRAME_TIMEOUT_MS as g, TASK_TERMINAL_STATUSES as h, AGENT_BIND_REJECT_REASONS as i, updateChatSchema as it, createOrganizationSchema as j, createChatSchema as k, AGENT_VISIBILITY as l, wsAuthFrameSchema as lt, TASK_STATUSES as m, bindFeishuUser as n, updateAgentRuntimeConfigSchema as nt, AGENT_SOURCES as o, updateOrganizationSchema as ot, TASK_HEALTH_SIGNALS as p, sendMessageSchema as q, feishu_exports as r, updateAgentSchema as rt, AGENT_STATUSES as s, updateSystemConfigSchema as st, bindFeishuBot as t, updateAdapterConfigSchema as tt, DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD as u, adminCreateTaskSchema as v, clientRegisterSchema as w, agentPinnedMessageSchema as x, adminUpdateTaskSchema as y, linkTaskChatSchema as z };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import "./observability-DV_fQKqV-CuLWzBxQ.mjs";
2
- import { A as checkServerHealth, B as blank, C as runMigrations, D as checkDocker, E as checkDatabase, F as isDockerAvailable, G as SdkError, I as stopPostgres, L as ClientRuntime, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as ensurePostgres, R as createOwner, S as uninstallClientService, T as checkClientConfig, U as status, W as FirstTreeHubSDK, _ as runHomeMigration, b as isServiceSupported, d as promptMissingFields, f as formatCheckReport, h as onboardCreate, j as checkServerReachable, k as checkServerConfig, l as isInteractive, m as onboardCheck, s as startServer, u as promptAddAgent, v as getClientServiceStatus, w as checkAgentConfigs, x as resolveCliInvocation, y as installClientService, z as hasUser } from "./core-USyOOh7y.mjs";
2
+ import { A as checkServerHealth, B as createOwner, C as runMigrations, D as checkDocker, E as checkDatabase, F as isDockerAvailable, I as stopPostgres, J as FirstTreeHubSDK, K as status, L as ClientRuntime, M as checkWebSocket, N as printResults, O as checkNodeVersion, P as ensurePostgres, R as handleClientOrgMismatch, S as uninstallClientService, T as checkClientConfig, U as blank, V as hasUser, Y as SdkError, _ as runHomeMigration, b as isServiceSupported, d as promptMissingFields, f as formatCheckReport, h as onboardCreate, j as checkServerReachable, k as checkServerConfig, l as isInteractive, m as onboardCheck, s as startServer, u as promptAddAgent, v as getClientServiceStatus, w as checkAgentConfigs, x as resolveCliInvocation, y as installClientService, z as rotateClientIdWithBackup } from "./core-B2YUTpgg.mjs";
3
3
  import "./logger-core-BTmvdflj-DhdipBkV.mjs";
4
- import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-DWifXj9b.mjs";
5
- import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-GlaczcVf.mjs";
6
- export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };
4
+ import { a as resolveAccessToken, n as ensureFreshAccessToken, o as resolveServerUrl, r as ensureFreshAdminToken } from "./bootstrap-hh_PkTu6.mjs";
5
+ import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-B1Kiq7S6.mjs";
6
+ export { ClientRuntime, FirstTreeHubSDK, SdkError, bindFeishuBot, bindFeishuUser, blank, checkAgentConfigs, checkClientConfig, checkDatabase, checkDocker, checkNodeVersion, checkServerConfig, checkServerHealth, checkServerReachable, checkWebSocket, createOwner, ensureFreshAccessToken, ensureFreshAdminToken, ensurePostgres, formatCheckReport, getClientServiceStatus, handleClientOrgMismatch, hasUser, installClientService, isDockerAvailable, isInteractive, isServiceSupported, onboardCheck, onboardCreate, printResults, promptAddAgent, promptMissingFields, resolveAccessToken, resolveCliInvocation, resolveServerUrl, rotateClientIdWithBackup, runHomeMigration, runMigrations, startServer, status, stopPostgres, uninstallClientService };