@agent-team-foundation/first-tree-hub 0.10.7 → 0.10.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.
- package/dist/cli/index.mjs +233 -27
- package/dist/{dist-CbX9mUVH.mjs → dist-DSr_I5Ia.mjs} +12 -0
- package/dist/{feishu-DvjRZMdZ.mjs → feishu-eynC54km.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-BljIolbO-DLeHfURd.mjs → invitation-B1pjAyOz-BaCA9PII.mjs} +20 -6
- package/dist/invitation-CBnQyB7o-DVOpS_Ts.mjs +3 -0
- package/dist/{saas-connect-vLyx73kJ.mjs → saas-connect-CKQ15VLz.mjs} +664 -173
- package/dist/web/assets/{index-CKiF80RI.js → index-B33n1w2k.js} +86 -86
- package/dist/web/assets/{index-BADzOlHO.js → index-DIgiOalZ.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/invitation-D3feYxet-366MNOor.mjs +0 -3
|
@@ -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-
|
|
5
|
-
import { a as
|
|
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";
|
|
@@ -19,11 +19,11 @@ import { execFileSync, execSync, spawn, spawnSync } from "node:child_process";
|
|
|
19
19
|
import { Codex } from "@openai/codex-sdk";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import * as semver from "semver";
|
|
22
|
+
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
22
23
|
import bcrypt from "bcrypt";
|
|
23
24
|
import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, or, sql } from "drizzle-orm";
|
|
24
25
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
25
26
|
import postgres from "postgres";
|
|
26
|
-
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
27
27
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
28
28
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
29
29
|
import cors from "@fastify/cors";
|
|
@@ -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.
|
|
1765
|
-
*
|
|
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,
|
|
@@ -5169,6 +5196,12 @@ var AgentSlot = class {
|
|
|
5169
5196
|
agentId: config.agentId
|
|
5170
5197
|
});
|
|
5171
5198
|
}
|
|
5199
|
+
get name() {
|
|
5200
|
+
return this.config.name;
|
|
5201
|
+
}
|
|
5202
|
+
get agentId() {
|
|
5203
|
+
return this.config.agentId;
|
|
5204
|
+
}
|
|
5172
5205
|
get clientConnection() {
|
|
5173
5206
|
return this.config.clientConnection;
|
|
5174
5207
|
}
|
|
@@ -5971,6 +6004,130 @@ function makeUuidV7() {
|
|
|
5971
6004
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
5972
6005
|
}
|
|
5973
6006
|
//#endregion
|
|
6007
|
+
//#region src/core/agent-prune.ts
|
|
6008
|
+
const minimalAgentYamlSchema = z.object({ agentId: z.string().min(1) }).passthrough();
|
|
6009
|
+
/**
|
|
6010
|
+
* Cross-reference local `agents/<name>/agent.yaml` files against the
|
|
6011
|
+
* server's pinned-agent set, returning every alias that won't bind on
|
|
6012
|
+
* THIS client.
|
|
6013
|
+
*
|
|
6014
|
+
* Why we don't use `loadAgents`:
|
|
6015
|
+
* `shared/config/loader.loadAgents` is fail-fast — one malformed
|
|
6016
|
+
* agent.yaml throws and the whole scan dies. The dominant prune target
|
|
6017
|
+
* IS the malformed dir (typo `agent add d`, half-written yaml, missing
|
|
6018
|
+
* agentId), so we walk dirs ourselves and degrade per-entry instead.
|
|
6019
|
+
*
|
|
6020
|
+
* Why we filter by clientId, not just userId:
|
|
6021
|
+
* `listPinnedAgents` (`/api/v1/clients/me/agents`) returns every agent
|
|
6022
|
+
* pinned to ANY client this user owns (cross-machine). For prune the
|
|
6023
|
+
* relevant question is "will R-RUN accept it on THIS machine", which
|
|
6024
|
+
* needs `agents.client_id === current client.id`. Anything pinned on
|
|
6025
|
+
* another client is reported with `pinned-elsewhere` so the operator
|
|
6026
|
+
* can either re-pin or delete the local alias deliberately.
|
|
6027
|
+
*/
|
|
6028
|
+
async function findStaleAliases(opts) {
|
|
6029
|
+
const agentsDir = opts.agentsDir ?? join(DEFAULT_CONFIG_DIR, "agents");
|
|
6030
|
+
if (!existsSync(agentsDir)) return [];
|
|
6031
|
+
const remote = await opts.listPinnedAgents();
|
|
6032
|
+
const pinnedHere = /* @__PURE__ */ new Set();
|
|
6033
|
+
const pinnedElsewhere = /* @__PURE__ */ new Map();
|
|
6034
|
+
for (const r of remote) if (r.clientId === opts.clientId) pinnedHere.add(r.agentId);
|
|
6035
|
+
else pinnedElsewhere.set(r.agentId, r.clientId);
|
|
6036
|
+
const stale = [];
|
|
6037
|
+
for (const entry of readdirSync(agentsDir)) {
|
|
6038
|
+
const agentDir = join(agentsDir, entry);
|
|
6039
|
+
let isDir = false;
|
|
6040
|
+
try {
|
|
6041
|
+
isDir = statSync(agentDir).isDirectory();
|
|
6042
|
+
} catch {
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
if (!isDir) continue;
|
|
6046
|
+
const yamlPath = join(agentDir, "agent.yaml");
|
|
6047
|
+
if (!existsSync(yamlPath)) {
|
|
6048
|
+
stale.push({
|
|
6049
|
+
name: entry,
|
|
6050
|
+
agentId: null,
|
|
6051
|
+
reason: {
|
|
6052
|
+
kind: "unreadable",
|
|
6053
|
+
error: "missing agent.yaml"
|
|
6054
|
+
}
|
|
6055
|
+
});
|
|
6056
|
+
continue;
|
|
6057
|
+
}
|
|
6058
|
+
let agentId;
|
|
6059
|
+
try {
|
|
6060
|
+
const raw = parse(readFileSync(yamlPath, "utf-8"));
|
|
6061
|
+
const parsed = minimalAgentYamlSchema.safeParse(raw);
|
|
6062
|
+
if (!parsed.success) {
|
|
6063
|
+
const issue = parsed.error.issues[0]?.message ?? "schema error";
|
|
6064
|
+
stale.push({
|
|
6065
|
+
name: entry,
|
|
6066
|
+
agentId: null,
|
|
6067
|
+
reason: {
|
|
6068
|
+
kind: "unreadable",
|
|
6069
|
+
error: issue
|
|
6070
|
+
}
|
|
6071
|
+
});
|
|
6072
|
+
continue;
|
|
6073
|
+
}
|
|
6074
|
+
agentId = parsed.data.agentId;
|
|
6075
|
+
} catch (err) {
|
|
6076
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6077
|
+
stale.push({
|
|
6078
|
+
name: entry,
|
|
6079
|
+
agentId: null,
|
|
6080
|
+
reason: {
|
|
6081
|
+
kind: "unreadable",
|
|
6082
|
+
error: msg
|
|
6083
|
+
}
|
|
6084
|
+
});
|
|
6085
|
+
continue;
|
|
6086
|
+
}
|
|
6087
|
+
if (pinnedHere.has(agentId)) continue;
|
|
6088
|
+
const otherClient = pinnedElsewhere.get(agentId);
|
|
6089
|
+
if (otherClient !== void 0) stale.push({
|
|
6090
|
+
name: entry,
|
|
6091
|
+
agentId,
|
|
6092
|
+
reason: {
|
|
6093
|
+
kind: "pinned-elsewhere",
|
|
6094
|
+
clientId: otherClient
|
|
6095
|
+
}
|
|
6096
|
+
});
|
|
6097
|
+
else stale.push({
|
|
6098
|
+
name: entry,
|
|
6099
|
+
agentId,
|
|
6100
|
+
reason: { kind: "unowned" }
|
|
6101
|
+
});
|
|
6102
|
+
}
|
|
6103
|
+
return stale;
|
|
6104
|
+
}
|
|
6105
|
+
/** Human-readable suffix for the per-alias listing. */
|
|
6106
|
+
function formatStaleReason(reason) {
|
|
6107
|
+
switch (reason.kind) {
|
|
6108
|
+
case "unreadable": return `unreadable: ${reason.error}`;
|
|
6109
|
+
case "unowned": return "no longer owned by you (deleted or transferred)";
|
|
6110
|
+
case "pinned-elsewhere": return `pinned to another client: ${reason.clientId}`;
|
|
6111
|
+
}
|
|
6112
|
+
}
|
|
6113
|
+
/**
|
|
6114
|
+
* Remove an agent's local footprint: the YAML alias dir, the workspace
|
|
6115
|
+
* tree under `data/workspaces/<name>`, and the session-mapping file under
|
|
6116
|
+
* `data/sessions/<name>.json`. Mirrors what `agent remove` does, exposed
|
|
6117
|
+
* separately so prune and claim can share it.
|
|
6118
|
+
*/
|
|
6119
|
+
function removeLocalAgent(name) {
|
|
6120
|
+
rmSync(join(DEFAULT_CONFIG_DIR, "agents", name), {
|
|
6121
|
+
recursive: true,
|
|
6122
|
+
force: true
|
|
6123
|
+
});
|
|
6124
|
+
rmSync(join(DEFAULT_DATA_DIR$1, "workspaces", name), {
|
|
6125
|
+
recursive: true,
|
|
6126
|
+
force: true
|
|
6127
|
+
});
|
|
6128
|
+
rmSync(join(DEFAULT_DATA_DIR$1, "sessions", `${name}.json`), { force: true });
|
|
6129
|
+
}
|
|
6130
|
+
//#endregion
|
|
5974
6131
|
//#region src/core/client-reidentify.ts
|
|
5975
6132
|
/**
|
|
5976
6133
|
* Handle a `CLIENT_ORG_MISMATCH` from the server by rotating the local
|
|
@@ -7063,6 +7220,69 @@ function checkAgentConfigs() {
|
|
|
7063
7220
|
};
|
|
7064
7221
|
}
|
|
7065
7222
|
}
|
|
7223
|
+
/**
|
|
7224
|
+
* Server-aware agent reconciliation. Walks `agents/<name>/agent.yaml` and
|
|
7225
|
+
* cross-references each `agentId` with `/api/v1/clients/me/agents`,
|
|
7226
|
+
* filtering by `clientId` so the "stale" verdict matches what R-RUN will
|
|
7227
|
+
* actually accept on this machine.
|
|
7228
|
+
*
|
|
7229
|
+
* Categorises each local alias into:
|
|
7230
|
+
* - pinned — bind would succeed on this client.
|
|
7231
|
+
* - pinned-elsewhere — owned by you, but pinned to a different client
|
|
7232
|
+
* (alias is dead weight here; real agent is alive
|
|
7233
|
+
* on the other machine).
|
|
7234
|
+
* - unowned — agentId not in the server's response at all.
|
|
7235
|
+
* - unreadable — yaml missing/malformed/no agentId.
|
|
7236
|
+
*
|
|
7237
|
+
* The plain `checkAgentConfigs` (sync, local-only) is retained for
|
|
7238
|
+
* back-compat with external consumers but its "N configured" wording is
|
|
7239
|
+
* misleading because stale aliases never bind at runtime.
|
|
7240
|
+
*
|
|
7241
|
+
* Skipped reconciliation (server unreachable / unauthenticated) returns
|
|
7242
|
+
* `ok: false` — doctor's other server-touching checks already report
|
|
7243
|
+
* connectivity loss as a failure, and silently passing here would hide
|
|
7244
|
+
* the very issue the operator is running doctor to diagnose.
|
|
7245
|
+
*/
|
|
7246
|
+
async function reconcileAgentConfigs(opts) {
|
|
7247
|
+
const agentsDir = opts.agentsDir ?? join(DEFAULT_CONFIG_DIR, "agents");
|
|
7248
|
+
let localCount = 0;
|
|
7249
|
+
if (existsSync(agentsDir)) for (const entry of readdirSync(agentsDir)) try {
|
|
7250
|
+
if (statSync(join(agentsDir, entry)).isDirectory()) localCount++;
|
|
7251
|
+
} catch {}
|
|
7252
|
+
if (localCount === 0) return {
|
|
7253
|
+
label: "Agents",
|
|
7254
|
+
ok: false,
|
|
7255
|
+
detail: "no agents configured"
|
|
7256
|
+
};
|
|
7257
|
+
let stale;
|
|
7258
|
+
try {
|
|
7259
|
+
stale = await findStaleAliases({
|
|
7260
|
+
clientId: opts.clientId,
|
|
7261
|
+
listPinnedAgents: opts.listPinnedAgents,
|
|
7262
|
+
agentsDir
|
|
7263
|
+
});
|
|
7264
|
+
} catch (err) {
|
|
7265
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7266
|
+
return {
|
|
7267
|
+
label: "Agents",
|
|
7268
|
+
ok: false,
|
|
7269
|
+
detail: `${localCount} configured locally — server reconciliation failed (${msg.slice(0, 60)})`
|
|
7270
|
+
};
|
|
7271
|
+
}
|
|
7272
|
+
const pinnedCount = localCount - stale.length;
|
|
7273
|
+
if (stale.length === 0) return {
|
|
7274
|
+
label: "Agents",
|
|
7275
|
+
ok: true,
|
|
7276
|
+
detail: `${localCount} configured, all pinned to this client`
|
|
7277
|
+
};
|
|
7278
|
+
const staleSummary = stale.map((s) => `${s.name} [${formatStaleReason(s.reason)}]`).slice(0, 5).join("; ");
|
|
7279
|
+
const truncated = stale.length > 5 ? `; ...+${stale.length - 5} more` : "";
|
|
7280
|
+
return {
|
|
7281
|
+
label: "Agents",
|
|
7282
|
+
ok: false,
|
|
7283
|
+
detail: `${localCount} configured locally, ${pinnedCount} pinned to this client; ${stale.length} stale: ${staleSummary}${truncated} — run \`first-tree-hub agent prune\` to clean up`
|
|
7284
|
+
};
|
|
7285
|
+
}
|
|
7066
7286
|
function checkBackgroundService() {
|
|
7067
7287
|
const info = getClientServiceStatus();
|
|
7068
7288
|
if (info.platform === "unsupported") return {
|
|
@@ -7556,7 +7776,7 @@ async function onboardCreate(args) {
|
|
|
7556
7776
|
}
|
|
7557
7777
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
7558
7778
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
7559
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
7779
|
+
const { bindFeishuBot } = await import("./feishu-eynC54km.mjs").then((n) => n.r);
|
|
7560
7780
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
7561
7781
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
7562
7782
|
else {
|
|
@@ -8536,7 +8756,7 @@ function createFeedbackHandler(config) {
|
|
|
8536
8756
|
return { handle };
|
|
8537
8757
|
}
|
|
8538
8758
|
//#endregion
|
|
8539
|
-
//#region ../server/dist/app-
|
|
8759
|
+
//#region ../server/dist/app-XRPDwwkj.mjs
|
|
8540
8760
|
var __defProp = Object.defineProperty;
|
|
8541
8761
|
var __exportAll = (all, no_symbols) => {
|
|
8542
8762
|
let target = {};
|
|
@@ -8631,6 +8851,20 @@ const chatParticipants = pgTable("chat_participants", {
|
|
|
8631
8851
|
mode: text("mode").notNull().default("full"),
|
|
8632
8852
|
joinedAt: timestamp("joined_at", { withTimezone: true }).notNull().defaultNow()
|
|
8633
8853
|
}, (table) => [primaryKey({ columns: [table.chatId, table.agentId] }), index("idx_participants_agent").on(table.agentId)]);
|
|
8854
|
+
/** Organization membership. Links a user to an org with a role and a 1:1 human agent. */
|
|
8855
|
+
const members = pgTable("members", {
|
|
8856
|
+
id: text("id").primaryKey(),
|
|
8857
|
+
userId: text("user_id").notNull().references(() => users.id),
|
|
8858
|
+
organizationId: text("organization_id").notNull().references(() => organizations.id),
|
|
8859
|
+
agentId: text("agent_id").unique().notNull().references(() => agents.uuid),
|
|
8860
|
+
role: text("role").notNull(),
|
|
8861
|
+
status: text("status").notNull().default("active"),
|
|
8862
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
8863
|
+
}, (table) => [
|
|
8864
|
+
unique("uq_members_user_org").on(table.userId, table.organizationId),
|
|
8865
|
+
index("idx_members_user").on(table.userId),
|
|
8866
|
+
index("idx_members_org").on(table.organizationId)
|
|
8867
|
+
]);
|
|
8634
8868
|
function requireAgent(request) {
|
|
8635
8869
|
const agent = request.agent;
|
|
8636
8870
|
if (!agent) throw new UnauthorizedError("Agent authentication required");
|
|
@@ -8675,18 +8909,41 @@ function memberScope(request) {
|
|
|
8675
8909
|
/**
|
|
8676
8910
|
* SQL WHERE conditions for agents visible to a member.
|
|
8677
8911
|
* Visibility is the same for all roles:
|
|
8678
|
-
*
|
|
8912
|
+
* target org + not deleted + (organization-visible OR managerId = caller's member)
|
|
8913
|
+
*
|
|
8914
|
+
* Takes explicit `orgId` + `memberId` rather than reading them from
|
|
8915
|
+
* MemberScope: an admin viewing a non-default org passes
|
|
8916
|
+
* `requireMemberInOrg(db, request, orgId).memberId` to derive the right
|
|
8917
|
+
* memberId for that org (decouple-client-from-identity §4.5.1).
|
|
8679
8918
|
*/
|
|
8680
|
-
function agentVisibilityCondition(
|
|
8681
|
-
return and(eq(agents.organizationId,
|
|
8919
|
+
function agentVisibilityCondition(orgId, memberId) {
|
|
8920
|
+
return and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED), or(eq(agents.visibility, AGENT_VISIBILITY.ORGANIZATION), eq(agents.managerId, memberId)));
|
|
8682
8921
|
}
|
|
8683
8922
|
/**
|
|
8684
|
-
* Assert a single agent is visible to the
|
|
8685
|
-
*
|
|
8686
|
-
*
|
|
8923
|
+
* Assert a single agent is visible to the caller.
|
|
8924
|
+
*
|
|
8925
|
+
* The agent is identified by UUID, so its organization is *intrinsic to the
|
|
8926
|
+
* row itself* — we don't gate on the JWT default org. Instead we resolve the
|
|
8927
|
+
* caller's active membership in `agent.organizationId` and reuse the same
|
|
8928
|
+
* visibility rule (organization-visible OR managerId = caller's member in
|
|
8929
|
+
* that org). This lets a multi-org user hit `/admin/agents/:uuid` for an
|
|
8930
|
+
* agent in a non-default org without re-issuing the JWT — `/auth/switch-org`
|
|
8931
|
+
* now returns 204 only and the web client carries the selected org in
|
|
8932
|
+
* localStorage (decouple-client-from-identity §C.2 / §D fix).
|
|
8933
|
+
*
|
|
8934
|
+
* Returns 404 for both "not found" and "not visible" to prevent UUID
|
|
8935
|
+
* enumeration.
|
|
8687
8936
|
*/
|
|
8688
8937
|
async function assertAgentVisible(db, scope, agentUuid) {
|
|
8689
|
-
const [
|
|
8938
|
+
const [agent] = await db.select({
|
|
8939
|
+
uuid: agents.uuid,
|
|
8940
|
+
organizationId: agents.organizationId,
|
|
8941
|
+
status: agents.status
|
|
8942
|
+
}).from(agents).where(eq(agents.uuid, agentUuid)).limit(1);
|
|
8943
|
+
if (!agent || agent.status === AGENT_STATUSES.DELETED) throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
8944
|
+
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);
|
|
8945
|
+
if (!member) throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
8946
|
+
const [row] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.uuid, agentUuid), agentVisibilityCondition(agent.organizationId, member.id))).limit(1);
|
|
8690
8947
|
if (!row) throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
8691
8948
|
}
|
|
8692
8949
|
/**
|
|
@@ -8708,9 +8965,22 @@ async function assertChatAccess(db, scope, chatId) {
|
|
|
8708
8965
|
if (!managed) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
8709
8966
|
}
|
|
8710
8967
|
/**
|
|
8711
|
-
* Assert the
|
|
8712
|
-
*
|
|
8713
|
-
*
|
|
8968
|
+
* Assert the caller can manage (update/delete/token/suspend) an agent.
|
|
8969
|
+
*
|
|
8970
|
+
* Manageability is anchored on the agent's *own* organization, not on the
|
|
8971
|
+
* JWT default org. We resolve the caller's active membership in
|
|
8972
|
+
* `agent.organizationId` and grant manage access if either:
|
|
8973
|
+
* - that membership is `admin` (admin in the agent's org), or
|
|
8974
|
+
* - the agent's `managerId` equals the caller's memberId *in that org*.
|
|
8975
|
+
*
|
|
8976
|
+
* This is the cross-org switch-org fix: with `/auth/switch-org` now 204 the
|
|
8977
|
+
* scope.organizationId / scope.memberId are JWT defaults, so the previous
|
|
8978
|
+
* `agent.organizationId !== scope.organizationId` short-circuit was 404'ing
|
|
8979
|
+
* every `:uuid` route as soon as the user looked at a non-default org. We
|
|
8980
|
+
* authorize against the agent's actual org instead.
|
|
8981
|
+
*
|
|
8982
|
+
* Returns 404 for "not found", "not a member of agent's org", or "not
|
|
8983
|
+
* authorized" — same shape as before, to prevent UUID enumeration.
|
|
8714
8984
|
*/
|
|
8715
8985
|
async function assertCanManage(db, scope, agentUuid) {
|
|
8716
8986
|
const [agent] = await db.select({
|
|
@@ -8718,9 +8988,85 @@ async function assertCanManage(db, scope, agentUuid) {
|
|
|
8718
8988
|
managerId: agents.managerId,
|
|
8719
8989
|
organizationId: agents.organizationId
|
|
8720
8990
|
}).from(agents).where(and(eq(agents.uuid, agentUuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
8721
|
-
if (!agent
|
|
8722
|
-
|
|
8723
|
-
|
|
8991
|
+
if (!agent) throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
8992
|
+
const [memberRow] = await db.select({
|
|
8993
|
+
id: members.id,
|
|
8994
|
+
role: members.role
|
|
8995
|
+
}).from(members).where(and(eq(members.userId, scope.userId), eq(members.organizationId, agent.organizationId), eq(members.status, "active"))).limit(1);
|
|
8996
|
+
if (!memberRow) throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
8997
|
+
if (memberRow.role === "admin") return;
|
|
8998
|
+
if (agent.managerId === memberRow.id) return;
|
|
8999
|
+
throw new NotFoundError(`Agent "${agentUuid}" not found`);
|
|
9000
|
+
}
|
|
9001
|
+
/**
|
|
9002
|
+
* Assert the request's authenticated user has an active membership in
|
|
9003
|
+
* `orgId` and return its `(memberId, role)`. Used by admin routes that
|
|
9004
|
+
* accept an explicit `organizationId` from the request (query / body /
|
|
9005
|
+
* path) and must verify role realtime — JWT `organizationId` and `role`
|
|
9006
|
+
* claims are hints, not authoritative (decouple-client-from-identity §4.5).
|
|
9007
|
+
*
|
|
9008
|
+
* Throws {@link ForbiddenError} if the user is not an active member of the
|
|
9009
|
+
* target org.
|
|
9010
|
+
*/
|
|
9011
|
+
async function requireMemberInOrg(db, request, orgId) {
|
|
9012
|
+
const m = requireMember(request);
|
|
9013
|
+
const [row] = await db.select({
|
|
9014
|
+
id: members.id,
|
|
9015
|
+
role: members.role
|
|
9016
|
+
}).from(members).where(and(eq(members.userId, m.userId), eq(members.organizationId, orgId), eq(members.status, "active"))).limit(1);
|
|
9017
|
+
if (!row) throw new ForbiddenError("Not an active member of the target organization");
|
|
9018
|
+
return {
|
|
9019
|
+
memberId: row.id,
|
|
9020
|
+
role: row.role
|
|
9021
|
+
};
|
|
9022
|
+
}
|
|
9023
|
+
/**
|
|
9024
|
+
* Resolve the `(organizationId, memberId, role)` an admin route should
|
|
9025
|
+
* operate against, based on the unified PR-D scoping rule:
|
|
9026
|
+
* - If the request supplies `?organizationId=…` (query) or it appears in
|
|
9027
|
+
* the body, verify the caller is an active member there and return
|
|
9028
|
+
* that membership realtime.
|
|
9029
|
+
* - Otherwise fall back to the JWT default org (the existing
|
|
9030
|
+
* `MemberScope` from `memberScope(request)`).
|
|
9031
|
+
*
|
|
9032
|
+
* All admin listing routes call this so that the cross-org switch — driven
|
|
9033
|
+
* entirely client-side via `localStorage.selectedOrganizationId` after
|
|
9034
|
+
* `/auth/switch-org` returns 204 — funnels through one consistent gate
|
|
9035
|
+
* (decouple-client-from-identity §4.5 / §D, codex P1 #2 fix).
|
|
9036
|
+
*
|
|
9037
|
+
* @returns the effective scope for the rest of the route to read from.
|
|
9038
|
+
*/
|
|
9039
|
+
async function resolveAdminScope(db, request, scope, requestedOrganizationId) {
|
|
9040
|
+
if (!requestedOrganizationId || requestedOrganizationId === scope.organizationId) return scope;
|
|
9041
|
+
const probe = await requireMemberInOrg(db, request, requestedOrganizationId);
|
|
9042
|
+
return {
|
|
9043
|
+
...scope,
|
|
9044
|
+
memberId: probe.memberId,
|
|
9045
|
+
organizationId: requestedOrganizationId,
|
|
9046
|
+
role: probe.role
|
|
9047
|
+
};
|
|
9048
|
+
}
|
|
9049
|
+
/**
|
|
9050
|
+
* Cross-org listing helper for "agents I personally manage". Used by the
|
|
9051
|
+
* CLI `agent list` view (decouple-client-from-identity §4.5.1 case (b)) —
|
|
9052
|
+
* the web roster, by contrast, stays org-scoped via
|
|
9053
|
+
* {@link agentVisibilityCondition}.
|
|
9054
|
+
*
|
|
9055
|
+
* Returns every active agent whose manager is an active member of the
|
|
9056
|
+
* caller. JOINs `agents → members.id` and filters by `members.user_id`.
|
|
9057
|
+
*/
|
|
9058
|
+
async function listAgentsManagedByUser(db, userId) {
|
|
9059
|
+
return db.select({
|
|
9060
|
+
uuid: agents.uuid,
|
|
9061
|
+
name: agents.name,
|
|
9062
|
+
displayName: agents.displayName,
|
|
9063
|
+
type: agents.type,
|
|
9064
|
+
organizationId: agents.organizationId,
|
|
9065
|
+
inboxId: agents.inboxId,
|
|
9066
|
+
visibility: agents.visibility,
|
|
9067
|
+
runtimeProvider: agents.runtimeProvider,
|
|
9068
|
+
clientId: agents.clientId
|
|
9069
|
+
}).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
9070
|
}
|
|
8725
9071
|
/**
|
|
8726
9072
|
* Maps internal Chats to external IM platform channels.
|
|
@@ -8997,6 +9343,7 @@ async function findExternalMessageByInternalId(db, platform, messageId) {
|
|
|
8997
9343
|
}).from(adapterMessageReferences).where(and(eq(adapterMessageReferences.platform, platform), eq(adapterMessageReferences.messageId, messageId))).limit(1);
|
|
8998
9344
|
return row ?? null;
|
|
8999
9345
|
}
|
|
9346
|
+
const orgQuerySchema$3 = z.object({ organizationId: z.string().min(1).optional() });
|
|
9000
9347
|
function parseId$1(raw) {
|
|
9001
9348
|
const id = Number(raw);
|
|
9002
9349
|
if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid mapping ID: "${raw}"`);
|
|
@@ -9005,8 +9352,11 @@ function parseId$1(raw) {
|
|
|
9005
9352
|
async function adminAdapterMappingRoutes(app) {
|
|
9006
9353
|
app.get("/", async (request) => {
|
|
9007
9354
|
const scope = memberScope(request);
|
|
9008
|
-
const
|
|
9009
|
-
|
|
9355
|
+
const { organizationId } = orgQuerySchema$3.parse(request.query);
|
|
9356
|
+
const targetOrgId = organizationId ?? scope.organizationId;
|
|
9357
|
+
const probe = await requireMemberInOrg(app.db, request, targetOrgId);
|
|
9358
|
+
const conditions = [eq(agents.organizationId, targetOrgId)];
|
|
9359
|
+
if (probe.role !== "admin") conditions.push(eq(agents.managerId, probe.memberId));
|
|
9010
9360
|
return (await app.db.select({
|
|
9011
9361
|
id: adapterAgentMappings.id,
|
|
9012
9362
|
platform: adapterAgentMappings.platform,
|
|
@@ -9073,11 +9423,15 @@ const adapterConfigs = pgTable("adapter_configs", {
|
|
|
9073
9423
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
9074
9424
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
9075
9425
|
}, (t) => [unique("uq_adapter_configs_agent_platform").on(t.agentId, t.platform)]);
|
|
9426
|
+
const orgQuerySchema$2 = z.object({ organizationId: z.string().min(1).optional() });
|
|
9076
9427
|
async function adminAdapterStatusRoutes(app) {
|
|
9077
9428
|
app.get("/", async (request) => {
|
|
9078
9429
|
const scope = memberScope(request);
|
|
9079
|
-
const
|
|
9080
|
-
|
|
9430
|
+
const { organizationId } = orgQuerySchema$2.parse(request.query);
|
|
9431
|
+
const targetOrgId = organizationId ?? scope.organizationId;
|
|
9432
|
+
const probe = await requireMemberInOrg(app.db, request, targetOrgId);
|
|
9433
|
+
const conditions = [eq(agents.organizationId, targetOrgId), ne(agents.status, "deleted")];
|
|
9434
|
+
if (probe.role !== "admin") conditions.push(eq(agents.managerId, probe.memberId));
|
|
9081
9435
|
const visibleRows = await app.db.select({ id: adapterConfigs.id }).from(adapterConfigs).innerJoin(agents, eq(agents.uuid, adapterConfigs.agentId)).where(and(...conditions));
|
|
9082
9436
|
const visibleIds = new Set(visibleRows.map((r) => r.id));
|
|
9083
9437
|
if (visibleIds.size === 0) return [];
|
|
@@ -9264,6 +9618,7 @@ async function deleteAdapterConfig(db, id) {
|
|
|
9264
9618
|
if (!row) throw new NotFoundError(`Adapter config "${id}" not found`);
|
|
9265
9619
|
}
|
|
9266
9620
|
const log$5 = createLogger$1("AdminAdapters");
|
|
9621
|
+
const orgQuerySchema$1 = z.object({ organizationId: z.string().min(1).optional() });
|
|
9267
9622
|
function parseId(raw) {
|
|
9268
9623
|
const id = Number(raw);
|
|
9269
9624
|
if (!Number.isInteger(id) || id <= 0) throw new BadRequestError(`Invalid adapter ID: "${raw}"`);
|
|
@@ -9272,7 +9627,9 @@ function parseId(raw) {
|
|
|
9272
9627
|
async function adminAdapterRoutes(app) {
|
|
9273
9628
|
app.get("/", async (request) => {
|
|
9274
9629
|
const scope = memberScope(request);
|
|
9275
|
-
|
|
9630
|
+
const { organizationId } = orgQuerySchema$1.parse(request.query);
|
|
9631
|
+
const effective = await resolveAdminScope(app.db, request, scope, organizationId);
|
|
9632
|
+
return (await listAdapterConfigsForMember(app.db, effective)).map((c) => ({
|
|
9276
9633
|
...c,
|
|
9277
9634
|
createdAt: c.createdAt.toISOString(),
|
|
9278
9635
|
updatedAt: c.updatedAt.toISOString()
|
|
@@ -9401,20 +9758,6 @@ const agentConfigs = pgTable("agent_configs", {
|
|
|
9401
9758
|
updatedBy: text("updated_by").notNull(),
|
|
9402
9759
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
9403
9760
|
});
|
|
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
9761
|
/**
|
|
9419
9762
|
* Names beginning with `__` are reserved for Hub-internal pseudo agents
|
|
9420
9763
|
* (e.g. the task notifier). User-facing creation must not be able to
|
|
@@ -9506,19 +9849,14 @@ async function resolveAgentClient(db, data) {
|
|
|
9506
9849
|
return null;
|
|
9507
9850
|
}
|
|
9508
9851
|
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);
|
|
9852
|
+
const [manager] = await db.select({ userId: members.userId }).from(members).where(eq(members.id, data.managerId)).limit(1);
|
|
9513
9853
|
if (!manager) throw new BadRequestError(`Manager "${data.managerId}" not found`);
|
|
9514
9854
|
const [client] = await db.select({
|
|
9515
9855
|
id: clients.id,
|
|
9516
|
-
userId: clients.userId
|
|
9517
|
-
organizationId: clients.organizationId
|
|
9856
|
+
userId: clients.userId
|
|
9518
9857
|
}).from(clients).where(eq(clients.id, data.clientId)).limit(1);
|
|
9519
9858
|
if (!client) throw new BadRequestError(`Client "${data.clientId}" not found`);
|
|
9520
9859
|
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
9860
|
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
9861
|
return client.id;
|
|
9524
9862
|
}
|
|
@@ -9662,7 +10000,7 @@ async function listAgentsForAdmin(db, scope, limit, cursor) {
|
|
|
9662
10000
|
* Uses agentVisibilityCondition from access-control (same rules for all roles).
|
|
9663
10001
|
*/
|
|
9664
10002
|
async function listAgentsForMember(db, scope, limit, cursor, type) {
|
|
9665
|
-
const conditions = [agentVisibilityCondition(scope)];
|
|
10003
|
+
const conditions = [agentVisibilityCondition(scope.organizationId, scope.memberId)];
|
|
9666
10004
|
if (cursor) conditions.push(lt(agents.createdAt, new Date(cursor)));
|
|
9667
10005
|
if (type) conditions.push(eq(agents.type, type));
|
|
9668
10006
|
const where = and(...conditions);
|
|
@@ -10249,54 +10587,40 @@ async function cleanupStalePresence(db, staleSeconds = 60) {
|
|
|
10249
10587
|
}
|
|
10250
10588
|
/**
|
|
10251
10589
|
* Assert the caller can act on this client. Throws 404 for both "not found"
|
|
10252
|
-
* and "not yours" to prevent UUID enumeration
|
|
10253
|
-
*
|
|
10254
|
-
*
|
|
10255
|
-
*
|
|
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.
|
|
10590
|
+
* and "not yours" to prevent UUID enumeration. The client is owned by exactly
|
|
10591
|
+
* one user; cross-user admin access is no longer supported by this code path
|
|
10592
|
+
* (see decouple-client-from-identity-design §4.10.5 option A). Cross-user
|
|
10593
|
+
* ownership transfer goes through `claimClient` in PR-B.
|
|
10262
10594
|
*/
|
|
10263
10595
|
async function assertClientOwner(db, clientId, scope) {
|
|
10264
10596
|
const [row] = await db.select({
|
|
10265
10597
|
id: clients.id,
|
|
10266
|
-
userId: clients.userId
|
|
10267
|
-
organizationId: clients.organizationId
|
|
10598
|
+
userId: clients.userId
|
|
10268
10599
|
}).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`);
|
|
10600
|
+
if (!row || row.userId !== scope.userId) throw new NotFoundError(`Client "${clientId}" not found`);
|
|
10277
10601
|
}
|
|
10278
10602
|
/**
|
|
10279
10603
|
* Upsert the clients row for a given `client_id` under an authenticated user.
|
|
10280
10604
|
*
|
|
10281
|
-
* Claim semantics (
|
|
10282
|
-
* - New client_id → INSERT with the authenticated user_id
|
|
10283
|
-
*
|
|
10284
|
-
*
|
|
10285
|
-
*
|
|
10286
|
-
*
|
|
10287
|
-
*
|
|
10288
|
-
*
|
|
10289
|
-
*
|
|
10605
|
+
* Claim semantics (decouple-client-from-identity §4.1.1):
|
|
10606
|
+
* - New client_id → INSERT with the authenticated user_id. `organization_id`
|
|
10607
|
+
* is written as a placeholder (NOT NULL legacy column; no longer consumed
|
|
10608
|
+
* by any read path) sourced from the caller-supplied JWT default org.
|
|
10609
|
+
* - Existing row with the same user_id → refresh runtime columns.
|
|
10610
|
+
* `organization_id` is **not** updated on conflict, so the placeholder set
|
|
10611
|
+
* at first insert sticks for the row's lifetime.
|
|
10612
|
+
* - Existing row with a different user_id → raises
|
|
10613
|
+
* {@link ClientUserMismatchError} (WS close 4403). The CLI guides the
|
|
10614
|
+
* operator through `first-tree-hub client claim --confirm` to take
|
|
10615
|
+
* ownership, which unpins the previous owner's agents from the machine.
|
|
10290
10616
|
*/
|
|
10291
10617
|
async function registerClient(db, data) {
|
|
10292
10618
|
const now = /* @__PURE__ */ new Date();
|
|
10293
10619
|
const [existing] = await db.select({
|
|
10294
10620
|
id: clients.id,
|
|
10295
|
-
userId: clients.userId
|
|
10296
|
-
organizationId: clients.organizationId
|
|
10621
|
+
userId: clients.userId
|
|
10297
10622
|
}).from(clients).where(eq(clients.id, data.clientId)).limit(1);
|
|
10298
|
-
if (existing && existing.
|
|
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.`);
|
|
10623
|
+
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
10624
|
await db.insert(clients).values({
|
|
10301
10625
|
id: data.clientId,
|
|
10302
10626
|
userId: data.userId,
|
|
@@ -10322,6 +10646,54 @@ async function registerClient(db, data) {
|
|
|
10322
10646
|
}
|
|
10323
10647
|
});
|
|
10324
10648
|
}
|
|
10649
|
+
/**
|
|
10650
|
+
* Transfer ownership of a client row to a new user, unpinning any agents
|
|
10651
|
+
* whose manager belonged to the previous owner. Atomic: caller is guaranteed
|
|
10652
|
+
* either a fully-applied ownership flip + bulk unpin, or no change. Idempotent
|
|
10653
|
+
* when `newUserId` already owns the row.
|
|
10654
|
+
*
|
|
10655
|
+
* Manager → user resolution goes through the members JOIN (the agents table
|
|
10656
|
+
* carries only `manager_id`); cross-org agents under the same previous owner
|
|
10657
|
+
* are unpinned together (decouple-client-from-identity §4.4).
|
|
10658
|
+
*
|
|
10659
|
+
* Caller is responsible for the caller-side authorization (the new owner must
|
|
10660
|
+
* be the authenticated request's user). The structured log
|
|
10661
|
+
* `event: client.owner_transfer` is emitted by the caller after the
|
|
10662
|
+
* transaction commits, using the returned `previousUserId` /
|
|
10663
|
+
* `unpinnedAgentIds`.
|
|
10664
|
+
*/
|
|
10665
|
+
async function claimClient(db, clientId, newUserId) {
|
|
10666
|
+
return db.transaction(async (tx) => {
|
|
10667
|
+
const [locked] = await tx.execute(sql`SELECT id, user_id FROM clients WHERE id = ${clientId} FOR UPDATE`);
|
|
10668
|
+
if (!locked) throw new NotFoundError(`Client "${clientId}" not found`);
|
|
10669
|
+
const previousUserId = locked.user_id;
|
|
10670
|
+
if (previousUserId === newUserId) return {
|
|
10671
|
+
previousUserId,
|
|
10672
|
+
unpinnedAgentIds: []
|
|
10673
|
+
};
|
|
10674
|
+
let unpinnedAgentIds = [];
|
|
10675
|
+
if (previousUserId !== null) {
|
|
10676
|
+
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);
|
|
10677
|
+
if (unpinnedAgentIds.length > 0) {
|
|
10678
|
+
const now = /* @__PURE__ */ new Date();
|
|
10679
|
+
await tx.update(agents).set({
|
|
10680
|
+
clientId: null,
|
|
10681
|
+
updatedAt: now
|
|
10682
|
+
}).where(inArray(agents.uuid, unpinnedAgentIds));
|
|
10683
|
+
await tx.update(agentPresence).set({
|
|
10684
|
+
status: "offline",
|
|
10685
|
+
clientId: null,
|
|
10686
|
+
...runtimeFieldsReset(now)
|
|
10687
|
+
}).where(inArray(agentPresence.agentId, unpinnedAgentIds));
|
|
10688
|
+
}
|
|
10689
|
+
}
|
|
10690
|
+
await tx.update(clients).set({ userId: newUserId }).where(eq(clients.id, clientId));
|
|
10691
|
+
return {
|
|
10692
|
+
previousUserId,
|
|
10693
|
+
unpinnedAgentIds
|
|
10694
|
+
};
|
|
10695
|
+
});
|
|
10696
|
+
}
|
|
10325
10697
|
async function disconnectClient(db, clientId) {
|
|
10326
10698
|
const now = /* @__PURE__ */ new Date();
|
|
10327
10699
|
await db.update(agentPresence).set({
|
|
@@ -10360,16 +10732,17 @@ async function listActiveAgentsPinnedToClient(db, clientId) {
|
|
|
10360
10732
|
}).from(agents).where(and(eq(agents.clientId, clientId), ne(agents.status, "deleted")));
|
|
10361
10733
|
}
|
|
10362
10734
|
/**
|
|
10363
|
-
* Member-scoped: every active agent pinned to a client owned by this user
|
|
10364
|
-
*
|
|
10365
|
-
*
|
|
10735
|
+
* Member-scoped: every active agent pinned to a client owned by this user.
|
|
10736
|
+
* Used by client startup to reconcile its local YAML against the authoritative
|
|
10737
|
+
* `agents.runtime_provider`. Cross-org by design — a client is owned by a
|
|
10738
|
+
* user, not an org (decouple-client-from-identity §4.1).
|
|
10366
10739
|
*/
|
|
10367
10740
|
async function listMyPinnedAgents(db, scope) {
|
|
10368
10741
|
return (await db.select({
|
|
10369
10742
|
agentId: agents.uuid,
|
|
10370
10743
|
clientId: agents.clientId,
|
|
10371
10744
|
runtimeProvider: agents.runtimeProvider
|
|
10372
|
-
}).from(agents).innerJoin(clients, eq(agents.clientId, clients.id)).where(and(eq(clients.userId, scope.userId),
|
|
10745
|
+
}).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
10746
|
agentId: r.agentId,
|
|
10374
10747
|
clientId: r.clientId,
|
|
10375
10748
|
runtimeProvider: r.runtimeProvider
|
|
@@ -10394,17 +10767,28 @@ async function updateClientCapabilities(db, clientId, capabilities) {
|
|
|
10394
10767
|
await db.update(clients).set({ metadata: merged }).where(eq(clients.id, clientId));
|
|
10395
10768
|
}
|
|
10396
10769
|
/**
|
|
10397
|
-
* Scope-aware client listing.
|
|
10398
|
-
*
|
|
10399
|
-
*
|
|
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.
|
|
10770
|
+
* Scope-aware client listing. Returns the caller's own clients (cross-org —
|
|
10771
|
+
* a client is owned by a user, not an org). The admin route adds a separate
|
|
10772
|
+
* `?organizationId=` cross-user view via {@link listClientsForOrgAdmin}.
|
|
10403
10773
|
*/
|
|
10404
10774
|
async function listClients(db, scope) {
|
|
10405
|
-
|
|
10775
|
+
return attachAgentCounts(db, await db.select().from(clients).where(eq(clients.userId, scope.userId)));
|
|
10776
|
+
}
|
|
10777
|
+
/**
|
|
10778
|
+
* Admin-only cross-user listing: every client owned by an active member of
|
|
10779
|
+
* `orgId`. Joining `clients → members.user_id` instead of `clients.organization_id`
|
|
10780
|
+
* keeps the read path consistent with the rule that connection has no
|
|
10781
|
+
* runtime relationship to organization (decouple-client-from-identity §A).
|
|
10782
|
+
*
|
|
10783
|
+
* The caller must verify admin role realtime via `requireMemberInOrg` before
|
|
10784
|
+
* invoking this function — the service does not re-check, so it is
|
|
10785
|
+
* unsafe to expose without that gate.
|
|
10786
|
+
*/
|
|
10787
|
+
async function listClientsForOrgAdmin(db, orgId) {
|
|
10788
|
+
return attachAgentCounts(db, await db.select({
|
|
10406
10789
|
id: clients.id,
|
|
10407
10790
|
userId: clients.userId,
|
|
10791
|
+
organizationId: clients.organizationId,
|
|
10408
10792
|
status: clients.status,
|
|
10409
10793
|
sdkVersion: clients.sdkVersion,
|
|
10410
10794
|
hostname: clients.hostname,
|
|
@@ -10413,7 +10797,9 @@ async function listClients(db, scope) {
|
|
|
10413
10797
|
connectedAt: clients.connectedAt,
|
|
10414
10798
|
lastSeenAt: clients.lastSeenAt,
|
|
10415
10799
|
metadata: clients.metadata
|
|
10416
|
-
}).from(clients).
|
|
10800
|
+
}).from(clients).innerJoin(members, eq(members.userId, clients.userId)).where(and(eq(members.organizationId, orgId), eq(members.status, "active"))));
|
|
10801
|
+
}
|
|
10802
|
+
async function attachAgentCounts(db, rows) {
|
|
10417
10803
|
const counts = await db.select({
|
|
10418
10804
|
clientId: agents.clientId,
|
|
10419
10805
|
count: sql`count(*)::int`
|
|
@@ -10723,7 +11109,7 @@ async function listAgentsWithRuntime(db, scope) {
|
|
|
10723
11109
|
totalSessions: agentPresence.totalSessions,
|
|
10724
11110
|
runtimeUpdatedAt: agentPresence.runtimeUpdatedAt,
|
|
10725
11111
|
type: agents.type
|
|
10726
|
-
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope)));
|
|
11112
|
+
}).from(agentPresence).innerJoin(agents, eq(agentPresence.agentId, agents.uuid)).where(and(isNotNull(agentPresence.runtimeState), agentVisibilityCondition(scope.organizationId, scope.memberId)));
|
|
10727
11113
|
}
|
|
10728
11114
|
const log$4 = createLogger$1("message");
|
|
10729
11115
|
async function sendMessage(db, chatId, senderId, data, options = {}) {
|
|
@@ -11089,12 +11475,26 @@ async function adminAgentRoutes(app) {
|
|
|
11089
11475
|
}
|
|
11090
11476
|
sendToClient(agent.clientId, parsed.data);
|
|
11091
11477
|
}
|
|
11092
|
-
const listAgentsFilterSchema = z.object({
|
|
11478
|
+
const listAgentsFilterSchema = z.object({
|
|
11479
|
+
type: agentTypeSchema$1.optional(),
|
|
11480
|
+
organizationId: z.string().min(1).optional()
|
|
11481
|
+
});
|
|
11093
11482
|
app.get("/", async (request) => {
|
|
11094
11483
|
const query = paginationQuerySchema.parse(request.query);
|
|
11095
|
-
const { type } = listAgentsFilterSchema.parse(request.query);
|
|
11484
|
+
const { type, organizationId } = listAgentsFilterSchema.parse(request.query);
|
|
11096
11485
|
const scope = memberScope(request);
|
|
11097
|
-
const
|
|
11486
|
+
const targetOrgId = organizationId ?? scope.organizationId;
|
|
11487
|
+
let queryScope = scope;
|
|
11488
|
+
if (targetOrgId !== scope.organizationId) {
|
|
11489
|
+
const probe = await requireMemberInOrg(app.db, request, targetOrgId);
|
|
11490
|
+
queryScope = {
|
|
11491
|
+
...scope,
|
|
11492
|
+
memberId: probe.memberId,
|
|
11493
|
+
organizationId: targetOrgId,
|
|
11494
|
+
role: probe.role
|
|
11495
|
+
};
|
|
11496
|
+
}
|
|
11497
|
+
const result = await listAgentsForMember(app.db, queryScope, query.limit, query.cursor, type);
|
|
11098
11498
|
return {
|
|
11099
11499
|
items: result.items.map((a) => ({
|
|
11100
11500
|
...a,
|
|
@@ -11119,9 +11519,18 @@ async function adminAgentRoutes(app) {
|
|
|
11119
11519
|
*/
|
|
11120
11520
|
app.get("/all", async (request) => {
|
|
11121
11521
|
const scope = memberScope(request);
|
|
11122
|
-
if (scope.role !== "admin") throw new ForbiddenError("Admin role required");
|
|
11123
11522
|
const query = paginationQuerySchema.parse(request.query);
|
|
11124
|
-
const
|
|
11523
|
+
const { organizationId } = listAgentsFilterSchema.parse(request.query);
|
|
11524
|
+
const targetOrgId = organizationId ?? scope.organizationId;
|
|
11525
|
+
const probe = await requireMemberInOrg(app.db, request, targetOrgId);
|
|
11526
|
+
if (probe.role !== "admin") throw new ForbiddenError("Admin role required");
|
|
11527
|
+
const adminScope = {
|
|
11528
|
+
...scope,
|
|
11529
|
+
memberId: probe.memberId,
|
|
11530
|
+
organizationId: targetOrgId,
|
|
11531
|
+
role: probe.role
|
|
11532
|
+
};
|
|
11533
|
+
const result = await listAgentsForAdmin(app.db, adminScope, query.limit, query.cursor);
|
|
11125
11534
|
return {
|
|
11126
11535
|
items: result.items.map((a) => ({
|
|
11127
11536
|
...a,
|
|
@@ -11152,9 +11561,12 @@ async function adminAgentRoutes(app) {
|
|
|
11152
11561
|
app.post("/", async (request, reply) => {
|
|
11153
11562
|
const scope = memberScope(request);
|
|
11154
11563
|
const body = createAgentSchema.parse(request.body);
|
|
11155
|
-
const
|
|
11564
|
+
const targetOrgId = body.organizationId ?? scope.organizationId;
|
|
11565
|
+
const probe = await requireMemberInOrg(app.db, request, targetOrgId);
|
|
11566
|
+
const managerId = probe.role === "admin" ? body.managerId ?? probe.memberId : probe.memberId;
|
|
11156
11567
|
const agent = await createAgent(app.db, {
|
|
11157
11568
|
...body,
|
|
11569
|
+
organizationId: targetOrgId,
|
|
11158
11570
|
source: body.source ?? "admin-api",
|
|
11159
11571
|
managerId
|
|
11160
11572
|
});
|
|
@@ -11461,7 +11873,8 @@ async function adminChatRoutes(app) {
|
|
|
11461
11873
|
/** List all chats in org (admin-only, for audit). Members should use GET /mine. */
|
|
11462
11874
|
app.get("/", { preHandler: requireAdminRoleHook() }, async (request) => {
|
|
11463
11875
|
const query = paginationQuerySchema.parse(request.query);
|
|
11464
|
-
const
|
|
11876
|
+
const rawQuery = request.query;
|
|
11877
|
+
const orgParam = rawQuery.organizationId ?? rawQuery.org;
|
|
11465
11878
|
let orgId;
|
|
11466
11879
|
if (orgParam) orgId = (await resolveOrganization(app.db, orgParam)).id;
|
|
11467
11880
|
else orgId = await resolveDefaultOrgId(app.db);
|
|
@@ -11655,14 +12068,15 @@ async function adminChatRoutes(app) {
|
|
|
11655
12068
|
function serializeDate(d) {
|
|
11656
12069
|
return d ? d.toISOString() : null;
|
|
11657
12070
|
}
|
|
12071
|
+
const listClientsQuerySchema = z.object({ organizationId: z.string().min(1).optional() });
|
|
11658
12072
|
async function adminClientRoutes(app) {
|
|
11659
12073
|
app.get("/", async (request) => {
|
|
11660
12074
|
const scope = memberScope(request);
|
|
11661
|
-
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
|
|
11665
|
-
})).map((c) => ({
|
|
12075
|
+
const { organizationId } = listClientsQuerySchema.parse(request.query);
|
|
12076
|
+
return (organizationId ? await (async () => {
|
|
12077
|
+
if ((await requireMemberInOrg(app.db, request, organizationId)).role !== "admin") throw new ForbiddenError("Admin role required");
|
|
12078
|
+
return listClientsForOrgAdmin(app.db, organizationId);
|
|
12079
|
+
})() : await listClients(app.db, { userId: scope.userId })).map((c) => ({
|
|
11666
12080
|
id: c.id,
|
|
11667
12081
|
userId: c.userId,
|
|
11668
12082
|
status: c.status,
|
|
@@ -11676,10 +12090,7 @@ async function adminClientRoutes(app) {
|
|
|
11676
12090
|
});
|
|
11677
12091
|
app.get("/me/agents", async (request) => {
|
|
11678
12092
|
const scope = memberScope(request);
|
|
11679
|
-
return await listMyPinnedAgents(app.db, {
|
|
11680
|
-
userId: scope.userId,
|
|
11681
|
-
organizationId: scope.organizationId
|
|
11682
|
-
});
|
|
12093
|
+
return await listMyPinnedAgents(app.db, { userId: scope.userId });
|
|
11683
12094
|
});
|
|
11684
12095
|
app.patch("/:clientId/capabilities", async (request, reply) => {
|
|
11685
12096
|
const scope = memberScope(request);
|
|
@@ -12014,25 +12425,33 @@ async function pushToWebhook(db, notification) {
|
|
|
12014
12425
|
});
|
|
12015
12426
|
} catch {}
|
|
12016
12427
|
}
|
|
12428
|
+
const orgQuerySchema = z.object({ organizationId: z.string().min(1).optional() });
|
|
12017
12429
|
async function adminNotificationRoutes(app) {
|
|
12018
12430
|
/**
|
|
12019
|
-
* GET /admin/notifications — list notifications visible to the caller
|
|
12431
|
+
* GET /admin/notifications — list notifications visible to the caller in
|
|
12432
|
+
* the **selected** organization. The web client passes
|
|
12433
|
+
* `?organizationId=<selectedOrgId>` so a non-default org doesn't reuse
|
|
12434
|
+
* the JWT default org's bell feed (codex P1 #2). Defaults to JWT org
|
|
12435
|
+
* when omitted.
|
|
12020
12436
|
*
|
|
12021
|
-
*
|
|
12022
|
-
*
|
|
12023
|
-
*
|
|
12024
|
-
*
|
|
12025
|
-
* WebSocket route enforces on live pushes — REST and WS stay in sync.
|
|
12437
|
+
* Per-agent visibility filter: the member only sees notifications whose
|
|
12438
|
+
* agentId is visible to them in that org (organization-visible agents or
|
|
12439
|
+
* agents they manage), plus org-wide system notifications with no
|
|
12440
|
+
* agentId. This mirrors the admin-WS push gate so REST and WS stay in sync.
|
|
12026
12441
|
*/
|
|
12027
12442
|
app.get("/", async (request) => {
|
|
12028
|
-
const
|
|
12443
|
+
const scope = memberScope(request);
|
|
12444
|
+
const { organizationId } = orgQuerySchema.parse(request.query);
|
|
12445
|
+
const effective = await resolveAdminScope(app.db, request, scope, organizationId);
|
|
12029
12446
|
const query = notificationQuerySchema.parse(request.query);
|
|
12030
|
-
return listNotifications(app.db,
|
|
12447
|
+
return listNotifications(app.db, effective.organizationId, effective.memberId, query);
|
|
12031
12448
|
});
|
|
12032
12449
|
/** POST /admin/notifications/:id/read — mark a single notification as read */
|
|
12033
12450
|
app.post("/:id/read", async (request) => {
|
|
12034
|
-
const
|
|
12035
|
-
const
|
|
12451
|
+
const scope = memberScope(request);
|
|
12452
|
+
const { organizationId } = orgQuerySchema.parse(request.query);
|
|
12453
|
+
const effective = await resolveAdminScope(app.db, request, scope, organizationId);
|
|
12454
|
+
const result = await markRead(app.db, request.params.id, effective.organizationId, effective.memberId);
|
|
12036
12455
|
if (!result) throw new NotFoundError(`Notification "${request.params.id}" not found`);
|
|
12037
12456
|
return {
|
|
12038
12457
|
...result,
|
|
@@ -12041,8 +12460,10 @@ async function adminNotificationRoutes(app) {
|
|
|
12041
12460
|
});
|
|
12042
12461
|
/** POST /admin/notifications/read-all — mark all visible notifications as read */
|
|
12043
12462
|
app.post("/read-all", async (request) => {
|
|
12044
|
-
const
|
|
12045
|
-
|
|
12463
|
+
const scope = memberScope(request);
|
|
12464
|
+
const { organizationId } = orgQuerySchema.parse(request.query);
|
|
12465
|
+
const effective = await resolveAdminScope(app.db, request, scope, organizationId);
|
|
12466
|
+
await markAllRead(app.db, effective.organizationId, effective.memberId);
|
|
12046
12467
|
return { status: "ok" };
|
|
12047
12468
|
});
|
|
12048
12469
|
}
|
|
@@ -12094,7 +12515,8 @@ async function adminOrganizationRoutes(app) {
|
|
|
12094
12515
|
}
|
|
12095
12516
|
async function adminOverviewRoutes(app) {
|
|
12096
12517
|
app.get("/", async (request) => {
|
|
12097
|
-
const
|
|
12518
|
+
const q = request.query ?? {};
|
|
12519
|
+
const orgParam = q.organizationId ?? q.org;
|
|
12098
12520
|
let orgId;
|
|
12099
12521
|
if (orgParam) orgId = (await resolveOrganization(app.db, orgParam)).id;
|
|
12100
12522
|
else orgId = await resolveDefaultOrgId(app.db);
|
|
@@ -12409,17 +12831,21 @@ const globalSessionFilterSchema = paginationQuerySchema.extend({
|
|
|
12409
12831
|
"suspended",
|
|
12410
12832
|
"evicted"
|
|
12411
12833
|
]).optional(),
|
|
12412
|
-
agentId: z.string().optional()
|
|
12834
|
+
agentId: z.string().optional(),
|
|
12835
|
+
organizationId: z.string().min(1).optional()
|
|
12413
12836
|
});
|
|
12414
12837
|
async function adminSessionRoutes(app) {
|
|
12415
|
-
/** GET /admin/sessions — global session list, scoped to
|
|
12838
|
+
/** GET /admin/sessions — global session list, scoped to the selected
|
|
12839
|
+
* org. The web client passes `?organizationId=<selectedOrgId>` after a
|
|
12840
|
+
* /auth/switch-org 204 (codex P1 #2); falls back to the JWT default. */
|
|
12416
12841
|
app.get("/", async (request) => {
|
|
12417
|
-
const
|
|
12842
|
+
const scope = memberScope(request);
|
|
12418
12843
|
const query = globalSessionFilterSchema.parse(request.query);
|
|
12844
|
+
const effective = await resolveAdminScope(app.db, request, scope, query.organizationId);
|
|
12419
12845
|
return listAllSessions(app.db, query.limit, query.cursor, {
|
|
12420
12846
|
state: query.state,
|
|
12421
12847
|
agentId: query.agentId,
|
|
12422
|
-
organizationId:
|
|
12848
|
+
organizationId: effective.organizationId
|
|
12423
12849
|
});
|
|
12424
12850
|
});
|
|
12425
12851
|
/** GET /admin/sessions/agents/:agentId — sessions for a specific agent.
|
|
@@ -13113,11 +13539,11 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
13113
13539
|
endWsConnectionSpan(socket, 4001);
|
|
13114
13540
|
return;
|
|
13115
13541
|
}
|
|
13542
|
+
let userId;
|
|
13116
13543
|
let organizationId;
|
|
13117
|
-
let memberId;
|
|
13118
13544
|
try {
|
|
13119
13545
|
const { payload } = await jwtVerify(token, secret);
|
|
13120
|
-
if (payload.type !== "access" ||
|
|
13546
|
+
if (payload.type !== "access" || typeof payload.sub !== "string" || typeof payload.organizationId !== "string") {
|
|
13121
13547
|
socket.send(JSON.stringify({
|
|
13122
13548
|
type: "error",
|
|
13123
13549
|
message: "Invalid token type"
|
|
@@ -13126,8 +13552,8 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
13126
13552
|
endWsConnectionSpan(socket, 4001);
|
|
13127
13553
|
return;
|
|
13128
13554
|
}
|
|
13555
|
+
userId = payload.sub;
|
|
13129
13556
|
organizationId = payload.organizationId;
|
|
13130
|
-
memberId = payload.memberId;
|
|
13131
13557
|
} catch {
|
|
13132
13558
|
socket.send(JSON.stringify({
|
|
13133
13559
|
type: "error",
|
|
@@ -13137,6 +13563,20 @@ function adminWsRoutes(notifier, jwtSecret) {
|
|
|
13137
13563
|
endWsConnectionSpan(socket, 4001);
|
|
13138
13564
|
return;
|
|
13139
13565
|
}
|
|
13566
|
+
const [memberRow] = await app.db.select({
|
|
13567
|
+
id: members.id,
|
|
13568
|
+
role: members.role
|
|
13569
|
+
}).from(members).where(and(eq(members.userId, userId), eq(members.organizationId, organizationId), eq(members.status, "active"))).limit(1);
|
|
13570
|
+
if (!memberRow) {
|
|
13571
|
+
socket.send(JSON.stringify({
|
|
13572
|
+
type: "error",
|
|
13573
|
+
message: "Not an active member of this organization"
|
|
13574
|
+
}));
|
|
13575
|
+
socket.close(4403, "Not a member");
|
|
13576
|
+
endWsConnectionSpan(socket, 4403);
|
|
13577
|
+
return;
|
|
13578
|
+
}
|
|
13579
|
+
const memberId = memberRow.id;
|
|
13140
13580
|
setWsConnectionAttrs(socket, {
|
|
13141
13581
|
organizationId,
|
|
13142
13582
|
memberId
|
|
@@ -13965,6 +14405,7 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
13965
14405
|
}, async (socket) => {
|
|
13966
14406
|
startWsConnectionSpan(socket);
|
|
13967
14407
|
let session = null;
|
|
14408
|
+
let jwtDefaultOrgId = null;
|
|
13968
14409
|
let clientId = null;
|
|
13969
14410
|
let authExpiryTimer = null;
|
|
13970
14411
|
const boundAgents = /* @__PURE__ */ new Map();
|
|
@@ -14195,28 +14636,15 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14195
14636
|
try {
|
|
14196
14637
|
const { payload } = await jwtVerify(authParsed.data.token, jwtSecretBytes);
|
|
14197
14638
|
const claims = payload;
|
|
14198
|
-
if (claims.type !== "access" || !claims.sub
|
|
14639
|
+
if (claims.type !== "access" || !claims.sub) throw new Error("Invalid token claims");
|
|
14199
14640
|
const [user] = await app.db.select({
|
|
14200
14641
|
id: users.id,
|
|
14201
14642
|
status: users.status
|
|
14202
14643
|
}).from(users).where(eq(users.id, claims.sub)).limit(1);
|
|
14203
14644
|
if (!user || user.status !== "active") throw new Error("User not found or suspended");
|
|
14204
|
-
|
|
14205
|
-
|
|
14206
|
-
|
|
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
|
-
});
|
|
14645
|
+
session = { userId: user.id };
|
|
14646
|
+
jwtDefaultOrgId = typeof claims.organizationId === "string" ? claims.organizationId : null;
|
|
14647
|
+
setWsConnectionAttrs(socket, { "user.id": user.id });
|
|
14220
14648
|
clearTimeout(authTimeout);
|
|
14221
14649
|
scheduleAuthExpiry(claims.exp);
|
|
14222
14650
|
socket.send(JSON.stringify({ type: "auth:ok" }));
|
|
@@ -14242,11 +14670,19 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14242
14670
|
if (type === "client:register") {
|
|
14243
14671
|
const data = clientRegisterSchema.parse(msg);
|
|
14244
14672
|
clientWantsWsInboxDeliver = data.wireCapabilities?.wsInboxDeliver === true;
|
|
14673
|
+
if (!jwtDefaultOrgId) {
|
|
14674
|
+
socket.send(JSON.stringify({
|
|
14675
|
+
type: "client:register:rejected",
|
|
14676
|
+
message: "JWT missing organizationId claim"
|
|
14677
|
+
}));
|
|
14678
|
+
socket.close(4401, "client register rejected");
|
|
14679
|
+
return;
|
|
14680
|
+
}
|
|
14245
14681
|
try {
|
|
14246
14682
|
await registerClient(app.db, {
|
|
14247
14683
|
clientId: data.clientId,
|
|
14248
14684
|
userId: session.userId,
|
|
14249
|
-
organizationId:
|
|
14685
|
+
organizationId: jwtDefaultOrgId,
|
|
14250
14686
|
instanceId,
|
|
14251
14687
|
hostname: data.hostname,
|
|
14252
14688
|
os: data.os,
|
|
@@ -14254,7 +14690,7 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14254
14690
|
});
|
|
14255
14691
|
} catch (err) {
|
|
14256
14692
|
const message = err instanceof Error ? err.message : "client register failed";
|
|
14257
|
-
const code = err instanceof ClientOrgMismatchError$1 ? err.code : void 0;
|
|
14693
|
+
const code = err instanceof ClientUserMismatchError$1 ? err.code : err instanceof ClientOrgMismatchError$1 ? err.code : void 0;
|
|
14258
14694
|
socket.send(JSON.stringify({
|
|
14259
14695
|
type: "client:register:rejected",
|
|
14260
14696
|
message,
|
|
@@ -14317,25 +14753,24 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14317
14753
|
clientId: agents.clientId,
|
|
14318
14754
|
runtimeProvider: agents.runtimeProvider,
|
|
14319
14755
|
clientUserId: clients.userId,
|
|
14320
|
-
managerUserId: members.userId
|
|
14756
|
+
managerUserId: members.userId,
|
|
14757
|
+
managerMemberStatus: members.status
|
|
14321
14758
|
}).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
14759
|
if (!agent) {
|
|
14323
14760
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.UNKNOWN_AGENT);
|
|
14324
14761
|
return;
|
|
14325
14762
|
}
|
|
14326
|
-
if (agent.organizationId !== session.organizationId) {
|
|
14327
|
-
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.WRONG_ORG);
|
|
14328
|
-
return;
|
|
14329
|
-
}
|
|
14330
14763
|
if (agent.status !== "active") {
|
|
14331
14764
|
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.AGENT_SUSPENDED);
|
|
14332
14765
|
return;
|
|
14333
14766
|
}
|
|
14767
|
+
const ownerOk = agent.managerUserId !== null && agent.managerUserId === session.userId;
|
|
14768
|
+
const membershipActive = agent.managerMemberStatus === "active";
|
|
14769
|
+
if (!ownerOk || !membershipActive) {
|
|
14770
|
+
sendRejected(socket, ref, AGENT_BIND_REJECT_REASONS.NOT_OWNED);
|
|
14771
|
+
return;
|
|
14772
|
+
}
|
|
14334
14773
|
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
14774
|
if ((await app.db.update(agents).set({
|
|
14340
14775
|
clientId,
|
|
14341
14776
|
updatedAt: /* @__PURE__ */ new Date()
|
|
@@ -14363,7 +14798,8 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14363
14798
|
bindAgentToClient(clientId, agent.id);
|
|
14364
14799
|
boundAgents.set(agent.id, {
|
|
14365
14800
|
agentId: agent.id,
|
|
14366
|
-
inboxId: agent.inboxId
|
|
14801
|
+
inboxId: agent.inboxId,
|
|
14802
|
+
organizationId: agent.organizationId
|
|
14367
14803
|
});
|
|
14368
14804
|
const wsPushActive = pushUseWsDataPlane();
|
|
14369
14805
|
if (wsPushActive) notifier.subscribe(agent.inboxId, socket, makeInboxPushHandler(agent.id, agent.inboxId));
|
|
@@ -14423,7 +14859,9 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14423
14859
|
}, "session:state rejected — stale client wire");
|
|
14424
14860
|
return;
|
|
14425
14861
|
}
|
|
14426
|
-
|
|
14862
|
+
const boundAgentInfo = boundAgents.get(agentId);
|
|
14863
|
+
if (!boundAgentInfo) return;
|
|
14864
|
+
await upsertSessionState(app.db, agentId, payloadResult.data.chatId, payloadResult.data.state, boundAgentInfo.organizationId, notifier);
|
|
14427
14865
|
} else if (type === "session:reconcile") {
|
|
14428
14866
|
const agentId = parsed.data.agentId;
|
|
14429
14867
|
if (!agentId || !boundAgents.has(agentId)) {
|
|
@@ -14460,8 +14898,10 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
14460
14898
|
return;
|
|
14461
14899
|
}
|
|
14462
14900
|
const payload = runtimeStateMessageSchema.parse(msg);
|
|
14901
|
+
const boundAgentInfo = boundAgents.get(agentId);
|
|
14902
|
+
if (!boundAgentInfo) return;
|
|
14463
14903
|
await setRuntimeState(app.db, agentId, payload.runtimeState, {
|
|
14464
|
-
organizationId:
|
|
14904
|
+
organizationId: boundAgentInfo.organizationId,
|
|
14465
14905
|
notifier
|
|
14466
14906
|
});
|
|
14467
14907
|
if (payload.runtimeState === "error" && shouldNotify(agentId, "agent_error")) notifyAgentEvent(app.db, agentId, "agent_error", "high").catch(() => {});
|
|
@@ -14983,6 +15423,7 @@ async function listActiveMemberships(db, userId) {
|
|
|
14983
15423
|
memberId: members.id,
|
|
14984
15424
|
organizationId: members.organizationId,
|
|
14985
15425
|
role: members.role,
|
|
15426
|
+
agentId: members.agentId,
|
|
14986
15427
|
orgName: organizations.name,
|
|
14987
15428
|
orgDisplayName: organizations.displayName,
|
|
14988
15429
|
createdAt: members.createdAt
|
|
@@ -15324,6 +15765,36 @@ async function bootstrapConfigRoutes(app) {
|
|
|
15324
15765
|
return { allowedOrg: app.config.github.allowedOrg ?? null };
|
|
15325
15766
|
});
|
|
15326
15767
|
}
|
|
15768
|
+
/**
|
|
15769
|
+
* Member-scoped client routes.
|
|
15770
|
+
*
|
|
15771
|
+
* Mounted at `/me/clients` to keep them off the admin surface (the legacy
|
|
15772
|
+
* admin clients router lives at `/clients`). The only operation here is
|
|
15773
|
+
* `POST /:clientId/claim`: ownership transfer driven by the operator running
|
|
15774
|
+
* `first-tree-hub client claim --confirm` after a 4403 handshake mismatch
|
|
15775
|
+
* (decouple-client-from-identity §4.4).
|
|
15776
|
+
*/
|
|
15777
|
+
async function memberClientRoutes(app) {
|
|
15778
|
+
app.post("/:clientId/claim", async (request, reply) => {
|
|
15779
|
+
const m = requireMember(request);
|
|
15780
|
+
const { clientId } = request.params;
|
|
15781
|
+
const result = await claimClient(app.db, clientId, m.userId);
|
|
15782
|
+
const droppedAgentIds = forceDisconnectClient(clientId);
|
|
15783
|
+
request.log.info({
|
|
15784
|
+
event: "client.owner_transfer",
|
|
15785
|
+
clientId,
|
|
15786
|
+
fromUserId: result.previousUserId,
|
|
15787
|
+
toUserId: m.userId,
|
|
15788
|
+
unpinnedAgentCount: result.unpinnedAgentIds.length,
|
|
15789
|
+
droppedSocketAgentCount: droppedAgentIds.length
|
|
15790
|
+
}, "client ownership transferred via /me/clients/:clientId/claim");
|
|
15791
|
+
return reply.status(200).send({
|
|
15792
|
+
clientId,
|
|
15793
|
+
previousUserId: result.previousUserId,
|
|
15794
|
+
unpinnedAgentCount: result.unpinnedAgentIds.length
|
|
15795
|
+
});
|
|
15796
|
+
});
|
|
15797
|
+
}
|
|
15327
15798
|
async function contextTreeInfoRoutes(app) {
|
|
15328
15799
|
/** Public endpoint — returns Context Tree repo metadata for CLI auto-discovery. */
|
|
15329
15800
|
app.get("/info", async () => {
|
|
@@ -15453,6 +15924,7 @@ async function meRoutes(app) {
|
|
|
15453
15924
|
const inv = await getActiveInvitation(app.db, m.organizationId);
|
|
15454
15925
|
if (inv) inviteUrl = buildInviteUrl(resolvePublicUrl(app, request), inv.token);
|
|
15455
15926
|
}
|
|
15927
|
+
const memberships = await listActiveMemberships(app.db, m.userId);
|
|
15456
15928
|
return {
|
|
15457
15929
|
user: user ?? null,
|
|
15458
15930
|
member: {
|
|
@@ -15461,6 +15933,13 @@ async function meRoutes(app) {
|
|
|
15461
15933
|
role: m.role,
|
|
15462
15934
|
agentId: m.agentId
|
|
15463
15935
|
},
|
|
15936
|
+
memberships: memberships.map((mb) => ({
|
|
15937
|
+
id: mb.memberId,
|
|
15938
|
+
organizationId: mb.organizationId,
|
|
15939
|
+
organizationName: mb.orgDisplayName,
|
|
15940
|
+
role: mb.role,
|
|
15941
|
+
agentId: mb.agentId
|
|
15942
|
+
})),
|
|
15464
15943
|
agent: agent ?? null,
|
|
15465
15944
|
wizard: { step: wizardStep },
|
|
15466
15945
|
inviteUrl
|
|
@@ -15494,6 +15973,20 @@ async function meRoutes(app) {
|
|
|
15494
15973
|
command: `first-tree-hub connect ${token}`
|
|
15495
15974
|
};
|
|
15496
15975
|
});
|
|
15976
|
+
app.get("/me/managed-agents", async (request) => {
|
|
15977
|
+
const m = requireMember(request);
|
|
15978
|
+
return (await listAgentsManagedByUser(app.db, m.userId)).map((r) => ({
|
|
15979
|
+
uuid: r.uuid,
|
|
15980
|
+
name: r.name,
|
|
15981
|
+
displayName: r.displayName,
|
|
15982
|
+
type: r.type,
|
|
15983
|
+
organizationId: r.organizationId,
|
|
15984
|
+
inboxId: r.inboxId,
|
|
15985
|
+
visibility: r.visibility,
|
|
15986
|
+
runtimeProvider: r.runtimeProvider,
|
|
15987
|
+
clientId: r.clientId
|
|
15988
|
+
}));
|
|
15989
|
+
});
|
|
15497
15990
|
app.get("/me/organizations", async (request) => {
|
|
15498
15991
|
const m = requireMember(request);
|
|
15499
15992
|
return (await listActiveMemberships(app.db, m.userId)).map((r) => ({
|
|
@@ -15581,15 +16074,9 @@ async function meRoutes(app) {
|
|
|
15581
16074
|
app.post("/auth/switch-org", async (request, reply) => {
|
|
15582
16075
|
const m = requireMember(request);
|
|
15583
16076
|
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);
|
|
16077
|
+
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
16078
|
if (!target) throw new ForbiddenError("You do not belong to that organization");
|
|
15586
|
-
|
|
15587
|
-
userId: m.userId,
|
|
15588
|
-
memberId: target.id,
|
|
15589
|
-
organizationId: target.organizationId,
|
|
15590
|
-
role: target.role
|
|
15591
|
-
});
|
|
15592
|
-
return reply.send(tokens);
|
|
16079
|
+
return reply.status(204).send();
|
|
15593
16080
|
});
|
|
15594
16081
|
}
|
|
15595
16082
|
/**
|
|
@@ -15605,7 +16092,7 @@ async function meRoutes(app) {
|
|
|
15605
16092
|
* that's the explicit reset path.
|
|
15606
16093
|
*/
|
|
15607
16094
|
async function inferWizardStep(app, m) {
|
|
15608
|
-
const [hasClient] = await app.db.select({ id: clients.id }).from(clients).where(
|
|
16095
|
+
const [hasClient] = await app.db.select({ id: clients.id }).from(clients).where(eq(clients.userId, m.userId)).limit(1);
|
|
15609
16096
|
if (!hasClient) return "connect";
|
|
15610
16097
|
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
16098
|
if (!hasAgent) return "create_agent";
|
|
@@ -15617,7 +16104,7 @@ async function inferWizardStep(app, m) {
|
|
|
15617
16104
|
* landing page.
|
|
15618
16105
|
*/
|
|
15619
16106
|
async function publicInvitePreviewRoute(app) {
|
|
15620
|
-
const { previewInvitation } = await import("./invitation-
|
|
16107
|
+
const { previewInvitation } = await import("./invitation-CBnQyB7o-DVOpS_Ts.mjs");
|
|
15621
16108
|
app.get("/:token/preview", async (request, reply) => {
|
|
15622
16109
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
15623
16110
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -15647,7 +16134,7 @@ async function adminInvitationRoutes(app) {
|
|
|
15647
16134
|
const m = requireMember(request);
|
|
15648
16135
|
if (m.role !== "admin") throw new ForbiddenError("Admin role required");
|
|
15649
16136
|
if (request.params.id !== m.organizationId) throw new ForbiddenError("Cannot rotate invitations for another organization");
|
|
15650
|
-
const { rotateInvitation } = await import("./invitation-
|
|
16137
|
+
const { rotateInvitation } = await import("./invitation-CBnQyB7o-DVOpS_Ts.mjs");
|
|
15651
16138
|
const inv = await rotateInvitation(app.db, m.organizationId, m.userId);
|
|
15652
16139
|
return {
|
|
15653
16140
|
id: inv.id,
|
|
@@ -17629,6 +18116,10 @@ async function buildApp(config) {
|
|
|
17629
18116
|
adminApp.addHook("onRequest", memberAuth);
|
|
17630
18117
|
await adminApp.register(adminClientRoutes);
|
|
17631
18118
|
}, { prefix: "/clients" });
|
|
18119
|
+
await api.register(async (memberApp) => {
|
|
18120
|
+
memberApp.addHook("onRequest", memberAuth);
|
|
18121
|
+
await memberApp.register(memberClientRoutes);
|
|
18122
|
+
}, { prefix: "/me/clients" });
|
|
17632
18123
|
await api.register(async (adminApp) => {
|
|
17633
18124
|
adminApp.addHook("onRequest", memberAuth);
|
|
17634
18125
|
await adminApp.register(adminActivityRoutes);
|
|
@@ -18262,4 +18753,4 @@ function registerSaaSConnectCommand(program) {
|
|
|
18262
18753
|
});
|
|
18263
18754
|
}
|
|
18264
18755
|
//#endregion
|
|
18265
|
-
export {
|
|
18756
|
+
export { success as $, checkServerHealth as A, ensurePostgres as B, checkAgentConfigs as C, checkDocker as D, checkDatabase as E, getClientServiceStatus as F, rotateClientIdWithBackup as G, stopPostgres as H, installClientService as I, removeLocalAgent as J, findStaleAliases as K, isServiceSupported as L, checkWebSocket as M, printResults as N, checkNodeVersion as O, reconcileAgentConfigs as P, fail as Q, resolveCliInvocation as R, runMigrations as S, checkClientConfig as T, ClientRuntime as U, isDockerAvailable as V, handleClientOrgMismatch as W, hasUser as X, createOwner as Y, resolveReplyToFromEnv as Z, onboardCreate as _, declineUpdate as a, ClientUserMismatchError as at, createApiNameResolver as b, COMMAND_VERSION as c, SessionRegistry as ct, isInteractive as d, applyClientLoggerConfig as dt, blank as et, promptAddAgent as f, configureClientLoggerForService as ft, onboardCheck as g, loadOnboardState as h, createExecuteUpdate as i, ClientOrgMismatchError as it, checkServerReachable as j, checkServerConfig as k, reconcileLocalRuntimeProviders as l, cleanWorkspaces as lt, formatCheckReport as m, deriveHubUrlFromToken as n, setJsonMode as nt, promptUpdate as o, FirstTreeHubSDK as ot, promptMissingFields as p, formatStaleReason as q, registerSaaSConnectCommand as r, status as rt, startServer as s, SdkError as st, HubUrlDerivationError as t, print as tt, uploadClientCapabilities as u, probeCapabilities as ut, saveOnboardState as v, checkBackgroundService as w, migrateLocalAgentDirs as x, runHomeMigration as y, uninstallClientService as z };
|