feat(github): unify workspace GitHub access (#2084)
This commit is contained in:
@@ -1,162 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { Button } from "@kandev/ui/button";
|
||||
import { CardContent } from "@kandev/ui/card";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@kandev/ui/drawer";
|
||||
import { RadioGroup, RadioGroupItem } from "@kandev/ui/radio-group";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@kandev/ui/tooltip";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsSection } from "@/components/settings/settings-section";
|
||||
import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { useTouchDrawer } from "@/hooks/use-compact-task-chrome";
|
||||
import {
|
||||
fetchGitHubWorkspaceSettings,
|
||||
updateGitHubWorkspaceSettings,
|
||||
} from "@/lib/api/domains/github-api";
|
||||
import type { TaskGitCredentialsState } from "@/hooks/domains/github/use-task-git-credentials";
|
||||
import type { TaskGitCredentialsMode } from "@/lib/types/github";
|
||||
import { GitHubAccessHelp } from "./github-access-help";
|
||||
|
||||
const deliveryHelp =
|
||||
"PAT, named GitHub CLI, and GitHub App connections choose Kandev's workspace automation identity. In managed mode Kandev brokers that identity to the task for GitHub HTTPS and gh. Inherit executor credentials leaves task Git and gh to the selected executor. An explicit executor-profile GH_TOKEN or GITHUB_TOKEN takes precedence in managed mode.";
|
||||
const taskAccessLabels: Record<TaskGitCredentialsMode, string> = {
|
||||
managed: "Managed workspace credentials",
|
||||
executor: "Inherit executor Git credentials",
|
||||
};
|
||||
|
||||
function TaskCredentialsHelp() {
|
||||
const usesDrawer = useTouchDrawer();
|
||||
const [open, setOpen] = useState(false);
|
||||
const button = (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-11 w-11 cursor-pointer text-muted-foreground sm:h-7 sm:w-7"
|
||||
aria-label="Explain task Git credentials"
|
||||
>
|
||||
<IconInfoCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
const trigger = <DrawerTrigger asChild>{button}</DrawerTrigger>;
|
||||
export function GitHubTaskAccessSummary({
|
||||
mode,
|
||||
loading,
|
||||
error,
|
||||
}: Omit<TaskGitCredentialsState, "save">) {
|
||||
let value = taskAccessLabels[mode];
|
||||
if (loading) value = "Loading task access…";
|
||||
if (error) value = "Task access unavailable";
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
{usesDrawer ? (
|
||||
trigger
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[320px] text-xs leading-relaxed">
|
||||
{deliveryHelp}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div
|
||||
className="flex items-center gap-1 text-xs text-muted-foreground"
|
||||
data-testid="github-task-access-summary"
|
||||
>
|
||||
<GitHubAccessHelp
|
||||
label="Explain task Git access"
|
||||
title="Task Git access"
|
||||
description="Controls how newly launched tasks authenticate to GitHub for Git HTTPS and gh commands. Managed workspace credentials use Kandev's workspace connection; executor credentials use the selected executor's Git or SSH setup."
|
||||
/>
|
||||
<span>
|
||||
<span className="font-medium text-foreground">Task access: </span>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitHubTaskAccessForm({
|
||||
taskAccess,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
taskAccess: TaskGitCredentialsState;
|
||||
value: TaskGitCredentialsMode;
|
||||
onChange: (value: TaskGitCredentialsMode) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-4 border-t pt-5" data-testid="github-task-access-settings">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1">
|
||||
<h3 className="text-sm font-medium">Task Git access</h3>
|
||||
<GitHubAccessHelp
|
||||
label="Explain how managed task credentials work"
|
||||
title="How managed task credentials work"
|
||||
description="With Managed workspace credentials, Kandev configures agentctl as Git's credential helper inside newly launched task processes. When Git requests credentials for an attached GitHub HTTPS repository, the helper redeems a task- and repository-scoped broker lease. The credential is returned to Git on demand and is not written to the repository or Git config. The gh command uses Kandev's broker-aware shim. Executor inheritance skips both and uses credentials visible in the selected executor. Changes apply to new task launches and the next resume, not already-running processes."
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
Choose how newly launched tasks authenticate to GitHub. This does not change the workspace
|
||||
automation connection above.
|
||||
</p>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onValueChange={(nextValue) => onChange(nextValue as TaskGitCredentialsMode)}
|
||||
disabled={taskAccess.loading || taskAccess.error || disabled}
|
||||
className="gap-2"
|
||||
>
|
||||
<label
|
||||
className="flex min-h-11 cursor-pointer items-start gap-3 rounded-md border p-3"
|
||||
data-testid="github-task-access-option-managed"
|
||||
>
|
||||
<RadioGroupItem value="managed" className="mt-0.5" />
|
||||
<span>
|
||||
<span className="font-medium">Managed workspace credentials</span>
|
||||
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
||||
Kandev brokers the workspace PAT, named GitHub CLI account, or App identity to this
|
||||
task for GitHub HTTPS and gh.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className="flex min-h-11 cursor-pointer items-start gap-3 rounded-md border p-3"
|
||||
data-testid="github-task-access-option-executor"
|
||||
>
|
||||
<RadioGroupItem value="executor" className="mt-0.5" />
|
||||
<span>
|
||||
<span className="font-medium">Inherit executor Git credentials</span>
|
||||
<span className="mt-1 block text-xs leading-5 text-muted-foreground">
|
||||
Local and Worktree tasks use host-visible Git or SSH credentials. Docker, SSH, and
|
||||
cloud tasks use credentials configured in that executor.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
An executor-profile GH_TOKEN or GITHUB_TOKEN overrides managed workspace credentials for
|
||||
that task.
|
||||
</p>
|
||||
{taskAccess.error && (
|
||||
<p className="text-sm text-destructive">Unable to load the current task access setting.</p>
|
||||
)}
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>How task Git credentials work</DrawerTitle>
|
||||
<DrawerDescription>{deliveryHelp}</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export function GitHubTaskCredentialsSection({ workspaceId }: { workspaceId: string }) {
|
||||
const { toast } = useToast();
|
||||
const [mode, setMode] = useState<TaskGitCredentialsMode>("managed");
|
||||
const [baseline, setBaseline] = useState<TaskGitCredentialsMode>("managed");
|
||||
const [loading, setLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
void fetchGitHubWorkspaceSettings(workspaceId)
|
||||
.then((settings) => {
|
||||
if (!cancelled) {
|
||||
const next = settings.task_git_credentials_mode ?? "managed";
|
||||
setMode(next);
|
||||
setBaseline(next);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled)
|
||||
toast({ description: "Failed to load task Git credential settings", variant: "error" });
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [toast, workspaceId]);
|
||||
const save = useCallback(async () => {
|
||||
try {
|
||||
const updated = await updateGitHubWorkspaceSettings({
|
||||
workspace_id: workspaceId,
|
||||
task_git_credentials_mode: mode,
|
||||
});
|
||||
const next = updated.task_git_credentials_mode ?? "managed";
|
||||
setBaseline(next);
|
||||
setMode((current) => (current === mode ? next : current));
|
||||
toast({ description: "Task Git credential settings saved", variant: "success" });
|
||||
} catch {
|
||||
toast({ description: "Failed to save task Git credential settings", variant: "error" });
|
||||
throw new Error("Failed to save task Git credential settings");
|
||||
}
|
||||
}, [mode, toast, workspaceId]);
|
||||
const dirty = mode !== baseline;
|
||||
useSettingsSaveContributor({
|
||||
id: `github-task-credentials:${workspaceId}`,
|
||||
revision: mode,
|
||||
isDirty: dirty,
|
||||
canSave: !loading,
|
||||
save,
|
||||
discard: () => setMode(baseline),
|
||||
});
|
||||
return (
|
||||
<SettingsSection
|
||||
title="Task Git credentials"
|
||||
description="Choose how task processes authenticate to GitHub. This does not change Kandev's workspace automation identity."
|
||||
action={<TaskCredentialsHelp />}
|
||||
>
|
||||
<SettingsCard isDirty={dirty}>
|
||||
<CardContent className="space-y-4 py-4">
|
||||
<RadioGroup
|
||||
value={mode}
|
||||
onValueChange={(value) => setMode(value as TaskGitCredentialsMode)}
|
||||
disabled={loading}
|
||||
data-testid="github-task-credentials-mode"
|
||||
>
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-md border p-3">
|
||||
<RadioGroupItem value="managed" className="mt-0.5" />
|
||||
<span>
|
||||
<span className="font-medium">Managed workspace credentials</span>
|
||||
<span className="mt-1 block text-sm text-muted-foreground">
|
||||
Kandev brokers the workspace PAT, named GitHub CLI account, or App identity to
|
||||
this task for GitHub HTTPS and gh.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex cursor-pointer items-start gap-3 rounded-md border p-3">
|
||||
<RadioGroupItem value="executor" className="mt-0.5" />
|
||||
<span>
|
||||
<span className="font-medium">Inherit executor Git credentials</span>
|
||||
<span className="mt-1 block text-sm text-muted-foreground">
|
||||
Local and Worktree tasks use host-visible Git or SSH credentials. Docker, SSH, and
|
||||
cloud tasks use credentials configured in that executor.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</RadioGroup>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
An executor-profile GH_TOKEN or GITHUB_TOKEN overrides managed workspace credentials for
|
||||
that task.
|
||||
</p>
|
||||
</CardContent>
|
||||
</SettingsCard>
|
||||
</SettingsSection>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user