@agent-team-foundation/first-tree-hub 0.11.2 → 0.11.4
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/{bootstrap-B-FRMuvL.mjs → bootstrap-D-Yf8yOc.mjs} +2 -16
- package/dist/cli/index.mjs +44 -41
- package/dist/cli-fetch--tiwKm5S.mjs +167 -0
- package/dist/client-By1K4VVT-DuI6EnSh.mjs +4 -0
- package/dist/{client-CLdRbuml-B416INrm.mjs → client-CLdRbuml-svTO0Eat.mjs} +2 -2
- package/dist/{dist-BLY7Bu-l.mjs → dist-BQtAQNRD.mjs} +1 -1
- package/dist/{dist-FuUBFTEB.mjs → dist-ClFs4WMj.mjs} +55 -1
- package/dist/drizzle/0032_organization_settings.sql +36 -0
- package/dist/drizzle/meta/_journal.json +8 -1
- package/dist/{feishu-GvFABWW5.mjs → feishu-AI3pwmqN.mjs} +4 -3
- package/dist/{getMachineId-bsd-DjLgZlll.mjs → getMachineId-bsd-DyySs8xz.mjs} +2 -2
- package/dist/{getMachineId-bsd-DR4-Dysy.mjs → getMachineId-bsd-c2VImogj.mjs} +2 -2
- package/dist/{getMachineId-darwin-CaD2juTg.mjs → getMachineId-darwin-Cl7TSzgO.mjs} +2 -2
- package/dist/{getMachineId-darwin-B6WCAhc4.mjs → getMachineId-darwin-DKgI8b1d.mjs} +2 -2
- package/dist/{getMachineId-linux-Dk3gWdQK.mjs → getMachineId-linux-1OIMWfdh.mjs} +1 -1
- package/dist/{getMachineId-linux-BeWHG1gK.mjs → getMachineId-linux-cT7EbP10.mjs} +1 -1
- package/dist/{getMachineId-unsupported-BMJQItvF.mjs → getMachineId-unsupported-CkX-YOG1.mjs} +1 -1
- package/dist/{getMachineId-unsupported-Bgz_Je1J.mjs → getMachineId-unsupported-CmVlhzIo.mjs} +1 -1
- package/dist/{getMachineId-win-vJ6VfDRI.mjs → getMachineId-win-C2cM60YT.mjs} +2 -2
- package/dist/{getMachineId-win-CdgcrzCW.mjs → getMachineId-win-Chl03TYe.mjs} +2 -2
- package/dist/index.mjs +10 -9
- package/dist/invitation-DWlyNb8x-BvXubk24.mjs +4 -0
- package/dist/{invitation-Dnn5gGGX-Ce7zbZpn.mjs → invitation-Dnn5gGGX-DXryyvRG.mjs} +1 -1
- package/dist/{multipart-parser-BIksYTkk.mjs → multipart-parser-QRu3OKK4.mjs} +1 -1
- package/dist/{observability-C3nY6Jcz-Bk7FX689.mjs → observability-BAScT_5S-gw1ODB_o.mjs} +140 -17
- package/dist/observability-CYsdAcoF.mjs +5 -0
- package/dist/{saas-connect-Df2CVAGp.mjs → saas-connect-CVoRK0Ex.mjs} +462 -214
- package/dist/{src-CzQ5KF6D.mjs → src-DFlbpJfU.mjs} +2 -2
- package/dist/web/assets/{index-CD7rTdqm.js → index-Bm6hgcvt.js} +1 -1
- package/dist/web/assets/{index-43trJLR8.js → index-k2bWRKc-.js} +87 -87
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/client-By1K4VVT-nVOhsXBy.mjs +0 -4
- package/dist/invitation-DWlyNb8x-BEgoZ9k1.mjs +0 -4
- package/dist/observability-DttujCqj.mjs +0 -5
- /package/dist/{errors-BmyRwN0Y-CIZZ_sDc.mjs → errors-BmyRwN0Y-Dad3eV8F.mjs} +0 -0
- /package/dist/{esm-iadMkGbV.mjs → esm-Ci8E1Gtj.mjs} +0 -0
- /package/dist/{execAsync-pImxPKN5.mjs → execAsync-DUfRkc4a.mjs} +0 -0
- /package/dist/{execAsync-CCyouKZM.mjs → execAsync-YbEZSOYd.mjs} +0 -0
- /package/dist/{from-CaD373S1.mjs → from-DQ7eNRwu.mjs} +0 -0
- /package/dist/{src-DNBS5Yjj.mjs → src-aJMV60mR.mjs} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw8zbkd.mjs";
|
|
2
|
-
import { C as
|
|
3
|
-
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-
|
|
4
|
-
import {
|
|
5
|
-
import { a as
|
|
6
|
-
import {
|
|
7
|
-
import { n as
|
|
8
|
-
import {
|
|
2
|
+
import { A as FIRST_TREE_HUB_ATTR, C as stampOrgScope, D as untrustedAttrs, E as startWsConnectionSpan, M as require_pino, O as withSpan, S as stampChatResource, _ as rootLogger$1, a as buildRateLimitError, c as currentTraceId, f as messageAttrs, g as reportErrorToRoot, i as bodyCaptureOnSendHook, j as redactUrl, k as withWsMessageSpan, l as decodeJwtForTrace, m as observabilityPlugin, n as applyLoggerConfig, o as classifyJoseError, r as attachRequestContext, s as createLogger$1, t as adapterAttrs, u as endWsConnectionSpan, x as stampAgentResource, y as setWsConnectionAttrs } from "./observability-BAScT_5S-gw1ODB_o.mjs";
|
|
3
|
+
import { C as resetConfigMeta, E as setConfigValue, S as resetConfig, T as serverConfigSchema, b as migrateLegacyHome, c as resolveServerUrl, d as DEFAULT_CONFIG_DIR, f as DEFAULT_DATA_DIR$1, g as collectMissingPrompts, h as clientConfigSchema, i as ensureFreshAccessToken, l as saveAgentConfig, m as agentConfigSchema, o as loadCredentials, p as DEFAULT_HOME_DIR$1, u as saveCredentials, v as initConfig, w as resolveConfigReadonly, y as loadAgents } from "./bootstrap-D-Yf8yOc.mjs";
|
|
4
|
+
import { a as print, i as blank, n as CLI_USER_AGENT, r as COMMAND_VERSION, s as status, t as cliFetch } from "./cli-fetch--tiwKm5S.mjs";
|
|
5
|
+
import { $ as loginSchema, A as createAgentSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateMemberSchema, D as contextTreeSnapshotSchema, E as connectTokenExchangeSchema, Et as wsAuthFrameSchema, F as createTaskSchema, G as inboxDeliverFrameSchema$1, H as githubStartQuerySchema, I as defaultRuntimeConfigPayload, J as isRedactedEnvValue, K as inboxPollQuerySchema, L as delegateFeishuUserSchema, M as createMeChatSchema, N as createMemberSchema, O as createAdapterConfigSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as updateClientCapabilitiesSchema, T as clientRegisterSchema, Tt as updateTaskStatusSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as taskListQuerySchema, a as AGENT_STATUSES, at as runtimeStateMessageSchema, b as agentBindRequestSchema, bt as updateAgentSchema, ct as selfServiceFeishuBotSchema, d as TASK_CREATOR_TYPES, dt as sessionCompletionMessageSchema, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as sessionEventMessageSchema, g as addMeChatParticipantsSchema, gt as stripCode, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionStateMessageSchema, i as AGENT_SOURCES, it as refreshTokenSchema, j as createChatSchema, k as createAdapterMappingSchema, l as MENTION_REGEX, lt as sendMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sessionReconcileRequestSchema, n as AGENT_NAME_REGEX$1, nt as paginationQuerySchema, o as AGENT_TYPES, ot as safeRedirectPath, p as TASK_STATUSES, pt as sessionEventSchema$1, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as rebindAgentSchema, s as AGENT_VISIBILITY, st as scanMentionTokens, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as sendToAgentSchema, v as adminCreateTaskSchema, vt as updateAdapterConfigSchema, wt as updateOrganizationSchema, x as agentPinnedMessageSchema$1, xt as updateChatSchema, y as adminUpdateTaskSchema, yt as updateAgentRuntimeConfigSchema, z as extractMentions } from "./dist-ClFs4WMj.mjs";
|
|
6
|
+
import { a as ConflictError, c as UnauthorizedError, i as ClientUserMismatchError$1, l as organizations, n as BadRequestError, o as ForbiddenError, r as ClientOrgMismatchError$1, s as NotFoundError, t as AppError, u as users } from "./errors-BmyRwN0Y-Dad3eV8F.mjs";
|
|
7
|
+
import { C as retireClient, D as touchAgent, E as setRuntimeState, O as unbindAgent, S as registerClient, T as setOffline, _ as listClients, a as claimClient, b as markStaleAgents, c as clients, d as getClient, f as getOnlineCount, g as listActiveAgentsPinnedToClient, h as heartbeatInstance, i as bindAgent, k as updateClientCapabilities, l as deriveAuthState, m as heartbeatClient, n as agents, o as cleanupStaleClients, p as getPresence, r as assertClientOwner, s as cleanupStalePresence, t as agentPresence, u as disconnectClient, v as listClientsForOrgAdmin, w as serverInstances, x as members } from "./client-CLdRbuml-svTO0Eat.mjs";
|
|
8
|
+
import { n as init_esm, r as trace, t as esm_exports } from "./esm-Ci8E1Gtj.mjs";
|
|
9
|
+
import { a as invitationRedemptions, c as recordRedemption, i as getActiveInvitation, l as rotateInvitation, n as ensureActiveInvitation, o as invitations, r as findActiveByToken, t as buildInviteUrl, u as uuidv7 } from "./invitation-Dnn5gGGX-DXryyvRG.mjs";
|
|
9
10
|
import { createRequire } from "node:module";
|
|
10
11
|
import { ZodError, z } from "zod";
|
|
11
12
|
import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
@@ -29,12 +30,12 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|
|
29
30
|
import postgres from "postgres";
|
|
30
31
|
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
31
32
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
33
|
+
import { SignJWT, jwtVerify } from "jose";
|
|
32
34
|
import cors from "@fastify/cors";
|
|
33
35
|
import rateLimit from "@fastify/rate-limit";
|
|
34
36
|
import fastifyStatic from "@fastify/static";
|
|
35
37
|
import websocket from "@fastify/websocket";
|
|
36
38
|
import Fastify from "fastify";
|
|
37
|
-
import { SignJWT, jwtVerify } from "jose";
|
|
38
39
|
import { promisify } from "node:util";
|
|
39
40
|
import matter from "gray-matter";
|
|
40
41
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
@@ -1291,6 +1292,57 @@ z.object({
|
|
|
1291
1292
|
displayName: z.string().optional(),
|
|
1292
1293
|
next: z.string().max(256).optional()
|
|
1293
1294
|
});
|
|
1295
|
+
/**
|
|
1296
|
+
* Per-organization settings — schemas, namespaces, and the registry that
|
|
1297
|
+
* dispatches `(orgId, namespace)` lookups to the right validator.
|
|
1298
|
+
*
|
|
1299
|
+
* Each namespace has three schemas:
|
|
1300
|
+
* - `storage` — what is persisted in `organization_settings.value`. For
|
|
1301
|
+
* namespaces with secrets, the storage schema names the *cipher* field
|
|
1302
|
+
* (e.g. `webhookSecretCipher`); plaintext never touches the row.
|
|
1303
|
+
* - `input` — what the admin API accepts in PUT bodies. For namespaces
|
|
1304
|
+
* with secrets, `webhookSecret` is plaintext; the service layer
|
|
1305
|
+
* encrypts it before merging into storage.
|
|
1306
|
+
* - `output` — what GET returns. Secrets are replaced by a boolean
|
|
1307
|
+
* `…Configured` flag — plaintext is never echoed.
|
|
1308
|
+
*
|
|
1309
|
+
* Adding a new per-org config group:
|
|
1310
|
+
* 1. Define three schemas (storage / input / output).
|
|
1311
|
+
* 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
|
|
1312
|
+
* 3. Done. No DB migration, no new API route.
|
|
1313
|
+
*/
|
|
1314
|
+
const orgContextTreeStorageSchema = z.object({
|
|
1315
|
+
repo: z.string().url().optional(),
|
|
1316
|
+
branch: z.string().default("main")
|
|
1317
|
+
});
|
|
1318
|
+
const orgContextTreeInputSchema = z.object({
|
|
1319
|
+
repo: z.string().url().min(1).nullish(),
|
|
1320
|
+
branch: z.string().min(1).nullish()
|
|
1321
|
+
});
|
|
1322
|
+
const orgContextTreeOutputSchema = z.object({
|
|
1323
|
+
repo: z.string().optional(),
|
|
1324
|
+
branch: z.string().optional()
|
|
1325
|
+
});
|
|
1326
|
+
const orgGithubIntegrationStorageSchema = z.object({ webhookSecretCipher: z.string().optional() });
|
|
1327
|
+
const orgGithubIntegrationInputSchema = z.object({ webhookSecret: z.string().min(1).nullish() });
|
|
1328
|
+
const orgGithubIntegrationOutputSchema = z.object({
|
|
1329
|
+
webhookSecretConfigured: z.boolean(),
|
|
1330
|
+
webhookUrl: z.string()
|
|
1331
|
+
});
|
|
1332
|
+
const ORG_SETTINGS_NAMESPACES = {
|
|
1333
|
+
context_tree: {
|
|
1334
|
+
storage: orgContextTreeStorageSchema,
|
|
1335
|
+
input: orgContextTreeInputSchema,
|
|
1336
|
+
output: orgContextTreeOutputSchema
|
|
1337
|
+
},
|
|
1338
|
+
github_integration: {
|
|
1339
|
+
storage: orgGithubIntegrationStorageSchema,
|
|
1340
|
+
input: orgGithubIntegrationInputSchema,
|
|
1341
|
+
output: orgGithubIntegrationOutputSchema
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
const ORG_SETTINGS_NAMESPACE_KEYS = Object.keys(ORG_SETTINGS_NAMESPACES);
|
|
1345
|
+
z.enum(ORG_SETTINGS_NAMESPACE_KEYS);
|
|
1294
1346
|
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1295
1347
|
z.object({
|
|
1296
1348
|
name: z.string().min(2).max(50).regex(/^[a-z0-9][a-z0-9-]*$/, "Must start with a letter or digit and contain only lowercase alphanumeric and hyphens").refine((v) => !UUID_PATTERN.test(v), "Name must not be a UUID format"),
|
|
@@ -1683,21 +1735,6 @@ defineConfig({
|
|
|
1683
1735
|
refreshTokenExpiry: field(z.string().default("30d"), { env: "FIRST_TREE_HUB_AUTH_REFRESH_TOKEN_EXPIRY" }),
|
|
1684
1736
|
connectTokenExpiry: field(z.string().default("10m"), { env: "FIRST_TREE_HUB_AUTH_CONNECT_TOKEN_EXPIRY" })
|
|
1685
1737
|
},
|
|
1686
|
-
contextTree: optional({
|
|
1687
|
-
repo: field(z.string().optional(), {
|
|
1688
|
-
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
1689
|
-
prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
|
|
1690
|
-
}),
|
|
1691
|
-
localPath: field(z.string().optional(), { env: "FIRST_TREE_HUB_CONTEXT_TREE_PATH" }),
|
|
1692
|
-
branch: field(z.string().default("main"))
|
|
1693
|
-
}),
|
|
1694
|
-
github: {
|
|
1695
|
-
webhookSecret: field(z.string().optional(), {
|
|
1696
|
-
env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
|
|
1697
|
-
secret: true
|
|
1698
|
-
}),
|
|
1699
|
-
allowedOrg: field(z.string().optional(), { env: "FIRST_TREE_HUB_GITHUB_ALLOWED_ORG" })
|
|
1700
|
-
},
|
|
1701
1738
|
oauth: optional({ github: optional({
|
|
1702
1739
|
clientId: field(z.string(), { env: "FIRST_TREE_HUB_GITHUB_OAUTH_CLIENT_ID" }),
|
|
1703
1740
|
clientSecret: field(z.string(), {
|
|
@@ -1808,10 +1845,12 @@ var FirstTreeHubSDK = class {
|
|
|
1808
1845
|
_baseUrl;
|
|
1809
1846
|
getAccessToken;
|
|
1810
1847
|
_agentId;
|
|
1848
|
+
_userAgent;
|
|
1811
1849
|
constructor(config) {
|
|
1812
1850
|
this._baseUrl = config.serverUrl.replace(/\/+$/, "");
|
|
1813
1851
|
this.getAccessToken = config.getAccessToken;
|
|
1814
1852
|
this._agentId = config.agentId;
|
|
1853
|
+
this._userAgent = config.userAgent;
|
|
1815
1854
|
}
|
|
1816
1855
|
/** Server base URL (without trailing slash). */
|
|
1817
1856
|
get serverUrl() {
|
|
@@ -1863,7 +1902,12 @@ var FirstTreeHubSDK = class {
|
|
|
1863
1902
|
async isHubReachable(timeoutMs = 3e3) {
|
|
1864
1903
|
try {
|
|
1865
1904
|
const url = `${this._baseUrl}/api/v1/health`;
|
|
1866
|
-
|
|
1905
|
+
const headers = {};
|
|
1906
|
+
if (this._userAgent) headers["User-Agent"] = this._userAgent;
|
|
1907
|
+
return (await fetch(url, {
|
|
1908
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1909
|
+
headers
|
|
1910
|
+
})).ok;
|
|
1867
1911
|
} catch {
|
|
1868
1912
|
return false;
|
|
1869
1913
|
}
|
|
@@ -1922,6 +1966,7 @@ var FirstTreeHubSDK = class {
|
|
|
1922
1966
|
const url = `${this._baseUrl}${path}`;
|
|
1923
1967
|
const headers = { Authorization: `Bearer ${await this.getAccessToken()}` };
|
|
1924
1968
|
if (this._agentId) headers[AGENT_SELECTOR_HEADER] = this._agentId;
|
|
1969
|
+
if (this._userAgent) headers["User-Agent"] = this._userAgent;
|
|
1925
1970
|
if (init?.body) headers["Content-Type"] = "application/json";
|
|
1926
1971
|
const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
1927
1972
|
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
@@ -2031,6 +2076,7 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2031
2076
|
clientId;
|
|
2032
2077
|
serverUrl;
|
|
2033
2078
|
sdkVersion;
|
|
2079
|
+
userAgent;
|
|
2034
2080
|
getAccessToken;
|
|
2035
2081
|
ws = null;
|
|
2036
2082
|
wsConnectTimer = null;
|
|
@@ -2084,6 +2130,7 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2084
2130
|
this.clientId = config.clientId ?? process.env.FIRST_TREE_HUB_CLIENT_ID ?? `client_${randomUUID().slice(0, 8)}`;
|
|
2085
2131
|
this.serverUrl = config.serverUrl.replace(/\/+$/, "");
|
|
2086
2132
|
this.sdkVersion = config.sdkVersion;
|
|
2133
|
+
this.userAgent = config.userAgent;
|
|
2087
2134
|
this.getAccessToken = config.getAccessToken;
|
|
2088
2135
|
this.wsLogger = createLogger("ws").child({ clientId: this.clientId });
|
|
2089
2136
|
this.authLogger = createLogger("auth").child({ clientId: this.clientId });
|
|
@@ -2225,7 +2272,7 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2225
2272
|
return new Promise((resolve, reject) => {
|
|
2226
2273
|
const wsUrl = `${this.serverUrl.replace(/^http/, "ws")}/api/v1/agent/ws/client`;
|
|
2227
2274
|
this.wsLogger.info({ url: wsUrl }, "connecting");
|
|
2228
|
-
const ws = new WebSocket(wsUrl);
|
|
2275
|
+
const ws = new WebSocket(wsUrl, this.userAgent ? { headers: { "User-Agent": this.userAgent } } : void 0);
|
|
2229
2276
|
let settled = false;
|
|
2230
2277
|
const settle = (fn, value) => {
|
|
2231
2278
|
if (settled) return;
|
|
@@ -2376,7 +2423,8 @@ var ClientConnection = class extends EventEmitter {
|
|
|
2376
2423
|
const sdk = new FirstTreeHubSDK({
|
|
2377
2424
|
serverUrl: this.serverUrl,
|
|
2378
2425
|
getAccessToken: this.getAccessToken,
|
|
2379
|
-
agentId
|
|
2426
|
+
agentId,
|
|
2427
|
+
userAgent: this.userAgent
|
|
2380
2428
|
});
|
|
2381
2429
|
const agent = {
|
|
2382
2430
|
agentId,
|
|
@@ -6062,71 +6110,6 @@ function sleep(ms) {
|
|
|
6062
6110
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6063
6111
|
}
|
|
6064
6112
|
//#endregion
|
|
6065
|
-
//#region src/core/output.ts
|
|
6066
|
-
/**
|
|
6067
|
-
* Print layer — the only place CLI code should write to stdout/stderr.
|
|
6068
|
-
*
|
|
6069
|
-
* Contract:
|
|
6070
|
-
* - `print.result(data)` / `print.fail(...)` emit machine-readable JSON on
|
|
6071
|
-
* stdout / stderr respectively. Scripts pipe into `jq` and expect a clean
|
|
6072
|
-
* envelope, so nothing else may touch stdout.
|
|
6073
|
-
* - `print.status` / `print.check` / `print.blank` / `print.line` are
|
|
6074
|
-
* human-friendly and go to stderr so they never pollute a redirected stdout.
|
|
6075
|
-
* In `--json` mode they are silenced — scripted consumers only care about
|
|
6076
|
-
* the envelope.
|
|
6077
|
-
*/
|
|
6078
|
-
let jsonMode = false;
|
|
6079
|
-
function setJsonMode(enabled) {
|
|
6080
|
-
jsonMode = enabled;
|
|
6081
|
-
}
|
|
6082
|
-
function result(data) {
|
|
6083
|
-
process.stdout.write(`${JSON.stringify({
|
|
6084
|
-
ok: true,
|
|
6085
|
-
data
|
|
6086
|
-
})}\n`);
|
|
6087
|
-
}
|
|
6088
|
-
function fail$1(code, message, exitCode = 1) {
|
|
6089
|
-
process.stderr.write(`${JSON.stringify({
|
|
6090
|
-
ok: false,
|
|
6091
|
-
error: {
|
|
6092
|
-
code,
|
|
6093
|
-
message
|
|
6094
|
-
}
|
|
6095
|
-
})}\n`);
|
|
6096
|
-
process.exit(exitCode);
|
|
6097
|
-
}
|
|
6098
|
-
function status(label, message) {
|
|
6099
|
-
if (jsonMode) return;
|
|
6100
|
-
process.stderr.write(` ${label.padEnd(20)} ${message}\n`);
|
|
6101
|
-
}
|
|
6102
|
-
function check(pass, label, detail = "") {
|
|
6103
|
-
if (jsonMode) return;
|
|
6104
|
-
const icon = pass ? "✓" : "✗";
|
|
6105
|
-
const tail = detail ? ` ${detail}` : "";
|
|
6106
|
-
process.stderr.write(` ${icon} ${label.padEnd(22)}${tail}\n`);
|
|
6107
|
-
}
|
|
6108
|
-
function blank() {
|
|
6109
|
-
if (jsonMode) return;
|
|
6110
|
-
process.stderr.write("\n");
|
|
6111
|
-
}
|
|
6112
|
-
/**
|
|
6113
|
-
* Generic stderr writer for pre-formatted human text (multi-line tables,
|
|
6114
|
-
* interactive prompts). Prefer `status` / `check` when the text fits; this
|
|
6115
|
-
* exists so the `--json` mode gate can silence arbitrary human chatter.
|
|
6116
|
-
*/
|
|
6117
|
-
function line(text) {
|
|
6118
|
-
if (jsonMode) return;
|
|
6119
|
-
process.stderr.write(text);
|
|
6120
|
-
}
|
|
6121
|
-
const print = {
|
|
6122
|
-
result,
|
|
6123
|
-
fail: fail$1,
|
|
6124
|
-
status,
|
|
6125
|
-
check,
|
|
6126
|
-
blank,
|
|
6127
|
-
line
|
|
6128
|
-
};
|
|
6129
|
-
//#endregion
|
|
6130
6113
|
//#region src/cli/output.ts
|
|
6131
6114
|
/**
|
|
6132
6115
|
* CLI output re-exports. The underlying implementation lives in
|
|
@@ -6491,6 +6474,7 @@ var ClientRuntime = class {
|
|
|
6491
6474
|
serverUrl,
|
|
6492
6475
|
clientId,
|
|
6493
6476
|
sdkVersion: options.currentVersion,
|
|
6477
|
+
userAgent: CLI_USER_AGENT,
|
|
6494
6478
|
getAccessToken: (opts) => ensureFreshAccessToken(opts)
|
|
6495
6479
|
});
|
|
6496
6480
|
registerBuiltinHandlers();
|
|
@@ -7645,7 +7629,7 @@ async function checkServerHealth() {
|
|
|
7645
7629
|
const port = get(config, "server.port") ?? 8e3;
|
|
7646
7630
|
const url = `http://${host}:${port}/healthz`;
|
|
7647
7631
|
try {
|
|
7648
|
-
const res = await
|
|
7632
|
+
const res = await cliFetch(url, { signal: AbortSignal.timeout(3e3) });
|
|
7649
7633
|
if (res.ok) return {
|
|
7650
7634
|
label: "Server Health",
|
|
7651
7635
|
ok: true,
|
|
@@ -7696,7 +7680,7 @@ async function checkServerReachable() {
|
|
|
7696
7680
|
detail: "not configured (FIRST_TREE_HUB_SERVER_URL or config file)"
|
|
7697
7681
|
};
|
|
7698
7682
|
try {
|
|
7699
|
-
const res = await
|
|
7683
|
+
const res = await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
|
|
7700
7684
|
if (res.ok) return {
|
|
7701
7685
|
label: "Server URL",
|
|
7702
7686
|
ok: true,
|
|
@@ -7841,7 +7825,7 @@ async function checkWebSocket() {
|
|
|
7841
7825
|
};
|
|
7842
7826
|
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
7843
7827
|
try {
|
|
7844
|
-
if ((await
|
|
7828
|
+
if ((await cliFetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
|
|
7845
7829
|
label: "WebSocket",
|
|
7846
7830
|
ok: true,
|
|
7847
7831
|
detail: `${wsUrl} (server reachable)`
|
|
@@ -7872,6 +7856,12 @@ function printResults(results) {
|
|
|
7872
7856
|
}
|
|
7873
7857
|
//#endregion
|
|
7874
7858
|
//#region src/core/migrate.ts
|
|
7859
|
+
function sslOptions$1(url) {
|
|
7860
|
+
try {
|
|
7861
|
+
if (new URL(url).hostname.endsWith(".rds.amazonaws.com")) return { ssl: { rejectUnauthorized: false } };
|
|
7862
|
+
} catch {}
|
|
7863
|
+
return {};
|
|
7864
|
+
}
|
|
7875
7865
|
/**
|
|
7876
7866
|
* Resolve the drizzle migrations directory.
|
|
7877
7867
|
* 1. npm install: embedded at dist/drizzle/ (relative to the built CLI)
|
|
@@ -7905,14 +7895,21 @@ function validateJournalOrder(migrationsFolder) {
|
|
|
7905
7895
|
async function runMigrations(databaseUrl) {
|
|
7906
7896
|
const migrationsFolder = resolveMigrationsFolder();
|
|
7907
7897
|
validateJournalOrder(migrationsFolder);
|
|
7908
|
-
const
|
|
7898
|
+
const ssl = sslOptions$1(databaseUrl);
|
|
7899
|
+
const client = postgres(databaseUrl, {
|
|
7900
|
+
max: 1,
|
|
7901
|
+
...ssl
|
|
7902
|
+
});
|
|
7909
7903
|
const db = drizzle(client);
|
|
7910
7904
|
try {
|
|
7911
7905
|
await migrate(db, { migrationsFolder });
|
|
7912
7906
|
} finally {
|
|
7913
7907
|
await client.end();
|
|
7914
7908
|
}
|
|
7915
|
-
const countClient = postgres(databaseUrl, {
|
|
7909
|
+
const countClient = postgres(databaseUrl, {
|
|
7910
|
+
max: 1,
|
|
7911
|
+
...ssl
|
|
7912
|
+
});
|
|
7916
7913
|
try {
|
|
7917
7914
|
return (await countClient`
|
|
7918
7915
|
SELECT count(*)::int AS count
|
|
@@ -7931,7 +7928,7 @@ function createApiNameResolver(serverUrl, getAccessToken) {
|
|
|
7931
7928
|
if (cache) return cache;
|
|
7932
7929
|
const token = await getAccessToken();
|
|
7933
7930
|
const map = /* @__PURE__ */ new Map();
|
|
7934
|
-
const res = await
|
|
7931
|
+
const res = await cliFetch(`${serverUrl}/api/v1/me/managed-agents`, {
|
|
7935
7932
|
method: "GET",
|
|
7936
7933
|
headers: { Authorization: `Bearer ${token}` },
|
|
7937
7934
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -8163,7 +8160,7 @@ async function onboardCheck(args) {
|
|
|
8163
8160
|
value: serverUrl
|
|
8164
8161
|
});
|
|
8165
8162
|
try {
|
|
8166
|
-
const res = await
|
|
8163
|
+
const res = await cliFetch(`${serverUrl}/api/v1/health`);
|
|
8167
8164
|
items.push({
|
|
8168
8165
|
key: "server_reachable",
|
|
8169
8166
|
label: "Server reachable",
|
|
@@ -8235,7 +8232,7 @@ function formatCheckReport(items) {
|
|
|
8235
8232
|
return lines.join("\n");
|
|
8236
8233
|
}
|
|
8237
8234
|
async function resolveDefaultOrgId$1(serverUrl, accessToken) {
|
|
8238
|
-
const res = await
|
|
8235
|
+
const res = await cliFetch(`${serverUrl}/api/v1/me`, {
|
|
8239
8236
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
8240
8237
|
signal: AbortSignal.timeout(1e4)
|
|
8241
8238
|
});
|
|
@@ -8247,7 +8244,7 @@ async function resolveDefaultOrgId$1(serverUrl, accessToken) {
|
|
|
8247
8244
|
throw new Error("Multiple organizations — pass --org explicitly to onboard");
|
|
8248
8245
|
}
|
|
8249
8246
|
async function createAgentViaAdmin(serverUrl, accessToken, orgId, body) {
|
|
8250
|
-
const res = await
|
|
8247
|
+
const res = await cliFetch(`${serverUrl}/api/v1/orgs/${encodeURIComponent(orgId)}/agents`, {
|
|
8251
8248
|
method: "POST",
|
|
8252
8249
|
headers: {
|
|
8253
8250
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -8306,7 +8303,7 @@ async function onboardCreate(args) {
|
|
|
8306
8303
|
}
|
|
8307
8304
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
8308
8305
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
8309
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
8306
|
+
const { bindFeishuBot } = await import("./feishu-AI3pwmqN.mjs").then((n) => n.r);
|
|
8310
8307
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
8311
8308
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
8312
8309
|
else {
|
|
@@ -8406,7 +8403,7 @@ async function promptAddAgent(opts = {}) {
|
|
|
8406
8403
|
validate: (v) => v.length > 0 ? true : "Agent UUID is required"
|
|
8407
8404
|
});
|
|
8408
8405
|
const token = await ensureFreshAccessToken();
|
|
8409
|
-
const res = await
|
|
8406
|
+
const res = await cliFetch(`${serverUrl}/api/v1/agents/${encodeURIComponent(agentId)}`, {
|
|
8410
8407
|
headers: { Authorization: `Bearer ${token}` },
|
|
8411
8408
|
signal: AbortSignal.timeout(1e4)
|
|
8412
8409
|
});
|
|
@@ -8478,7 +8475,7 @@ function setNestedByDot(obj, dotPath, value) {
|
|
|
8478
8475
|
* value (the in-band repair path catches any remaining drift on first bind).
|
|
8479
8476
|
*/
|
|
8480
8477
|
async function reconcileLocalRuntimeProviders(opts) {
|
|
8481
|
-
const res = await
|
|
8478
|
+
const res = await cliFetch(`${opts.serverUrl}/api/v1/me/pinned-agents`, { headers: { Authorization: `Bearer ${opts.accessToken}` } });
|
|
8482
8479
|
if (!res.ok) throw new Error(`hub returned ${res.status} on /clients/me/agents`);
|
|
8483
8480
|
const items = await res.json();
|
|
8484
8481
|
const byAgentId = new Map(items.map((it) => [it.agentId, it]));
|
|
@@ -8518,7 +8515,7 @@ async function reconcileLocalRuntimeProviders(opts) {
|
|
|
8518
8515
|
* client startup since capabilities only matter for UI / admin checks.
|
|
8519
8516
|
*/
|
|
8520
8517
|
async function uploadClientCapabilities(opts) {
|
|
8521
|
-
const res = await
|
|
8518
|
+
const res = await cliFetch(`${opts.serverUrl}/api/v1/clients/${encodeURIComponent(opts.clientId)}/capabilities`, {
|
|
8522
8519
|
method: "PATCH",
|
|
8523
8520
|
headers: {
|
|
8524
8521
|
Authorization: `Bearer ${opts.accessToken}`,
|
|
@@ -9519,7 +9516,7 @@ function createFeedbackHandler(config) {
|
|
|
9519
9516
|
return { handle };
|
|
9520
9517
|
}
|
|
9521
9518
|
//#endregion
|
|
9522
|
-
//#region ../server/dist/app-
|
|
9519
|
+
//#region ../server/dist/app-B8Ncyl76.mjs
|
|
9523
9520
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
9524
9521
|
init_esm();
|
|
9525
9522
|
var __defProp = Object.defineProperty;
|
|
@@ -13653,8 +13650,12 @@ function clientWsRoutes(notifier, instanceId) {
|
|
|
13653
13650
|
return async (app) => {
|
|
13654
13651
|
const jwtSecretBytes = new TextEncoder().encode(app.config.secrets.jwtSecret);
|
|
13655
13652
|
const inboxMaxInFlightPerAgent = app.config.inbox?.maxInFlightPerAgent ?? DEFAULT_INBOX_MAX_IN_FLIGHT_PER_AGENT;
|
|
13656
|
-
app.get("/client", { websocket: true }, async (socket) => {
|
|
13657
|
-
|
|
13653
|
+
app.get("/client", { websocket: true }, async (socket, request) => {
|
|
13654
|
+
const ua = request.headers["user-agent"];
|
|
13655
|
+
startWsConnectionSpan(socket, {
|
|
13656
|
+
remoteIp: request.ip,
|
|
13657
|
+
userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
|
|
13658
|
+
});
|
|
13658
13659
|
let session = null;
|
|
13659
13660
|
let jwtDefaultOrgId = null;
|
|
13660
13661
|
let clientId = null;
|
|
@@ -14566,8 +14567,12 @@ async function refreshAccessToken(db, refreshToken, jwtSecretKey, expiries) {
|
|
|
14566
14567
|
try {
|
|
14567
14568
|
const { payload: p } = await jwtVerify(refreshToken, secret);
|
|
14568
14569
|
payload = p;
|
|
14569
|
-
} catch {
|
|
14570
|
-
|
|
14570
|
+
} catch (err) {
|
|
14571
|
+
const untrusted = decodeJwtForTrace(refreshToken);
|
|
14572
|
+
throw new UnauthorizedError("Invalid or expired refresh token", {
|
|
14573
|
+
"auth.refresh.reason": classifyJoseError(err),
|
|
14574
|
+
...untrustedAttrs("auth.refresh", untrusted)
|
|
14575
|
+
});
|
|
14571
14576
|
}
|
|
14572
14577
|
if (payload.type !== "refresh" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
|
|
14573
14578
|
"auth.refresh.reason": "wrong_token_type",
|
|
@@ -14618,10 +14623,17 @@ async function exchangeConnectToken(db, connectToken, jwtSecretKey, expiries) {
|
|
|
14618
14623
|
try {
|
|
14619
14624
|
const { payload: p } = await jwtVerify(connectToken, secret);
|
|
14620
14625
|
payload = p;
|
|
14621
|
-
} catch {
|
|
14622
|
-
|
|
14626
|
+
} catch (err) {
|
|
14627
|
+
const untrusted = decodeJwtForTrace(connectToken);
|
|
14628
|
+
throw new UnauthorizedError("Invalid or expired connect token", {
|
|
14629
|
+
"auth.connect.reason": classifyJoseError(err),
|
|
14630
|
+
...untrustedAttrs("auth.connect", untrusted)
|
|
14631
|
+
});
|
|
14623
14632
|
}
|
|
14624
|
-
if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token"
|
|
14633
|
+
if (payload.type !== "connect" || !payload.sub) throw new UnauthorizedError("Invalid token type — expected connect token", {
|
|
14634
|
+
"auth.connect.reason": "wrong_token_type",
|
|
14635
|
+
"auth.connect.actual_type": String(payload.type ?? "<missing>")
|
|
14636
|
+
});
|
|
14625
14637
|
const jti = payload.jti;
|
|
14626
14638
|
if (jti) {
|
|
14627
14639
|
if (consumedConnectJtis.has(jti)) throw new UnauthorizedError("Connect token has already been used");
|
|
@@ -15255,10 +15267,18 @@ async function authRoutes(app) {
|
|
|
15255
15267
|
return reply.send(result);
|
|
15256
15268
|
});
|
|
15257
15269
|
}
|
|
15258
|
-
async function bootstrapConfigRoutes(
|
|
15259
|
-
/**
|
|
15260
|
-
|
|
15261
|
-
|
|
15270
|
+
async function bootstrapConfigRoutes(_app) {
|
|
15271
|
+
/**
|
|
15272
|
+
* Public endpoint — returns bootstrap prerequisites for CLI auto-discovery.
|
|
15273
|
+
*
|
|
15274
|
+
* `allowedOrg` used to surface here from the global `github.allowedOrg`
|
|
15275
|
+
* config; it is now a per-org setting (see issue #255). A public bootstrap
|
|
15276
|
+
* endpoint can't resolve an org without a caller, so the field is
|
|
15277
|
+
* surfaced as `null` and consumers should fetch the per-org value via
|
|
15278
|
+
* `/api/v1/orgs/:orgId/settings/github_integration` after auth.
|
|
15279
|
+
*/
|
|
15280
|
+
_app.get("/config", async () => {
|
|
15281
|
+
return { allowedOrg: null };
|
|
15262
15282
|
});
|
|
15263
15283
|
}
|
|
15264
15284
|
/** Extract a plain-text summary from a message's JSONB content field.
|
|
@@ -16027,12 +16047,212 @@ async function clientRoutes(app) {
|
|
|
16027
16047
|
});
|
|
16028
16048
|
});
|
|
16029
16049
|
}
|
|
16050
|
+
/**
|
|
16051
|
+
* Per-organization settings, keyed by `(organization_id, namespace)`.
|
|
16052
|
+
*
|
|
16053
|
+
* One row holds an entire group of related config as a JSONB blob — schema
|
|
16054
|
+
* for each namespace lives in `@agent-team-foundation/first-tree-hub-shared`
|
|
16055
|
+
* (`ORG_SETTINGS_NAMESPACES`) and is enforced by the service layer on every
|
|
16056
|
+
* read/write. Adding a new config group means registering a new namespace +
|
|
16057
|
+
* Zod schema in shared; the DB does not change.
|
|
16058
|
+
*
|
|
16059
|
+
* `version` is reserved for future optimistic locking (PUT with If-Match)
|
|
16060
|
+
* and is currently set unconditionally. We keep it on the table from day
|
|
16061
|
+
* one so tightening to compare-and-swap later is a code-only change.
|
|
16062
|
+
*
|
|
16063
|
+
* Sensitive fields inside `value` (e.g. `github_integration.webhookSecret`)
|
|
16064
|
+
* are AES-256-GCM-encrypted at the service layer using `crypto.ts`'s
|
|
16065
|
+
* `encryptValue` / `decryptValue` — same pattern as `adapter_configs`.
|
|
16066
|
+
*/
|
|
16067
|
+
const organizationSettings = pgTable("organization_settings", {
|
|
16068
|
+
organizationId: text("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
|
16069
|
+
namespace: text("namespace").notNull(),
|
|
16070
|
+
value: jsonb("value").$type().notNull().default({}),
|
|
16071
|
+
version: integer("version").notNull().default(0),
|
|
16072
|
+
updatedBy: text("updated_by").references(() => users.id, { onDelete: "set null" }),
|
|
16073
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
16074
|
+
}, (table) => [primaryKey({ columns: [table.organizationId, table.namespace] }), index("idx_org_settings_namespace").on(table.namespace)]);
|
|
16075
|
+
/**
|
|
16076
|
+
* Per-organization settings, keyed by `(organizationId, namespace)`. The
|
|
16077
|
+
* registry of valid namespaces and their storage / input / output schemas
|
|
16078
|
+
* lives in `@agent-team-foundation/first-tree-hub-shared`.
|
|
16079
|
+
*
|
|
16080
|
+
* Read path: storage row → decrypt secrets → output (mask)
|
|
16081
|
+
* Write path: input → validate → encrypt secrets → merge with current storage → upsert (in tx)
|
|
16082
|
+
*
|
|
16083
|
+
* The generic getter returns the masked output. Callers needing plaintext
|
|
16084
|
+
* for a specific secret use a purpose-built helper (e.g.
|
|
16085
|
+
* `getDecryptedGithubWebhookSecret`) rather than the generic storage shape
|
|
16086
|
+
* — this avoids a `…Cipher` field name silently holding plaintext at
|
|
16087
|
+
* call-sites and limits secret exposure to one explicit code path per
|
|
16088
|
+
* secret. (#4)
|
|
16089
|
+
*/
|
|
16090
|
+
function assertNamespace(ns) {
|
|
16091
|
+
if (!isOrgSettingNamespace(ns)) throw new BadRequestError(`Unknown organization-settings namespace: "${ns}"`);
|
|
16092
|
+
}
|
|
16093
|
+
async function fetchStorageRow(db, orgId, namespace) {
|
|
16094
|
+
const [row] = await db.select({ value: organizationSettings.value }).from(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace))).limit(1);
|
|
16095
|
+
if (!row) return null;
|
|
16096
|
+
return ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse(row.value);
|
|
16097
|
+
}
|
|
16098
|
+
function emptyStorage(namespace) {
|
|
16099
|
+
return ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse({});
|
|
16100
|
+
}
|
|
16101
|
+
function ensureEncrypted(value, encryptionKey) {
|
|
16102
|
+
return isEncryptedValue(value) ? value : encryptValue(value, encryptionKey);
|
|
16103
|
+
}
|
|
16104
|
+
/**
|
|
16105
|
+
* Merge a validated input into the current storage row for a namespace.
|
|
16106
|
+
* Secret fields are encrypted here.
|
|
16107
|
+
*
|
|
16108
|
+
* Input semantics per nullish field:
|
|
16109
|
+
* `undefined` → unchanged
|
|
16110
|
+
* `null` → cleared
|
|
16111
|
+
* value → set / replace (already validated as non-empty by the input schema)
|
|
16112
|
+
*/
|
|
16113
|
+
function applyInputDelta(namespace, current, input, encryptionKey) {
|
|
16114
|
+
if (namespace === "context_tree") {
|
|
16115
|
+
const cur = current;
|
|
16116
|
+
const inp = input;
|
|
16117
|
+
return {
|
|
16118
|
+
repo: inp.repo === void 0 ? cur.repo : inp.repo ?? void 0,
|
|
16119
|
+
branch: inp.branch === void 0 ? cur.branch : inp.branch ?? "main"
|
|
16120
|
+
};
|
|
16121
|
+
}
|
|
16122
|
+
if (namespace === "github_integration") {
|
|
16123
|
+
const cur = current;
|
|
16124
|
+
const inp = input;
|
|
16125
|
+
return { webhookSecretCipher: inp.webhookSecret === void 0 ? cur.webhookSecretCipher : inp.webhookSecret === null ? void 0 : ensureEncrypted(inp.webhookSecret, encryptionKey) };
|
|
16126
|
+
}
|
|
16127
|
+
return namespace;
|
|
16128
|
+
}
|
|
16129
|
+
/**
|
|
16130
|
+
* Project the storage row into the API output for a namespace, masking
|
|
16131
|
+
* any secret fields. `webhookUrl` for `github_integration` is left as an
|
|
16132
|
+
* empty string here — the route layer enriches it with the resolved
|
|
16133
|
+
* `server.publicUrl` (the service stays config-agnostic).
|
|
16134
|
+
*/
|
|
16135
|
+
function toOutput(namespace, storage) {
|
|
16136
|
+
if (namespace === "context_tree") {
|
|
16137
|
+
const s = storage;
|
|
16138
|
+
return {
|
|
16139
|
+
repo: s.repo,
|
|
16140
|
+
branch: s.branch
|
|
16141
|
+
};
|
|
16142
|
+
}
|
|
16143
|
+
if (namespace === "github_integration") {
|
|
16144
|
+
const s = storage;
|
|
16145
|
+
return {
|
|
16146
|
+
webhookSecretConfigured: typeof s.webhookSecretCipher === "string" && s.webhookSecretCipher.length > 0,
|
|
16147
|
+
webhookUrl: ""
|
|
16148
|
+
};
|
|
16149
|
+
}
|
|
16150
|
+
return namespace;
|
|
16151
|
+
}
|
|
16152
|
+
/**
|
|
16153
|
+
* Read a setting masked for the API. Missing rows → namespace defaults
|
|
16154
|
+
* (parse `{}` against the storage schema).
|
|
16155
|
+
*/
|
|
16156
|
+
async function getOrgSetting(db, orgId, namespace) {
|
|
16157
|
+
assertNamespace(namespace);
|
|
16158
|
+
return toOutput(namespace, await fetchStorageRow(db, orgId, namespace) ?? emptyStorage(namespace));
|
|
16159
|
+
}
|
|
16160
|
+
/**
|
|
16161
|
+
* Read the per-org Context Tree binding for server-internal consumers
|
|
16162
|
+
* (`/context-tree/info`, snapshot service). No secrets in this namespace,
|
|
16163
|
+
* so the storage shape is safe to expose directly. Missing row → defaults.
|
|
16164
|
+
*/
|
|
16165
|
+
async function getOrgContextTree(db, orgId) {
|
|
16166
|
+
return await fetchStorageRow(db, orgId, "context_tree") ?? emptyStorage("context_tree");
|
|
16167
|
+
}
|
|
16168
|
+
/**
|
|
16169
|
+
* Decrypt and return the plaintext GitHub webhook secret for an org.
|
|
16170
|
+
* Returns `null` when the org has not configured one. The only intended
|
|
16171
|
+
* caller is the webhook route's signature verifier — the result must
|
|
16172
|
+
* never leak through HTTP responses or logs. (#4)
|
|
16173
|
+
*/
|
|
16174
|
+
async function getDecryptedGithubWebhookSecret(db, orgId, encryptionKey) {
|
|
16175
|
+
const cipher = (await fetchStorageRow(db, orgId, "github_integration"))?.webhookSecretCipher;
|
|
16176
|
+
if (!cipher) return null;
|
|
16177
|
+
return isEncryptedValue(cipher) ? decryptValue(cipher, encryptionKey) : cipher;
|
|
16178
|
+
}
|
|
16179
|
+
/**
|
|
16180
|
+
* Upsert a setting. Returns the masked output of the resulting row.
|
|
16181
|
+
*
|
|
16182
|
+
* The fetch + merge + upsert sequence runs inside a single transaction so
|
|
16183
|
+
* two concurrent admin writes can't both base their delta on the same
|
|
16184
|
+
* pre-image and silently lose each other's fields. Optimistic locking
|
|
16185
|
+
* (the `version` column) remains reserved for a future If-Match flip.
|
|
16186
|
+
* (#6)
|
|
16187
|
+
*/
|
|
16188
|
+
async function putOrgSetting(db, orgId, namespace, rawInput, options) {
|
|
16189
|
+
assertNamespace(namespace);
|
|
16190
|
+
const input = ORG_SETTINGS_NAMESPACES$1[namespace].input.parse(rawInput);
|
|
16191
|
+
return db.transaction(async (tx) => {
|
|
16192
|
+
const txDb = tx;
|
|
16193
|
+
const [org] = await txDb.select({ id: organizations.id }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
|
|
16194
|
+
if (!org) throw new NotFoundError(`Organization "${orgId}" not found`);
|
|
16195
|
+
const merged = applyInputDelta(namespace, await fetchStorageRow(txDb, orgId, namespace) ?? emptyStorage(namespace), input, options.encryptionKey);
|
|
16196
|
+
const validated = ORG_SETTINGS_NAMESPACES$1[namespace].storage.parse(merged);
|
|
16197
|
+
await tx.insert(organizationSettings).values({
|
|
16198
|
+
organizationId: orgId,
|
|
16199
|
+
namespace,
|
|
16200
|
+
value: validated,
|
|
16201
|
+
version: 1,
|
|
16202
|
+
updatedBy: options.updatedBy,
|
|
16203
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
16204
|
+
}).onConflictDoUpdate({
|
|
16205
|
+
target: [organizationSettings.organizationId, organizationSettings.namespace],
|
|
16206
|
+
set: {
|
|
16207
|
+
value: validated,
|
|
16208
|
+
version: sql`${organizationSettings.version} + 1`,
|
|
16209
|
+
updatedBy: options.updatedBy,
|
|
16210
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
16211
|
+
}
|
|
16212
|
+
});
|
|
16213
|
+
return toOutput(namespace, validated);
|
|
16214
|
+
});
|
|
16215
|
+
}
|
|
16216
|
+
/**
|
|
16217
|
+
* Delete a namespace row; subsequent GETs return defaults.
|
|
16218
|
+
*/
|
|
16219
|
+
async function deleteOrgSetting(db, orgId, namespace) {
|
|
16220
|
+
assertNamespace(namespace);
|
|
16221
|
+
await db.delete(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace)));
|
|
16222
|
+
}
|
|
16223
|
+
/**
|
|
16224
|
+
* Resolve the caller's "primary org" — the earliest-joined active
|
|
16225
|
+
* membership for the given user. Used by user-scoped routes that
|
|
16226
|
+
* historically didn't take an `:orgId` (e.g. `/context-tree/info`) so
|
|
16227
|
+
* the SDK call shape doesn't have to change while the per-tenant lookup
|
|
16228
|
+
* still happens correctly.
|
|
16229
|
+
*
|
|
16230
|
+
* Returns `null` for users with no active membership. Tightening to
|
|
16231
|
+
* "explicit org selector" is a future change-once-multi-org-clients-arrive
|
|
16232
|
+
* concern. (#7)
|
|
16233
|
+
*/
|
|
16234
|
+
async function resolveUserPrimaryOrgId(db, userId) {
|
|
16235
|
+
const [row] = await db.select({ organizationId: members.organizationId }).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))).orderBy(asc(members.createdAt)).limit(1);
|
|
16236
|
+
return row?.organizationId ?? null;
|
|
16237
|
+
}
|
|
16030
16238
|
async function contextTreeInfoRoutes(app) {
|
|
16031
|
-
/**
|
|
16032
|
-
|
|
16239
|
+
/**
|
|
16240
|
+
* Class A — `/api/v1/context-tree/info`. Returns the caller's
|
|
16241
|
+
* organization-scoped Context Tree binding for CLI auto-discovery.
|
|
16242
|
+
* Responds with `{ repo: null, branch: null }` when the user is not in
|
|
16243
|
+
* any org or the org hasn't configured a tree yet.
|
|
16244
|
+
*/
|
|
16245
|
+
app.get("/info", async (request) => {
|
|
16246
|
+
const { userId } = requireUser(request);
|
|
16247
|
+
const orgId = await resolveUserPrimaryOrgId(app.db, userId);
|
|
16248
|
+
if (!orgId) return {
|
|
16249
|
+
repo: null,
|
|
16250
|
+
branch: null
|
|
16251
|
+
};
|
|
16252
|
+
const tree = await getOrgContextTree(app.db, orgId);
|
|
16033
16253
|
return {
|
|
16034
|
-
repo:
|
|
16035
|
-
branch:
|
|
16254
|
+
repo: tree.repo ?? null,
|
|
16255
|
+
branch: tree.branch ?? null
|
|
16036
16256
|
};
|
|
16037
16257
|
});
|
|
16038
16258
|
}
|
|
@@ -16058,10 +16278,10 @@ const WINDOW_DAYS = {
|
|
|
16058
16278
|
"30d": 30
|
|
16059
16279
|
};
|
|
16060
16280
|
const snapshotCache = /* @__PURE__ */ new Map();
|
|
16061
|
-
async function getContextTreeSnapshot(
|
|
16062
|
-
const repo =
|
|
16063
|
-
const branch =
|
|
16064
|
-
const resolved = resolveContextTreeRoot(repo,
|
|
16281
|
+
async function getContextTreeSnapshot(binding, window = CONTEXT_TREE_SNAPSHOT_WINDOWS.SEVEN_DAYS) {
|
|
16282
|
+
const repo = binding.repo ?? null;
|
|
16283
|
+
const branch = binding.branch ?? null;
|
|
16284
|
+
const resolved = resolveContextTreeRoot(repo, binding.localPath);
|
|
16065
16285
|
if (!resolved.root) return unavailableSnapshot(repo, branch, resolved.reason);
|
|
16066
16286
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
16067
16287
|
try {
|
|
@@ -16733,7 +16953,9 @@ async function contextTreeSnapshotRoutes(app) {
|
|
|
16733
16953
|
keyGenerator: (request) => request.user?.userId ?? request.ip
|
|
16734
16954
|
} } }, async (request) => {
|
|
16735
16955
|
const query = querySchema.parse(request.query);
|
|
16736
|
-
const
|
|
16956
|
+
const { userId } = requireUser(request);
|
|
16957
|
+
const orgId = await resolveUserPrimaryOrgId(app.db, userId);
|
|
16958
|
+
const snapshot = await getContextTreeSnapshot(orgId ? await getOrgContextTree(app.db, orgId) : {}, query.window ?? "7d");
|
|
16737
16959
|
return contextTreeSnapshotSchema.parse(snapshot);
|
|
16738
16960
|
});
|
|
16739
16961
|
}
|
|
@@ -16838,7 +17060,7 @@ async function healthzRoutes(app) {
|
|
|
16838
17060
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
16839
17061
|
*/
|
|
16840
17062
|
async function publicInvitationRoutes(app) {
|
|
16841
|
-
const { previewInvitation } = await import("./invitation-DWlyNb8x-
|
|
17063
|
+
const { previewInvitation } = await import("./invitation-DWlyNb8x-BvXubk24.mjs");
|
|
16842
17064
|
app.get("/:token/preview", async (request, reply) => {
|
|
16843
17065
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16844
17066
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -16935,7 +17157,7 @@ async function meRoutes(app) {
|
|
|
16935
17157
|
*/
|
|
16936
17158
|
app.get("/me/pinned-agents", async (request) => {
|
|
16937
17159
|
const { userId } = requireUser(request);
|
|
16938
|
-
const { listMyPinnedAgents } = await import("./client-By1K4VVT-
|
|
17160
|
+
const { listMyPinnedAgents } = await import("./client-By1K4VVT-DuI6EnSh.mjs");
|
|
16939
17161
|
return listMyPinnedAgents(app.db, { userId });
|
|
16940
17162
|
});
|
|
16941
17163
|
/**
|
|
@@ -17658,6 +17880,64 @@ async function orgSessionRoutes(app) {
|
|
|
17658
17880
|
});
|
|
17659
17881
|
});
|
|
17660
17882
|
}
|
|
17883
|
+
/**
|
|
17884
|
+
* Class B — `/api/v1/orgs/:orgId/settings/:namespace`.
|
|
17885
|
+
*
|
|
17886
|
+
* Generic per-org settings surface. The `:namespace` URL parameter is
|
|
17887
|
+
* dispatched against `ORG_SETTINGS_NAMESPACES` (in the shared package);
|
|
17888
|
+
* adding a new config group only requires registering it there — no new
|
|
17889
|
+
* route file.
|
|
17890
|
+
*
|
|
17891
|
+
* All three verbs are admin-only. Even GET, because the masked output
|
|
17892
|
+
* still leaks "configured / not-configured" booleans for secret fields,
|
|
17893
|
+
* which we don't want to expose to non-admin members.
|
|
17894
|
+
*/
|
|
17895
|
+
async function orgSettingsRoutes(app) {
|
|
17896
|
+
app.get("/:namespace", async (request) => {
|
|
17897
|
+
const scope = await requireOrgAdmin(request, app.db);
|
|
17898
|
+
const namespace = parseNamespace(request.params.namespace);
|
|
17899
|
+
return enrichOutput(namespace, await getOrgSetting(app.db, scope.organizationId, namespace), scope.organizationId, app.config.server.publicUrl);
|
|
17900
|
+
});
|
|
17901
|
+
app.put("/:namespace", { config: { otelRecordBody: true } }, async (request) => {
|
|
17902
|
+
const scope = await requireOrgAdmin(request, app.db);
|
|
17903
|
+
const namespace = parseNamespace(request.params.namespace);
|
|
17904
|
+
return enrichOutput(namespace, await putOrgSetting(app.db, scope.organizationId, namespace, request.body, {
|
|
17905
|
+
updatedBy: scope.userId,
|
|
17906
|
+
encryptionKey: app.config.secrets.encryptionKey
|
|
17907
|
+
}), scope.organizationId, app.config.server.publicUrl);
|
|
17908
|
+
});
|
|
17909
|
+
app.delete("/:namespace", async (request, reply) => {
|
|
17910
|
+
const scope = await requireOrgAdmin(request, app.db);
|
|
17911
|
+
const namespace = parseNamespace(request.params.namespace);
|
|
17912
|
+
await deleteOrgSetting(app.db, scope.organizationId, namespace);
|
|
17913
|
+
reply.status(204).send();
|
|
17914
|
+
});
|
|
17915
|
+
}
|
|
17916
|
+
function parseNamespace(raw) {
|
|
17917
|
+
if (!isOrgSettingNamespace(raw)) throw new BadRequestError(`Unknown organization-settings namespace: "${raw}"`);
|
|
17918
|
+
return raw;
|
|
17919
|
+
}
|
|
17920
|
+
/**
|
|
17921
|
+
* Resolve namespace-specific server-config-derived fields. The service
|
|
17922
|
+
* layer stays config-agnostic — namespace knowledge that needs `app.config`
|
|
17923
|
+
* lives here. Currently only `github_integration.webhookUrl` qualifies.
|
|
17924
|
+
*
|
|
17925
|
+
* If `server.publicUrl` is unset on the Hub, `webhookUrl` is left as `""`
|
|
17926
|
+
* so the UI can render a "contact your site administrator" notice rather
|
|
17927
|
+
* than fall back to `window.location.origin` (which is wrong behind a
|
|
17928
|
+
* reverse proxy). (#12)
|
|
17929
|
+
*/
|
|
17930
|
+
function enrichOutput(namespace, out, orgId, publicUrl) {
|
|
17931
|
+
if (namespace === "github_integration") {
|
|
17932
|
+
const o = out;
|
|
17933
|
+
const webhookUrl = publicUrl ? `${publicUrl.replace(/\/+$/, "")}/api/v1/webhooks/github/${orgId}` : "";
|
|
17934
|
+
return {
|
|
17935
|
+
...o,
|
|
17936
|
+
webhookUrl
|
|
17937
|
+
};
|
|
17938
|
+
}
|
|
17939
|
+
return out;
|
|
17940
|
+
}
|
|
17661
17941
|
function dispatch$1(notifier, result) {
|
|
17662
17942
|
if (!result) return;
|
|
17663
17943
|
notifyRecipients(notifier, result.recipients, result.message.id);
|
|
@@ -17760,7 +18040,11 @@ function orgWsRoutes(notifier, jwtSecret) {
|
|
|
17760
18040
|
}
|
|
17761
18041
|
return async (app) => {
|
|
17762
18042
|
app.get("/", { websocket: true }, async (socket, request) => {
|
|
17763
|
-
|
|
18043
|
+
const ua = request.headers["user-agent"];
|
|
18044
|
+
startWsConnectionSpan(socket, {
|
|
18045
|
+
remoteIp: request.ip,
|
|
18046
|
+
userAgent: typeof ua === "string" ? ua.slice(0, 200) : void 0
|
|
18047
|
+
});
|
|
17764
18048
|
const orgIdFromPath = request.params.orgId;
|
|
17765
18049
|
const token = request.query.token;
|
|
17766
18050
|
if (!token || !orgIdFromPath) {
|
|
@@ -17953,16 +18237,15 @@ function verifySignature(secret, rawBody, signatureHeader) {
|
|
|
17953
18237
|
const receivedBuf = Buffer.from(signatureHeader, "utf8");
|
|
17954
18238
|
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) throw new UnauthorizedError("Invalid webhook signature");
|
|
17955
18239
|
}
|
|
17956
|
-
async function ensureGitHubAdapterAgent(db) {
|
|
17957
|
-
const
|
|
17958
|
-
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, defaultOrgId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
18240
|
+
async function ensureGitHubAdapterAgent(db, organizationId) {
|
|
18241
|
+
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
17959
18242
|
if (existing) return existing.uuid;
|
|
17960
18243
|
try {
|
|
17961
18244
|
return (await createAgent(db, {
|
|
17962
18245
|
name: GITHUB_ADAPTER_ID,
|
|
17963
18246
|
type: "autonomous_agent",
|
|
17964
18247
|
displayName: "GitHub Adapter",
|
|
17965
|
-
organizationId
|
|
18248
|
+
organizationId,
|
|
17966
18249
|
metadata: {
|
|
17967
18250
|
source: "github",
|
|
17968
18251
|
managed: true
|
|
@@ -17970,19 +18253,19 @@ async function ensureGitHubAdapterAgent(db) {
|
|
|
17970
18253
|
})).uuid;
|
|
17971
18254
|
} catch (err) {
|
|
17972
18255
|
if (err instanceof ConflictError) {
|
|
17973
|
-
const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId,
|
|
18256
|
+
const [created] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.name, GITHUB_ADAPTER_ID))).limit(1);
|
|
17974
18257
|
if (created) return created.uuid;
|
|
17975
18258
|
}
|
|
17976
18259
|
throw err;
|
|
17977
18260
|
}
|
|
17978
18261
|
}
|
|
17979
|
-
async function findTargetAgent(db, repoFullName) {
|
|
18262
|
+
async function findTargetAgent(db, organizationId, repoFullName) {
|
|
17980
18263
|
const allAgents = await db.select({
|
|
17981
18264
|
id: agents.uuid,
|
|
17982
18265
|
name: agents.name,
|
|
17983
18266
|
metadata: agents.metadata,
|
|
17984
18267
|
type: agents.type
|
|
17985
|
-
}).from(agents).where(eq(agents.status, "active"));
|
|
18268
|
+
}).from(agents).where(and(eq(agents.organizationId, organizationId), eq(agents.status, "active")));
|
|
17986
18269
|
for (const agent of allAgents) {
|
|
17987
18270
|
if (agent.name === GITHUB_ADAPTER_ID) continue;
|
|
17988
18271
|
const meta = agent.metadata;
|
|
@@ -18016,14 +18299,14 @@ function extractMentions$1(text) {
|
|
|
18016
18299
|
* For each mentioned user who has delegate_mention configured,
|
|
18017
18300
|
* send a card message from the mentioned user to their delegate.
|
|
18018
18301
|
*/
|
|
18019
|
-
async function routeMentionDelegations(app, mentionedNames, ctx) {
|
|
18302
|
+
async function routeMentionDelegations(app, organizationId, mentionedNames, ctx) {
|
|
18020
18303
|
if (mentionedNames.length === 0) return 0;
|
|
18021
18304
|
const delegates = await app.db.select({
|
|
18022
18305
|
id: agents.uuid,
|
|
18023
18306
|
name: agents.name,
|
|
18024
18307
|
delegateMention: agents.delegateMention,
|
|
18025
18308
|
status: agents.status
|
|
18026
|
-
}).from(agents).where(and(inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
|
|
18309
|
+
}).from(agents).where(and(eq(agents.organizationId, organizationId), inArray(agents.name, mentionedNames), isNotNull(agents.delegateMention)));
|
|
18027
18310
|
let routed = 0;
|
|
18028
18311
|
for (const agent of delegates) {
|
|
18029
18312
|
if (agent.status !== "active" || !agent.delegateMention) continue;
|
|
@@ -18111,13 +18394,14 @@ async function githubWebhookRoutes(app) {
|
|
|
18111
18394
|
app.addContentTypeParser("application/json", { parseAs: "buffer" }, (_request, body, done) => {
|
|
18112
18395
|
done(null, body);
|
|
18113
18396
|
});
|
|
18114
|
-
const webhookSecret = app.config.github.webhookSecret;
|
|
18115
18397
|
const webhookMax = app.config.rateLimit?.webhookMax ?? 60;
|
|
18116
|
-
app.post("/github", { config: { rateLimit: {
|
|
18398
|
+
app.post("/github/:orgId", { config: { rateLimit: {
|
|
18117
18399
|
max: webhookMax,
|
|
18118
18400
|
timeWindow: "1 minute"
|
|
18119
18401
|
} } }, async (request, reply) => {
|
|
18120
|
-
|
|
18402
|
+
const { orgId } = request.params;
|
|
18403
|
+
const webhookSecret = await getDecryptedGithubWebhookSecret(app.db, orgId, app.config.secrets.encryptionKey);
|
|
18404
|
+
if (!webhookSecret) return reply.status(501).send({ error: "GitHub webhook is not configured for this organization. An admin must set the webhook secret in Team settings." });
|
|
18121
18405
|
const rawBody = request.body;
|
|
18122
18406
|
if (!Buffer.isBuffer(rawBody)) throw new BadRequestError("Expected raw body buffer");
|
|
18123
18407
|
const signatureHeader = request.headers["x-hub-signature-256"];
|
|
@@ -18135,12 +18419,12 @@ async function githubWebhookRoutes(app) {
|
|
|
18135
18419
|
ok: true,
|
|
18136
18420
|
event: "ping"
|
|
18137
18421
|
});
|
|
18138
|
-
if (eventType === "issues") return handleIssuesEvent(app, eventType, payload, reply);
|
|
18139
|
-
if (eventType === "issue_comment") return handleIssueCommentEvent(app, eventType, payload, reply);
|
|
18422
|
+
if (eventType === "issues") return handleIssuesEvent(app, orgId, eventType, payload, reply);
|
|
18423
|
+
if (eventType === "issue_comment") return handleIssueCommentEvent(app, orgId, eventType, payload, reply);
|
|
18140
18424
|
let mentionsRouted = 0;
|
|
18141
18425
|
const allowedActions = MENTION_ACTIONS[eventType];
|
|
18142
18426
|
const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
|
|
18143
|
-
if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, eventType, payload);
|
|
18427
|
+
if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
|
|
18144
18428
|
return reply.status(200).send({
|
|
18145
18429
|
ok: true,
|
|
18146
18430
|
event: eventType,
|
|
@@ -18294,10 +18578,10 @@ function extractEventContext(eventType, payload) {
|
|
|
18294
18578
|
* Run mention delegation for a given event type and payload.
|
|
18295
18579
|
* Only called after action gating confirms this is a "new content" event.
|
|
18296
18580
|
*/
|
|
18297
|
-
async function handleMentionDelegation(app, eventType, payload) {
|
|
18581
|
+
async function handleMentionDelegation(app, organizationId, eventType, payload) {
|
|
18298
18582
|
const mentions = extractMentions$1(extractEventText(eventType, payload));
|
|
18299
18583
|
const mentionCtx = extractEventContext(eventType, payload);
|
|
18300
|
-
if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, mentions, mentionCtx);
|
|
18584
|
+
if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
|
|
18301
18585
|
return 0;
|
|
18302
18586
|
}
|
|
18303
18587
|
/** Actions that represent new/changed content (worth scanning for @mentions). */
|
|
@@ -18311,9 +18595,9 @@ const MENTION_ACTIONS = {
|
|
|
18311
18595
|
discussion_comment: ["created"],
|
|
18312
18596
|
commit_comment: ["created"]
|
|
18313
18597
|
};
|
|
18314
|
-
async function handleIssuesEvent(app, eventType, payload, reply) {
|
|
18598
|
+
async function handleIssuesEvent(app, organizationId, eventType, payload, reply) {
|
|
18315
18599
|
const data = parseIssuesPayload(payload);
|
|
18316
|
-
if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, eventType, payload);
|
|
18600
|
+
if (MENTION_ACTIONS.issues?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18317
18601
|
if (![
|
|
18318
18602
|
"opened",
|
|
18319
18603
|
"edited",
|
|
@@ -18324,7 +18608,7 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
|
|
|
18324
18608
|
action: data.action,
|
|
18325
18609
|
handled: false
|
|
18326
18610
|
});
|
|
18327
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
|
|
18611
|
+
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18328
18612
|
if (!targetAgentId) {
|
|
18329
18613
|
log$1.warn({
|
|
18330
18614
|
repo: data.repository.full_name,
|
|
@@ -18370,16 +18654,16 @@ async function handleIssuesEvent(app, eventType, payload, reply) {
|
|
|
18370
18654
|
routed: true
|
|
18371
18655
|
});
|
|
18372
18656
|
}
|
|
18373
|
-
async function handleIssueCommentEvent(app, eventType, payload, reply) {
|
|
18657
|
+
async function handleIssueCommentEvent(app, organizationId, eventType, payload, reply) {
|
|
18374
18658
|
const data = parseIssueCommentPayload(payload);
|
|
18375
|
-
if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, eventType, payload);
|
|
18659
|
+
if (MENTION_ACTIONS.issue_comment?.includes(data.action)) await handleMentionDelegation(app, organizationId, eventType, payload);
|
|
18376
18660
|
if (data.action !== "created") return reply.status(200).send({
|
|
18377
18661
|
ok: true,
|
|
18378
18662
|
event: "issue_comment",
|
|
18379
18663
|
action: data.action,
|
|
18380
18664
|
handled: false
|
|
18381
18665
|
});
|
|
18382
|
-
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db), findTargetAgent(app.db, data.repository.full_name)]);
|
|
18666
|
+
const [senderId, targetAgentId] = await Promise.all([ensureGitHubAdapterAgent(app.db, organizationId), findTargetAgent(app.db, organizationId, data.repository.full_name)]);
|
|
18383
18667
|
if (!targetAgentId) {
|
|
18384
18668
|
log$1.warn({
|
|
18385
18669
|
repo: data.repository.full_name,
|
|
@@ -18449,6 +18733,7 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18449
18733
|
members: () => members,
|
|
18450
18734
|
messages: () => messages,
|
|
18451
18735
|
notifications: () => notifications,
|
|
18736
|
+
organizationSettings: () => organizationSettings,
|
|
18452
18737
|
organizations: () => organizations,
|
|
18453
18738
|
serverInstances: () => serverInstances,
|
|
18454
18739
|
sessionEvents: () => sessionEvents,
|
|
@@ -18457,10 +18742,16 @@ var schema_exports = /* @__PURE__ */ __exportAll({
|
|
|
18457
18742
|
users: () => users
|
|
18458
18743
|
});
|
|
18459
18744
|
function connectDatabase(url) {
|
|
18460
|
-
const client = postgres(url);
|
|
18745
|
+
const client = postgres(url, sslOptions(url));
|
|
18461
18746
|
const db = drizzle(client, { schema: schema_exports });
|
|
18462
18747
|
return Object.assign(db, { end: () => client.end() });
|
|
18463
18748
|
}
|
|
18749
|
+
function sslOptions(url) {
|
|
18750
|
+
try {
|
|
18751
|
+
if (new URL(url).hostname.endsWith(".rds.amazonaws.com")) return { ssl: { rejectUnauthorized: false } };
|
|
18752
|
+
} catch {}
|
|
18753
|
+
return {};
|
|
18754
|
+
}
|
|
18464
18755
|
/**
|
|
18465
18756
|
* Agent-scoped HTTP authentication hook. Must run **after** userAuthHook
|
|
18466
18757
|
* so `request.user` is populated.
|
|
@@ -18531,8 +18822,12 @@ function userAuthHook(db, jwtSecret) {
|
|
|
18531
18822
|
try {
|
|
18532
18823
|
const { payload: p } = await jwtVerify(token, secret);
|
|
18533
18824
|
payload = p;
|
|
18534
|
-
} catch {
|
|
18535
|
-
|
|
18825
|
+
} catch (err) {
|
|
18826
|
+
const untrusted = decodeJwtForTrace(token);
|
|
18827
|
+
throw new UnauthorizedError("Invalid or expired token", {
|
|
18828
|
+
"auth.failure_reason": classifyJoseError(err),
|
|
18829
|
+
...untrustedAttrs("auth", untrusted)
|
|
18830
|
+
});
|
|
18536
18831
|
}
|
|
18537
18832
|
if (payload.type !== "access" || !payload.sub) throw new UnauthorizedError("Invalid token type", {
|
|
18538
18833
|
"auth.failure_reason": "wrong_token_type",
|
|
@@ -19736,7 +20031,7 @@ function createPulseAggregator(options) {
|
|
|
19736
20031
|
* Returning a string (rather than undefined) keeps the welcome frame well-
|
|
19737
20032
|
* formed — the client treats the value advisorily.
|
|
19738
20033
|
*/
|
|
19739
|
-
function resolveCommandVersion
|
|
20034
|
+
function resolveCommandVersion(injected) {
|
|
19740
20035
|
if (injected && injected.trim().length > 0) return injected;
|
|
19741
20036
|
try {
|
|
19742
20037
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
@@ -19828,10 +20123,13 @@ async function buildApp(config) {
|
|
|
19828
20123
|
const db = connectDatabase(config.database.url);
|
|
19829
20124
|
app.decorate("db", db);
|
|
19830
20125
|
app.decorate("config", config);
|
|
19831
|
-
const commandVersion = resolveCommandVersion
|
|
20126
|
+
const commandVersion = resolveCommandVersion(config.commandVersion);
|
|
19832
20127
|
app.decorate("commandVersion", commandVersion);
|
|
19833
20128
|
app.log.info({ commandVersion }, "Hub server advertising command version");
|
|
19834
|
-
const listenClient = postgres(config.database.url, {
|
|
20129
|
+
const listenClient = postgres(config.database.url, {
|
|
20130
|
+
max: 1,
|
|
20131
|
+
...sslOptions(config.database.url)
|
|
20132
|
+
});
|
|
19835
20133
|
const notifier = createNotifier(listenClient);
|
|
19836
20134
|
await app.register(websocket, { options: { maxPayload: config.ws?.maxPayload ?? 65536 } });
|
|
19837
20135
|
const corsOrigin = config.cors?.origin;
|
|
@@ -19843,7 +20141,8 @@ async function buildApp(config) {
|
|
|
19843
20141
|
await app.register(rateLimit, {
|
|
19844
20142
|
max: config.rateLimit?.max ?? 100,
|
|
19845
20143
|
timeWindow: "1 minute",
|
|
19846
|
-
hook: "preHandler"
|
|
20144
|
+
hook: "preHandler",
|
|
20145
|
+
errorResponseBuilder: buildRateLimitError
|
|
19847
20146
|
});
|
|
19848
20147
|
app.addHook("onSend", bodyCaptureOnSendHook);
|
|
19849
20148
|
const userAuth = userAuthHook(db, config.secrets.jwtSecret);
|
|
@@ -19915,9 +20214,9 @@ async function buildApp(config) {
|
|
|
19915
20214
|
await api.register(authRoutes, { prefix: "/auth" });
|
|
19916
20215
|
await api.register(githubOauthRoutes, { prefix: "/auth/github" });
|
|
19917
20216
|
await api.register(publicInvitationRoutes, { prefix: "/invitations" });
|
|
19918
|
-
await api.register(contextTreeInfoRoutes, { prefix: "/context-tree" });
|
|
19919
20217
|
await api.register(bootstrapConfigRoutes, { prefix: "/bootstrap" });
|
|
19920
20218
|
await api.register(userScope("contextTreeScope", async (scope) => {
|
|
20219
|
+
await scope.register(contextTreeInfoRoutes);
|
|
19921
20220
|
await scope.register(contextTreeSnapshotRoutes);
|
|
19922
20221
|
}), { prefix: "/context-tree" });
|
|
19923
20222
|
await api.register(userScope("meRoutesScope", async (scope) => {
|
|
@@ -19938,6 +20237,7 @@ async function buildApp(config) {
|
|
|
19938
20237
|
await scope.register(orgClientRoutes, { prefix: "/clients" });
|
|
19939
20238
|
await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
|
|
19940
20239
|
await scope.register(orgMemberRoutes, { prefix: "/members" });
|
|
20240
|
+
await scope.register(orgSettingsRoutes, { prefix: "/settings" });
|
|
19941
20241
|
}), { prefix: "/orgs/:orgId" });
|
|
19942
20242
|
await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
|
|
19943
20243
|
await api.register(userScope("resourcesScope", async (scope) => {
|
|
@@ -20042,58 +20342,6 @@ async function buildApp(config) {
|
|
|
20042
20342
|
return app;
|
|
20043
20343
|
}
|
|
20044
20344
|
//#endregion
|
|
20045
|
-
//#region src/core/version.ts
|
|
20046
|
-
/**
|
|
20047
|
-
* Version of the consumer-facing `@agent-team-foundation/first-tree-hub`
|
|
20048
|
-
* package. Read once at module load so the CLI, client runtime, and server
|
|
20049
|
-
* bootstrap all quote the same string.
|
|
20050
|
-
*
|
|
20051
|
-
* Path-based lookups (`require("../../package.json")`) do not survive the
|
|
20052
|
-
* tsdown bundle: the source lives at `src/core/version.ts` but every
|
|
20053
|
-
* emitted chunk lands in `dist/` — shifting the relative depth by one and
|
|
20054
|
-
* pointing at `packages/package.json` instead of our own manifest (the
|
|
20055
|
-
* v0.9.1 "Cannot find module ../../package.json" crash). Walk up from this
|
|
20056
|
-
* module's URL and accept the first `package.json` whose `name` matches, so
|
|
20057
|
-
* dev runs (`tsx src/cli/index.ts`) and the published bundle
|
|
20058
|
-
* (`dist/cli/index.mjs`) both resolve the same file.
|
|
20059
|
-
*/
|
|
20060
|
-
const PACKAGE_NAME$1 = "@agent-team-foundation/first-tree-hub";
|
|
20061
|
-
/**
|
|
20062
|
-
* Sentinel returned when the walker exhausts every parent directory without
|
|
20063
|
-
* finding our manifest. Deliberately NOT valid SemVer so the client-side
|
|
20064
|
-
* `UpdateManager` drops into its `semver.valid(current) === false` warn-and-
|
|
20065
|
-
* skip branch instead of treating it as `< target` and triggering a spurious
|
|
20066
|
-
* self-update loop (the scenario where the startup crash this module fixes
|
|
20067
|
-
* would otherwise quietly reincarnate as repeated `npm install -g @latest`).
|
|
20068
|
-
*/
|
|
20069
|
-
const UNRESOLVED_VERSION = "unknown";
|
|
20070
|
-
/**
|
|
20071
|
-
* Exported for tests. Walks up from `moduleUrl`'s directory looking for a
|
|
20072
|
-
* `package.json` whose `name` field equals {@link PACKAGE_NAME}. Returns
|
|
20073
|
-
* {@link UNRESOLVED_VERSION} as a last-resort fallback so the CLI never
|
|
20074
|
-
* crashes on a missing manifest.
|
|
20075
|
-
*/
|
|
20076
|
-
function resolveCommandVersion(moduleUrl = import.meta.url) {
|
|
20077
|
-
let dir = dirname(fileURLToPath(moduleUrl));
|
|
20078
|
-
for (let i = 0; i < 10; i++) {
|
|
20079
|
-
try {
|
|
20080
|
-
const pkg = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8"));
|
|
20081
|
-
if (pkg.name === PACKAGE_NAME$1 && typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
|
|
20082
|
-
} catch (err) {
|
|
20083
|
-
const code = err.code;
|
|
20084
|
-
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
20085
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
20086
|
-
print.line(`[first-tree-hub] warning: could not read ${dir}/package.json: ${message}\n`);
|
|
20087
|
-
}
|
|
20088
|
-
}
|
|
20089
|
-
const parent = dirname(dir);
|
|
20090
|
-
if (parent === dir) break;
|
|
20091
|
-
dir = parent;
|
|
20092
|
-
}
|
|
20093
|
-
return UNRESOLVED_VERSION;
|
|
20094
|
-
}
|
|
20095
|
-
const COMMAND_VERSION = resolveCommandVersion();
|
|
20096
|
-
//#endregion
|
|
20097
20345
|
//#region src/core/server.ts
|
|
20098
20346
|
/**
|
|
20099
20347
|
* Full server start orchestration:
|
|
@@ -20148,7 +20396,7 @@ async function startServer(options) {
|
|
|
20148
20396
|
instanceId: `srv_${randomUUID().slice(0, 8)}`,
|
|
20149
20397
|
commandVersion: COMMAND_VERSION
|
|
20150
20398
|
};
|
|
20151
|
-
const { initTelemetry, shutdownTelemetry } = await import("./observability-
|
|
20399
|
+
const { initTelemetry, shutdownTelemetry } = await import("./observability-CYsdAcoF.mjs");
|
|
20152
20400
|
await initTelemetry(serverConfig.observability.tracing, config.instanceId);
|
|
20153
20401
|
const app = await buildApp(config);
|
|
20154
20402
|
const SHUTDOWN_FORCE_EXIT_MS = 8e3;
|
|
@@ -20509,7 +20757,7 @@ async function promptReplaceOrCancel(newMemberId) {
|
|
|
20509
20757
|
}) === "replace" ? "proceed" : "cancel";
|
|
20510
20758
|
}
|
|
20511
20759
|
async function exchangeToken(url, token) {
|
|
20512
|
-
const res = await
|
|
20760
|
+
const res = await cliFetch(`${url}/api/v1/auth/connect-token`, {
|
|
20513
20761
|
method: "POST",
|
|
20514
20762
|
headers: { "Content-Type": "application/json" },
|
|
20515
20763
|
body: JSON.stringify({ token }),
|
|
@@ -20621,4 +20869,4 @@ function registerSaaSConnectCommand(program) {
|
|
|
20621
20869
|
});
|
|
20622
20870
|
}
|
|
20623
20871
|
//#endregion
|
|
20624
|
-
export {
|
|
20872
|
+
export { formatStaleReason as $, checkDocker as A, isServiceSupported as B, createApiNameResolver as C, checkBackgroundService as D, checkAgentConfigs as E, checkWebSocket as F, uninstallClientService as G, restartClientService as H, printResults as I, stopPostgres as J, ensurePostgres as K, reconcileAgentConfigs as L, checkServerConfig as M, checkServerHealth as N, checkClientConfig as O, checkServerReachable as P, findStaleAliases as Q, getClientServiceStatus as R, runHomeMigration as S, runMigrations as T, startClientService as U, resolveCliInvocation as V, stopClientService as W, handleClientOrgMismatch as X, ClientRuntime as Y, rotateClientIdWithBackup as Z, formatCheckReport as _, declineUpdate as a, success as at, onboardCreate as b, detectInstallMode as c, FirstTreeHubSDK as ct, startServer as d, cleanWorkspaces as dt, removeLocalAgent as et, reconcileLocalRuntimeProviders as f, probeCapabilities as ft, promptMissingFields as g, promptAddAgent as h, createExecuteUpdate as i, fail as it, checkNodeVersion as j, checkDatabase as k, fetchLatestVersion as l, SdkError as lt, isInteractive as m, configureClientLoggerForService as mt, deriveHubUrlFromToken as n, hasUser as nt, promptUpdate as o, ClientOrgMismatchError as ot, uploadClientCapabilities as p, applyClientLoggerConfig as pt, isDockerAvailable as q, registerSaaSConnectCommand as r, resolveReplyToFromEnv as rt, PACKAGE_NAME as s, ClientUserMismatchError as st, HubUrlDerivationError as t, createOwner as tt, installGlobalLatest as u, SessionRegistry as ut, loadOnboardState as v, migrateLocalAgentDirs as w, saveOnboardState as x, onboardCheck as y, installClientService as z };
|