openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
664 lines (663 loc) • 31.3 kB
JavaScript
import { l as normalizeOptionalString, m as readNonBlankString } from "./string-coerce-1VuiLaaR.mjs";
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs";
import { o as racePromiseWithAbortSignal } from "./src-D1BHGNTj.mjs";
import "./abort-signal-QqAmLWTl.mjs";
import { r as isPathInside } from "./path-guards-CH7noY0Z.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { t as areDiagnosticsEnabledForProcess } from "./diagnostic-events-DT4ejwoa.mjs";
import "./session-key-DY3MOX4N.mjs";
import { h as parseAgentSessionKey } from "./session-key-utils-CDyBC9Gn.mjs";
import { o as getRuntimeConfigSnapshot } from "./runtime-snapshot-De_QxZI9.mjs";
import { u as WRITE_SCOPE } from "./operator-scopes-BO7_XQCg.mjs";
import { f as isTrustedSecretSurfaceUnavailableError } from "./runtime-degraded-state-Dyd8aD2x.mjs";
import { Cr as validateProjectsAddParams, Dr as validateProjectsSearchRemoteParams, Er as validateProjectsRemoveParams, Tr as validateProjectsRegisterParams, wr as validateProjectsListParams } from "./src-D6kL8tMP.mjs";
import { r as GatewayErrorDetailCodes, t as ErrorCodes } from "./gateway-error-details-gTekQWj2.mjs";
import { d as errorShape } from "./error-codes-D0WRV4Bq.mjs";
import { u as prepareCurrentUserProfileAliases } from "./user-profiles-C9fRzhrI.mjs";
import { g as readGatewayAccessRevision } from "./operator-role-policy-BPiQfn95.mjs";
import { d as withIncognitoSessionStoreEntries, t as captureIncognitoSessionBinding } from "./session-incognito-binding-Dp6RTei2.mjs";
import { d as getSessionRepositoryWorkspaceStore } from "./session-accessor.sqlite-deletion-CHbs1aOr.mjs";
import { c as sessionCreatorProfileId } from "./session-entry-provenance-C8CX2hws.mjs";
import { n as authorizeOperatorScopesForMethod, r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-CRSVCw9J.mjs";
import { t as createStageTimingTracker } from "./stage-timing-BUG4Fnsv.mjs";
import { i as resolveConfiguredGitHubHost, r as resolveConfiguredGitHubApiBaseUrl } from "./github-host-N2fYlLEb.mjs";
import { o as readCachedNativeGitHubToken } from "./github-read-identity-CnnDW8pa.mjs";
import { n as gitHubPublicApi, r as githubApiToken, t as CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE } from "./github-public-api-BAGEzuBb.mjs";
import { i as prepareCombinedSessionStore, n as loadCombinedSessionStoreForGatewayCore, r as mergeCombinedSessionStore } from "./combined-store-gateway-fr9Kqa9d.mjs";
import { n as getSessionRowProjection, r as requireSessionRowProjection } from "./session-row-projection-access-C2RDnj28.mjs";
import "./session-sharing-BFM0s33P.mjs";
import { m as createSessionListEntryFilter } from "./session-sharing-target-read-CNtvN3Ha.mjs";
import { i as prepareSessionRowSelection } from "./session-utils-list-BCxRVRtb.mjs";
import { i as captureWorktreeRunEndContext, n as captureWorktreeRegistryAuthority } from "./run-end-lifecycle-CRbsD4Aj.mjs";
import { f as readRegistryWorktrees } from "./registry-read-dp7dBS18.mjs";
import { a as managedWorktrees } from "./service-C8uzIOqd.mjs";
import { a as resolveProjectRegistry, d as ProjectCheckoutError, i as removeProjectRegistry, n as listWorkspaceProjects, r as registerProjectRegistry, t as listProjectRegistry } from "./project-registry-hZM6QWbe.mjs";
import "./session-utils-BLqrCe75.mjs";
import { i as ProjectCloneError, r as removeClonedProjectCheckout, t as materializeProjectClone } from "./project-clone-B3UadGFr.mjs";
import { t as parseConfiguredProjectGitUrl } from "./project-git-url.runtime.js";
import { n as defineValidatedGatewayHandler, t as assertValidParams } from "./validation-UW_NLjlV.mjs";
import { t as createGitHubReadGroup } from "./control-ui-session-pr-request-DdfHgCtM.mjs";
import { t as configuredDefaultRepository } from "./configured-default-repository-BW_t361n.mjs";
import path from "node:path";
import { channel } from "node:diagnostics_channel";
import { performance } from "node:perf_hooks";
//#region src/gateway/project-github-search.ts
const SEARCH_CACHE_MS = 6e4;
const SEARCH_CACHE_LIMIT = 100;
const SEARCH_RESULT_LIMIT = 10;
const AFFILIATED_RESULT_LIMIT = 10;
const EXACT_REPO_QUERY = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9._-]+$/;
const searchCache = /* @__PURE__ */ new Map();
function boundedString(value, maxLength) {
return normalizeOptionalString(value)?.slice(0, maxLength);
}
function parseRepository(value) {
if (!isRecord(value)) return null;
const fullName = readNonBlankString(value.full_name);
const name = readNonBlankString(value.name);
if (!fullName || !name) return null;
const clone = parseConfiguredProjectGitUrl(readNonBlankString(value.clone_url) ?? "");
const webUrl = boundedString(value.html_url, 2048);
if (!clone || !webUrl) return null;
const description = boundedString(value.description, 500);
const defaultBranch = boundedString(value.default_branch, 255);
return {
name: name.slice(0, 100),
fullName: fullName.slice(0, 200),
cloneUrl: clone.url,
webUrl,
private: value.private === true,
...description ? { description } : {},
...defaultBranch ? { defaultBranch } : {}
};
}
function repositoryArray(value) {
return (Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.items) ? value.items : []).flatMap((item) => {
const parsed = parseRepository(item);
return parsed ? [parsed] : [];
});
}
function matchesAffiliatedQuery(project, query) {
const needle = query.toLowerCase();
return [
project.name,
project.fullName,
project.description ?? ""
].join("\n").toLowerCase().includes(needle);
}
async function loadExactRepository(query, request) {
const url = new URL(`repos/${query}`, `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
try {
return parseRepository(await request(url.href));
} catch {
return null;
}
}
async function loadAffiliatedRepositories(request) {
const url = new URL("user/repos", `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
url.searchParams.set("affiliation", "owner,collaborator,organization_member");
url.searchParams.set("sort", "updated");
url.searchParams.set("direction", "desc");
url.searchParams.set("per_page", String(AFFILIATED_RESULT_LIMIT));
try {
return repositoryArray(await request(url.href, false));
} catch {
return [];
}
}
async function loadRepositorySearch(query, request) {
const url = new URL("search/repositories", `${gitHubPublicApi.GITHUB_API_BASE_URL}/`);
url.searchParams.set("q", `${query} in:name,description`);
url.searchParams.set("per_page", String(SEARCH_RESULT_LIMIT));
return repositoryArray(await request(url.href));
}
async function searchProjectsUncached(params) {
const [exact, affiliated, global] = await Promise.all([
EXACT_REPO_QUERY.test(params.query) ? loadExactRepository(params.query, params.request) : null,
params.token ? loadAffiliatedRepositories(params.request) : [],
loadRepositorySearch(params.query, params.request)
]);
const ranked = [
...exact ? [exact] : [],
...affiliated.filter((project) => matchesAffiliatedQuery(project, params.query)),
...global
];
const deduped = /* @__PURE__ */ new Map();
for (const project of ranked) {
const key = project.fullName.toLowerCase();
if (!deduped.has(key)) deduped.set(key, project);
}
return {
credential: params.token ? "configured" : "missing",
projects: [...deduped.values()].slice(0, SEARCH_RESULT_LIMIT)
};
}
/** Searches affiliated and public GitHub repositories for the project picker. */
async function searchRemoteProjects(query, options = {}) {
const config = getRuntimeConfigSnapshot();
const host = options.host ?? resolveConfiguredGitHubHost(config);
const apiBaseUrl = options.apiBaseUrl ?? resolveConfiguredGitHubApiBaseUrl(config);
const assertSelected = () => {
options.signal?.throwIfAborted();
options.assertCurrent?.();
const current = getRuntimeConfigSnapshot();
if (resolveConfiguredGitHubHost(current) !== host || resolveConfiguredGitHubApiBaseUrl(current) !== apiBaseUrl || gitHubPublicApi.GITHUB_API_BASE_URL !== apiBaseUrl) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub host changed during project search");
};
assertSelected();
const normalizedQuery = query.trim().toLowerCase();
const { token, cacheScope } = options.token === void 0 ? gitHubPublicApi.resolveGitHubApiCredentialScope(options.env) : {
token: options.token,
cacheScope: gitHubPublicApi.githubApiCredentialCacheScope(options.token)
};
const cacheKey = `${normalizedQuery}\0${host}\0${apiBaseUrl}\0${cacheScope}`;
const now = options.now ?? Date.now();
const cached = searchCache.get(cacheKey);
const reusable = cached && cached.expiresAt > now && !cached.access.signal.aborted;
const entry = reusable ? cached : {
expiresAt: now + SEARCH_CACHE_MS,
access: createGitHubReadGroup(),
promise: Promise.resolve({
credential: "missing",
projects: []
})
};
const release = entry.access.add(assertSelected, options.signal);
if (!reusable) {
const fetchImpl = options.fetchImpl ?? fetch;
const identity = {
assertSelected: entry.access.assertCurrent,
revalidate: async () => entry.access.assertCurrent()
};
const request = (url, optionalAuth = true) => {
const readJson = async (requestToken) => gitHubPublicApi.readGitHubJsonResponse(await gitHubPublicApi.fetchGitHubApi(url, fetchImpl, requestToken, void 0, identity, void 0, entry.access.signal, void 0, apiBaseUrl));
return optionalAuth ? gitHubPublicApi.withOptionalGitHubAuth(token, readJson) : readJson(token);
};
entry.promise = searchProjectsUncached({
query: query.trim(),
request,
token
}).catch((error) => {
if (searchCache.get(cacheKey) === entry) searchCache.delete(cacheKey);
throw error;
});
}
searchCache.delete(cacheKey);
searchCache.set(cacheKey, entry);
pruneMapToMaxSize(searchCache, SEARCH_CACHE_LIMIT);
try {
return await racePromiseWithAbortSignal(entry.promise, options.signal);
} finally {
release();
assertSelected();
}
}
//#endregion
//#region src/gateway/server-methods/projects-list-diagnostics.ts
const diagnostics = channel("openclaw.projects.list");
function startProjectsListDiagnostics(context) {
if (!areDiagnosticsEnabledForProcess() && !diagnostics.hasSubscribers) return;
const timing = createStageTimingTracker(() => performance.now());
let phase = "registry";
const mark = (next) => {
timing.mark(phase);
phase = next;
};
return {
mark,
finish() {
timing.mark(phase);
const { totalMs, stages } = timing.snapshot();
const fields = {
operation: "projects.list",
elapsedMs: totalMs,
phaseDurationsMs: Object.fromEntries(stages.map(({ name, durationMs }) => [name, durationMs]))
};
try {
if (diagnostics.hasSubscribers) diagnostics.publish(fields);
if (totalMs >= 1e3 && areDiagnosticsEnabledForProcess()) context.logGateway?.warn("projects.list: slow request", fields);
} catch {}
}
};
}
//#endregion
//#region src/gateway/server-methods/projects-recents.ts
function folderDisplayName(folder) {
return folder.replace(/[\\/]+$/u, "").split(/[\\/]/u).at(-1) || folder;
}
function indexPathProjects(projects) {
const byPath = /* @__PURE__ */ new Map();
const byAgent = /* @__PURE__ */ new Map();
for (const project of projects) {
if (project.source === "workspace") byAgent.set(project.agentId, project);
const previous = byPath.get(project.repoRoot);
if (!previous || (Number(project.source === "workspace") - Number(previous.source === "workspace") || project.id.localeCompare(previous.id)) < 0) byPath.set(project.repoRoot, project);
}
return {
byPath,
byAgent
};
}
async function listProjectRecents(store, profileIds, projects) {
const candidates = Object.entries(store).filter(([, entry]) => Boolean(sessionCreatorProfileId(entry.createdActor)) && Boolean(entry.createdActor?.id && profileIds.has(entry.createdActor.id))).toSorted(([leftKey, left], [rightKey, right]) => (right.updatedAt ?? 0) - (left.updatedAt ?? 0) || leftKey.localeCompare(rightKey));
const projectsById = new Map(projects.map((project) => [project.id, project]));
const seen = /* @__PURE__ */ new Set();
const recents = [];
let pathProjects;
for (const [sessionKey, entry] of candidates) {
if (entry.repositoryWorkspaceId) {
const repository = await getSessionRepositoryWorkspaceStore().get(entry.repositoryWorkspaceId);
const sessionAgentId = parseAgentSessionKey(sessionKey)?.agentId;
if (!repository || repository.sessionKey !== sessionKey || sessionAgentId && repository.agentId !== sessionAgentId || seen.has(repository.url)) continue;
seen.add(repository.url);
recents.push({
kind: "repository",
url: repository.url,
displayName: path.posix.basename(repository.url, ".git")
});
if (recents.length === 8) break;
continue;
}
const projectId = normalizeOptionalString(entry.projectId);
const explicitProject = projectId ? projectsById.get(projectId) : void 0;
const worktreeRoot = normalizeOptionalString(entry.worktree?.repoRoot);
const spawnedCwd = normalizeOptionalString(entry.spawnedCwd);
const execCwd = normalizeOptionalString(entry.execCwd);
const folder = worktreeRoot ?? spawnedCwd ?? execCwd;
let project = explicitProject;
if (!project && folder) {
const agentId = parseAgentSessionKey(sessionKey)?.agentId;
const indexed = pathProjects ??= indexPathProjects(projects);
const workspace = indexed.byAgent.get(agentId);
project = workspace?.repoRoot === folder ? workspace : indexed.byPath.get(folder);
}
const key = project ? `project:${project.id}` : folder ? `folder:${normalizeOptionalString(entry.execNode) ?? ""}\0${folder}` : void 0;
if (!key || seen.has(key)) continue;
seen.add(key);
recents.push(project ? {
kind: "project",
projectId: project.id,
displayName: project.displayName
} : {
kind: "folder",
folder,
displayName: folderDisplayName(folder),
...normalizeOptionalString(entry.execNode) ? { execNode: normalizeOptionalString(entry.execNode) } : {}
});
if (recents.length === 8) break;
}
return recents;
}
//#endregion
//#region src/gateway/server-methods/projects-session-store.ts
function loadProjectSessionStore(projection, incognitoStores) {
const { cfg } = projection.state;
const selection = prepareSessionRowSelection(projection, {}, {
metadataPrepared: true,
ordered: true
});
const paths = projection.state.scope({}).paths;
const entries = selection.entries.map(([key, entry]) => ({
key,
entry,
keyBytes: Buffer.from(key),
order: paths.get(selection.getTarget(key).storeTarget.storePath)
})).toSorted((left, right) => left.order - right.order || Buffer.compare(left.keyBytes, right.keyBytes));
const store = Object.fromEntries(entries.map(({ key, entry }) => [key, entry]));
const options = {
projection: "list",
includeIncognito: !incognitoStores
};
const prepared = prepareCombinedSessionStore(cfg, options);
if (incognitoStores) prepared.targets = {
...prepared.targets,
incognitoTargets: incognitoStores
};
if (prepared.targets.incognitoTargets.length > 0) Object.assign(store, mergeCombinedSessionStore(cfg, options, prepared, () => [], incognitoStores && ((target) => incognitoStores.find((source) => source.storePath === target.storePath).entries)).store);
return store;
}
//#endregion
//#region src/gateway/server-methods/projects.ts
const PROJECTS_LIST_MAX_RAW_CANDIDATES = Math.max(50, 50, 32);
function checkoutName(checkoutPath) {
const trimmed = checkoutPath.replace(/[\\/]+$/u, "");
return trimmed.split(/[\\/]/u).at(-1) || trimmed;
}
function compareRawProjectCandidates(left, right) {
return right.lastUsedAt - left.lastUsedAt || left.checkoutPath.localeCompare(right.checkoutPath) || left.kind.localeCompare(right.kind);
}
function retainNewestRawProjectCandidate(candidates, candidate) {
const insertionIndex = candidates.findIndex((existing) => compareRawProjectCandidates(candidate, existing) < 0);
if (insertionIndex < 0 && candidates.length >= PROJECTS_LIST_MAX_RAW_CANDIDATES) return;
candidates.splice(insertionIndex < 0 ? candidates.length : insertionIndex, 0, candidate);
if (candidates.length > PROJECTS_LIST_MAX_RAW_CANDIDATES) candidates.pop();
}
function sanitizePublicOriginUrl(originUrl) {
const trimmed = originUrl.trim();
const suffixIndex = trimmed.search(/[?#]/u);
const withoutSuffix = suffixIndex < 0 ? trimmed : trimmed.slice(0, suffixIndex);
const scp = /^[^@\s/:]+@(\[[^\]]+\]|[^:\s]+):(.+)$/u.exec(withoutSuffix);
if (scp) return `${scp[1]}:${scp[2]}`;
let parsed;
try {
parsed = new URL(withoutSuffix);
} catch {
return;
}
if (!parsed.username && !parsed.password) return withoutSuffix;
parsed.username = "";
parsed.password = "";
return parsed.toString();
}
function sanitizeProjectRecord(project) {
const { originUrl, ...record } = project;
const sanitizedOriginUrl = originUrl ? sanitizePublicOriginUrl(originUrl) : void 0;
return {
...record,
...sanitizedOriginUrl ? { originUrl: sanitizedOriginUrl } : {}
};
}
function projectCheckoutError(error) {
return errorShape(error instanceof ProjectCheckoutError ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, formatErrorMessage(error));
}
function projectCandidatesToSummaries(candidates) {
const groups = /* @__PURE__ */ new Map();
for (const candidate of candidates) {
const group = groups.get(candidate.fingerprint) ?? {
checkouts: /* @__PURE__ */ new Map(),
lastUsedAt: candidate.lastUsedAt,
name: checkoutName(candidate.checkoutPath),
nameUsedAt: candidate.lastUsedAt
};
const checkout = group.checkouts.get(candidate.checkoutPath);
if (!checkout || candidate.lastUsedAt > checkout.lastUsedAt) group.checkouts.set(candidate.checkoutPath, {
path: candidate.checkoutPath,
lastUsedAt: candidate.lastUsedAt
});
group.lastUsedAt = Math.max(group.lastUsedAt, candidate.lastUsedAt);
if (candidate.lastUsedAt > group.nameUsedAt) {
group.name = checkoutName(candidate.checkoutPath);
group.nameUsedAt = candidate.lastUsedAt;
}
if (!group.originUrl && candidate.originUrl) group.originUrl = candidate.originUrl;
groups.set(candidate.fingerprint, group);
}
return [...groups.values()].toSorted((left, right) => right.lastUsedAt - left.lastUsedAt || left.name.localeCompare(right.name)).slice(0, 50).map((group) => {
const summary = {
name: group.name,
checkouts: [...group.checkouts.values()].toSorted((left, right) => right.lastUsedAt - left.lastUsedAt || left.path.localeCompare(right.path)).slice(0, 50).map((checkout) => ({
runnerId: "gateway",
path: checkout.path
})),
lastUsedAt: group.lastUsedAt
};
if (group.originUrl) {
const originUrl = sanitizePublicOriginUrl(group.originUrl);
if (originUrl) summary.originUrl = originUrl;
}
return summary;
});
}
async function listObservedProjects(service, context, client, store, diagnostics) {
diagnostics?.mark("worktreeRegistry");
const worktrees = await service.listRegistryRecords();
diagnostics?.mark("sessionCandidates");
const cfg = context.getRuntimeConfig();
const rawCandidates = [];
const visibilityFilter = createSessionListEntryFilter({
client,
cfg
});
const canSeeAll = !visibilityFilter;
for (const [sessionKey, entry] of Object.entries(store)) {
if (visibilityFilter && !visibilityFilter(sessionKey, entry)) continue;
const checkoutPath = entry.execCwd?.trim();
if (checkoutPath && !entry.execNode?.trim()) retainNewestRawProjectCandidate(rawCandidates, {
kind: "session",
checkoutPath,
lastUsedAt: entry.updatedAt
});
}
diagnostics?.mark("worktreeCandidates");
for (const worktree of worktrees) {
if (worktree.removedAt !== void 0) continue;
if (!canSeeAll) {
const ownerId = worktree.ownerKind === "session" ? worktree.ownerId?.trim() : void 0;
const ownerEntry = ownerId ? store[ownerId] : void 0;
if (!ownerId || !ownerEntry || !visibilityFilter?.(ownerId, ownerEntry)) continue;
}
retainNewestRawProjectCandidate(rawCandidates, {
kind: "worktree",
checkoutPath: worktree.path,
fingerprint: worktree.repoFingerprint,
lastUsedAt: worktree.lastActiveAt,
repoRoot: worktree.repoRoot
});
}
diagnostics?.mark("identityProbes");
const probePaths = [...new Set(rawCandidates.map((raw) => raw.kind === "worktree" ? raw.repoRoot : raw.checkoutPath))].slice(0, 32).toSorted();
const results = probePaths.length ? await service.resolveRepositoryIdentities(probePaths) : [];
const identities = new Map(probePaths.map((checkoutPath, index) => [checkoutPath, results[index]]));
diagnostics?.mark("candidateProcessing");
const candidates = [];
for (const raw of rawCandidates) {
const identity = identities.get(raw.kind === "worktree" ? raw.repoRoot : raw.checkoutPath);
const checkout = raw.kind === "worktree" ? {
checkoutPath: raw.checkoutPath,
fingerprint: raw.fingerprint
} : identity && {
checkoutPath: identity.checkoutRoot,
fingerprint: identity.fingerprint
};
if (!checkout) continue;
candidates.push({
...checkout,
lastUsedAt: raw.lastUsedAt,
...identity?.originUrl ? { originUrl: identity.originUrl } : {}
});
}
return projectCandidatesToSummaries(candidates);
}
function findProjectCheckoutReference(cfg, repoRoot, worktrees, incognitoStores) {
const normalizedRoot = path.resolve(repoRoot);
const workspaceReference = listWorkspaceProjects(cfg).find((candidate) => path.resolve(candidate.repoRoot) === normalizedRoot);
const worktreeReference = worktrees.find((worktree) => !worktree.removedAt && path.resolve(worktree.repoRoot) === normalizedRoot);
const sessionReference = [...Object.entries(loadCombinedSessionStoreForGatewayCore(cfg, {
projection: "list",
...incognitoStores && { includeIncognito: false }
}).store), ...incognitoStores?.flatMap((store) => store.entries.map(({ sessionKey, entry }) => [sessionKey, entry])) ?? []].find(([, entry]) => {
if (entry.archivedAt) return false;
const sessionRoot = entry.worktree?.repoRoot;
if (sessionRoot && path.resolve(sessionRoot) === normalizedRoot) return true;
const cwd = entry.spawnedCwd;
return Boolean(cwd && (path.resolve(cwd) === normalizedRoot || isPathInside(normalizedRoot, path.resolve(cwd))));
});
return workspaceReference ? `agent workspace ${workspaceReference.displayName}` : worktreeReference ? `managed worktree ${worktreeReference.name}` : sessionReference ? `session ${sessionReference[0]}` : void 0;
}
function createProjectsHandlers(service) {
return {
"projects.list": async ({ params, respond, context, client }) => {
if (!assertValidParams(params, validateProjectsListParams, "projects.list", respond)) return;
const diagnostics = startProjectsListDiagnostics(context);
const cfg = context.getRuntimeConfig();
const defaultRepository = configuredDefaultRepository(cfg);
const requesterProfileId = client?.authenticatedUserProfile?.profileId;
const requesterUserId = client?.authenticatedUserId;
const accessRevision = readGatewayAccessRevision();
const assertCurrent = () => {
if (client?.authenticatedUserProfile?.profileId !== requesterProfileId || client?.authenticatedUserId !== requesterUserId || readGatewayAccessRevision() !== accessRevision || context.getRuntimeConfig() !== cfg) throw new Error("Project access changed while preparing the listing. Retry the request.");
};
const list = async (incognitoStores, assertIncognitoCurrent) => {
const registryProjects = await listProjectRegistry(cfg);
assertCurrent();
diagnostics?.mark("sessions");
const projects = registryProjects.map(sanitizeProjectRecord);
const canWrite = () => authorizeOperatorScopesForRequiredScope(WRITE_SCOPE, Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).allowed;
const canCreateSession = () => authorizeOperatorScopesForMethod("sessions.create", Array.isArray(client?.connect.scopes) ? client.connect.scopes : []).allowed;
let store = {};
let observedProjects;
if (client?.authenticatedUserProfile?.profileId || params.includeObserved && canWrite()) {
const projection = requireSessionRowProjection(context);
do
await projection.prepareSelection();
while (projection.needsSelectionPreparation());
assertCurrent();
if (getSessionRowProjection(context) !== projection || projection.state.cfg !== cfg) throw new Error("Session projection changed while preparing the listing. Retry the request.");
store = loadProjectSessionStore(projection, incognitoStores);
assertCurrent();
}
if (params.includeObserved && canWrite()) {
observedProjects = await listObservedProjects(service, context, client, store, diagnostics);
assertCurrent();
}
diagnostics?.mark("recents");
const profileId = client?.authenticatedUserProfile?.profileId;
const recentProfile = profileId ? await prepareCurrentUserProfileAliases(profileId) : void 0;
assertCurrent();
const recents = recentProfile ? await listProjectRecents(store, recentProfile.aliases, registryProjects) : void 0;
recentProfile?.assertCurrent();
diagnostics?.mark("response");
assertCurrent();
const writable = canWrite();
const canCreate = canCreateSession();
assertIncognitoCurrent?.();
respond(true, {
projects: writable ? projects : projects.map(({ id, displayName, source, agentId }) => agentId ? {
id,
displayName,
source,
agentId
} : {
id,
displayName,
source
}),
...canCreate ? { githubHost: resolveConfiguredGitHubHost(cfg) } : {},
...defaultRepository && (writable || canCreate) ? { defaultRepository } : {},
...recents ? { recents: writable ? recents : recents.filter((recent) => recent.kind === "project") } : {},
...writable && observedProjects ? { observedProjects } : {}
}, void 0);
};
try {
if (captureIncognitoSessionBinding()) await withIncognitoSessionStoreEntries(list);
else await list();
} catch (error) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error)));
} finally {
diagnostics?.finish();
}
},
"projects.register": defineValidatedGatewayHandler("projects.register", validateProjectsRegisterParams, async ({ params, respond }) => {
respond(true, sanitizeProjectRecord(await registerProjectRegistry({
path: params.path,
name: params.name
})), void 0);
}, projectCheckoutError),
"projects.add": defineValidatedGatewayHandler("projects.add", validateProjectsAddParams, async ({ params, respond, context, signal }) => {
const cfg = context.getRuntimeConfig();
respond(true, await materializeProjectClone({
cfg,
gitUrl: params.gitUrl,
name: params.name
}, {
signal,
token: githubApiToken(process.env, cfg)
}), void 0);
}, (error) => {
const failure = isTrustedSecretSurfaceUnavailableError(error) ? {
cause: "auth_required",
message: CONTROL_UI_GITHUB_CREDENTIAL_UNAVAILABLE_MESSAGE
} : error instanceof ProjectCloneError ? {
cause: error.failure,
message: error.message
} : void 0;
return failure ? errorShape(failure.cause === "invalid_url" ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, failure.message, {
details: {
code: GatewayErrorDetailCodes.PROJECT_CLONE_FAILED,
cause: failure.cause
},
retryable: failure.cause === "network" || failure.cause === "clone_failed"
}) : errorShape(ErrorCodes.UNAVAILABLE, formatErrorMessage(error));
}),
"projects.searchRemote": defineValidatedGatewayHandler("projects.searchRemote", validateProjectsSearchRemoteParams, async ({ params, respond, context, signal, hasCurrentClientAuthority }) => {
const cfg = context.getRuntimeConfig();
const host = resolveConfiguredGitHubHost(cfg);
const apiBaseUrl = resolveConfiguredGitHubApiBaseUrl(cfg);
const assertCurrent = () => {
signal?.throwIfAborted();
if (hasCurrentClientAuthority?.() === false) throw new Error("Project requester authority changed during search");
if (context.getRuntimeConfig() !== cfg) throw new gitHubPublicApi.ControlUiGitHubError(502, "GitHub host changed during project search");
};
assertCurrent();
const nativeToken = cfg.gateway?.projects?.nativeGitHubSearch === true ? await readCachedNativeGitHubToken(process.env) : void 0;
assertCurrent();
const result = await searchRemoteProjects(params.query, {
assertCurrent,
signal,
host,
apiBaseUrl,
...cfg.gateway?.projects?.nativeGitHubSearch === true ? { token: nativeToken ?? "" } : {}
});
assertCurrent();
respond(true, result, void 0);
}, (error) => {
const { message, ...details } = error instanceof gitHubPublicApi.ControlUiGitHubError || isTrustedSecretSurfaceUnavailableError(error) ? gitHubPublicApi.formatControlUiGitHubPreviewError(error) : {
message: "GitHub project search is unavailable. Retry shortly.",
retryable: true
};
return errorShape(ErrorCodes.UNAVAILABLE, message, details);
}),
"projects.remove": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateProjectsRemoveParams, "projects.remove", respond)) return;
const worktreeContext = captureWorktreeRunEndContext(process.env);
const respondUnknownProject = () => {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `unknown project id: ${params.id}`));
};
const project = await resolveProjectRegistry(context.getRuntimeConfig(), params.id);
if (!project || project.source === "workspace") {
respondUnknownProject();
return;
}
let removed;
if (params.deleteCheckout) {
if (project.source !== "cloned") {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Only projects cloned by the Gateway can delete their checkout."));
return;
}
try {
const assertWorktreesCurrent = captureWorktreeRegistryAuthority(worktreeContext, [{
id: "*",
fields: ["identity", "removal"]
}]);
const worktrees = await readRegistryWorktrees(process.env, {}, worktreeContext);
worktreeContext.admission.assertCurrent();
assertWorktreesCurrent();
const remove = (incognitoStores, assertIncognitoCurrent) => {
const assertCurrent = () => {
worktreeContext.admission.assertCurrent();
assertWorktreesCurrent();
assertIncognitoCurrent?.();
};
return removeClonedProjectCheckout(project, () => {
assertCurrent();
const reference = findProjectCheckoutReference(context.getRuntimeConfig(), project.repoRoot, worktrees, incognitoStores);
if (reference) throw new ProjectCheckoutError(`Project checkout is still referenced by ${reference}. Remove that reference before deleting the checkout.`);
}, { assertCurrent });
};
removed = captureIncognitoSessionBinding() ? await withIncognitoSessionStoreEntries(remove) : await remove();
} catch (error) {
respond(false, void 0, projectCheckoutError(error));
return;
}
} else removed = await removeProjectRegistry(project);
if (!removed) {
respondUnknownProject();
return;
}
respond(true, { removed: true }, void 0);
}
};
}
const projectsHandlers = createProjectsHandlers(managedWorktrees);
//#endregion
export { projectsHandlers };