@agent-team-foundation/first-tree-hub 0.10.6 → 0.10.8

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.
@@ -1,8 +1,8 @@
1
1
  import { m as __toESM } from "./esm-CYu4tXXn.mjs";
2
2
  import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DPyf745N-BSc8QNcR.mjs";
3
3
  import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue, x as resetConfigMeta } from "./bootstrap-jx5nN1qZ.mjs";
4
- import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-CbX9mUVH.mjs";
5
- import { a as ForbiddenError, c as buildInviteUrl, d as getActiveInvitation, f as invitationRedemptions, g as recordRedemption, i as ConflictError, l as ensureActiveInvitation, m as organizations, n as BadRequestError, o as NotFoundError, p as invitations, r as ClientOrgMismatchError$1, s as UnauthorizedError, t as AppError, u as findActiveByToken, v as users, y as uuidv7 } from "./invitation-BljIolbO-DLeHfURd.mjs";
4
+ import { $ as refreshTokenSchema, A as createOrgFromMeSchema, B as imageInlineContentSchema, C as clientRegisterSchema, D as createAgentSchema, E as createAdapterMappingSchema, F as dryRunAgentRuntimeConfigSchema, G as isReservedAgentName$1, H as inboxDeliverFrameSchema$1, I as extractMentions, J as loginSchema, K as joinByInvitationSchema, L as githubCallbackQuerySchema, M as createTaskSchema, N as defaultRuntimeConfigPayload, O as createChatSchema, P as delegateFeishuUserSchema, Q as rebindAgentSchema, R as githubDevCallbackQuerySchema, S as clientCapabilitiesSchema$1, St as wsAuthFrameSchema, T as createAdapterConfigSchema, U as inboxPollQuerySchema, V as inboxAckFrameSchema, W as isRedactedEnvValue, X as notificationQuerySchema, Y as messageSourceSchema$1, Z as paginationQuerySchema, _ as adminUpdateTaskSchema, _t as updateClientCapabilitiesSchema, a as AGENT_STATUSES, at as sendToAgentSchema, b as agentRuntimeConfigPayloadSchema$1, bt as updateSystemConfigSchema, ct as sessionEventSchema$1, d as TASK_HEALTH_SIGNALS, dt as switchOrgSchema, et as runtimeStateMessageSchema, f as TASK_STATUSES, ft as taskListQuerySchema, g as adminCreateTaskSchema, gt as updateChatSchema, h as addParticipantSchema, ht as updateAgentSchema, i as AGENT_SOURCES, it as sendMessageSchema, j as createOrganizationSchema, k as createMemberSchema, l as SYSTEM_CONFIG_DEFAULTS, lt as sessionReconcileRequestSchema, m as WS_AUTH_FRAME_TIMEOUT_MS, mt as updateAgentRuntimeConfigSchema, n as AGENT_NAME_REGEX$1, nt as scanMentionTokens, o as AGENT_TYPES, ot as sessionCompletionMessageSchema, p as TASK_TERMINAL_STATUSES, pt as updateAdapterConfigSchema, q as linkTaskChatSchema, r as AGENT_SELECTOR_HEADER$1, rt as selfServiceFeishuBotSchema, s as AGENT_VISIBILITY, st as sessionEventMessageSchema, t as AGENT_BIND_REJECT_REASONS, tt as safeRedirectPath, u as TASK_CREATOR_TYPES, ut as sessionStateMessageSchema, v as agentBindRequestSchema, vt as updateMemberSchema, w as connectTokenExchangeSchema, x as agentTypeSchema$1, xt as updateTaskStatusSchema, y as agentPinnedMessageSchema$1, yt as updateOrganizationSchema, z as githubStartQuerySchema } from "./dist-DSr_I5Ia.mjs";
5
+ import { _ as recordRedemption, a as ConflictError, b as uuidv7, c as UnauthorizedError, d as findActiveByToken, f as getActiveInvitation, h as organizations, i as ClientUserMismatchError$1, l as buildInviteUrl, m as invitations, n as BadRequestError, o as ForbiddenError, p as invitationRedemptions, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as ensureActiveInvitation, y as users } from "./invitation-B1pjAyOz-BaCA9PII.mjs";
6
6
  import { createRequire } from "node:module";
7
7
  import { ZodError, z } from "zod";
8
8
  import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
@@ -770,6 +770,11 @@ z.object({
770
770
  sdkVersion: z.string().max(50).optional(),
771
771
  wireCapabilities: clientWireCapabilitiesSchema.optional()
772
772
  });
773
+ z.object({
774
+ clientId: z.string(),
775
+ previousUserId: z.string().nullable(),
776
+ unpinnedAgentCount: z.number().int().nonnegative()
777
+ });
773
778
  const capabilityStateSchema = z.enum([
774
779
  "ok",
775
780
  "missing",
@@ -1037,6 +1042,13 @@ z.object({
1037
1042
  displayName: z.string().min(1).max(200)
1038
1043
  });
1039
1044
  z.object({ organizationId: z.string().min(1) });
1045
+ z.object({
1046
+ id: z.string(),
1047
+ organizationId: z.string(),
1048
+ organizationName: z.string(),
1049
+ role: z.enum(["admin", "member"]),
1050
+ agentId: z.string()
1051
+ });
1040
1052
  const memberRoleSchema = z.enum(["admin", "member"]);
1041
1053
  const memberSchema = z.object({
1042
1054
  id: z.string(),
@@ -1761,8 +1773,8 @@ var SdkError = class extends Error {
1761
1773
  /**
1762
1774
  * Thrown (emitted on `error` and rejected from `connect()`) when the server
1763
1775
  * refuses a `client:register` because the local clientId is bound to a
1764
- * different organization. The CLI layer detects this via `instanceof` and
1765
- * prompts the user before rotating the local clientId.
1776
+ * different organization. Retained for wire compatibility the read paths
1777
+ * that produced this code were retired in decouple-client-from-identity §4.1.
1766
1778
  */
1767
1779
  var ClientOrgMismatchError = class extends Error {
1768
1780
  code = "CLIENT_ORG_MISMATCH";
@@ -1771,6 +1783,21 @@ var ClientOrgMismatchError = class extends Error {
1771
1783
  this.name = "ClientOrgMismatchError";
1772
1784
  }
1773
1785
  };
1786
+ /**
1787
+ * Thrown when the server refuses `client:register` because the local
1788
+ * client.yaml is owned by a different user. The CLI detects this via
1789
+ * `instanceof` and guides the operator to run
1790
+ * `first-tree-hub client claim --confirm` to take ownership (which unpins
1791
+ * the previous owner's agents from this machine). See decouple-client-from-
1792
+ * identity §4.4.
1793
+ */
1794
+ var ClientUserMismatchError = class extends Error {
1795
+ code = "CLIENT_USER_MISMATCH";
1796
+ constructor(message = "Client belongs to a different user") {
1797
+ super(message);
1798
+ this.name = "ClientUserMismatchError";
1799
+ }
1800
+ };
1774
1801
  const RECONNECT_BASE_MS = 1e3;
1775
1802
  const RECONNECT_MAX_MS = 3e4;
1776
1803
  const WS_CONNECT_TIMEOUT_MS = 1e4;
@@ -2121,7 +2148,7 @@ var ClientConnection = class extends EventEmitter {
2121
2148
  const code = typeof msg.code === "string" ? msg.code : void 0;
2122
2149
  const message = typeof msg.message === "string" ? msg.message : "unknown";
2123
2150
  this.closing = true;
2124
- const err = code === "CLIENT_ORG_MISMATCH" ? new ClientOrgMismatchError(message) : /* @__PURE__ */ new Error(`client:register rejected: ${message}`);
2151
+ const err = code === "CLIENT_USER_MISMATCH" ? new ClientUserMismatchError(message) : code === "CLIENT_ORG_MISMATCH" ? new ClientOrgMismatchError(message) : /* @__PURE__ */ new Error(`client:register rejected: ${message}`);
2125
2152
  this.lastHandshakeError = err;
2126
2153
  this.wsLogger.error({
2127
2154
  code,
@@ -7556,7 +7583,7 @@ async function onboardCreate(args) {
7556
7583
  }
7557
7584
  const runtimeAgent = args.type === "human" ? args.assistant : args.id;
7558
7585
  if (args.feishuBotAppId && args.feishuBotAppSecret) {
7559
- const { bindFeishuBot } = await import("./feishu-DvjRZMdZ.mjs").then((n) => n.r);
7586
+ const { bindFeishuBot } = await import("./feishu-eynC54km.mjs").then((n) => n.r);
7560
7587
  const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
7561
7588
  if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
7562
7589
  else {
@@ -8536,7 +8563,7 @@ function createFeedbackHandler(config) {
8536
8563
  return { handle };
8537
8564
  }
8538
8565
  //#endregion
8539
- //#region ../server/dist/app-Ed9CsDC-.mjs
8566
+ //#region ../server/dist/app-XRPDwwkj.mjs
8540
8567
  var __defProp = Object.defineProperty;
8541
8568
  var __exportAll = (all, no_symbols) => {
8542
8569
  let target = {};
@@ -8631,6 +8658,20 @@ const chatParticipants = pgTable("chat_participants", {
8631
8658
  mode: text("mode").notNull().default("full"),
8632
8659
  joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow()
8633
8660
  }, (table) => [primaryKey({ columns: [table.chatId, table.agentId] }), index("idx_participants_agent").on(table.agentId)]);
8661
+ /** Organization membership. Links a user to an org with a role and a 1:1 human agent. */
8662
+ const members = pgTable("members", {
8663
+ id: text("id").primaryKey(),
8664
+ userId: text("user_id").notNull().references(() => users.id),
8665
+ organizationId: text("organization_id").notNull().references(() => organizations.id),
8666
+ agentId: text("agent_id").unique().notNull().references(() => agents.uuid),
8667
+ role: text("role").notNull(),
8668
+ status: text("status").notNull().default("active"),
8669
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
8670
+ }, (table) => [
8671
+ unique("uq_members_user_org").on(table.userId, table.organizationId),
8672
+ index("idx_members_user").on(table.userId),
8673
+ index("idx_members_org").on(table.organizationId)
8674
+ ]);
8634
8675
  function requireAgent(request) {
8635
8676
  const agent = request.agent;
8636
8677
  if (!agent) throw new UnauthorizedError("Agent authentication required");
@@ -8675,18 +8716,41 @@ function memberScope(request) {
8675
8716
  /**
8676
8717
  * SQL WHERE conditions for agents visible to a member.
8677
8718
  * Visibility is the same for all roles:
8678
- * same org + not deleted + (organization-visible OR managerId = self)
8719
+ * target org + not deleted + (organization-visible OR managerId = caller's member)
8720
+ *
8721
+ * Takes explicit `orgId` + `memberId` rather than reading them from
8722
+ * MemberScope: an admin viewing a non-default org passes
8723
+ * `requireMemberInOrg(db, request, orgId).memberId` to derive the right
8724
+ * memberId for that org (decouple-client-from-identity §4.5.1).
8679
8725
  */
8680
- function agentVisibilityCondition(scope) {
8681
- return and(eq(agents.organizationId, scope.organizationId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, scope.memberId)));
8726
+ function agentVisibilityCondition(orgId, memberId) {
8727
+ return and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId)));
8682
8728
  }
8683
8729
  /**
8684
- * Assert a single agent is visible to the member.
8685
- * Single query — returns 404 for both "not found" and "not visible"
8686
- * to prevent UUID enumeration.
8730
+ * Assert a single agent is visible to the caller.
8731
+ *
8732
+ * The agent is identified by UUID, so its organization is *intrinsic to the
8733
+ * row itself* — we don't gate on the JWT default org. Instead we resolve the
8734
+ * caller's active membership in `agent.organizationId` and reuse the same
8735
+ * visibility rule (organization-visible OR managerId = caller's member in
8736
+ * that org). This lets a multi-org user hit `/admin/agents/:uuid` for an
8737
+ * agent in a non-default org without re-issuing the JWT — `/auth/switch-org`
8738
+ * now returns 204 only and the web client carries the selected org in
8739
+ * localStorage (decouple-client-from-identity §C.2 / §D fix).
8740
+ *
8741
+ * Returns 404 for both "not found" and "not visible" to prevent UUID
8742
+ * enumeration.
8687
8743
  */
8688
8744
  async function assertAgentVisible(db, scope, agentUuid) {
8689
- const [row] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.uuid, agentUuid), agentVisibilityCondition(scope))).limit(1);
8745
+ const [agent] = await db.select({
8746
+ uuid: agents.uuid,
8747
+ organizationId: agents.organizationId,
8748
+ status: agents.status
8749
+ }).from(agents).where(eq(agents.uuid, agentUuid)).limit(1);
8750
+ if (!agent || agent.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8751
+ const [member] = await db.select({ id: members.id }).from(members).where(and(eq(members.userId, scope.userId), eq(members.organizationId, agent.organizationId), eq(members.status, "active"))).limit(1);
8752
+ if (!member) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8753
+ const [row] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.uuid, agentUuid), agentVisibilityCondition(agent.organizationId, member.id))).limit(1);
8690
8754
  if (!row) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8691
8755
  }
8692
8756
  /**
@@ -8708,9 +8772,22 @@ async function assertChatAccess(db, scope, chatId) {
8708
8772
  if (!managed) throw new NotFoundError(`Chat "${chatId}" not found`);
8709
8773
  }
8710
8774
  /**
8711
- * Assert the member can manage (update/delete/token/suspend) an agent.
8712
- * Admin can manage all agents in their org. Non-admin can only manage agents where managerId = self.
8713
- * Always verifies the agent exists and belongs to the same org (throws 404 if not).
8775
+ * Assert the caller can manage (update/delete/token/suspend) an agent.
8776
+ *
8777
+ * Manageability is anchored on the agent's *own* organization, not on the
8778
+ * JWT default org. We resolve the caller's active membership in
8779
+ * `agent.organizationId` and grant manage access if either:
8780
+ * - that membership is `admin` (admin in the agent's org), or
8781
+ * - the agent's `managerId` equals the caller's memberId *in that org*.
8782
+ *
8783
+ * This is the cross-org switch-org fix: with `/auth/switch-org` now 204 the
8784
+ * scope.organizationId / scope.memberId are JWT defaults, so the previous
8785
+ * `agent.organizationId !== scope.organizationId` short-circuit was 404'ing
8786
+ * every `:uuid` route as soon as the user looked at a non-default org. We
8787
+ * authorize against the agent's actual org instead.
8788
+ *
8789
+ * Returns 404 for "not found", "not a member of agent's org", or "not
8790
+ * authorized" — same shape as before, to prevent UUID enumeration.
8714
8791
  */
8715
8792
  async function assertCanManage(db, scope, agentUuid) {
8716
8793
  const [agent] = await db.select({
@@ -8718,9 +8795,85 @@ async function assertCanManage(db, scope, agentUuid) {
8718
8795
  managerId: agents.managerId,
8719
8796
  organizationId: agents.organizationId
8720
8797
  }).from(agents).where(and(eq(agents.uuid, agentUuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
8721
- if (!agent || agent.organizationId !== scope.organizationId) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8722
- if (scope.role === "admin") return;
8723
- if (agent.managerId !== scope.memberId) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8798
+ if (!agent) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8799
+ const [memberRow] = await db.select({
8800
+ id: members.id,
8801
+ role: members.role
8802
+ }).from(members).where(and(eq(members.userId, scope.userId), eq(members.organizationId, agent.organizationId), eq(members.status, "active"))).limit(1);
8803
+ if (!memberRow) throw new NotFoundError(`Agent "${agentUuid}" not found`);
8804
+ if (memberRow.role === "admin") return;
8805
+ if (agent.managerId === memberRow.id) return;
8806
+ throw new NotFoundError(`Agent "${agentUuid}" not found`);
8807
+ }
8808
+ /**
8809
+ * Assert the request's authenticated user has an active membership in
8810
+ * `orgId` and return its `(memberId, role)`. Used by admin routes that
8811
+ * accept an explicit `organizationId` from the request (query / body /
8812
+ * path) and must verify role realtime — JWT `organizationId` and `role`
8813
+ * claims are hints, not authoritative (decouple-client-from-identity §4.5).
8814
+ *
8815
+ * Throws {@link ForbiddenError} if the user is not an active member of the
8816
+ * target org.
8817
+ */
8818
+ async function requireMemberInOrg(db, request, orgId) {
8819
+ const m = requireMember(request);
8820
+ const [row] = await db.select({
8821
+ id: members.id,
8822
+ role: members.role
8823
+ }).from(members).where(and(eq(members.userId, m.userId), eq(members.organizationId, orgId), eq(members.status, "active"))).limit(1);
8824
+ if (!row) throw new ForbiddenError("Not an active member of the target organization");
8825
+ return {
8826
+ memberId: row.id,
8827
+ role: row.role
8828
+ };
8829
+ }
8830
+ /**
8831
+ * Resolve the `(organizationId, memberId, role)` an admin route should
8832
+ * operate against, based on the unified PR-D scoping rule:
8833
+ * - If the request supplies `?organizationId=…` (query) or it appears in
8834
+ * the body, verify the caller is an active member there and return
8835
+ * that membership realtime.
8836
+ * - Otherwise fall back to the JWT default org (the existing
8837
+ * `MemberScope` from `memberScope(request)`).
8838
+ *
8839
+ * All admin listing routes call this so that the cross-org switch — driven
8840
+ * entirely client-side via `localStorage.selectedOrganizationId` after
8841
+ * `/auth/switch-org` returns 204 — funnels through one consistent gate
8842
+ * (decouple-client-from-identity §4.5 / §D, codex P1 #2 fix).
8843
+ *
8844
+ * @returns the effective scope for the rest of the route to read from.
8845
+ */
8846
+ async function resolveAdminScope(db, request, scope, requestedOrganizationId) {
8847
+ if (!requestedOrganizationId || requestedOrganizationId === scope.organizationId) return scope;
8848
+ const probe = await requireMemberInOrg(db, request, requestedOrganizationId);
8849
+ return {
8850
+ ...scope,
8851
+ memberId: probe.memberId,
8852
+ organizationId: requestedOrganizationId,
8853
+ role: probe.role
8854
+ };
8855
+ }
8856
+ /**
8857
+ * Cross-org listing helper for "agents I personally manage". Used by the
8858
+ * CLI `agent list` view (decouple-client-from-identity §4.5.1 case (b)) —
8859
+ * the web roster, by contrast, stays org-scoped via
8860
+ * {@link agentVisibilityCondition}.
8861
+ *
8862
+ * Returns every active agent whose manager is an active member of the
8863
+ * caller. JOINs `agents → members.id` and filters by `members.user_id`.
8864
+ */
8865
+ async function listAgentsManagedByUser(db, userId) {
8866
+ return db.select({
8867
+ uuid: agents.uuid,
8868
+ name: agents.name,
8869
+ displayName: agents.displayName,
8870
+ type: agents.type,
8871
+ organizationId: agents.organizationId,
8872
+ inboxId: agents.inboxId,
8873
+ visibility: agents.visibility,
8874
+ runtimeProvider: agents.runtimeProvider,
8875
+ clientId: agents.clientId
8876
+ }).from(agents).innerJoin(members, eq(agents.managerId, members.id)).where(and(eq(members.userId, userId), eq(members.status, "active"), ne(agents.status, AGENT_STATUSES.DELETED)));
8724
8877
  }
8725
8878
  /**
8726
8879
  * Maps internal Chats to external IM platform channels.
@@ -8997,6 +9150,7 @@ async function findExternalMessageByInternalId(db, platform, messageId) {
8997
9150
  }).from(adapterMessageReferences).where(and(eq(adapterMessageReferences.platform, platform), eq(adapterMessageReferences.messageId, messageId))).limit(1);
8998
9151
  return row ?? null;
8999
9152
  }
9153
+ const orgQuerySchema$3 = z.object({ organizationId: z.string().min(1).optional() });
9000
9154
  function parseId$1(raw) {
9001
9155
  const id = Number(raw);
9002
9156
  if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid mapping ID: "${raw}"`);
@@ -9005,8 +9159,11 @@ function parseId$1(raw) {
9005
9159
  async function adminAdapterMappingRoutes(app) {
9006
9160
  app.get("/", async (request) => {
9007
9161
  const scope = memberScope(request);
9008
- const conditions = [eq(agents.organizationId, scope.organizationId)];
9009
- if (scope.role !== "admin") conditions.push(eq(agents.managerId, scope.memberId));
9162
+ const { organizationId } = orgQuerySchema$3.parse(request.query);
9163
+ const targetOrgId = organizationId ?? scope.organizationId;
9164
+ const probe = await requireMemberInOrg(app.db, request, targetOrgId);
9165
+ const conditions = [eq(agents.organizationId, targetOrgId)];
9166
+ if (probe.role !== "admin") conditions.push(eq(agents.managerId, probe.memberId));
9010
9167
  return (await app.db.select({
9011
9168
  id: adapterAgentMappings.id,
9012
9169
  platform: adapterAgentMappings.platform,
@@ -9073,11 +9230,15 @@ const adapterConfigs = pgTable("adapter_configs", {
9073
9230
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
9074
9231
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
9075
9232
  }, (t) => [unique("uq_adapter_configs_agent_platform").on(t.agentId, t.platform)]);
9233
+ const orgQuerySchema$2 = z.object({ organizationId: z.string().min(1).optional() });
9076
9234
  async function adminAdapterStatusRoutes(app) {
9077
9235
  app.get("/", async (request) => {
9078
9236
  const scope = memberScope(request);
9079
- const conditions = [eq(agents.organizationId, scope.organizationId), ne(agents.status, "deleted")];
9080
- if (scope.role !== "admin") conditions.push(eq(agents.managerId, scope.memberId));
9237
+ const { organizationId } = orgQuerySchema$2.parse(request.query);
9238
+ const targetOrgId = organizationId ?? scope.organizationId;
9239
+ const probe = await requireMemberInOrg(app.db, request, targetOrgId);
9240
+ const conditions = [eq(agents.organizationId, targetOrgId), ne(agents.status, "deleted")];
9241
+ if (probe.role !== "admin") conditions.push(eq(agents.managerId, probe.memberId));
9081
9242
  const visibleRows = await app.db.select({ id: adapterConfigs.id }).from(adapterConfigs).innerJoin(agents, eq(agents.uuid, adapterConfigs.agentId)).where(and(...conditions));
9082
9243
  const visibleIds = new Set(visibleRows.map((r) => r.id));
9083
9244
  if (visibleIds.size === 0) return [];
@@ -9264,6 +9425,7 @@ async function deleteAdapterConfig(db, id) {
9264
9425
  if (!row) throw new NotFoundError(`Adapter config "${id}" not found`);
9265
9426
  }
9266
9427
  const log$5 = createLogger$1("AdminAdapters");
9428
+ const orgQuerySchema$1 = z.object({ organizationId: z.string().min(1).optional() });
9267
9429
  function parseId(raw) {
9268
9430
  const id = Number(raw);
9269
9431
  if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid adapter ID: "${raw}"`);
@@ -9272,7 +9434,9 @@ function parseId(raw) {
9272
9434
  async function adminAdapterRoutes(app) {
9273
9435
  app.get("/", async (request) => {
9274
9436
  const scope = memberScope(request);
9275
- return (await listAdapterConfigsForMember(app.db, scope)).map((c) => ({
9437
+ const { organizationId } = orgQuerySchema$1.parse(request.query);
9438
+ const effective = await resolveAdminScope(app.db, request, scope, organizationId);
9439
+ return (await listAdapterConfigsForMember(app.db, effective)).map((c) => ({
9276
9440
  ...c,
9277
9441
  createdAt: c.createdAt.toISOString(),
9278
9442
  updatedAt: c.updatedAt.toISOString()
@@ -9401,20 +9565,6 @@ const agentConfigs = pgTable("agent_configs", {
9401
9565
  updatedBy: text("updated_by").notNull(),
9402
9566
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
9403
9567
  });
9404
- /** Organization membership. Links a user to an org with a role and a 1:1 human agent. */
9405
- const members = pgTable("members", {
9406
- id: text("id").primaryKey(),
9407
- userId: text("user_id").notNull().references(() => users.id),
9408
- organizationId: text("organization_id").notNull().references(() => organizations.id),
9409
- agentId: text("agent_id").unique().notNull().references(() => agents.uuid),
9410
- role: text("role").notNull(),
9411
- status: text("status").notNull().default("active"),
9412
- createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
9413
- }, (table) => [
9414
- unique("uq_members_user_org").on(table.userId, table.organizationId),
9415
- index("idx_members_user").on(table.userId),
9416
- index("idx_members_org").on(table.organizationId)
9417
- ]);
9418
9568
  /**
9419
9569
  * Names beginning with `__` are reserved for Hub-internal pseudo agents
9420
9570
  * (e.g. the task notifier). User-facing creation must not be able to
@@ -9506,19 +9656,14 @@ async function resolveAgentClient(db, data) {
9506
9656
  return null;
9507
9657
  }
9508
9658
  if (!data.clientId) return null;
9509
- const [manager] = await db.select({
9510
- userId: members.userId,
9511
- organizationId: members.organizationId
9512
- }).from(members).where(eq(members.id, data.managerId)).limit(1);
9659
+ const [manager] = await db.select({ userId: members.userId }).from(members).where(eq(members.id, data.managerId)).limit(1);
9513
9660
  if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
9514
9661
  const [client] = await db.select({
9515
9662
  id: clients.id,
9516
- userId: clients.userId,
9517
- organizationId: clients.organizationId
9663
+ userId: clients.userId
9518
9664
  }).from(clients).where(eq(clients.id, data.clientId)).limit(1);
9519
9665
  if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
9520
9666
  if (!client.userId) throw new BadRequestError(`Client "${data.clientId}" has not been claimed by a user yet. Have the operator run \`first-tree-hub client connect\` on that machine before pinning an agent to it.`);
9521
- if (client.organizationId !== manager.organizationId) throw new ForbiddenError(`Client "${data.clientId}" belongs to a different organization — pick a client registered in the manager's org.`);
9522
9667
  if (client.userId !== manager.userId) throw new ForbiddenError(`Client "${data.clientId}" is not owned by the manager's user — pick a client belonging to that user.`);
9523
9668
  return client.id;
9524
9669
  }
@@ -9662,7 +9807,7 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
9662
9807
  * Uses agentVisibilityCondition from access-control (same rules for all roles).
9663
9808
  */
9664
9809
  async function listAgentsForMember(db, scope, limit, cursor, type) {
9665
- const conditions = [agentVisibilityCondition(scope)];
9810
+ const conditions = [agentVisibilityCondition(scope.organizationId, scope.memberId)];
9666
9811
  if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
9667
9812
  if (type) conditions.push(eq(agents.type, type));
9668
9813
  const where = and(...conditions);
@@ -10249,54 +10394,40 @@ async function cleanupStalePresence(db, staleSeconds = 60) {
10249
10394
  }
10250
10395
  /**
10251
10396
  * Assert the caller can act on this client. Throws 404 for both "not found"
10252
- * and "not yours" to prevent UUID enumeration across org/user boundaries.
10253
- *
10254
- * A client is bound to exactly one organization (`clients.organization_id`).
10255
- * Access is granted when:
10256
- * - member: row.user_id == scope.userId AND row.organization_id == scope.organizationId.
10257
- * - admin: row.organization_id == scope.organizationId AND the owner is a
10258
- * member of that same org (defense in depth).
10259
- *
10260
- * Same user across two orgs has two distinct client rows; operating on one
10261
- * while logged into the other is refused by the org filter.
10397
+ * and "not yours" to prevent UUID enumeration. The client is owned by exactly
10398
+ * one user; cross-user admin access is no longer supported by this code path
10399
+ * (see decouple-client-from-identity-design §4.10.5 option A). Cross-user
10400
+ * ownership transfer goes through `claimClient` in PR-B.
10262
10401
  */
10263
10402
  async function assertClientOwner(db, clientId, scope) {
10264
10403
  const [row] = await db.select({
10265
10404
  id: clients.id,
10266
- userId: clients.userId,
10267
- organizationId: clients.organizationId
10405
+ userId: clients.userId
10268
10406
  }).from(clients).where(eq(clients.id, clientId)).limit(1);
10269
- if (!row) throw new NotFoundError(`Client "${clientId}" not found`);
10270
- if (row.organizationId !== scope.organizationId) throw new NotFoundError(`Client "${clientId}" not found`);
10271
- if (row.userId === scope.userId) return;
10272
- if (scope.role === "admin" && row.userId !== null) {
10273
- const [sibling] = await db.select({ id: members.id }).from(members).where(and(eq(members.userId, row.userId), eq(members.organizationId, scope.organizationId))).limit(1);
10274
- if (sibling) return;
10275
- }
10276
- throw new NotFoundError(`Client "${clientId}" not found`);
10407
+ if (!row || row.userId !== scope.userId) throw new NotFoundError(`Client "${clientId}" not found`);
10277
10408
  }
10278
10409
  /**
10279
10410
  * Upsert the clients row for a given `client_id` under an authenticated user.
10280
10411
  *
10281
- * Claim semantics (see proposal M13 + multi-tenancy hardening):
10282
- * - New client_id → INSERT with the authenticated user_id and org_id.
10283
- * - Existing row with the same user_id + org_id refresh runtime columns.
10284
- * - Existing row in a different org {@link ClientOrgMismatchError}. A
10285
- * client is bound to one org for its lifetime; the CLI reacts by
10286
- * abandoning the local clientId and registering a new one.
10287
- * - Existing row with a different user_id (same org) → {@link ForbiddenError};
10288
- * the operator must pick a different clientId. Hard conflict because
10289
- * pinned agents under that client belong to the original owner.
10412
+ * Claim semantics (decouple-client-from-identity §4.1.1):
10413
+ * - New client_id → INSERT with the authenticated user_id. `organization_id`
10414
+ * is written as a placeholder (NOT NULL legacy column; no longer consumed
10415
+ * by any read path) sourced from the caller-supplied JWT default org.
10416
+ * - Existing row with the same user_id refresh runtime columns.
10417
+ * `organization_id` is **not** updated on conflict, so the placeholder set
10418
+ * at first insert sticks for the row's lifetime.
10419
+ * - Existing row with a different user_id raises
10420
+ * {@link ClientUserMismatchError} (WS close 4403). The CLI guides the
10421
+ * operator through `first-tree-hub client claim --confirm` to take
10422
+ * ownership, which unpins the previous owner's agents from the machine.
10290
10423
  */
10291
10424
  async function registerClient(db, data) {
10292
10425
  const now = /* @__PURE__ */ new Date();
10293
10426
  const [existing] = await db.select({
10294
10427
  id: clients.id,
10295
- userId: clients.userId,
10296
- organizationId: clients.organizationId
10428
+ userId: clients.userId
10297
10429
  }).from(clients).where(eq(clients.id, data.clientId)).limit(1);
10298
- if (existing && existing.organizationId !== data.organizationId) throw new ClientOrgMismatchError$1(`Client "${data.clientId}" is bound to a different organization. Re-register as a new client under the current org.`);
10299
- if (existing?.userId && existing.userId !== data.userId) throw new ForbiddenError(`Client "${data.clientId}" is already claimed by a different user. Pick a unique client_id.`);
10430
+ if (existing?.userId && existing.userId !== data.userId) throw new ClientUserMismatchError$1(`Client "${data.clientId}" is owned by a different user. Run \`first-tree-hub client claim --confirm\` to transfer ownership.`);
10300
10431
  await db.insert(clients).values({
10301
10432
  id: data.clientId,
10302
10433
  userId: data.userId,
@@ -10322,6 +10453,54 @@ async function registerClient(db, data) {
10322
10453
  }
10323
10454
  });
10324
10455
  }
10456
+ /**
10457
+ * Transfer ownership of a client row to a new user, unpinning any agents
10458
+ * whose manager belonged to the previous owner. Atomic: caller is guaranteed
10459
+ * either a fully-applied ownership flip + bulk unpin, or no change. Idempotent
10460
+ * when `newUserId` already owns the row.
10461
+ *
10462
+ * Manager → user resolution goes through the members JOIN (the agents table
10463
+ * carries only `manager_id`); cross-org agents under the same previous owner
10464
+ * are unpinned together (decouple-client-from-identity §4.4).
10465
+ *
10466
+ * Caller is responsible for the caller-side authorization (the new owner must
10467
+ * be the authenticated request's user). The structured log
10468
+ * `event: client.owner_transfer` is emitted by the caller after the
10469
+ * transaction commits, using the returned `previousUserId` /
10470
+ * `unpinnedAgentIds`.
10471
+ */
10472
+ async function claimClient(db, clientId, newUserId) {
10473
+ return db.transaction(async (tx) => {
10474
+ const [locked] = await tx.execute(sql`SELECT id, user_id FROM clients WHERE id = ${clientId} FOR UPDATE`);
10475
+ if (!locked) throw new NotFoundError(`Client "${clientId}" not found`);
10476
+ const previousUserId = locked.user_id;
10477
+ if (previousUserId === newUserId) return {
10478
+ previousUserId,
10479
+ unpinnedAgentIds: []
10480
+ };
10481
+ let unpinnedAgentIds = [];
10482
+ if (previousUserId !== null) {
10483
+ unpinnedAgentIds = (await tx.select({ uuid: agents.uuid }).from(agents).innerJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.clientId, clientId), eq(members.userId, previousUserId)))).map((r) => r.uuid);
10484
+ if (unpinnedAgentIds.length > 0) {
10485
+ const now = /* @__PURE__ */ new Date();
10486
+ await tx.update(agents).set({
10487
+ clientId: null,
10488
+ updatedAt: now
10489
+ }).where(inArray(agents.uuid, unpinnedAgentIds));
10490
+ await tx.update(agentPresence).set({
10491
+ status: "offline",
10492
+ clientId: null,
10493
+ ...runtimeFieldsReset(now)
10494
+ }).where(inArray(agentPresence.agentId, unpinnedAgentIds));
10495
+ }
10496
+ }
10497
+ await tx.update(clients).set({ userId: newUserId }).where(eq(clients.id, clientId));
10498
+ return {
10499
+ previousUserId,
10500
+ unpinnedAgentIds
10501
+ };
10502
+ });
10503
+ }
10325
10504
  async function disconnectClient(db, clientId) {
10326
10505
  const now = /* @__PURE__ */ new Date();
10327
10506
  await db.update(agentPresence).set({
@@ -10360,16 +10539,17 @@ async function listActiveAgentsPinnedToClient(db, clientId) {
10360
10539
  }).from(agents).where(and(eq(agents.clientId, clientId), ne(agents.status, "deleted")));
10361
10540
  }
10362
10541
  /**
10363
- * Member-scoped: every active agent pinned to a client owned by this user
10364
- * within the given organization. Used by client startup to reconcile its
10365
- * local YAML against the authoritative `agents.runtime_provider`.
10542
+ * Member-scoped: every active agent pinned to a client owned by this user.
10543
+ * Used by client startup to reconcile its local YAML against the authoritative
10544
+ * `agents.runtime_provider`. Cross-org by design a client is owned by a
10545
+ * user, not an org (decouple-client-from-identity §4.1).
10366
10546
  */
10367
10547
  async function listMyPinnedAgents(db, scope) {
10368
10548
  return (await db.select({
10369
10549
  agentId: agents.uuid,
10370
10550
  clientId: agents.clientId,
10371
10551
  runtimeProvider: agents.runtimeProvider
10372
- }).from(agents).innerJoin(clients, eq(agents.clientId, clients.id)).where(and(eq(clients.userId, scope.userId), eq(clients.organizationId, scope.organizationId), ne(agents.status, "deleted")))).filter((r) => r.clientId !== null).map((r) => ({
10552
+ }).from(agents).innerJoin(clients, eq(agents.clientId, clients.id)).where(and(eq(clients.userId, scope.userId), ne(agents.status, "deleted")))).filter((r) => r.clientId !== null).map((r) => ({
10373
10553
  agentId: r.agentId,
10374
10554
  clientId: r.clientId,
10375
10555
  runtimeProvider: r.runtimeProvider
@@ -10394,17 +10574,28 @@ async function updateClientCapabilities(db, clientId, capabilities) {
10394
10574
  await db.update(clients).set({ metadata: merged }).where(eq(clients.id, clientId));
10395
10575
  }
10396
10576
  /**
10397
- * Scope-aware client listing.
10398
- *
10399
- * - member: rows where `user_id = scope.userId` AND `organization_id = scope.organizationId`
10400
- * — protects against a user listing their own clients registered under a
10401
- * different org when they're logged into this one.
10402
- * - admin: every row in `scope.organizationId`, regardless of owner.
10577
+ * Scope-aware client listing. Returns the caller's own clients (cross-org —
10578
+ * a client is owned by a user, not an org). The admin route adds a separate
10579
+ * `?organizationId=` cross-user view via {@link listClientsForOrgAdmin}.
10403
10580
  */
10404
10581
  async function listClients(db, scope) {
10405
- const rows = scope.role === "admin" ? await db.select({
10582
+ return attachAgentCounts(db, await db.select().from(clients).where(eq(clients.userId, scope.userId)));
10583
+ }
10584
+ /**
10585
+ * Admin-only cross-user listing: every client owned by an active member of
10586
+ * `orgId`. Joining `clients → members.user_id` instead of `clients.organization_id`
10587
+ * keeps the read path consistent with the rule that connection has no
10588
+ * runtime relationship to organization (decouple-client-from-identity §A).
10589
+ *
10590
+ * The caller must verify admin role realtime via `requireMemberInOrg` before
10591
+ * invoking this function — the service does not re-check, so it is
10592
+ * unsafe to expose without that gate.
10593
+ */
10594
+ async function listClientsForOrgAdmin(db, orgId) {
10595
+ return attachAgentCounts(db, await db.select({
10406
10596
  id: clients.id,
10407
10597
  userId: clients.userId,
10598
+ organizationId: clients.organizationId,
10408
10599
  status: clients.status,
10409
10600
  sdkVersion: clients.sdkVersion,
10410
10601
  hostname: clients.hostname,
@@ -10413,7 +10604,9 @@ async function listClients(db, scope) {
10413
10604
  connectedAt: clients.connectedAt,
10414
10605
  lastSeenAt: clients.lastSeenAt,
10415
10606
  metadata: clients.metadata
10416
- }).from(clients).where(eq(clients.organizationId, scope.organizationId)) : await db.select().from(clients).where(and(eq(clients.userId, scope.userId), eq(clients.organizationId, scope.organizationId)));
10607
+ }).from(clients).innerJoin(members, eq(members.userId, clients.userId)).where(and(eq(members.organizationId, orgId), eq(members.status, "active"))));
10608
+ }
10609
+ async function attachAgentCounts(db, rows) {
10417
10610
  const counts = await db.select({
10418
10611
  clientId: agents.clientId,
10419
10612
  count: sql`count(*)::int`
@@ -10723,7 +10916,7 @@ async function listAgentsWithRuntime(db, scope) {
10723
10916
  totalSessions: agentPresence.totalSessions,
10724
10917
  runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
10725
10918
  type: agents.type
10726
- }).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
10919
+ }).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope.organizationId, scope.memberId)));
10727
10920
  }
10728
10921
  const log$4 = createLogger$1("message");
10729
10922
  async function sendMessage(db, chatId, senderId, data, options = {}) {
@@ -11089,12 +11282,26 @@ async function adminAgentRoutes(app) {
11089
11282
  }
11090
11283
  sendToClient(agent.clientId, parsed.data);
11091
11284
  }
11092
- const listAgentsFilterSchema = z.object({ type: agentTypeSchema$1.optional() });
11285
+ const listAgentsFilterSchema = z.object({
11286
+ type: agentTypeSchema$1.optional(),
11287
+ organizationId: z.string().min(1).optional()
11288
+ });
11093
11289
  app.get("/", async (request) => {
11094
11290
  const query = paginationQuerySchema.parse(request.query);
11095
- const { type } = listAgentsFilterSchema.parse(request.query);
11291
+ const { type, organizationId } = listAgentsFilterSchema.parse(request.query);
11096
11292
  const scope = memberScope(request);
11097
- const result = await listAgentsForMember(app.db, scope, query.limit, query.cursor, type);
11293
+ const targetOrgId = organizationId ?? scope.organizationId;
11294
+ let queryScope = scope;
11295
+ if (targetOrgId !== scope.organizationId) {
11296
+ const probe = await requireMemberInOrg(app.db, request, targetOrgId);
11297
+ queryScope = {
11298
+ ...scope,
11299
+ memberId: probe.memberId,
11300
+ organizationId: targetOrgId,
11301
+ role: probe.role
11302
+ };
11303
+ }
11304
+ const result = await listAgentsForMember(app.db, queryScope, query.limit, query.cursor, type);
11098
11305
  return {
11099
11306
  items: result.items.map((a) => ({
11100
11307
  ...a,
@@ -11119,9 +11326,18 @@ async function adminAgentRoutes(app) {
11119
11326
  */
11120
11327
  app.get("/all", async (request) => {
11121
11328
  const scope = memberScope(request);
11122
- if (scope.role !== "admin") throw new ForbiddenError("Admin role required");
11123
11329
  const query = paginationQuerySchema.parse(request.query);
11124
- const result = await listAgentsForAdmin(app.db, scope, query.limit, query.cursor);
11330
+ const { organizationId } = listAgentsFilterSchema.parse(request.query);
11331
+ const targetOrgId = organizationId ?? scope.organizationId;
11332
+ const probe = await requireMemberInOrg(app.db, request, targetOrgId);
11333
+ if (probe.role !== "admin") throw new ForbiddenError("Admin role required");
11334
+ const adminScope = {
11335
+ ...scope,
11336
+ memberId: probe.memberId,
11337
+ organizationId: targetOrgId,
11338
+ role: probe.role
11339
+ };
11340
+ const result = await listAgentsForAdmin(app.db, adminScope, query.limit, query.cursor);
11125
11341
  return {
11126
11342
  items: result.items.map((a) => ({
11127
11343
  ...a,
@@ -11152,9 +11368,12 @@ async function adminAgentRoutes(app) {
11152
11368
  app.post("/", async (request, reply) => {
11153
11369
  const scope = memberScope(request);
11154
11370
  const body = createAgentSchema.parse(request.body);
11155
- const managerId = scope.role === "admin" ? body.managerId ?? scope.memberId : scope.memberId;
11371
+ const targetOrgId = body.organizationId ?? scope.organizationId;
11372
+ const probe = await requireMemberInOrg(app.db, request, targetOrgId);
11373
+ const managerId = probe.role === "admin" ? body.managerId ?? probe.memberId : probe.memberId;
11156
11374
  const agent = await createAgent(app.db, {
11157
11375
  ...body,
11376
+ organizationId: targetOrgId,
11158
11377
  source: body.source ?? "admin-api",
11159
11378
  managerId
11160
11379
  });
@@ -11461,7 +11680,8 @@ async function adminChatRoutes(app) {
11461
11680
  /** List all chats in org (admin-only, for audit). Members should use GET /mine. */
11462
11681
  app.get("/", { preHandler: requireAdminRoleHook() }, async (request) => {
11463
11682
  const query = paginationQuerySchema.parse(request.query);
11464
- const orgParam = request.query.org;
11683
+ const rawQuery = request.query;
11684
+ const orgParam = rawQuery.organizationId ?? rawQuery.org;
11465
11685
  let orgId;
11466
11686
  if (orgParam) orgId = (await resolveOrganization(app.db, orgParam)).id;
11467
11687
  else orgId = await resolveDefaultOrgId(app.db);
@@ -11655,14 +11875,15 @@ async function adminChatRoutes(app) {
11655
11875
  function serializeDate(d) {
11656
11876
  return d ? d.toISOString() : null;
11657
11877
  }
11878
+ const listClientsQuerySchema = z.object({ organizationId: z.string().min(1).optional() });
11658
11879
  async function adminClientRoutes(app) {
11659
11880
  app.get("/", async (request) => {
11660
11881
  const scope = memberScope(request);
11661
- return (await listClients(app.db, {
11662
- userId: scope.userId,
11663
- organizationId: scope.organizationId,
11664
- role: scope.role
11665
- })).map((c) => ({
11882
+ const { organizationId } = listClientsQuerySchema.parse(request.query);
11883
+ return (organizationId ? await (async () => {
11884
+ if ((await requireMemberInOrg(app.db, request, organizationId)).role !== "admin") throw new ForbiddenError("Admin role required");
11885
+ return listClientsForOrgAdmin(app.db, organizationId);
11886
+ })() : await listClients(app.db, { userId: scope.userId })).map((c) => ({
11666
11887
  id: c.id,
11667
11888
  userId: c.userId,
11668
11889
  status: c.status,
@@ -11676,10 +11897,7 @@ async function adminClientRoutes(app) {
11676
11897
  });
11677
11898
  app.get("/me/agents", async (request) => {
11678
11899
  const scope = memberScope(request);
11679
- return await listMyPinnedAgents(app.db, {
11680
- userId: scope.userId,
11681
- organizationId: scope.organizationId
11682
- });
11900
+ return await listMyPinnedAgents(app.db, { userId: scope.userId });
11683
11901
  });
11684
11902
  app.patch("/:clientId/capabilities", async (request, reply) => {
11685
11903
  const scope = memberScope(request);
@@ -12014,25 +12232,33 @@ async function pushToWebhook(db, notification) {
12014
12232
  });
12015
12233
  } catch {}
12016
12234
  }
12235
+ const orgQuerySchema = z.object({ organizationId: z.string().min(1).optional() });
12017
12236
  async function adminNotificationRoutes(app) {
12018
12237
  /**
12019
- * GET /admin/notifications — list notifications visible to the caller.
12238
+ * GET /admin/notifications — list notifications visible to the caller in
12239
+ * the **selected** organization. The web client passes
12240
+ * `?organizationId=<selectedOrgId>` so a non-default org doesn't reuse
12241
+ * the JWT default org's bell feed (codex P1 #2). Defaults to JWT org
12242
+ * when omitted.
12020
12243
  *
12021
- * Scoped by (a) organization (via JWT) and (b) per-agent visibility: the
12022
- * member only sees notifications whose agentId is visible to them
12023
- * (organization-visible agents or agents they manage), plus org-wide
12024
- * system notifications with no agentId. This mirrors the rule the admin
12025
- * WebSocket route enforces on live pushes — REST and WS stay in sync.
12244
+ * Per-agent visibility filter: the member only sees notifications whose
12245
+ * agentId is visible to them in that org (organization-visible agents or
12246
+ * agents they manage), plus org-wide system notifications with no
12247
+ * agentId. This mirrors the admin-WS push gate so REST and WS stay in sync.
12026
12248
  */
12027
12249
  app.get("/", async (request) => {
12028
- const member = requireMember(request);
12250
+ const scope = memberScope(request);
12251
+ const { organizationId } = orgQuerySchema.parse(request.query);
12252
+ const effective = await resolveAdminScope(app.db, request, scope, organizationId);
12029
12253
  const query = notificationQuerySchema.parse(request.query);
12030
- return listNotifications(app.db, member.organizationId, member.memberId, query);
12254
+ return listNotifications(app.db, effective.organizationId, effective.memberId, query);
12031
12255
  });
12032
12256
  /** POST /admin/notifications/:id/read — mark a single notification as read */
12033
12257
  app.post("/:id/read", async (request) => {
12034
- const member = requireMember(request);
12035
- const result = await markRead(app.db, request.params.id, member.organizationId, member.memberId);
12258
+ const scope = memberScope(request);
12259
+ const { organizationId } = orgQuerySchema.parse(request.query);
12260
+ const effective = await resolveAdminScope(app.db, request, scope, organizationId);
12261
+ const result = await markRead(app.db, request.params.id, effective.organizationId, effective.memberId);
12036
12262
  if (!result) throw new NotFoundError(`Notification "${request.params.id}" not found`);
12037
12263
  return {
12038
12264
  ...result,
@@ -12041,8 +12267,10 @@ async function adminNotificationRoutes(app) {
12041
12267
  });
12042
12268
  /** POST /admin/notifications/read-all — mark all visible notifications as read */
12043
12269
  app.post("/read-all", async (request) => {
12044
- const member = requireMember(request);
12045
- await markAllRead(app.db, member.organizationId, member.memberId);
12270
+ const scope = memberScope(request);
12271
+ const { organizationId } = orgQuerySchema.parse(request.query);
12272
+ const effective = await resolveAdminScope(app.db, request, scope, organizationId);
12273
+ await markAllRead(app.db, effective.organizationId, effective.memberId);
12046
12274
  return { status: "ok" };
12047
12275
  });
12048
12276
  }
@@ -12094,7 +12322,8 @@ async function adminOrganizationRoutes(app) {
12094
12322
  }
12095
12323
  async function adminOverviewRoutes(app) {
12096
12324
  app.get("/", async (request) => {
12097
- const orgParam = (request.query ?? {}).org;
12325
+ const q = request.query ?? {};
12326
+ const orgParam = q.organizationId ?? q.org;
12098
12327
  let orgId;
12099
12328
  if (orgParam) orgId = (await resolveOrganization(app.db, orgParam)).id;
12100
12329
  else orgId = await resolveDefaultOrgId(app.db);
@@ -12409,17 +12638,21 @@ const globalSessionFilterSchema = paginationQuerySchema.extend({
12409
12638
  "suspended",
12410
12639
  "evicted"
12411
12640
  ]).optional(),
12412
- agentId: z.string().optional()
12641
+ agentId: z.string().optional(),
12642
+ organizationId: z.string().min(1).optional()
12413
12643
  });
12414
12644
  async function adminSessionRoutes(app) {
12415
- /** GET /admin/sessions — global session list, scoped to caller's org */
12645
+ /** GET /admin/sessions — global session list, scoped to the selected
12646
+ * org. The web client passes `?organizationId=<selectedOrgId>` after a
12647
+ * /auth/switch-org 204 (codex P1 #2); falls back to the JWT default. */
12416
12648
  app.get("/", async (request) => {
12417
- const member = requireMember(request);
12649
+ const scope = memberScope(request);
12418
12650
  const query = globalSessionFilterSchema.parse(request.query);
12651
+ const effective = await resolveAdminScope(app.db, request, scope, query.organizationId);
12419
12652
  return listAllSessions(app.db, query.limit, query.cursor, {
12420
12653
  state: query.state,
12421
12654
  agentId: query.agentId,
12422
- organizationId: member.organizationId
12655
+ organizationId: effective.organizationId
12423
12656
  });
12424
12657
  });
12425
12658
  /** GET /admin/sessions/agents/:agentId — sessions for a specific agent.
@@ -13113,11 +13346,11 @@ function adminWsRoutes(notifier, jwtSecret) {
13113
13346
  endWsConnectionSpan(socket, 4001);
13114
13347
  return;
13115
13348
  }
13349
+ let userId;
13116
13350
  let organizationId;
13117
- let memberId;
13118
13351
  try {
13119
13352
  const { payload } = await jwtVerify(token, secret);
13120
- if (payload.type !== "access" || !payload.sub || typeof payload.organizationId !== "string" || typeof payload.memberId !== "string") {
13353
+ if (payload.type !== "access" || typeof payload.sub !== "string" || typeof payload.organizationId !== "string") {
13121
13354
  socket.send(JSON.stringify({
13122
13355
  type: "error",
13123
13356
  message: "Invalid token type"
@@ -13126,8 +13359,8 @@ function adminWsRoutes(notifier, jwtSecret) {
13126
13359
  endWsConnectionSpan(socket, 4001);
13127
13360
  return;
13128
13361
  }
13362
+ userId = payload.sub;
13129
13363
  organizationId = payload.organizationId;
13130
- memberId = payload.memberId;
13131
13364
  } catch {
13132
13365
  socket.send(JSON.stringify({
13133
13366
  type: "error",
@@ -13137,6 +13370,20 @@ function adminWsRoutes(notifier, jwtSecret) {
13137
13370
  endWsConnectionSpan(socket, 4001);
13138
13371
  return;
13139
13372
  }
13373
+ const [memberRow] = await app.db.select({
13374
+ id: members.id,
13375
+ role: members.role
13376
+ }).from(members).where(and(eq(members.userId, userId), eq(members.organizationId, organizationId), eq(members.status, "active"))).limit(1);
13377
+ if (!memberRow) {
13378
+ socket.send(JSON.stringify({
13379
+ type: "error",
13380
+ message: "Not an active member of this organization"
13381
+ }));
13382
+ socket.close(4403, "Not a member");
13383
+ endWsConnectionSpan(socket, 4403);
13384
+ return;
13385
+ }
13386
+ const memberId = memberRow.id;
13140
13387
  setWsConnectionAttrs(socket, {
13141
13388
  organizationId,
13142
13389
  memberId
@@ -13965,6 +14212,7 @@ function clientWsRoutes(notifier, instanceId) {
13965
14212
  }, async (socket) => {
13966
14213
  startWsConnectionSpan(socket);
13967
14214
  let session = null;
14215
+ let jwtDefaultOrgId = null;
13968
14216
  let clientId = null;
13969
14217
  let authExpiryTimer = null;
13970
14218
  const boundAgents = /* @__PURE__ */ new Map();
@@ -14195,28 +14443,15 @@ function clientWsRoutes(notifier, instanceId) {
14195
14443
  try {
14196
14444
  const { payload } = await jwtVerify(authParsed.data.token, jwtSecretBytes);
14197
14445
  const claims = payload;
14198
- if (claims.type !== "access" || !claims.sub || !claims.memberId) throw new Error("Invalid token claims");
14446
+ if (claims.type !== "access" || !claims.sub) throw new Error("Invalid token claims");
14199
14447
  const [user] = await app.db.select({
14200
14448
  id: users.id,
14201
14449
  status: users.status
14202
14450
  }).from(users).where(eq(users.id, claims.sub)).limit(1);
14203
14451
  if (!user || user.status !== "active") throw new Error("User not found or suspended");
14204
- const [member] = await app.db.select({
14205
- id: members.id,
14206
- organizationId: members.organizationId,
14207
- role: members.role
14208
- }).from(members).where(eq(members.id, claims.memberId)).limit(1);
14209
- if (!member) throw new Error("Membership not found");
14210
- session = {
14211
- userId: user.id,
14212
- memberId: member.id,
14213
- organizationId: member.organizationId,
14214
- role: member.role
14215
- };
14216
- setWsConnectionAttrs(socket, {
14217
- "organization.id": member.organizationId,
14218
- "member.id": member.id
14219
- });
14452
+ session = { userId: user.id };
14453
+ jwtDefaultOrgId = typeof claims.organizationId === "string" ? claims.organizationId : null;
14454
+ setWsConnectionAttrs(socket, { "user.id": user.id });
14220
14455
  clearTimeout(authTimeout);
14221
14456
  scheduleAuthExpiry(claims.exp);
14222
14457
  socket.send(JSON.stringify({ type: "auth:ok" }));
@@ -14242,11 +14477,19 @@ function clientWsRoutes(notifier, instanceId) {
14242
14477
  if (type === "client:register") {
14243
14478
  const data = clientRegisterSchema.parse(msg);
14244
14479
  clientWantsWsInboxDeliver = data.wireCapabilities?.wsInboxDeliver === true;
14480
+ if (!jwtDefaultOrgId) {
14481
+ socket.send(JSON.stringify({
14482
+ type: "client:register:rejected",
14483
+ message: "JWT missing organizationId claim"
14484
+ }));
14485
+ socket.close(4401, "client register rejected");
14486
+ return;
14487
+ }
14245
14488
  try {
14246
14489
  await registerClient(app.db, {
14247
14490
  clientId: data.clientId,
14248
14491
  userId: session.userId,
14249
- organizationId: session.organizationId,
14492
+ organizationId: jwtDefaultOrgId,
14250
14493
  instanceId,
14251
14494
  hostname: data.hostname,
14252
14495
  os: data.os,
@@ -14254,7 +14497,7 @@ function clientWsRoutes(notifier, instanceId) {
14254
14497
  });
14255
14498
  } catch (err) {
14256
14499
  const message = err instanceof Error ? err.message : "client register failed";
14257
- const code = err instanceof ClientOrgMismatchError$1 ? err.code : void 0;
14500
+ const code = err instanceof ClientUserMismatchError$1 ? err.code : err instanceof ClientOrgMismatchError$1 ? err.code : void 0;
14258
14501
  socket.send(JSON.stringify({
14259
14502
  type: "client:register:rejected",
14260
14503
  message,
@@ -14317,25 +14560,24 @@ function clientWsRoutes(notifier, instanceId) {
14317
14560
  clientId: agents.clientId,
14318
14561
  runtimeProvider: agents.runtimeProvider,
14319
14562
  clientUserId: clients.userId,
14320
- managerUserId: members.userId
14563
+ managerUserId: members.userId,
14564
+ managerMemberStatus: members.status
14321
14565
  }).from(agents).leftJoin(clients, eq(agents.clientId, clients.id)).leftJoin(members, eq(agents.managerId, members.id)).where(and(eq(agents.uuid, bindRequest.agentId))).limit(1);
14322
14566
  if (!agent) {
14323
14567
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
14324
14568
  return;
14325
14569
  }
14326
- if (agent.organizationId !== session.organizationId) {
14327
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_ORG);
14328
- return;
14329
- }
14330
14570
  if (agent.status !== "active") {
14331
14571
  sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
14332
14572
  return;
14333
14573
  }
14574
+ const ownerOk = agent.managerUserId !== null && agent.managerUserId === session.userId;
14575
+ const membershipActive = agent.managerMemberStatus === "active";
14576
+ if (!ownerOk || !membershipActive) {
14577
+ sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
14578
+ return;
14579
+ }
14334
14580
  if (agent.clientId === null) {
14335
- if (!agent.managerUserId || agent.managerUserId !== session.userId) {
14336
- sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
14337
- return;
14338
- }
14339
14581
  if ((await app.db.update(agents).set({
14340
14582
  clientId,
14341
14583
  updatedAt: /* @__PURE__ */ new Date()
@@ -14363,7 +14605,8 @@ function clientWsRoutes(notifier, instanceId) {
14363
14605
  bindAgentToClient(clientId, agent.id);
14364
14606
  boundAgents.set(agent.id, {
14365
14607
  agentId: agent.id,
14366
- inboxId: agent.inboxId
14608
+ inboxId: agent.inboxId,
14609
+ organizationId: agent.organizationId
14367
14610
  });
14368
14611
  const wsPushActive = pushUseWsDataPlane();
14369
14612
  if (wsPushActive) notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
@@ -14423,7 +14666,9 @@ function clientWsRoutes(notifier, instanceId) {
14423
14666
  }, "session:state rejected — stale client wire");
14424
14667
  return;
14425
14668
  }
14426
- await upsertSessionState(app.db, agentId, payloadResult.data.chatId, payloadResult.data.state, session.organizationId, notifier);
14669
+ const boundAgentInfo = boundAgents.get(agentId);
14670
+ if (!boundAgentInfo) return;
14671
+ await upsertSessionState(app.db, agentId, payloadResult.data.chatId, payloadResult.data.state, boundAgentInfo.organizationId, notifier);
14427
14672
  } else if (type === "session:reconcile") {
14428
14673
  const agentId = parsed.data.agentId;
14429
14674
  if (!agentId || !boundAgents.has(agentId)) {
@@ -14460,8 +14705,10 @@ function clientWsRoutes(notifier, instanceId) {
14460
14705
  return;
14461
14706
  }
14462
14707
  const payload = runtimeStateMessageSchema.parse(msg);
14708
+ const boundAgentInfo = boundAgents.get(agentId);
14709
+ if (!boundAgentInfo) return;
14463
14710
  await setRuntimeState(app.db, agentId, payload.runtimeState, {
14464
- organizationId: session.organizationId,
14711
+ organizationId: boundAgentInfo.organizationId,
14465
14712
  notifier
14466
14713
  });
14467
14714
  if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high").catch(() => {});
@@ -14983,6 +15230,7 @@ async function listActiveMemberships(db, userId) {
14983
15230
  memberId: members.id,
14984
15231
  organizationId: members.organizationId,
14985
15232
  role: members.role,
15233
+ agentId: members.agentId,
14986
15234
  orgName: organizations.name,
14987
15235
  orgDisplayName: organizations.displayName,
14988
15236
  createdAt: members.createdAt
@@ -15324,6 +15572,36 @@ async function bootstrapConfigRoutes(app) {
15324
15572
  return { allowedOrg: app.config.github.allowedOrg ?? null };
15325
15573
  });
15326
15574
  }
15575
+ /**
15576
+ * Member-scoped client routes.
15577
+ *
15578
+ * Mounted at `/me/clients` to keep them off the admin surface (the legacy
15579
+ * admin clients router lives at `/clients`). The only operation here is
15580
+ * `POST /:clientId/claim`: ownership transfer driven by the operator running
15581
+ * `first-tree-hub client claim --confirm` after a 4403 handshake mismatch
15582
+ * (decouple-client-from-identity §4.4).
15583
+ */
15584
+ async function memberClientRoutes(app) {
15585
+ app.post("/:clientId/claim", async (request, reply) => {
15586
+ const m = requireMember(request);
15587
+ const { clientId } = request.params;
15588
+ const result = await claimClient(app.db, clientId, m.userId);
15589
+ const droppedAgentIds = forceDisconnectClient(clientId);
15590
+ request.log.info({
15591
+ event: "client.owner_transfer",
15592
+ clientId,
15593
+ fromUserId: result.previousUserId,
15594
+ toUserId: m.userId,
15595
+ unpinnedAgentCount: result.unpinnedAgentIds.length,
15596
+ droppedSocketAgentCount: droppedAgentIds.length
15597
+ }, "client ownership transferred via /me/clients/:clientId/claim");
15598
+ return reply.status(200).send({
15599
+ clientId,
15600
+ previousUserId: result.previousUserId,
15601
+ unpinnedAgentCount: result.unpinnedAgentIds.length
15602
+ });
15603
+ });
15604
+ }
15327
15605
  async function contextTreeInfoRoutes(app) {
15328
15606
  /** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
15329
15607
  app.get("/info", async () => {
@@ -15453,6 +15731,7 @@ async function meRoutes(app) {
15453
15731
  const inv = await getActiveInvitation(app.db, m.organizationId);
15454
15732
  if (inv) inviteUrl = buildInviteUrl(resolvePublicUrl(app, request), inv.token);
15455
15733
  }
15734
+ const memberships = await listActiveMemberships(app.db, m.userId);
15456
15735
  return {
15457
15736
  user: user ?? null,
15458
15737
  member: {
@@ -15461,6 +15740,13 @@ async function meRoutes(app) {
15461
15740
  role: m.role,
15462
15741
  agentId: m.agentId
15463
15742
  },
15743
+ memberships: memberships.map((mb) => ({
15744
+ id: mb.memberId,
15745
+ organizationId: mb.organizationId,
15746
+ organizationName: mb.orgDisplayName,
15747
+ role: mb.role,
15748
+ agentId: mb.agentId
15749
+ })),
15464
15750
  agent: agent ?? null,
15465
15751
  wizard: { step: wizardStep },
15466
15752
  inviteUrl
@@ -15494,6 +15780,20 @@ async function meRoutes(app) {
15494
15780
  command: `first-tree-hub connect ${token}`
15495
15781
  };
15496
15782
  });
15783
+ app.get("/me/managed-agents", async (request) => {
15784
+ const m = requireMember(request);
15785
+ return (await listAgentsManagedByUser(app.db, m.userId)).map((r) => ({
15786
+ uuid: r.uuid,
15787
+ name: r.name,
15788
+ displayName: r.displayName,
15789
+ type: r.type,
15790
+ organizationId: r.organizationId,
15791
+ inboxId: r.inboxId,
15792
+ visibility: r.visibility,
15793
+ runtimeProvider: r.runtimeProvider,
15794
+ clientId: r.clientId
15795
+ }));
15796
+ });
15497
15797
  app.get("/me/organizations", async (request) => {
15498
15798
  const m = requireMember(request);
15499
15799
  return (await listActiveMemberships(app.db, m.userId)).map((r) => ({
@@ -15581,15 +15881,9 @@ async function meRoutes(app) {
15581
15881
  app.post("/auth/switch-org", async (request, reply) => {
15582
15882
  const m = requireMember(request);
15583
15883
  const body = switchOrgSchema.parse(request.body);
15584
- const [target] = await app.db.select().from(members).where(and(eq(members.userId, m.userId), eq(members.organizationId, body.organizationId), eq(members.status, "active"))).limit(1);
15884
+ const [target] = await app.db.select({ id: members.id }).from(members).where(and(eq(members.userId, m.userId), eq(members.organizationId, body.organizationId), eq(members.status, "active"))).limit(1);
15585
15885
  if (!target) throw new ForbiddenError("You do not belong to that organization");
15586
- const tokens = await signTokensForMember(app.config.secrets.jwtSecret, {
15587
- userId: m.userId,
15588
- memberId: target.id,
15589
- organizationId: target.organizationId,
15590
- role: target.role
15591
- });
15592
- return reply.send(tokens);
15886
+ return reply.status(204).send();
15593
15887
  });
15594
15888
  }
15595
15889
  /**
@@ -15605,7 +15899,7 @@ async function meRoutes(app) {
15605
15899
  * that's the explicit reset path.
15606
15900
  */
15607
15901
  async function inferWizardStep(app, m) {
15608
- const [hasClient] = await app.db.select({ id: clients.id }).from(clients).where(and(eq(clients.userId, m.userId), eq(clients.organizationId, m.organizationId))).limit(1);
15902
+ const [hasClient] = await app.db.select({ id: clients.id }).from(clients).where(eq(clients.userId, m.userId)).limit(1);
15609
15903
  if (!hasClient) return "connect";
15610
15904
  const [hasAgent] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.managerId, m.memberId), ne(agents.type, "human"), eq(agents.status, "active"))).limit(1);
15611
15905
  if (!hasAgent) return "create_agent";
@@ -15617,7 +15911,7 @@ async function inferWizardStep(app, m) {
15617
15911
  * landing page.
15618
15912
  */
15619
15913
  async function publicInvitePreviewRoute(app) {
15620
- const { previewInvitation } = await import("./invitation-D3feYxet-366MNOor.mjs");
15914
+ const { previewInvitation } = await import("./invitation-CBnQyB7o-DVOpS_Ts.mjs");
15621
15915
  app.get("/:token/preview", async (request, reply) => {
15622
15916
  if (!request.params.token) throw new UnauthorizedError("Token required");
15623
15917
  const preview = await previewInvitation(app.db, request.params.token);
@@ -15647,7 +15941,7 @@ async function adminInvitationRoutes(app) {
15647
15941
  const m = requireMember(request);
15648
15942
  if (m.role !== "admin") throw new ForbiddenError("Admin role required");
15649
15943
  if (request.params.id !== m.organizationId) throw new ForbiddenError("Cannot rotate invitations for another organization");
15650
- const { rotateInvitation } = await import("./invitation-D3feYxet-366MNOor.mjs");
15944
+ const { rotateInvitation } = await import("./invitation-CBnQyB7o-DVOpS_Ts.mjs");
15651
15945
  const inv = await rotateInvitation(app.db, m.organizationId, m.userId);
15652
15946
  return {
15653
15947
  id: inv.id,
@@ -17629,6 +17923,10 @@ async function buildApp(config) {
17629
17923
  adminApp.addHook("onRequest", memberAuth);
17630
17924
  await adminApp.register(adminClientRoutes);
17631
17925
  }, { prefix: "/clients" });
17926
+ await api.register(async (memberApp) => {
17927
+ memberApp.addHook("onRequest", memberAuth);
17928
+ await memberApp.register(memberClientRoutes);
17929
+ }, { prefix: "/me/clients" });
17632
17930
  await api.register(async (adminApp) => {
17633
17931
  adminApp.addHook("onRequest", memberAuth);
17634
17932
  await adminApp.register(adminActivityRoutes);
@@ -18262,4 +18560,4 @@ function registerSaaSConnectCommand(program) {
18262
18560
  });
18263
18561
  }
18264
18562
  //#endregion
18265
- export { status as $, checkServerHealth as A, isDockerAvailable as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, installClientService as F, createOwner as G, ClientRuntime as H, isServiceSupported as I, fail as J, hasUser as K, resolveCliInvocation as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, getClientServiceStatus as P, setJsonMode as Q, uninstallClientService as R, runMigrations as S, checkClientConfig as T, handleClientOrgMismatch as U, stopPostgres as V, rotateClientIdWithBackup as W, blank as X, success as Y, print as Z, onboardCreate as _, declineUpdate as a, probeCapabilities as at, createApiNameResolver as b, COMMAND_VERSION as c, isInteractive as d, ClientOrgMismatchError as et, promptAddAgent as f, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, cleanWorkspaces as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, formatCheckReport as m, deriveHubUrlFromToken as n, SdkError as nt, promptUpdate as o, applyClientLoggerConfig as ot, promptMissingFields as p, resolveReplyToFromEnv as q, registerSaaSConnectCommand as r, SessionRegistry as rt, startServer as s, configureClientLoggerForService as st, HubUrlDerivationError as t, FirstTreeHubSDK as tt, uploadClientCapabilities as u, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, ensurePostgres as z };
18563
+ export { status as $, checkServerHealth as A, isDockerAvailable as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, installClientService as F, createOwner as G, ClientRuntime as H, isServiceSupported as I, fail as J, hasUser as K, resolveCliInvocation as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, getClientServiceStatus as P, setJsonMode as Q, uninstallClientService as R, runMigrations as S, checkClientConfig as T, handleClientOrgMismatch as U, stopPostgres as V, rotateClientIdWithBackup as W, blank as X, success as Y, print as Z, onboardCreate as _, declineUpdate as a, cleanWorkspaces as at, createApiNameResolver as b, COMMAND_VERSION as c, configureClientLoggerForService as ct, isInteractive as d, ClientOrgMismatchError as et, promptAddAgent as f, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, SessionRegistry as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, formatCheckReport as m, deriveHubUrlFromToken as n, FirstTreeHubSDK as nt, promptUpdate as o, probeCapabilities as ot, promptMissingFields as p, resolveReplyToFromEnv as q, registerSaaSConnectCommand as r, SdkError as rt, startServer as s, applyClientLoggerConfig as st, HubUrlDerivationError as t, ClientUserMismatchError as tt, uploadClientCapabilities as u, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, ensurePostgres as z };