feat(foundry): implement provider credential management (Claude, Codex)

Add credential extraction, injection, and UI for managing Claude and Codex OAuth credentials in sandbox environments. Credentials are stored per-user in the user actor, injected on task owner swap, and periodically re-extracted to capture token refreshes. Frontend account settings show provider sign-in status.

Changes:
- User actor: new userProviderCredentials table with upsert/get actions
- Task workspace: extract/inject provider credentials, integrate with owner swap and polling
- App snapshot: include provider credential status (anthropic/openai booleans)
- Frontend: new Providers section in account settings

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-17 02:34:15 -07:00
parent 3895e34bdb
commit c1a4895303
14 changed files with 481 additions and 11 deletions

View file

@ -29,6 +29,9 @@ const signInRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/signin",
component: SignInRoute,
validateSearch: (search: Record<string, unknown>): { error?: string } => ({
error: typeof search.error === "string" ? search.error : undefined,
}),
});
const accountRoute = createRoute({
@ -150,6 +153,7 @@ function IndexRoute() {
function SignInRoute() {
const snapshot = useMockAppSnapshot();
const { error } = signInRoute.useSearch();
if (!isMockFrontendClient && isAppSnapshotBootstrapping(snapshot)) {
return <AppLoadingScreen label="Restoring Foundry session..." />;
}
@ -157,7 +161,7 @@ function SignInRoute() {
return <IndexRoute />;
}
return <MockSignInPage />;
return <MockSignInPage error={error} />;
}
function AccountRoute() {

View file

@ -188,10 +188,16 @@ function MemberRow({ member }: { member: FoundryOrganizationMember }) {
);
}
export function MockSignInPage() {
const AUTH_ERROR_MESSAGES: Record<string, string> = {
please_restart_the_process: "Sign-in failed. Please try again.",
state_mismatch: "Sign-in session expired. Please try again.",
};
export function MockSignInPage({ error }: { error?: string }) {
const client = useMockAppClient();
const navigate = useNavigate();
const t = useFoundryTokens();
const errorMessage = error ? (AUTH_ERROR_MESSAGES[error] ?? `Sign-in error: ${error}`) : undefined;
return (
<div
@ -253,6 +259,25 @@ export function MockSignInPage() {
Connect your GitHub account to get started.
</p>
{errorMessage && (
<p
style={{
fontSize: "13px",
fontWeight: 500,
color: "#f85149",
margin: "0 0 16px 0",
lineHeight: 1.5,
padding: "10px 14px",
background: "rgba(248, 81, 73, 0.1)",
borderRadius: "8px",
width: "100%",
textAlign: "center",
}}
>
{errorMessage}
</p>
)}
{/* GitHub sign-in button */}
<button
type="button"
@ -1172,6 +1197,8 @@ export function MockAccountSettingsPage() {
</div>
</SettingsContentSection>
<ProviderCredentialsSection />
<SettingsContentSection title="Sessions" description="Manage your active sessions across devices.">
<SettingsRow label="Current session" description="This device — signed in via GitHub OAuth." />
</SettingsContentSection>
@ -1222,6 +1249,48 @@ export function MockAccountSettingsPage() {
);
}
function ProviderCredentialsSection() {
const snapshot = useMockAppSnapshot();
const t = useFoundryTokens();
const providerCredentials = snapshot.providerCredentials ?? { anthropic: false, openai: false };
const providers = [
{
key: "anthropic" as const,
label: "Claude",
description: "Anthropic's Claude AI assistant.",
signedIn: providerCredentials.anthropic,
},
{
key: "openai" as const,
label: "Codex",
description: "OpenAI's Codex coding agent.",
signedIn: providerCredentials.openai,
},
];
return (
<SettingsContentSection title="Provider Credentials" description="Sign in to AI providers to use them in your tasks.">
{providers.map((provider) => (
<SettingsRow
key={provider.key}
label={provider.label}
description={provider.signedIn ? "Signed in" : "Not signed in"}
action={
provider.signedIn ? (
<span style={{ ...badgeStyle(t, "rgba(52, 211, 153, 0.12)", "rgb(52, 211, 153)"), fontSize: "10px" }}>Connected</span>
) : (
<button type="button" style={secondaryButtonStyle(t)}>
Sign in
</button>
)
}
/>
))}
</SettingsContentSection>
);
}
function AppearanceSection() {
const { colorMode, setColorMode } = useColorMode();
const t = useFoundryTokens();

View file

@ -32,6 +32,7 @@ const EMPTY_APP_SNAPSHOT: FoundryAppSnapshot = {
skippedAt: null,
},
},
providerCredentials: { anthropic: false, openai: false },
users: [],
organizations: [],
};