Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion desktop/src/apps/SecretsApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ interface Secret {
revealed?: boolean;
}

type CategoryFilter = "all" | "api-key" | "credential" | "token" | "config";
type CategoryFilter = "all" | "api-key" | "credential" | "token" | "config" | "github-installation";

/* ------------------------------------------------------------------ */
/* Constants */
Expand All @@ -35,6 +35,7 @@ const CATEGORY_STYLES: Record<string, string> = {
credential: "bg-cyan-500/20 text-cyan-400",
token: "bg-amber-500/20 text-amber-400",
config: "bg-emerald-500/20 text-emerald-400",
"github-installation": "bg-purple-500/20 text-purple-400",
};

const MASKED = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
Expand Down Expand Up @@ -158,6 +159,7 @@ function AddEditDialog({
<option value="token">Token</option>
<option value="config">Config</option>
<option value="ssh-keys">SSH Key</option>
<option value="github-installation">GitHub Installation</option>
</select>
</div>

Expand Down Expand Up @@ -345,6 +347,7 @@ export function SecretsApp({ windowId: _windowId }: { windowId: string }) {
<option value="credential">Credential</option>
<option value="token">Token</option>
<option value="config">Config</option>
<option value="github-installation">GitHub Installation</option>
</select>
</div>
<Button
Expand Down
149 changes: 138 additions & 11 deletions desktop/src/apps/secrets/GitHubConnect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { Github, Copy, Check, ExternalLink, Trash2, Plus, Loader2, Settings, X } from "lucide-react";
import { Github, Copy, Check, ExternalLink, Trash2, Plus, Loader2, Settings, X, Save } from "lucide-react";
import { Button, Card, CardContent } from "@/components/ui";
import {
startDeviceFlow,
Expand Down Expand Up @@ -76,18 +76,109 @@ export function GitHubConnect() {
[refreshInstallations],
);

// -- Per-agent GitHub repo grants ----------------------------------

const [repoAgents, setRepoAgents] = useState<Record<string, string>>({});
const [savingGrants, setSavingGrants] = useState<Set<string>>(new Set());
const [saveErrors, setSaveErrors] = useState<Record<string, string>>({});

const fetchAgentGrants = useCallback(async () => {
// Fetch existing github-installation secrets for all known repos.
try {
const res = await fetch("/api/secrets?category=github-installation", {
headers: { Accept: "application/json" },
});
if (!res.ok) return;
const ct = res.headers.get("content-type") ?? "";
if (!ct.includes("application/json")) return;
const data = await res.json();
if (Array.isArray(data)) {
const grants: Record<string, string> = {};
for (const s of data) {
if (s.name && s.agents && s.agents.length > 0) {
grants[s.name] = s.agents.join(", ");
}
}
setRepoAgents(grants);
}
} catch { /* ignore */ }
}, []);

const handleSaveGrants = useCallback(
async (repoFullName: string, installationId: number, permissions: string[]) => {
setSavingGrants((prev) => new Set(prev).add(repoFullName));
setSaveErrors((prev) => {
const next = { ...prev };
delete next[repoFullName];
return next;
});
try {
const agentsStr = repoAgents[repoFullName] || "";
const agents = agentsStr
.split(",")
.map((a) => a.trim())
.filter(Boolean);

const body = JSON.stringify({
name: repoFullName,
category: "github-installation",
value: JSON.stringify({
installation_id: installationId,
repo_full_name: repoFullName,
permissions,
}),
description: `GitHub App installation for ${repoFullName}`,
agents,
});

// Try create first; if exists, update.
let res = await fetch("/api/secrets", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body,
});
if (res.status === 409) {
// Already exists — update instead
res = await fetch(`/api/secrets/${encodeURIComponent(repoFullName)}`, {
method: "PUT",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ agents, value: JSON.stringify({
installation_id: installationId,
repo_full_name: repoFullName,
permissions,
}) }),
});
}
} catch {
setSaveErrors((prev) => ({
...prev,
[repoFullName]: "Save failed — please try again.",
}));
} finally {
setSavingGrants((prev) => {
const next = new Set(prev);
next.delete(repoFullName);
return next;
});
}
},
[repoAgents],
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(() => {
refreshIdentities();
refreshInstallations();
fetchAgentGrants();

// Refresh when the user returns from the external GitHub App install flow
const onFocus = () => {
refreshIdentities();
refreshInstallations();
fetchAgentGrants();
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, [refreshIdentities, refreshInstallations]);
}, [refreshIdentities, refreshInstallations, fetchAgentGrants]);

const stopPolling = useCallback(() => {
if (pollTimer.current) clearTimeout(pollTimer.current);
Expand Down Expand Up @@ -351,16 +442,52 @@ export function GitHubConnect() {
</Button>
</div>
{inst.repositories.length > 0 && (
<div className="ml-7 space-y-1">
<div className="ml-7 space-y-3">
{inst.repositories.map((repo) => (
<div
key={repo.full_name}
className="flex items-center gap-1.5 text-xs text-shell-text-secondary"
>
<span className="text-shell-text-tertiary">
{repo.private ? "🔒" : "📁"}
</span>
<span className="font-mono">{repo.full_name}</span>
<div key={repo.full_name} className="space-y-1.5">
<div className="flex items-center gap-1.5 text-xs text-shell-text-secondary">
<span className="text-shell-text-tertiary">
{repo.private ? "🔒" : "📁"}
</span>
<span className="font-mono">{repo.full_name}</span>
</div>
<div className="flex items-center gap-2">
<input
type="text"
placeholder="agent names, comma-separated"
className="flex-1 h-8 rounded-lg border border-white/10 bg-shell-bg-deep px-2.5 text-xs text-shell-text placeholder:text-shell-text-tertiary focus:outline-none focus:border-accent/40 focus:ring-2 focus:ring-accent/20"
value={repoAgents[repo.full_name] || ""}
onChange={(e) =>
setRepoAgents((prev) => ({
...prev,
[repo.full_name]: e.target.value,
}))
}
aria-label={`Agents for ${repo.full_name}`}
/>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() =>
handleSaveGrants(repo.full_name, inst.id, inst.permissions ?? [])
}
aria-label={`Save agent grants for ${repo.full_name}`}
title={`Save agent grants for ${repo.full_name}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: handleSaveGrants is always called with permissions: [] (the third argument is hardcoded to []).

Suggested change
title={`Save agent grants for ${repo.full_name}`}
onClick={() =>
handleSaveGrants(repo.full_name, inst.id, repo.permissions ?? [])
}

As written, every saved github-installation secret stores an empty permissions list regardless of what the installation actually grants. This makes the stored permissions field meaningless and inconsistent with the UI intent. Use the installation's real permissions (e.g. inst.permissions) when persisting.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

disabled={savingGrants.has(repo.full_name)}
>
{savingGrants.has(repo.full_name) ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Save size={14} />
)}
</Button>
</div>
{saveErrors[repo.full_name] && (
<p className="text-xs text-red-400" role="alert">
{saveErrors[repo.full_name]}
</p>
)}
</div>
))}
</div>
Expand Down
1 change: 1 addition & 0 deletions desktop/src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export interface GitHubAppInstallation {
avatar_url: string;
};
repository_selection: string;
permissions: string[];
repositories: GitHubAppRepo[];
created_at: string;
}
Expand Down
Loading
Loading