@agent-team-foundation/first-tree-hub 0.12.0 → 0.12.3
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 +4 -4
- package/dist/{client-BhCtO2df-BGOu-rRN.mjs → client-0RrgrMjR-DPyuu6Ls.mjs} +2 -2
- package/dist/{client-BPUdUaZT-CyCrpCTP.mjs → client-D1TDiik_-gxtXN9bj.mjs} +13 -11
- package/dist/{dist-UOZ6vMUW.mjs → dist-DHHd2dar.mjs} +39 -14
- package/dist/{feishu-C6qlhju2.mjs → feishu-fLnwqCOs.mjs} +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{invitation-C299fxkP-KyCNax4T.mjs → invitation-C299fxkP-B89eqDos.mjs} +1 -1
- package/dist/{saas-connect-Drn9g6cR.mjs → saas-connect-_lNV0Liy.mjs} +627 -109
- package/dist/web/assets/index-B4EaL8S9.css +1 -0
- package/dist/web/assets/index-CJr7zpx-.js +401 -0
- package/dist/web/assets/{index-Bnyz7inW.js → index-DcMORzyx.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B_Tf2I6v.css +0 -1
- package/dist/web/assets/index-Dy3jIUX5.js +0 -391
|
@@ -2,10 +2,10 @@ import { a as __toCommonJS, o as __toESM, t as __commonJSMin } from "./chunk-BSw
|
|
|
2
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, 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-BcW9HgkG.mjs";
|
|
3
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-C_K2CKXC.mjs";
|
|
4
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, At as updateTaskStatusSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateAdapterConfigSchema, D as contextTreeSnapshotSchema, Dt as updateClientCapabilitiesSchema, E as connectTokenExchangeSchema, Et as updateChatSchema, 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, Ot as updateMemberSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as taskListQuerySchema, T as clientRegisterSchema, Tt as updateAgentSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as sessionEventSchema$1, a as AGENT_STATUSES, b as agentBindRequestSchema, bt as stripCode, ct as refreshTokenSchema, d as TASK_CREATOR_TYPES, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as selfServiceFeishuBotSchema, g as addMeChatParticipantsSchema, gt as sessionEventMessageSchema, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionCompletionMessageSchema, i as AGENT_SOURCES, it as patchOnboardingSchema, j as createChatSchema, jt as wsAuthFrameSchema, k as createAdapterMappingSchema, kt as updateOrganizationSchema, l as MENTION_REGEX, lt as runtimeStateMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sendToAgentSchema, n as AGENT_NAME_REGEX$1, nt as onboardingEventSchema, o as AGENT_TYPES, p as TASK_STATUSES, pt as sendMessageSchema, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as paginationQuerySchema, s as AGENT_VISIBILITY, st as rebindAgentSchema, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as safeRedirectPath, v as adminCreateTaskSchema, vt as sessionReconcileRequestSchema, wt as updateAgentRuntimeConfigSchema, x as agentPinnedMessageSchema$1, xt as submitQuestionAnswerSchema, y as adminUpdateTaskSchema, yt as sessionStateMessageSchema } from "./dist-
|
|
5
|
+
import { $ as loginSchema, A as createAgentSchema, At as updateTaskStatusSchema, B as githubCallbackQuerySchema, C as agentTypeSchema$1, Ct as updateAdapterConfigSchema, D as contextTreeSnapshotSchema, Dt as updateClientCapabilitiesSchema, E as connectTokenExchangeSchema, Et as updateChatSchema, 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, Ot as updateMemberSchema, P as createOrgFromMeSchema, Q as listMeChatsQuerySchema, R as dryRunAgentRuntimeConfigSchema, S as agentRuntimeConfigPayloadSchema$1, St as taskListQuerySchema, T as clientRegisterSchema, Tt as updateAgentSchema, U as imageInlineContentSchema, V as githubDevCallbackQuerySchema, W as inboxAckFrameSchema, X as joinByInvitationSchema, Y as isReservedAgentName$1, Z as linkTaskChatSchema, _ as addParticipantSchema, _t as sessionEventSchema$1, a as AGENT_STATUSES, b as agentBindRequestSchema, bt as stripCode, ct as refreshTokenSchema, d as TASK_CREATOR_TYPES, et as messageSourceSchema$1, f as TASK_HEALTH_SIGNALS, ft as selfServiceFeishuBotSchema, g as addMeChatParticipantsSchema, gt as sessionEventMessageSchema, h as WS_AUTH_FRAME_TIMEOUT_MS, ht as sessionCompletionMessageSchema, i as AGENT_SOURCES, it as patchOnboardingSchema, j as createChatSchema, jt as wsAuthFrameSchema, k as createAdapterMappingSchema, kt as updateOrganizationSchema, l as MENTION_REGEX, lt as runtimeStateMessageSchema, m as TASK_TERMINAL_STATUSES, mt as sendToAgentSchema, n as AGENT_NAME_REGEX$1, nt as onboardingEventSchema, o as AGENT_TYPES, p as TASK_STATUSES, pt as sendMessageSchema, q as isOrgSettingNamespace, r as AGENT_SELECTOR_HEADER$1, rt as paginationQuerySchema, s as AGENT_VISIBILITY, st as rebindAgentSchema, t as AGENT_BIND_REJECT_REASONS, tt as notificationQuerySchema, u as ORG_SETTINGS_NAMESPACES$1, ut as safeRedirectPath, v as adminCreateTaskSchema, vt as sessionReconcileRequestSchema, wt as updateAgentRuntimeConfigSchema, x as agentPinnedMessageSchema$1, xt as submitQuestionAnswerSchema, y as adminUpdateTaskSchema, yt as sessionStateMessageSchema } from "./dist-DHHd2dar.mjs";
|
|
6
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-CF5evtJt-B0NTIVPt.mjs";
|
|
7
7
|
import { n as init_esm, r as trace, t as esm_exports } from "./esm-iadMkGbV.mjs";
|
|
8
|
-
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-
|
|
8
|
+
import { $ as pendingQuestions, A as heartbeatClient, B as listAgentsWithRuntime, C as findOrCreateDirectChat, D as getClient, E as getChatDetail, F as joinChat, G as listClientsForOrgAdmin, H as listChats, I as leaveAsParticipant, J as markStaleAgents, K as listMessages, L as leaveChat, M as inboxEntries, N as invalidateChatAudience, O as getOnlineCount, P as joinAsParticipant, Q as notifyRecipients, R as listActiveAgentsPinnedToClient, S as ensureParticipant$1, T as getCachedAudience, U as listChatsForMember, V as listChatParticipantsWithNames, W as listClients, X as members, Y as markSupersededByChat, Z as messages, _ as createNotifier, _t as updateClientCapabilities, a as agents, at as removeParticipant, b as editMessage, c as bindAgent, ct as retireClient, d as chats, dt as serverInstances, et as recomputeChatWatchers, f as claimClient, ft as setOffline, g as createChat, gt as unbindAgent, h as clients, ht as touchAgent, i as agentVisibilityCondition, it as registerClient, j as heartbeatInstance, k as getPresence, l as chatParticipants, lt as sendMessage, m as cleanupStalePresence, mt as submitAnswer, n as agentChatSessions, nt as recomputeWatchersForMember, o as assertClientOwner, ot as resetActivity, p as cleanupStaleClients, pt as setRuntimeState, r as agentPresence, rt as registerChatMessageDispatcher, s as assertParticipant, st as resolveChatMembership, t as addParticipant, tt as recomputeWatchersForAgent, u as chatSubscriptions, ut as sendToAgent$1, v as deriveAuthState, vt as upsertSessionState, w as getActivityOverview, x as ensureCanJoin, y as disconnectClient, z as listAgentsManagedByUser } from "./client-D1TDiik_-gxtXN9bj.mjs";
|
|
9
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-Bg0TRiyx-BsZH4GCS.mjs";
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { ZodError, z } from "zod";
|
|
@@ -1365,26 +1365,51 @@ z.object({
|
|
|
1365
1365
|
* 2. Add a key to `ORG_SETTINGS_NAMESPACES`.
|
|
1366
1366
|
* 3. Done. No DB migration, no new API route.
|
|
1367
1367
|
*/
|
|
1368
|
-
const
|
|
1368
|
+
const SCP_LIKE_SSH_RE = /^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9.-]+:(?!\d+(?:\/|$))[^/:@\s][^:@\s]*$/;
|
|
1369
|
+
function isScpLikeSshUrl(value) {
|
|
1370
|
+
if (value.includes("://")) return false;
|
|
1371
|
+
return SCP_LIKE_SSH_RE.test(value);
|
|
1372
|
+
}
|
|
1373
|
+
const repoUrlSchema = z.string().min(1).superRefine((value, ctx) => {
|
|
1374
|
+
if (isScpLikeSshUrl(value)) return;
|
|
1375
|
+
let url;
|
|
1369
1376
|
try {
|
|
1370
|
-
|
|
1377
|
+
url = new URL(value);
|
|
1371
1378
|
} catch {
|
|
1372
|
-
|
|
1379
|
+
ctx.addIssue({
|
|
1380
|
+
code: z.ZodIssueCode.custom,
|
|
1381
|
+
message: "Repo URL must be HTTPS, SSH (ssh://...), or scp-like (git@host:path)."
|
|
1382
|
+
});
|
|
1383
|
+
return;
|
|
1373
1384
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
return
|
|
1385
|
+
if (url.protocol !== "https:" && url.protocol !== "ssh:") {
|
|
1386
|
+
ctx.addIssue({
|
|
1387
|
+
code: z.ZodIssueCode.custom,
|
|
1388
|
+
message: "Repo URL must use HTTPS or SSH."
|
|
1389
|
+
});
|
|
1390
|
+
return;
|
|
1380
1391
|
}
|
|
1381
|
-
|
|
1392
|
+
if (url.password.length > 0) {
|
|
1393
|
+
ctx.addIssue({
|
|
1394
|
+
code: z.ZodIssueCode.custom,
|
|
1395
|
+
message: "Repo URL must not include credentials."
|
|
1396
|
+
});
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (url.protocol === "https:" && url.username.length > 0) {
|
|
1400
|
+
ctx.addIssue({
|
|
1401
|
+
code: z.ZodIssueCode.custom,
|
|
1402
|
+
message: "Repo URL must not include credentials."
|
|
1403
|
+
});
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1382
1407
|
const orgContextTreeStorageSchema = z.object({
|
|
1383
|
-
repo:
|
|
1408
|
+
repo: repoUrlSchema.optional(),
|
|
1384
1409
|
branch: z.string().default("main")
|
|
1385
1410
|
});
|
|
1386
1411
|
const orgContextTreeInputSchema = z.object({
|
|
1387
|
-
repo:
|
|
1412
|
+
repo: repoUrlSchema.nullish(),
|
|
1388
1413
|
branch: z.string().min(1).nullish()
|
|
1389
1414
|
});
|
|
1390
1415
|
const orgContextTreeOutputSchema = z.object({
|
|
@@ -1398,11 +1423,11 @@ const orgGithubIntegrationOutputSchema = z.object({
|
|
|
1398
1423
|
webhookUrl: z.string()
|
|
1399
1424
|
});
|
|
1400
1425
|
const orgSourceReposStorageSchema = z.object({ repos: z.array(z.object({
|
|
1401
|
-
url:
|
|
1426
|
+
url: repoUrlSchema,
|
|
1402
1427
|
defaultBranch: z.string().optional()
|
|
1403
1428
|
})).default([]) });
|
|
1404
1429
|
const orgSourceReposInputSchema = z.object({ repos: z.array(z.object({
|
|
1405
|
-
url:
|
|
1430
|
+
url: repoUrlSchema,
|
|
1406
1431
|
defaultBranch: z.string().min(1).optional()
|
|
1407
1432
|
})).optional() });
|
|
1408
1433
|
const orgSourceReposOutputSchema = z.object({ repos: z.array(z.object({
|
|
@@ -2923,8 +2948,6 @@ function installFirstTreeIntegration(options) {
|
|
|
2923
2948
|
const integrateArgs = [
|
|
2924
2949
|
"tree",
|
|
2925
2950
|
"integrate",
|
|
2926
|
-
"--source-path",
|
|
2927
|
-
workspacePath,
|
|
2928
2951
|
"--tree-path",
|
|
2929
2952
|
contextTreePath,
|
|
2930
2953
|
"--mode",
|
|
@@ -2959,8 +2982,13 @@ function installFirstTreeIntegration(options) {
|
|
|
2959
2982
|
} catch (err) {
|
|
2960
2983
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2961
2984
|
const binaryMissing = /ENOENT|not found|command not found/i.test(msg);
|
|
2985
|
+
const unsupportedByThisCli = /unknown (?:option|command|argument)|unrecognized option/i.test(msg);
|
|
2986
|
+
const shouldRetry = binaryMissing || unsupportedByThisCli;
|
|
2962
2987
|
const isLastAttempt = index === attempts.length - 1;
|
|
2963
|
-
if (
|
|
2988
|
+
if (shouldRetry && !isLastAttempt) {
|
|
2989
|
+
log(`First-tree integration via ${attempt.label} unusable; falling back: ${msg.slice(0, 200)}`);
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2964
2992
|
log(`First-tree integration skipped (${attempt.label}): ${msg.slice(0, 200)}`);
|
|
2965
2993
|
return false;
|
|
2966
2994
|
}
|
|
@@ -3029,8 +3057,65 @@ For content with quotes, \`$\`, backticks, or newlines, prefer stdin to avoid sh
|
|
|
3029
3057
|
const DEFAULT_CLONE_TIMEOUT_MS = 300 * 1e3;
|
|
3030
3058
|
const FETCH_REFSPEC = "+refs/heads/*:refs/remotes/origin/*";
|
|
3031
3059
|
const SESSION_BRANCH_PREFIX = "hub-session";
|
|
3060
|
+
/**
|
|
3061
|
+
* Hash a repo URL into the mirror directory name.
|
|
3062
|
+
*
|
|
3063
|
+
* The hash is computed over a *canonical* form (host + path, lowercased
|
|
3064
|
+
* host, no `.git` suffix, no leading/trailing slash, no protocol, no
|
|
3065
|
+
* `user@` prefix), so the same upstream repo addressed via any of the
|
|
3066
|
+
* accepted forms (HTTPS, `ssh://`, or scp-like) all land in the same
|
|
3067
|
+
* mirror dir. That matters because:
|
|
3068
|
+
* - admins may write any of those three forms into `source_repos[].url`
|
|
3069
|
+
* (the schema accepts all of them)
|
|
3070
|
+
* - the `fetchOrigin` fallback below transparently swaps protocols when
|
|
3071
|
+
* credentials are missing — without canonical hashing, the fallback
|
|
3072
|
+
* would silently maintain a second mirror dir
|
|
3073
|
+
*
|
|
3074
|
+
* Migration cost: pre-existing mirrors created before this change use the
|
|
3075
|
+
* raw-URL hash and will be orphaned after the upgrade. `gcMirrors` removes
|
|
3076
|
+
* them on the next run; the next fetch repopulates the canonical-keyed
|
|
3077
|
+
* mirror. No data loss — mirror is a cache, not state.
|
|
3078
|
+
*/
|
|
3032
3079
|
function hashUrl(url) {
|
|
3033
|
-
return createHash("sha256").update(url).digest("hex").slice(0, 32);
|
|
3080
|
+
return createHash("sha256").update(canonicalizeRepoUrl(url)).digest("hex").slice(0, 32);
|
|
3081
|
+
}
|
|
3082
|
+
/**
|
|
3083
|
+
* Reduce a repo URL to `<host[:port]>/<path-without-.git>`. Used as the
|
|
3084
|
+
* input to the mirror dir hash and exposed for unit testing.
|
|
3085
|
+
*
|
|
3086
|
+
* `https://github.com/foo/bar.git` → `github.com/foo/bar`
|
|
3087
|
+
* `git@github.com:foo/bar.git` → `github.com/foo/bar`
|
|
3088
|
+
* `ssh://git@github.com/foo/bar.git` → `github.com/foo/bar`
|
|
3089
|
+
* `ssh://git@gitlab.example.com:2222/x/y` → `gitlab.example.com:2222/x/y`
|
|
3090
|
+
*
|
|
3091
|
+
* Falls back to the raw input for un-parseable strings — better to keep
|
|
3092
|
+
* a stable mirror than to throw mid-bootstrap.
|
|
3093
|
+
*/
|
|
3094
|
+
function canonicalizeRepoUrl(url) {
|
|
3095
|
+
if (!url.includes("://")) {
|
|
3096
|
+
const m = url.match(/^(?:[A-Za-z0-9_.-]+@)?([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
|
|
3097
|
+
const host = m?.[1];
|
|
3098
|
+
const rawPath = m?.[2];
|
|
3099
|
+
if (host && rawPath && !/^\d+(?:\/|$)/.test(rawPath)) {
|
|
3100
|
+
const path = normalizePath(rawPath);
|
|
3101
|
+
return `${host.toLowerCase()}/${path}`;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
try {
|
|
3105
|
+
const parsed = new URL(url);
|
|
3106
|
+
const host = parsed.hostname.toLowerCase();
|
|
3107
|
+
return `${parsed.port === "" || parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "ssh:" && parsed.port === "22" ? host : `${host}:${parsed.port}`}/${normalizePath(parsed.pathname)}`;
|
|
3108
|
+
} catch {
|
|
3109
|
+
return url;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
/**
|
|
3113
|
+
* Strip leading slashes, trailing slashes, and a trailing `.git`. Used by
|
|
3114
|
+
* `canonicalizeRepoUrl` so that `…/foo/bar`, `…/foo/bar/`, and `…/foo/bar.git`
|
|
3115
|
+
* all collapse to the same canonical path (and therefore the same mirror dir).
|
|
3116
|
+
*/
|
|
3117
|
+
function normalizePath(rawPath) {
|
|
3118
|
+
return rawPath.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
3034
3119
|
}
|
|
3035
3120
|
function shortHash(input) {
|
|
3036
3121
|
return createHash("sha256").update(input).digest("hex").slice(0, 8);
|
|
@@ -3067,10 +3152,14 @@ function createGitMirrorManager(opts) {
|
|
|
3067
3152
|
}
|
|
3068
3153
|
async function git(args, cwd, timeoutMs, env) {
|
|
3069
3154
|
const start = Date.now();
|
|
3155
|
+
const finalEnv = {
|
|
3156
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
3157
|
+
...env ?? process.env
|
|
3158
|
+
};
|
|
3070
3159
|
return await new Promise((resolveExec, rejectExec) => {
|
|
3071
3160
|
const proc = spawn("git", args, {
|
|
3072
3161
|
cwd: cwd ?? void 0,
|
|
3073
|
-
env:
|
|
3162
|
+
env: finalEnv,
|
|
3074
3163
|
stdio: [
|
|
3075
3164
|
"ignore",
|
|
3076
3165
|
"pipe",
|
|
@@ -3114,6 +3203,116 @@ function createGitMirrorManager(opts) {
|
|
|
3114
3203
|
}
|
|
3115
3204
|
}
|
|
3116
3205
|
/**
|
|
3206
|
+
* `git fetch --prune origin` with one-shot bidirectional protocol fallback.
|
|
3207
|
+
*
|
|
3208
|
+
* Decides direction from `originUrl`'s protocol:
|
|
3209
|
+
* - HTTPS origin → on credential-shaped failure, retry as SSH
|
|
3210
|
+
* - SSH origin → on credential-shaped failure, retry as HTTPS
|
|
3211
|
+
* The fallback fires only when **all** hold:
|
|
3212
|
+
* 1. The first attempt failed with a credential-shaped error in the
|
|
3213
|
+
* direction-appropriate sense (`isLikelyHttpsAuthFailure` /
|
|
3214
|
+
* `isLikelySshAuthFailure`). Network failures, missing-ref errors,
|
|
3215
|
+
* and TLS/host-key surprises propagate as-is — those won't be cured
|
|
3216
|
+
* by switching transports and silently retrying would mask real bugs.
|
|
3217
|
+
* 2. The origin URL maps to a usable peer-protocol form
|
|
3218
|
+
* (see `httpsToSshBaseRewrite` / `sshToHttpsBaseRewrite`). Custom
|
|
3219
|
+
* ports beyond the protocol default disable the rewrite — there's no
|
|
3220
|
+
* universal mapping between `https://host:8443` and an SSH peer.
|
|
3221
|
+
*
|
|
3222
|
+
* Implementation: the retry uses `git -c url.<peer-base>.insteadOf=<origin-base>`
|
|
3223
|
+
* — git resolves origin's URL through that rewrite for the duration of
|
|
3224
|
+
* one subprocess and never persists anything to disk. The mirror's
|
|
3225
|
+
* `remote.origin.url` stays as configured, so the next fetch starts back
|
|
3226
|
+
* at the original protocol unless this fallback fires again.
|
|
3227
|
+
*/
|
|
3228
|
+
/**
|
|
3229
|
+
* `remote set-head --auto` is a remote-talking op too — under the same
|
|
3230
|
+
* credential rules as `fetch`. If we don't apply the same fallback,
|
|
3231
|
+
* mirrors whose HTTPS creds are missing end up with `refs/remotes/origin/HEAD`
|
|
3232
|
+
* unset, which then breaks `resolveBase()` for callers without an explicit
|
|
3233
|
+
* `ref` (the default-branch lookup throws).
|
|
3234
|
+
*
|
|
3235
|
+
* Strategy mirrors `fetchOrigin`: try the configured protocol first, fall
|
|
3236
|
+
* back to the peer protocol once via `-c url.<peer>.insteadOf=<origin>`.
|
|
3237
|
+
* Returns true on either success, false if both attempts failed (which
|
|
3238
|
+
* mirrors the historical `gitOk` semantics — non-fatal). No `GitMirrorAuthError`
|
|
3239
|
+
* here: callers that need origin/HEAD already get a clear
|
|
3240
|
+
* `GitMirrorError("Cannot resolve default branch …")` if both attempts fail.
|
|
3241
|
+
*/
|
|
3242
|
+
async function setHeadAuto(mirrorPath, originUrl) {
|
|
3243
|
+
if (await gitOk([
|
|
3244
|
+
"remote",
|
|
3245
|
+
"set-head",
|
|
3246
|
+
"origin",
|
|
3247
|
+
"--auto"
|
|
3248
|
+
], mirrorPath, 3e4)) return true;
|
|
3249
|
+
const direction = pickFallbackDirection(originUrl);
|
|
3250
|
+
if (!direction) return false;
|
|
3251
|
+
return await gitOk([
|
|
3252
|
+
"-c",
|
|
3253
|
+
`url.${direction.peerBase}.insteadOf=${direction.originBase}`,
|
|
3254
|
+
"remote",
|
|
3255
|
+
"set-head",
|
|
3256
|
+
"origin",
|
|
3257
|
+
"--auto"
|
|
3258
|
+
], mirrorPath, 3e4);
|
|
3259
|
+
}
|
|
3260
|
+
async function readOriginUrl(mirrorPath) {
|
|
3261
|
+
try {
|
|
3262
|
+
const { stdout } = await git([
|
|
3263
|
+
"config",
|
|
3264
|
+
"--get",
|
|
3265
|
+
"remote.origin.url"
|
|
3266
|
+
], mirrorPath, 1e4);
|
|
3267
|
+
return stdout.trim() || null;
|
|
3268
|
+
} catch {
|
|
3269
|
+
return null;
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
async function fetchOrigin(mirrorPath, originUrl) {
|
|
3273
|
+
const direction = pickFallbackDirection(originUrl);
|
|
3274
|
+
try {
|
|
3275
|
+
const { elapsedMs } = await git([
|
|
3276
|
+
"fetch",
|
|
3277
|
+
"--prune",
|
|
3278
|
+
"origin"
|
|
3279
|
+
], mirrorPath, cloneTimeoutMs);
|
|
3280
|
+
return {
|
|
3281
|
+
elapsedMs,
|
|
3282
|
+
usedFallback: false
|
|
3283
|
+
};
|
|
3284
|
+
} catch (primaryErr) {
|
|
3285
|
+
const primaryMessage = primaryErr instanceof Error ? primaryErr.message : String(primaryErr);
|
|
3286
|
+
if (!direction || !direction.shouldRetry(primaryMessage)) throw primaryErr;
|
|
3287
|
+
log?.info({
|
|
3288
|
+
gitUrl: originUrl,
|
|
3289
|
+
fromProtocol: direction.fromProtocol,
|
|
3290
|
+
toProtocol: direction.toProtocol,
|
|
3291
|
+
peerBase: direction.peerBase
|
|
3292
|
+
}, "fetch failed with credential-shaped error; retrying with peer-protocol insteadOf rewrite");
|
|
3293
|
+
try {
|
|
3294
|
+
const { elapsedMs } = await git([
|
|
3295
|
+
"-c",
|
|
3296
|
+
`url.${direction.peerBase}.insteadOf=${direction.originBase}`,
|
|
3297
|
+
"fetch",
|
|
3298
|
+
"--prune",
|
|
3299
|
+
"origin"
|
|
3300
|
+
], mirrorPath, cloneTimeoutMs);
|
|
3301
|
+
log?.info({
|
|
3302
|
+
gitUrl: originUrl,
|
|
3303
|
+
toProtocol: direction.toProtocol
|
|
3304
|
+
}, "protocol-fallback fetch succeeded");
|
|
3305
|
+
return {
|
|
3306
|
+
elapsedMs,
|
|
3307
|
+
usedFallback: true
|
|
3308
|
+
};
|
|
3309
|
+
} catch (peerErr) {
|
|
3310
|
+
const peerMessage = peerErr instanceof Error ? peerErr.message : String(peerErr);
|
|
3311
|
+
throw new GitMirrorAuthError(`Could not fetch ${originUrl} over ${direction.fromProtocol.toUpperCase()} or ${direction.toProtocol.toUpperCase()}. ${direction.fromProtocol.toUpperCase()} attempt failed: ${truncate(primaryMessage)} ${direction.toProtocol.toUpperCase()} retry (${direction.peerBase}) failed: ${truncate(peerMessage)}`);
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3117
3316
|
* Bring the mirror's config to the invariant expected by this module:
|
|
3118
3317
|
* fetch refspec = `+refs/heads/*:refs/remotes/origin/*`, `remote.origin.mirror`
|
|
3119
3318
|
* absent, `refs/remotes/origin/HEAD` resolvable.
|
|
@@ -3181,17 +3380,8 @@ function createGitMirrorManager(opts) {
|
|
|
3181
3380
|
migrated = true;
|
|
3182
3381
|
}
|
|
3183
3382
|
if (migrated) {
|
|
3184
|
-
await
|
|
3185
|
-
|
|
3186
|
-
"--prune",
|
|
3187
|
-
"origin"
|
|
3188
|
-
], mirrorPath, cloneTimeoutMs);
|
|
3189
|
-
await gitOk([
|
|
3190
|
-
"remote",
|
|
3191
|
-
"set-head",
|
|
3192
|
-
"origin",
|
|
3193
|
-
"--auto"
|
|
3194
|
-
], mirrorPath, 3e4);
|
|
3383
|
+
await fetchOrigin(mirrorPath, url);
|
|
3384
|
+
await setHeadAuto(mirrorPath, url);
|
|
3195
3385
|
log?.info({ gitUrl: url }, "mirror config migrated");
|
|
3196
3386
|
}
|
|
3197
3387
|
return { migrated };
|
|
@@ -3221,17 +3411,8 @@ function createGitMirrorManager(opts) {
|
|
|
3221
3411
|
"remote.origin.fetch",
|
|
3222
3412
|
FETCH_REFSPEC
|
|
3223
3413
|
], mirrorPath, 1e4);
|
|
3224
|
-
await
|
|
3225
|
-
|
|
3226
|
-
"--prune",
|
|
3227
|
-
"origin"
|
|
3228
|
-
], mirrorPath, cloneTimeoutMs);
|
|
3229
|
-
await gitOk([
|
|
3230
|
-
"remote",
|
|
3231
|
-
"set-head",
|
|
3232
|
-
"origin",
|
|
3233
|
-
"--auto"
|
|
3234
|
-
], mirrorPath, 3e4);
|
|
3414
|
+
await fetchOrigin(mirrorPath, url);
|
|
3415
|
+
await setHeadAuto(mirrorPath, url);
|
|
3235
3416
|
}
|
|
3236
3417
|
async function branchExists(mirrorPath, branchName) {
|
|
3237
3418
|
return await gitOk([
|
|
@@ -3257,6 +3438,21 @@ function createGitMirrorManager(opts) {
|
|
|
3257
3438
|
"--quiet",
|
|
3258
3439
|
"refs/remotes/origin/HEAD"
|
|
3259
3440
|
], mirrorPath, 1e4)) return "refs/remotes/origin/HEAD";
|
|
3441
|
+
const url = await readOriginUrl(mirrorPath);
|
|
3442
|
+
if (url && await setHeadAuto(mirrorPath, url)) {
|
|
3443
|
+
if (await gitOk([
|
|
3444
|
+
"rev-parse",
|
|
3445
|
+
"--verify",
|
|
3446
|
+
"--quiet",
|
|
3447
|
+
"refs/remotes/origin/HEAD"
|
|
3448
|
+
], mirrorPath, 1e4)) {
|
|
3449
|
+
log?.info({
|
|
3450
|
+
mirrorPath,
|
|
3451
|
+
gitUrl: url
|
|
3452
|
+
}, "origin/HEAD self-healed via setHeadAuto");
|
|
3453
|
+
return "refs/remotes/origin/HEAD";
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3260
3456
|
throw new GitMirrorError("Cannot resolve default branch: refs/remotes/origin/HEAD is missing. Re-run with an explicit `ref`.");
|
|
3261
3457
|
}
|
|
3262
3458
|
if (looksLikeCommitSha(ref)) {
|
|
@@ -3325,16 +3521,12 @@ function createGitMirrorManager(opts) {
|
|
|
3325
3521
|
const path = mirrorDir(url);
|
|
3326
3522
|
if (!existsSync(join(path, "HEAD"))) throw new GitMirrorError(`Cannot fetch — no mirror exists for "${url}"`);
|
|
3327
3523
|
try {
|
|
3328
|
-
const { elapsedMs } = await
|
|
3329
|
-
"fetch",
|
|
3330
|
-
"--prune",
|
|
3331
|
-
"origin"
|
|
3332
|
-
], path, cloneTimeoutMs);
|
|
3524
|
+
const { elapsedMs } = await fetchOrigin(path, url);
|
|
3333
3525
|
return { elapsedMs };
|
|
3334
3526
|
} catch (err) {
|
|
3335
3527
|
log?.warn({
|
|
3336
3528
|
gitUrl: url,
|
|
3337
|
-
errorCode: err instanceof GitMirrorError ? "git-failed" : "unknown",
|
|
3529
|
+
errorCode: err instanceof GitMirrorAuthError ? "auth-failed" : err instanceof GitMirrorTimeoutError ? "timeout" : err instanceof GitMirrorError ? "git-failed" : "unknown",
|
|
3338
3530
|
stderr: err instanceof Error ? err.message.slice(0, 1024) : String(err).slice(0, 1024)
|
|
3339
3531
|
}, "mirror fetch failed");
|
|
3340
3532
|
throw err;
|
|
@@ -3472,6 +3664,166 @@ var GitMirrorWorktreeConflictError = class extends GitMirrorError {
|
|
|
3472
3664
|
}
|
|
3473
3665
|
};
|
|
3474
3666
|
/**
|
|
3667
|
+
* Thrown when both the HTTPS fetch and the SSH fallback fail. The message
|
|
3668
|
+
* carries trimmed stderr from both attempts so the operator can see whether
|
|
3669
|
+
* the host's HTTPS credentials are missing, the SSH key is missing, or both.
|
|
3670
|
+
*/
|
|
3671
|
+
var GitMirrorAuthError = class extends GitMirrorError {
|
|
3672
|
+
constructor(message) {
|
|
3673
|
+
super(message);
|
|
3674
|
+
this.name = "GitMirrorAuthError";
|
|
3675
|
+
}
|
|
3676
|
+
};
|
|
3677
|
+
/**
|
|
3678
|
+
* Heuristic for HTTPS-side credential failures. Matches the indicators git
|
|
3679
|
+
* itself emits over libcurl / git-credential helpers, regardless of platform.
|
|
3680
|
+
*
|
|
3681
|
+
* Negative space (intentionally NOT matched): network errors
|
|
3682
|
+
* (`Could not resolve host`, `connection refused`), repo errors
|
|
3683
|
+
* (`Repository not found`, `couldn't find remote ref`), TLS errors
|
|
3684
|
+
* (`SSL certificate problem`). Those won't be cured by switching transports
|
|
3685
|
+
* and silently retrying would mask the real bug.
|
|
3686
|
+
*
|
|
3687
|
+
* Exported for unit testing.
|
|
3688
|
+
*/
|
|
3689
|
+
function isLikelyHttpsAuthFailure(message) {
|
|
3690
|
+
if (!message) return false;
|
|
3691
|
+
return /could not read Username/i.test(message) || /could not read Password/i.test(message) || /Authentication failed/i.test(message) || /terminal prompts disabled/i.test(message) || /HTTP\s*(?:Basic:\s*)?Access denied/i.test(message) || /\bHTTP[/ ]?(1\.[01]|2|2\.0)?\s*40[13]\b/i.test(message) || /\bfatal:\s*unable to access\b.*\b(401|403)\b/i.test(message) || /\bremote: Invalid username or password\b/i.test(message);
|
|
3692
|
+
}
|
|
3693
|
+
/**
|
|
3694
|
+
* Heuristic for SSH-side credential failures (no key on disk, key not
|
|
3695
|
+
* accepted by remote, agent has nothing usable, host key mismatch).
|
|
3696
|
+
*
|
|
3697
|
+
* Negative space (intentionally NOT matched): SSH-level network errors
|
|
3698
|
+
* (`Could not resolve hostname`, `Connection refused`, `Connection timed out`).
|
|
3699
|
+
* Those are network reachability issues — switching to HTTPS won't help
|
|
3700
|
+
* unless the network policy specifically blocks port 22, which is rare
|
|
3701
|
+
* enough that we'd rather surface the original error than guess.
|
|
3702
|
+
*
|
|
3703
|
+
* Exported for unit testing.
|
|
3704
|
+
*/
|
|
3705
|
+
function isLikelySshAuthFailure(message) {
|
|
3706
|
+
if (!message) return false;
|
|
3707
|
+
return /Permission denied\s*(?:\(|,)/i.test(message) || /Could not read from remote repository/i.test(message) || /Host key verification failed/i.test(message) || /no matching host key type/i.test(message) || /no mutual signature algorithm/i.test(message);
|
|
3708
|
+
}
|
|
3709
|
+
/**
|
|
3710
|
+
* Map an HTTPS git URL to the `insteadOf` rewrite needed to make git resolve
|
|
3711
|
+
* it through SSH. Returns *base* strings (suitable for
|
|
3712
|
+
* `git -c url.<sshBase>.insteadOf=<httpsBase>`) — git's `insteadOf` is a
|
|
3713
|
+
* prefix match, so we only need the host segment.
|
|
3714
|
+
*
|
|
3715
|
+
* `https://github.com/owner/repo.git` → `git@github.com:` / `https://github.com/`
|
|
3716
|
+
*
|
|
3717
|
+
* Returns `null` for inputs that should NOT trigger fallback:
|
|
3718
|
+
* - non-HTTPS URLs (already SSH, `git://`, `file://`, etc.)
|
|
3719
|
+
* - URLs with embedded credentials (schema rejects these on input;
|
|
3720
|
+
* belt-and-braces — never silently downgrade auth strength)
|
|
3721
|
+
* - HTTPS URLs with a non-default port — there is no portable mapping to
|
|
3722
|
+
* an SSH port (HTTPS:8443 ↔ SSH:???), so we refuse to guess
|
|
3723
|
+
* - URLs that fail to parse
|
|
3724
|
+
*
|
|
3725
|
+
* Exported for unit testing.
|
|
3726
|
+
*/
|
|
3727
|
+
function httpsToSshBaseRewrite(url) {
|
|
3728
|
+
if (!url || !/^https:\/\//i.test(url)) return null;
|
|
3729
|
+
let parsed;
|
|
3730
|
+
try {
|
|
3731
|
+
parsed = new URL(url);
|
|
3732
|
+
} catch {
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3735
|
+
if (parsed.protocol !== "https:") return null;
|
|
3736
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) return null;
|
|
3737
|
+
if (!parsed.hostname) return null;
|
|
3738
|
+
if (parsed.port && parsed.port !== "443") return null;
|
|
3739
|
+
return {
|
|
3740
|
+
httpsBase: `https://${parsed.hostname}/`,
|
|
3741
|
+
sshBase: `git@${parsed.hostname}:`
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Map an SSH git URL (either `ssh://` or scp-like `[user@]host:path`) to the
|
|
3746
|
+
* `insteadOf` rewrite for resolving via HTTPS. Mirror of
|
|
3747
|
+
* `httpsToSshBaseRewrite`.
|
|
3748
|
+
*
|
|
3749
|
+
* `git@github.com:owner/repo.git` → `git@github.com:` / `https://github.com/`
|
|
3750
|
+
* `ssh://git@github.com/owner/repo.git` → `ssh://git@github.com/` / `https://github.com/`
|
|
3751
|
+
* `ssh://git@gitlab.example.com:22/x/y.git` → `ssh://git@gitlab.example.com:22/` / `https://gitlab.example.com/`
|
|
3752
|
+
*
|
|
3753
|
+
* Returns `null` when:
|
|
3754
|
+
* - URL is not SSH-shaped
|
|
3755
|
+
* - URL has an embedded password (`user:pass@`)
|
|
3756
|
+
* - SSH URL has a non-default port (≠ 22) — no portable mapping to HTTPS
|
|
3757
|
+
*
|
|
3758
|
+
* Exported for unit testing.
|
|
3759
|
+
*/
|
|
3760
|
+
function sshToHttpsBaseRewrite(url) {
|
|
3761
|
+
if (!url) return null;
|
|
3762
|
+
if (!url.includes("://")) {
|
|
3763
|
+
const m = url.match(/^((?:[A-Za-z0-9_.-]+@)?)([A-Za-z0-9.-]+):([^/@:\s][^@:\s]*)$/);
|
|
3764
|
+
const userAt = m?.[1];
|
|
3765
|
+
const host = m?.[2];
|
|
3766
|
+
const path = m?.[3];
|
|
3767
|
+
if (userAt === void 0 || !host || !path) return null;
|
|
3768
|
+
if (/^\d+(?:\/|$)/.test(path)) return null;
|
|
3769
|
+
return {
|
|
3770
|
+
sshBase: `${userAt}${host}:`,
|
|
3771
|
+
httpsBase: `https://${host}/`
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
let parsed;
|
|
3775
|
+
try {
|
|
3776
|
+
parsed = new URL(url);
|
|
3777
|
+
} catch {
|
|
3778
|
+
return null;
|
|
3779
|
+
}
|
|
3780
|
+
if (parsed.protocol !== "ssh:") return null;
|
|
3781
|
+
if (parsed.password.length > 0) return null;
|
|
3782
|
+
if (!parsed.hostname) return null;
|
|
3783
|
+
if (parsed.port && parsed.port !== "22") return null;
|
|
3784
|
+
const userAt = parsed.username.length > 0 ? `${parsed.username}@` : "";
|
|
3785
|
+
return {
|
|
3786
|
+
sshBase: parsed.port ? `ssh://${userAt}${parsed.hostname}:${parsed.port}/` : `ssh://${userAt}${parsed.hostname}/`,
|
|
3787
|
+
httpsBase: `https://${parsed.hostname}/`
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Same shape as `SCP_LIKE_SSH_RE` in the shared schema — kept in sync so
|
|
3792
|
+
* what the schema accepts is exactly what we route through the SSH-side
|
|
3793
|
+
* fallback. Single source of truth would be ideal, but cross-package import
|
|
3794
|
+
* for a regex isn't worth the build coupling.
|
|
3795
|
+
*/
|
|
3796
|
+
const SCP_LIKE_RE = /^(?:[A-Za-z0-9_.-]+@)?[A-Za-z0-9.-]+:(?!\d+(?:\/|$))[^/:@\s][^:@\s]*$/;
|
|
3797
|
+
function pickFallbackDirection(originUrl) {
|
|
3798
|
+
if (/^https:\/\//i.test(originUrl)) {
|
|
3799
|
+
const r = httpsToSshBaseRewrite(originUrl);
|
|
3800
|
+
if (!r) return null;
|
|
3801
|
+
return {
|
|
3802
|
+
fromProtocol: "https",
|
|
3803
|
+
toProtocol: "ssh",
|
|
3804
|
+
originBase: r.httpsBase,
|
|
3805
|
+
peerBase: r.sshBase,
|
|
3806
|
+
shouldRetry: isLikelyHttpsAuthFailure
|
|
3807
|
+
};
|
|
3808
|
+
}
|
|
3809
|
+
if (/^ssh:\/\//i.test(originUrl) || SCP_LIKE_RE.test(originUrl)) {
|
|
3810
|
+
const r = sshToHttpsBaseRewrite(originUrl);
|
|
3811
|
+
if (!r) return null;
|
|
3812
|
+
return {
|
|
3813
|
+
fromProtocol: "ssh",
|
|
3814
|
+
toProtocol: "https",
|
|
3815
|
+
originBase: r.sshBase,
|
|
3816
|
+
peerBase: r.httpsBase,
|
|
3817
|
+
shouldRetry: isLikelySshAuthFailure
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
return null;
|
|
3821
|
+
}
|
|
3822
|
+
function truncate(text, max = 512) {
|
|
3823
|
+
if (text.length <= max) return text;
|
|
3824
|
+
return `${text.slice(0, max)}…[truncated]`;
|
|
3825
|
+
}
|
|
3826
|
+
/**
|
|
3475
3827
|
* InputController — push-based async iterable bridge.
|
|
3476
3828
|
*
|
|
3477
3829
|
* Bridges imperative `push()` calls to the `AsyncIterable` that
|
|
@@ -4259,6 +4611,8 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4259
4611
|
}
|
|
4260
4612
|
}
|
|
4261
4613
|
const contextTreePath = config.contextTreePath ?? null;
|
|
4614
|
+
const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
|
|
4615
|
+
const agentName = config.agentName ?? null;
|
|
4262
4616
|
/**
|
|
4263
4617
|
* Materialise the runtime config's `gitRepos` into worktrees under `cwd`.
|
|
4264
4618
|
* Idempotent across resumes: reuses an existing Hub-managed worktree if
|
|
@@ -4326,7 +4680,8 @@ const createClaudeCodeHandler = (config) => {
|
|
|
4326
4680
|
if (contextTreePath) installFirstTreeIntegration({
|
|
4327
4681
|
workspacePath: workspace,
|
|
4328
4682
|
contextTreePath,
|
|
4329
|
-
workspaceId: sessionCtx.chatId,
|
|
4683
|
+
workspaceId: agentName ?? sessionCtx.chatId,
|
|
4684
|
+
treeRepoUrl: contextTreeRepoUrl ?? void 0,
|
|
4330
4685
|
log: (msg) => sessionCtx.log(msg)
|
|
4331
4686
|
});
|
|
4332
4687
|
}
|
|
@@ -4515,6 +4870,8 @@ const createCodexHandler = (config) => {
|
|
|
4515
4870
|
const agentConfigCache = config.agentConfigCache ?? null;
|
|
4516
4871
|
const gitMirrorManager = config.gitMirrorManager ?? null;
|
|
4517
4872
|
const contextTreePath = config.contextTreePath ?? null;
|
|
4873
|
+
const contextTreeRepoUrl = config.contextTreeRepoUrl ?? null;
|
|
4874
|
+
const agentName = config.agentName ?? null;
|
|
4518
4875
|
let cwd = null;
|
|
4519
4876
|
let codex = null;
|
|
4520
4877
|
let thread = null;
|
|
@@ -4786,6 +5143,17 @@ const createCodexHandler = (config) => {
|
|
|
4786
5143
|
if (inputs.length === 0) return;
|
|
4787
5144
|
await runTurn(inputs.join("\n\n"), sessionCtx);
|
|
4788
5145
|
}
|
|
5146
|
+
/** Install the first-tree skill + binding block; no-op when context tree is unconfigured. */
|
|
5147
|
+
function ensureFirstTreeBinding(workspace, sessionCtx) {
|
|
5148
|
+
if (!contextTreePath) return;
|
|
5149
|
+
installFirstTreeIntegration({
|
|
5150
|
+
workspacePath: workspace,
|
|
5151
|
+
contextTreePath,
|
|
5152
|
+
workspaceId: agentName ?? sessionCtx.chatId,
|
|
5153
|
+
treeRepoUrl: contextTreeRepoUrl ?? void 0,
|
|
5154
|
+
log: (msg) => sessionCtx.log(msg)
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
4789
5157
|
return {
|
|
4790
5158
|
async start(message, sessionCtx) {
|
|
4791
5159
|
ctx = sessionCtx;
|
|
@@ -4811,6 +5179,7 @@ const createCodexHandler = (config) => {
|
|
|
4811
5179
|
content: buildAgentBriefing(payload)
|
|
4812
5180
|
}
|
|
4813
5181
|
});
|
|
5182
|
+
ensureFirstTreeBinding(cwd, sessionCtx);
|
|
4814
5183
|
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
4815
5184
|
codex = new Codex({
|
|
4816
5185
|
env: buildEnv(sessionCtx),
|
|
@@ -4835,17 +5204,20 @@ const createCodexHandler = (config) => {
|
|
|
4835
5204
|
env: [],
|
|
4836
5205
|
gitRepos: []
|
|
4837
5206
|
};
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
5207
|
+
if (!existsSync(join(cwd, ".agent", "identity.json"))) {
|
|
5208
|
+
bootstrapWorkspace({
|
|
5209
|
+
workspacePath: cwd,
|
|
5210
|
+
identity: sessionCtx.agent,
|
|
5211
|
+
contextTreePath,
|
|
5212
|
+
serverUrl: sessionCtx.sdk.serverUrl,
|
|
5213
|
+
chatId: sessionCtx.chatId,
|
|
5214
|
+
briefing: {
|
|
5215
|
+
format: "agents-md",
|
|
5216
|
+
content: buildAgentBriefing(payload)
|
|
5217
|
+
}
|
|
5218
|
+
});
|
|
5219
|
+
ensureFirstTreeBinding(cwd, sessionCtx);
|
|
5220
|
+
}
|
|
4849
5221
|
await prepareGitWorktrees(payload, cwd, sessionCtx.chatId);
|
|
4850
5222
|
codex = new Codex({
|
|
4851
5223
|
env: buildEnv(sessionCtx),
|
|
@@ -5878,7 +6250,7 @@ var AgentSlot = class {
|
|
|
5878
6250
|
lastActivityMs: 0
|
|
5879
6251
|
};
|
|
5880
6252
|
}
|
|
5881
|
-
async start(
|
|
6253
|
+
async start(contextTreeBinding) {
|
|
5882
6254
|
const sdk = (await this.clientConnection.bindAgent(this.config.agentId, this.config.runtimeType ?? this.config.type, this.config.runtimeVersion)).sdk;
|
|
5883
6255
|
this.sdk = sdk;
|
|
5884
6256
|
const agent = await sdk.register();
|
|
@@ -5956,7 +6328,9 @@ var AgentSlot = class {
|
|
|
5956
6328
|
handlerFactory: this.config.handlerFactory,
|
|
5957
6329
|
handlerConfig: {
|
|
5958
6330
|
workspaceRoot: join(DEFAULT_DATA_DIR, "workspaces", this.config.name),
|
|
5959
|
-
|
|
6331
|
+
agentName: this.config.name,
|
|
6332
|
+
contextTreePath: contextTreeBinding?.path,
|
|
6333
|
+
contextTreeRepoUrl: contextTreeBinding?.repoUrl,
|
|
5960
6334
|
gitMirrorManager
|
|
5961
6335
|
},
|
|
5962
6336
|
agentIdentity: {
|
|
@@ -8680,7 +9054,7 @@ async function onboardCreate(args) {
|
|
|
8680
9054
|
}
|
|
8681
9055
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
8682
9056
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
8683
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
9057
|
+
const { bindFeishuBot } = await import("./feishu-fLnwqCOs.mjs").then((n) => n.r);
|
|
8684
9058
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
8685
9059
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
8686
9060
|
else {
|
|
@@ -9893,7 +10267,7 @@ function createFeedbackHandler(config) {
|
|
|
9893
10267
|
return { handle };
|
|
9894
10268
|
}
|
|
9895
10269
|
//#endregion
|
|
9896
|
-
//#region ../server/dist/app-
|
|
10270
|
+
//#region ../server/dist/app-BXdU2BzM.mjs
|
|
9897
10271
|
var import_fastify_opentelemetry = /* @__PURE__ */ __toESM(require_fastify_opentelemetry(), 1);
|
|
9898
10272
|
init_esm();
|
|
9899
10273
|
var __defProp = Object.defineProperty;
|
|
@@ -10658,6 +11032,28 @@ async function resolveAgentClient(db, data) {
|
|
|
10658
11032
|
return client.id;
|
|
10659
11033
|
}
|
|
10660
11034
|
/**
|
|
11035
|
+
* Validate a `delegateMention` write at the service layer. Two checks:
|
|
11036
|
+
* 1. Target uuid must resolve to an existing agent — dangling references
|
|
11037
|
+
* would silently break webhook delegation at runtime.
|
|
11038
|
+
* 2. Target must belong to the same organization as the source agent —
|
|
11039
|
+
* cross-org delegate links are rejected here at the source so the
|
|
11040
|
+
* database never accumulates dirty rows. The webhook router has a
|
|
11041
|
+
* defense-in-depth check that filters them at fan-out time, but this
|
|
11042
|
+
* keeps the data clean and gives the admin UI an immediate 422 instead
|
|
11043
|
+
* of a silent runtime drop.
|
|
11044
|
+
*
|
|
11045
|
+
* `null` clears the field — handled by the caller; we are only invoked when
|
|
11046
|
+
* the caller wrote a non-null uuid.
|
|
11047
|
+
*/
|
|
11048
|
+
async function validateDelegateMentionTarget(db, targetUuid, sourceOrgId) {
|
|
11049
|
+
const [target] = await db.select({
|
|
11050
|
+
uuid: agents.uuid,
|
|
11051
|
+
organizationId: agents.organizationId
|
|
11052
|
+
}).from(agents).where(eq(agents.uuid, targetUuid)).limit(1);
|
|
11053
|
+
if (!target) throw new BadRequestError(`delegateMention target "${targetUuid}" not found`);
|
|
11054
|
+
if (target.organizationId !== sourceOrgId) throw new BadRequestError("delegateMention target must belong to the same organization as the agent");
|
|
11055
|
+
}
|
|
11056
|
+
/**
|
|
10661
11057
|
* Pick the first admin member in the org for internal system agents. Throws
|
|
10662
11058
|
* if the org has no admin — the caller should surface the error so an admin
|
|
10663
11059
|
* is created before the system tries to register more agents.
|
|
@@ -10697,6 +11093,7 @@ async function createAgent(db, data, options = {}) {
|
|
|
10697
11093
|
type: data.type
|
|
10698
11094
|
});
|
|
10699
11095
|
await ensureClientSupportsRuntimeProvider(db, clientId, runtimeProvider, { force: options.force });
|
|
11096
|
+
if (data.delegateMention) await validateDelegateMentionTarget(db, data.delegateMention, orgId);
|
|
10700
11097
|
const [org] = await db.select({ maxAgents: organizations.maxAgents }).from(organizations).where(eq(organizations.id, orgId)).limit(1);
|
|
10701
11098
|
if (org && org.maxAgents > 0) {
|
|
10702
11099
|
if (((await db.select({ value: count() }).from(agents).where(and(eq(agents.organizationId, orgId), ne(agents.status, AGENT_STATUSES.DELETED))))[0]?.value ?? 0) >= org.maxAgents) throw new ForbiddenError(`Organization "${orgId}" has reached its agent limit (${org.maxAgents}). Upgrade your plan or delete unused agents.`);
|
|
@@ -10843,7 +11240,10 @@ async function updateAgent(db, uuid, data) {
|
|
|
10843
11240
|
const updates = { updatedAt: /* @__PURE__ */ new Date() };
|
|
10844
11241
|
if (data.type !== void 0) updates.type = data.type;
|
|
10845
11242
|
if (data.displayName !== void 0) updates.displayName = data.displayName;
|
|
10846
|
-
if (data.delegateMention !== void 0)
|
|
11243
|
+
if (data.delegateMention !== void 0) {
|
|
11244
|
+
if (data.delegateMention !== null) await validateDelegateMentionTarget(db, data.delegateMention, agent.organizationId);
|
|
11245
|
+
updates.delegateMention = data.delegateMention;
|
|
11246
|
+
}
|
|
10847
11247
|
if (data.visibility !== void 0) updates.visibility = data.visibility;
|
|
10848
11248
|
if (data.metadata !== void 0) updates.metadata = data.metadata;
|
|
10849
11249
|
if (data.managerId !== void 0) {
|
|
@@ -13430,7 +13830,7 @@ async function agentRoutes(app) {
|
|
|
13430
13830
|
});
|
|
13431
13831
|
app.post("/:uuid/test", async (request, reply) => {
|
|
13432
13832
|
const { uuid } = request.params;
|
|
13433
|
-
await requireAgentAccess(request, app.db, "manage");
|
|
13833
|
+
const { agent: targetAgent } = await requireAgentAccess(request, app.db, "manage");
|
|
13434
13834
|
const presence = await getPresence(app.db, uuid);
|
|
13435
13835
|
const wsConnected = hasActiveConnection(uuid);
|
|
13436
13836
|
const clientId = getAgentClientId(uuid) ?? presence?.clientId ?? null;
|
|
@@ -13467,15 +13867,15 @@ async function agentRoutes(app) {
|
|
|
13467
13867
|
message: "Agent connection is stale — heartbeat lost. The client process may have crashed.",
|
|
13468
13868
|
connection
|
|
13469
13869
|
});
|
|
13470
|
-
const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"))).limit(1);
|
|
13870
|
+
const [owner] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.delegateMention, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
|
|
13471
13871
|
let senderId = owner?.uuid ?? null;
|
|
13472
13872
|
if (!senderId) {
|
|
13473
|
-
const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"))).limit(1);
|
|
13873
|
+
const [other] = await app.db.select({ uuid: agents.uuid }).from(agents).where(and(ne(agents.uuid, uuid), eq(agents.status, "active"), eq(agents.organizationId, targetAgent.organizationId))).limit(1);
|
|
13474
13874
|
senderId = other?.uuid ?? null;
|
|
13475
13875
|
}
|
|
13476
13876
|
if (!senderId) return reply.status(200).send({
|
|
13477
13877
|
status: "error",
|
|
13478
|
-
message: "No suitable sender found. Need at least one other active agent.",
|
|
13878
|
+
message: "No suitable sender found. Need at least one other active agent in the same organization.",
|
|
13479
13879
|
connection
|
|
13480
13880
|
});
|
|
13481
13881
|
const chat = await findOrCreateDirectChat(app.db, senderId, uuid);
|
|
@@ -14668,7 +15068,7 @@ function decodeCursor(cursor) {
|
|
|
14668
15068
|
* - Cursor narrows the result to rows STRICTLY before `(cursor.ts, cursor.id)`.
|
|
14669
15069
|
* - Followed by a small participant-list lookup for the page only.
|
|
14670
15070
|
*/
|
|
14671
|
-
async function listMeChats(db, humanAgentId, query) {
|
|
15071
|
+
async function listMeChats(db, humanAgentId, organizationId, query) {
|
|
14672
15072
|
const limit = query.limit;
|
|
14673
15073
|
const cursor = query.cursor ? decodeCursor(query.cursor) : null;
|
|
14674
15074
|
if (query.cursor && !cursor) throw new BadRequestError("Invalid cursor");
|
|
@@ -14709,6 +15109,11 @@ async function listMeChats(db, humanAgentId, query) {
|
|
|
14709
15109
|
FROM chats c
|
|
14710
15110
|
JOIN deduped d ON d.chat_id = c.id
|
|
14711
15111
|
WHERE c.parent_chat_id IS NULL
|
|
15112
|
+
/* Scope to the caller's org. Without this, cross-org dirty chats
|
|
15113
|
+
whose chat_participants still reference the caller's human agent
|
|
15114
|
+
(historical pollution — see fix/cross-org-direct-chat-pollution)
|
|
15115
|
+
would leak into the list and 404 on click via requireChatAccess. */
|
|
15116
|
+
AND c.organization_id = ${organizationId}
|
|
14712
15117
|
/* Filter: unread / watching */
|
|
14713
15118
|
AND (${!filterUnreadOnly}::bool OR d.unread_mention_count > 0)
|
|
14714
15119
|
AND (${!filterWatchingOnly}::bool OR d.membership_kind = 'watching')
|
|
@@ -15388,19 +15793,26 @@ async function deleteOrgSetting(db, orgId, namespace) {
|
|
|
15388
15793
|
await db.delete(organizationSettings).where(and(eq(organizationSettings.organizationId, orgId), eq(organizationSettings.namespace, namespace)));
|
|
15389
15794
|
}
|
|
15390
15795
|
/**
|
|
15391
|
-
* Resolve the caller's "primary org"
|
|
15392
|
-
*
|
|
15393
|
-
*
|
|
15394
|
-
* the SDK call shape doesn't have to change while the per-tenant lookup
|
|
15395
|
-
* still happens correctly.
|
|
15796
|
+
* Resolve the caller's "primary org" for user-scoped routes that
|
|
15797
|
+
* historically didn't take an `:orgId` (e.g. `/context-tree/info`,
|
|
15798
|
+
* `/context-tree/snapshot`).
|
|
15396
15799
|
*
|
|
15397
|
-
*
|
|
15398
|
-
*
|
|
15399
|
-
*
|
|
15800
|
+
* Uses the same `pickDefaultMembership` helper that `/me` uses to compute
|
|
15801
|
+
* `defaultOrganizationId` (most-recently-active membership, id desc tie-break).
|
|
15802
|
+
* That guarantees the org `/me` reports as the default is the same org these
|
|
15803
|
+
* server-internal lookups read from — earlier the two sides used opposite
|
|
15804
|
+
* orderings (`/me` desc, this fn asc), so multi-org users saw `/info`
|
|
15805
|
+
* resolve to a different (often unconfigured) org than the one Team Settings
|
|
15806
|
+
* was edited for.
|
|
15807
|
+
*
|
|
15808
|
+
* Returns `null` for users with no active membership.
|
|
15400
15809
|
*/
|
|
15401
15810
|
async function resolveUserPrimaryOrgId(db, userId) {
|
|
15402
|
-
|
|
15403
|
-
|
|
15811
|
+
return pickDefaultMembership(await db.select({
|
|
15812
|
+
id: members.id,
|
|
15813
|
+
organizationId: members.organizationId,
|
|
15814
|
+
createdAt: members.createdAt
|
|
15815
|
+
}).from(members).where(and(eq(members.userId, userId), eq(members.status, "active"))))?.organizationId ?? null;
|
|
15404
15816
|
}
|
|
15405
15817
|
async function contextTreeInfoRoutes(app) {
|
|
15406
15818
|
/**
|
|
@@ -15721,17 +16133,17 @@ function isGithubHttpsRepo(repoUrl) {
|
|
|
15721
16133
|
}
|
|
15722
16134
|
function contextStatus(warning) {
|
|
15723
16135
|
if (warning?.stale) return {
|
|
15724
|
-
label: "
|
|
16136
|
+
label: "Context Tree may be stale",
|
|
15725
16137
|
detail: warning.detail,
|
|
15726
16138
|
severity: "warning"
|
|
15727
16139
|
};
|
|
15728
16140
|
if (warning) return {
|
|
15729
|
-
label: "
|
|
16141
|
+
label: "Context Tree needs attention",
|
|
15730
16142
|
detail: warning.detail,
|
|
15731
16143
|
severity: "warning"
|
|
15732
16144
|
};
|
|
15733
16145
|
return {
|
|
15734
|
-
label: "
|
|
16146
|
+
label: "Context Tree is up to date",
|
|
15735
16147
|
detail: "Agents have a synced team context snapshot available.",
|
|
15736
16148
|
severity: "ok"
|
|
15737
16149
|
};
|
|
@@ -16333,7 +16745,7 @@ function ghostNodeId(path) {
|
|
|
16333
16745
|
function toPosix(path) {
|
|
16334
16746
|
return sep === "/" ? path : path.split(sep).join("/");
|
|
16335
16747
|
}
|
|
16336
|
-
const querySchema = z.object({ window: z.enum([
|
|
16748
|
+
const querySchema$1 = z.object({ window: z.enum([
|
|
16337
16749
|
"1d",
|
|
16338
16750
|
"7d",
|
|
16339
16751
|
"30d"
|
|
@@ -16344,7 +16756,7 @@ async function contextTreeSnapshotRoutes(app) {
|
|
|
16344
16756
|
timeWindow: "1 minute",
|
|
16345
16757
|
keyGenerator: (request) => request.user?.userId ?? request.ip
|
|
16346
16758
|
} } }, async (request) => {
|
|
16347
|
-
const query = querySchema.parse(request.query);
|
|
16759
|
+
const query = querySchema$1.parse(request.query);
|
|
16348
16760
|
const { userId } = requireUser(request);
|
|
16349
16761
|
const orgId = await resolveUserPrimaryOrgId(app.db, userId);
|
|
16350
16762
|
const binding = orgId ? await getOrgContextTree(app.db, orgId) : {};
|
|
@@ -16482,7 +16894,7 @@ async function healthzRoutes(app) {
|
|
|
16482
16894
|
* `api/orgs/invitations.ts` (Class B, admin-gated).
|
|
16483
16895
|
*/
|
|
16484
16896
|
async function publicInvitationRoutes(app) {
|
|
16485
|
-
const { previewInvitation } = await import("./invitation-C299fxkP-
|
|
16897
|
+
const { previewInvitation } = await import("./invitation-C299fxkP-B89eqDos.mjs");
|
|
16486
16898
|
app.get("/:token/preview", async (request, reply) => {
|
|
16487
16899
|
if (!request.params.token) throw new UnauthorizedError("Token required");
|
|
16488
16900
|
const preview = await previewInvitation(app.db, request.params.token);
|
|
@@ -16662,7 +17074,7 @@ async function meRoutes(app) {
|
|
|
16662
17074
|
*/
|
|
16663
17075
|
app.get("/me/pinned-agents", async (request) => {
|
|
16664
17076
|
const { userId } = requireUser(request);
|
|
16665
|
-
const { listMyPinnedAgents } = await import("./client-
|
|
17077
|
+
const { listMyPinnedAgents } = await import("./client-0RrgrMjR-DPyuu6Ls.mjs");
|
|
16666
17078
|
return listMyPinnedAgents(app.db, { userId });
|
|
16667
17079
|
});
|
|
16668
17080
|
/**
|
|
@@ -17095,7 +17507,7 @@ async function orgChatRoutes(app) {
|
|
|
17095
17507
|
}
|
|
17096
17508
|
if (view === "grouped") return listChatsForMember(app.db, scope.memberId, scope.humanAgentId);
|
|
17097
17509
|
const query = listMeChatsQuerySchema.parse(request.query);
|
|
17098
|
-
return listMeChats(app.db, scope.humanAgentId, query);
|
|
17510
|
+
return listMeChats(app.db, scope.humanAgentId, scope.organizationId, query);
|
|
17099
17511
|
});
|
|
17100
17512
|
/**
|
|
17101
17513
|
* POST /orgs/:orgId/chats — create a new chat. The :orgId path param
|
|
@@ -17136,6 +17548,34 @@ async function orgClientRoutes(app) {
|
|
|
17136
17548
|
}));
|
|
17137
17549
|
});
|
|
17138
17550
|
}
|
|
17551
|
+
const querySchema = z.object({ window: z.enum([
|
|
17552
|
+
"1d",
|
|
17553
|
+
"7d",
|
|
17554
|
+
"30d"
|
|
17555
|
+
]).optional() }).strict();
|
|
17556
|
+
async function orgContextTreeSnapshotRoutes(app) {
|
|
17557
|
+
app.get("/snapshot", { config: { rateLimit: {
|
|
17558
|
+
max: app.config.rateLimit?.contextTreeSnapshotMax ?? 6,
|
|
17559
|
+
timeWindow: "1 minute",
|
|
17560
|
+
keyGenerator: (request) => `${request.user?.userId ?? request.ip}:${orgIdParam(request.params) ?? "unknown-org"}`
|
|
17561
|
+
} } }, async (request) => {
|
|
17562
|
+
const query = querySchema.parse(request.query);
|
|
17563
|
+
const scope = await requireOrgMembership(request, app.db);
|
|
17564
|
+
const binding = await getOrgContextTree(app.db, scope.organizationId);
|
|
17565
|
+
const githubToken = contextTreeGithubTokenForRepo(binding.repo, app.config.contextTreeSync);
|
|
17566
|
+
const snapshot = await getContextTreeSnapshot({
|
|
17567
|
+
...binding,
|
|
17568
|
+
githubToken
|
|
17569
|
+
}, query.window ?? "7d");
|
|
17570
|
+
return contextTreeSnapshotSchema.parse(snapshot);
|
|
17571
|
+
});
|
|
17572
|
+
}
|
|
17573
|
+
function orgIdParam(params) {
|
|
17574
|
+
if (!params || typeof params !== "object") return null;
|
|
17575
|
+
if (!("orgId" in params)) return null;
|
|
17576
|
+
const orgId = params.orgId;
|
|
17577
|
+
return typeof orgId === "string" ? orgId : null;
|
|
17578
|
+
}
|
|
17139
17579
|
/**
|
|
17140
17580
|
* Class B — `/api/v1/orgs/:orgId` itself: read & rename the org row.
|
|
17141
17581
|
* Replaces the deleted `/admin/organizations/:id` GET/PATCH pair.
|
|
@@ -17803,6 +18243,31 @@ function extractMentions$1(text) {
|
|
|
17803
18243
|
}
|
|
17804
18244
|
return [...names];
|
|
17805
18245
|
}
|
|
18246
|
+
/** Extract mentions from structural payload fields (not free-form text).
|
|
18247
|
+
* GitHub's `pull_request.review_requested` puts the targeted reviewer in
|
|
18248
|
+
* `requested_reviewer.login`, not in any text body — `extractMentions` would
|
|
18249
|
+
* miss it. Team requests use `requested_team` instead, which we deliberately
|
|
18250
|
+
* skip to stay consistent with `extractMentions` ignoring `@org/team`. */
|
|
18251
|
+
function extractStructuralMentions(eventType, payload) {
|
|
18252
|
+
if (!isRecord(payload)) return [];
|
|
18253
|
+
if (eventType !== "pull_request") return [];
|
|
18254
|
+
if (payload.action !== "review_requested") return [];
|
|
18255
|
+
const reviewer = isRecord(payload.requested_reviewer) ? payload.requested_reviewer : null;
|
|
18256
|
+
const login = typeof reviewer?.login === "string" ? reviewer.login : null;
|
|
18257
|
+
return login ? [login.toLowerCase()] : [];
|
|
18258
|
+
}
|
|
18259
|
+
const DELEGATE_VERDICT_MESSAGES = {
|
|
18260
|
+
ok: "delegate_mention target eligible",
|
|
18261
|
+
not_found: "delegate_mention target not found, skipping",
|
|
18262
|
+
cross_org: "delegate_mention target belongs to another org, skipping",
|
|
18263
|
+
inactive: "delegate_mention target not active, skipping"
|
|
18264
|
+
};
|
|
18265
|
+
function evaluateDelegateTarget(target, sourceOrgId) {
|
|
18266
|
+
if (!target) return "not_found";
|
|
18267
|
+
if (target.organizationId !== sourceOrgId) return "cross_org";
|
|
18268
|
+
if (target.status !== "active") return "inactive";
|
|
18269
|
+
return "ok";
|
|
18270
|
+
}
|
|
17806
18271
|
/**
|
|
17807
18272
|
* Route @mentions to delegate agents.
|
|
17808
18273
|
* For each mentioned user who has delegate_mention configured,
|
|
@@ -17821,13 +18286,19 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17821
18286
|
if (agent.status !== "active" || !agent.delegateMention) continue;
|
|
17822
18287
|
const [target] = await app.db.select({
|
|
17823
18288
|
id: agents.uuid,
|
|
17824
|
-
status: agents.status
|
|
18289
|
+
status: agents.status,
|
|
18290
|
+
organizationId: agents.organizationId
|
|
17825
18291
|
}).from(agents).where(eq(agents.uuid, agent.delegateMention)).limit(1);
|
|
17826
|
-
|
|
18292
|
+
const verdict = evaluateDelegateTarget(target, organizationId);
|
|
18293
|
+
if (verdict !== "ok") {
|
|
17827
18294
|
log$1.warn({
|
|
17828
18295
|
targetAgent: agent.delegateMention,
|
|
17829
|
-
sourceAgent: agent.name
|
|
17830
|
-
|
|
18296
|
+
sourceAgent: agent.name,
|
|
18297
|
+
sourceOrg: organizationId,
|
|
18298
|
+
targetOrg: target?.organizationId,
|
|
18299
|
+
targetStatus: target?.status,
|
|
18300
|
+
verdict
|
|
18301
|
+
}, DELEGATE_VERDICT_MESSAGES[verdict]);
|
|
17831
18302
|
continue;
|
|
17832
18303
|
}
|
|
17833
18304
|
try {
|
|
@@ -17838,6 +18309,7 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17838
18309
|
type: "github_mention",
|
|
17839
18310
|
mentionedUser: agent.name,
|
|
17840
18311
|
event: ctx.event,
|
|
18312
|
+
action: ctx.action,
|
|
17841
18313
|
repository: ctx.repository,
|
|
17842
18314
|
sender: ctx.sender,
|
|
17843
18315
|
title: ctx.title,
|
|
@@ -17847,7 +18319,8 @@ async function routeMentionDelegations(app, organizationId, mentionedNames, ctx)
|
|
|
17847
18319
|
metadata: {
|
|
17848
18320
|
source: "github",
|
|
17849
18321
|
event: "mention_delegation",
|
|
17850
|
-
mentionedUser: agent.name
|
|
18322
|
+
mentionedUser: agent.name,
|
|
18323
|
+
action: ctx.action
|
|
17851
18324
|
}
|
|
17852
18325
|
});
|
|
17853
18326
|
notifyRecipients(app.notifier, recipients, msg.id);
|
|
@@ -17928,18 +18401,43 @@ async function githubWebhookRoutes(app) {
|
|
|
17928
18401
|
ok: true,
|
|
17929
18402
|
event: "ping"
|
|
17930
18403
|
});
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
18404
|
+
const deliveryHeader = request.headers["x-github-delivery"];
|
|
18405
|
+
const deliveryId = typeof deliveryHeader === "string" && deliveryHeader.length > 0 ? deliveryHeader : null;
|
|
18406
|
+
if (deliveryId) {
|
|
18407
|
+
if (!await claimEvent(app.db, deliveryId, "github")) {
|
|
18408
|
+
log$1.info({
|
|
18409
|
+
deliveryId,
|
|
18410
|
+
eventType
|
|
18411
|
+
}, "duplicate GitHub delivery, skipping");
|
|
18412
|
+
return reply.status(200).send({
|
|
18413
|
+
ok: true,
|
|
18414
|
+
event: eventType,
|
|
18415
|
+
deduped: true
|
|
18416
|
+
});
|
|
18417
|
+
}
|
|
18418
|
+
}
|
|
18419
|
+
try {
|
|
18420
|
+
if (eventType === "issues") return await handleIssuesEvent(app, orgId, eventType, payload, reply);
|
|
18421
|
+
if (eventType === "issue_comment") return await handleIssueCommentEvent(app, orgId, eventType, payload, reply);
|
|
18422
|
+
let mentionsRouted = 0;
|
|
18423
|
+
const allowedActions = MENTION_ACTIONS[eventType];
|
|
18424
|
+
const action = isRecord(payload) && typeof payload.action === "string" ? payload.action : void 0;
|
|
18425
|
+
if (allowedActions && action && allowedActions.includes(action)) mentionsRouted = await handleMentionDelegation(app, orgId, eventType, payload);
|
|
18426
|
+
return reply.status(200).send({
|
|
18427
|
+
ok: true,
|
|
18428
|
+
event: eventType,
|
|
18429
|
+
handled: mentionsRouted > 0,
|
|
18430
|
+
mentionsRouted
|
|
18431
|
+
});
|
|
18432
|
+
} catch (err) {
|
|
18433
|
+
if (deliveryId) await unclaimEvent(app.db, deliveryId, "github").catch((unclaimErr) => {
|
|
18434
|
+
log$1.error({
|
|
18435
|
+
err: unclaimErr,
|
|
18436
|
+
deliveryId
|
|
18437
|
+
}, "failed to unclaim GitHub delivery after handler error");
|
|
18438
|
+
});
|
|
18439
|
+
throw err;
|
|
18440
|
+
}
|
|
17943
18441
|
});
|
|
17944
18442
|
}
|
|
17945
18443
|
/** Extract text body from any GitHub webhook event for @mention scanning. */
|
|
@@ -17979,12 +18477,14 @@ function extractEventContext(eventType, payload) {
|
|
|
17979
18477
|
const sender = isRecord(payload.sender) ? payload.sender : null;
|
|
17980
18478
|
const repository = typeof repo?.full_name === "string" ? repo.full_name : "";
|
|
17981
18479
|
const senderLogin = typeof sender?.login === "string" ? sender.login : "";
|
|
18480
|
+
const action = typeof payload.action === "string" ? payload.action : void 0;
|
|
17982
18481
|
switch (eventType) {
|
|
17983
18482
|
case "issues": {
|
|
17984
18483
|
const issue = isRecord(payload.issue) ? payload.issue : null;
|
|
17985
18484
|
if (!issue) return null;
|
|
17986
18485
|
return {
|
|
17987
18486
|
event: "issues",
|
|
18487
|
+
action,
|
|
17988
18488
|
repository,
|
|
17989
18489
|
sender: senderLogin,
|
|
17990
18490
|
title: `Issue #${issue.number}: ${issue.title}`,
|
|
@@ -17998,6 +18498,7 @@ function extractEventContext(eventType, payload) {
|
|
|
17998
18498
|
if (!issue || !comment) return null;
|
|
17999
18499
|
return {
|
|
18000
18500
|
event: "issue_comment",
|
|
18501
|
+
action,
|
|
18001
18502
|
repository,
|
|
18002
18503
|
sender: senderLogin,
|
|
18003
18504
|
title: `Issue #${issue.number}: ${issue.title}`,
|
|
@@ -18010,6 +18511,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18010
18511
|
if (!pr) return null;
|
|
18011
18512
|
return {
|
|
18012
18513
|
event: "pull_request",
|
|
18514
|
+
action,
|
|
18013
18515
|
repository,
|
|
18014
18516
|
sender: senderLogin,
|
|
18015
18517
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18023,6 +18525,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18023
18525
|
if (!pr || !review) return null;
|
|
18024
18526
|
return {
|
|
18025
18527
|
event: "pull_request_review",
|
|
18528
|
+
action,
|
|
18026
18529
|
repository,
|
|
18027
18530
|
sender: senderLogin,
|
|
18028
18531
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18036,6 +18539,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18036
18539
|
if (!pr || !comment) return null;
|
|
18037
18540
|
return {
|
|
18038
18541
|
event: "pull_request_review_comment",
|
|
18542
|
+
action,
|
|
18039
18543
|
repository,
|
|
18040
18544
|
sender: senderLogin,
|
|
18041
18545
|
title: `PR #${pr.number}: ${pr.title}`,
|
|
@@ -18048,6 +18552,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18048
18552
|
if (!disc) return null;
|
|
18049
18553
|
return {
|
|
18050
18554
|
event: "discussion",
|
|
18555
|
+
action,
|
|
18051
18556
|
repository,
|
|
18052
18557
|
sender: senderLogin,
|
|
18053
18558
|
title: typeof disc.title === "string" ? disc.title : "",
|
|
@@ -18061,6 +18566,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18061
18566
|
if (!disc || !comment) return null;
|
|
18062
18567
|
return {
|
|
18063
18568
|
event: "discussion_comment",
|
|
18569
|
+
action,
|
|
18064
18570
|
repository,
|
|
18065
18571
|
sender: senderLogin,
|
|
18066
18572
|
title: typeof disc.title === "string" ? disc.title : "",
|
|
@@ -18073,6 +18579,7 @@ function extractEventContext(eventType, payload) {
|
|
|
18073
18579
|
if (!comment) return null;
|
|
18074
18580
|
return {
|
|
18075
18581
|
event: "commit_comment",
|
|
18582
|
+
action,
|
|
18076
18583
|
repository,
|
|
18077
18584
|
sender: senderLogin,
|
|
18078
18585
|
title: "Commit comment",
|
|
@@ -18088,16 +18595,26 @@ function extractEventContext(eventType, payload) {
|
|
|
18088
18595
|
* Only called after action gating confirms this is a "new content" event.
|
|
18089
18596
|
*/
|
|
18090
18597
|
async function handleMentionDelegation(app, organizationId, eventType, payload) {
|
|
18091
|
-
const
|
|
18598
|
+
const textMentions = extractMentions$1(extractEventText(eventType, payload));
|
|
18599
|
+
const structuralMentions = extractStructuralMentions(eventType, payload);
|
|
18600
|
+
const mentions = [...new Set([...textMentions, ...structuralMentions])];
|
|
18092
18601
|
const mentionCtx = extractEventContext(eventType, payload);
|
|
18093
18602
|
if (mentions.length > 0 && mentionCtx) return routeMentionDelegations(app, organizationId, mentions, mentionCtx);
|
|
18094
18603
|
return 0;
|
|
18095
18604
|
}
|
|
18096
|
-
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18605
|
+
/** Actions that represent new/changed content (worth scanning for @mentions).
|
|
18606
|
+
* Note: `pull_request.review_requested` doesn't carry an @mention in any
|
|
18607
|
+
* text body — the reviewer is in `requested_reviewer.login`. We pick it up
|
|
18608
|
+
* via `extractStructuralMentions`. The complementary `review_request_removed`
|
|
18609
|
+
* is intentionally omitted to avoid notifying the reviewer twice. */
|
|
18097
18610
|
const MENTION_ACTIONS = {
|
|
18098
18611
|
issues: ["opened", "edited"],
|
|
18099
18612
|
issue_comment: ["created"],
|
|
18100
|
-
pull_request: [
|
|
18613
|
+
pull_request: [
|
|
18614
|
+
"opened",
|
|
18615
|
+
"edited",
|
|
18616
|
+
"review_requested"
|
|
18617
|
+
],
|
|
18101
18618
|
pull_request_review: ["submitted"],
|
|
18102
18619
|
pull_request_review_comment: ["created"],
|
|
18103
18620
|
discussion: ["created", "edited"],
|
|
@@ -19748,6 +20265,7 @@ async function buildApp(config) {
|
|
|
19748
20265
|
await scope.register(orgInvitationRoutes, { prefix: "/invitations" });
|
|
19749
20266
|
await scope.register(orgMemberRoutes, { prefix: "/members" });
|
|
19750
20267
|
await scope.register(orgSettingsRoutes, { prefix: "/settings" });
|
|
20268
|
+
await scope.register(orgContextTreeSnapshotRoutes, { prefix: "/context-tree" });
|
|
19751
20269
|
}), { prefix: "/orgs/:orgId" });
|
|
19752
20270
|
await api.register(orgWsRoutes(notifier, config.secrets.jwtSecret), { prefix: "/orgs/:orgId/ws" });
|
|
19753
20271
|
await api.register(userScope("resourcesScope", async (scope) => {
|