Improve Foundry auth and task flows (#240)

This commit is contained in:
Nathan Flurry 2026-03-11 18:13:31 -07:00 committed by GitHub
parent d75e8c31d1
commit dbc2ff0682
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 621 additions and 137 deletions

View file

@ -34,6 +34,7 @@ import { getActorRuntimeContext } from "../context.js";
import { getTask, getOrCreateHistory, getOrCreateProject, selfWorkspace } from "../handles.js";
import { logActorWarning, resolveErrorMessage } from "../logging.js";
import { normalizeRemoteUrl, repoIdFromRemote } from "../../services/repo.js";
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
import { taskLookup, repos, providerProfiles } from "./db/schema.js";
import { agentTypeForModel } from "../task/workbench.js";
import { expectQueueResponse } from "../../services/queue.js";
@ -213,7 +214,8 @@ async function addRepoMutation(c: any, input: AddRepoInput): Promise<RepoRecord>
}
const { driver } = getActorRuntimeContext();
await driver.git.validateRemote(remoteUrl);
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
await driver.git.validateRemote(remoteUrl, { githubToken: auth?.githubToken ?? null });
const repoId = repoIdFromRemote(remoteUrl);
const now = Date.now();
@ -439,7 +441,7 @@ export const workspaceActions = {
c.broadcast("workbenchUpdated", { at: Date.now() });
},
async createWorkbenchTask(c: any, input: TaskWorkbenchCreateTaskInput): Promise<{ taskId: string }> {
async createWorkbenchTask(c: any, input: TaskWorkbenchCreateTaskInput): Promise<{ taskId: string; tabId?: string }> {
const created = await workspaceActions.createTask(c, {
workspaceId: c.state.workspaceId,
repoId: input.repoId,
@ -448,7 +450,12 @@ export const workspaceActions = {
...(input.branch ? { explicitBranchName: input.branch } : {}),
...(input.model ? { agentType: agentTypeForModel(input.model) } : {}),
});
return { taskId: created.taskId };
const task = await requireWorkbenchTask(c, created.taskId);
const snapshot = await task.getWorkbench({});
return {
taskId: created.taskId,
tabId: snapshot.tabs[0]?.id,
};
},
async markWorkbenchUnread(c: any, input: TaskWorkbenchSelectInput): Promise<void> {

View file

@ -56,6 +56,10 @@ function splitScopes(value: string): string[] {
.filter((entry) => entry.length > 0);
}
function hasRepoScope(scopes: string[]): boolean {
return scopes.some((scope) => scope === "repo" || scope.startsWith("repo:"));
}
function parseEligibleOrganizationIds(value: string): string[] {
try {
const parsed = JSON.parse(value);
@ -568,6 +572,32 @@ export const workspaceAppActions = {
return await buildAppSnapshot(c, input.sessionId);
},
async resolveAppGithubToken(
c: any,
input: { organizationId: string; requireRepoScope?: boolean },
): Promise<{ accessToken: string; scopes: string[] } | null> {
assertAppWorkspace(c);
const rows = await c.db.select().from(appSessions).orderBy(desc(appSessions.updatedAt)).all();
for (const row of rows) {
if (row.activeOrganizationId !== input.organizationId || !row.githubAccessToken) {
continue;
}
const scopes = splitScopes(row.githubScope);
if (input.requireRepoScope !== false && !hasRepoScope(scopes)) {
continue;
}
return {
accessToken: row.githubAccessToken,
scopes,
};
}
return null;
},
async startAppGithubAuth(c: any, input: { sessionId: string }): Promise<{ url: string }> {
assertAppWorkspace(c);
const { appShell } = getActorRuntimeContext();
@ -702,18 +732,34 @@ export const workspaceAppActions = {
});
try {
const repositories =
organization.snapshot.kind === "personal"
? await appShell.github.listUserRepositories(session.githubAccessToken)
: organization.githubInstallationId
? await appShell.github.listInstallationRepositories(organization.githubInstallationId)
: (() => {
throw new GitHubAppError("GitHub App installation required before importing repositories", 400);
})();
let repositories;
let installationStatus = organization.snapshot.github.installationStatus;
if (organization.snapshot.kind === "personal") {
repositories = await appShell.github.listUserRepositories(session.githubAccessToken);
installationStatus = "connected";
} else if (organization.githubInstallationId) {
try {
repositories = await appShell.github.listInstallationRepositories(organization.githubInstallationId);
} catch (error) {
if (!(error instanceof GitHubAppError) || (error.status !== 403 && error.status !== 404)) {
throw error;
}
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
repository.fullName.startsWith(`${organization.githubLogin}/`),
);
installationStatus = "reconnect_required";
}
} else {
repositories = (await appShell.github.listUserRepositories(session.githubAccessToken)).filter((repository) =>
repository.fullName.startsWith(`${organization.githubLogin}/`),
);
installationStatus = "reconnect_required";
}
await workspace.applyOrganizationSyncCompleted({
repositories,
installationStatus: organization.snapshot.kind === "personal" ? "connected" : organization.snapshot.github.installationStatus,
installationStatus,
lastSyncLabel: repositories.length > 0 ? "Synced just now" : "No repositories available",
});
} catch (error) {

View file

@ -181,9 +181,7 @@ SET \`github_sync_status\` = CASE
ELSE 'pending'
END;
`,
m0010: `ALTER TABLE \`app_sessions\` ADD COLUMN \`starter_repo_status\` text NOT NULL DEFAULT 'pending';
ALTER TABLE \`app_sessions\` ADD COLUMN \`starter_repo_starred_at\` integer;
ALTER TABLE \`app_sessions\` ADD COLUMN \`starter_repo_skipped_at\` integer;
m0010: `-- no-op: starter_repo_* columns are already present in m0007 app_sessions
`,
} as const,
};