feat: add ssh_key_id to config profiles for container key mounting

- Add ssh_key_id column to ConfigProfile model and migration
- Update config profile API to accept/return ssh_key_id
- Include ssh_key_id in ResolvedProfile and resolver logic
- Mount selected SSH key into container home dir at start_instance
- Frontend config profile form with SSH key selector dropdown
- Git mount URL validation defaults to profile's SSH key

Quality gates: pytest (231 passed, 6 pre-existing), tsc --noEmit clean
This commit is contained in:
Alex Blank
2026-05-29 12:53:51 +02:00
parent d413fb84a5
commit 57ff236f2d
11 changed files with 232 additions and 26 deletions
+31 -1
View File
@@ -18,6 +18,7 @@ import {
type CreateConfigProfileRequest,
type ResolvedProfile,
} from "../api/config_profiles";
import { listSSHKeys, type SSHKey } from "../api/ssh_keys";
import { listProjects } from "../api/projects";
import type { Project } from "../types";
import { listToolTypes, type ToolType } from "../api/tool_types";
@@ -33,6 +34,7 @@ export const ConfigProfilesPage = () => {
const [profiles, setProfiles] = useState<ConfigProfile[]>([]);
const [projects, setProjects] = useState<Project[]>([]);
const [toolTypes, setToolTypes] = useState<ToolType[]>([]);
const [sshKeys, setSshKeys] = useState<SSHKey[]>([]);
const [selectedProfileId, setSelectedProfileId] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
@@ -50,6 +52,7 @@ export const ConfigProfilesPage = () => {
mounts: [],
git_mounts: [],
files: {},
ssh_key_id: undefined,
is_default: false,
});
@@ -61,11 +64,13 @@ export const ConfigProfilesPage = () => {
const loadData = useCallback(async () => {
setStatus("loading");
try {
const [profs, projs, types] = await Promise.all([
const [profs, projs, types, keys] = await Promise.all([
listConfigProfiles(),
listProjects(),
listToolTypes(),
listSSHKeys(),
]);
setSshKeys(keys);
setProfiles(profs || []);
setProjects(projs || []);
setToolTypes(types || []);
@@ -89,6 +94,7 @@ export const ConfigProfilesPage = () => {
mounts: [],
git_mounts: [],
files: {},
ssh_key_id: undefined,
is_default: false,
});
setIncludedProfileIds([]);
@@ -108,6 +114,7 @@ export const ConfigProfilesPage = () => {
mounts: profile.mounts,
git_mounts: profile.git_mounts || [],
files: profile.files,
ssh_key_id: profile.ssh_key_id || undefined,
is_default: profile.is_default,
});
setIncludedProfileIds(
@@ -467,6 +474,7 @@ export const ConfigProfilesPage = () => {
{ label: "Description", value: selectedProfile.description || "-" },
{ label: "Scope", value: getScopeLabel(selectedProfile) },
{ label: "Default", value: selectedProfile.is_default ? "Yes" : "No" },
{ label: "SSH Key", value: sshKeys.find(k => k.id === selectedProfile.ssh_key_id)?.name || "-" },
{ label: "Environment Variables", value: Object.keys(selectedProfile.env_vars).length > 0 ? Object.entries(selectedProfile.env_vars).map(([k, v]) => `${k}=${v}`).join(", ") : "-" },
{ label: "Mounts", value: selectedProfile.mounts.length > 0 ? selectedProfile.mounts.map((m) => `${m.target} (${m.mode})`).join(", ") : "-" },
{ label: "Includes", value: selectedProfile.includes.length > 0 ? `${selectedProfile.includes.length} profile(s)` : "-" },
@@ -563,6 +571,27 @@ export const ConfigProfilesPage = () => {
</label>
</div>
<div className="form-group">
<label>SSH Key</label>
<select
value={formData.ssh_key_id || ""}
onChange={(e) =>
updateFormField("ssh_key_id", e.target.value || undefined)
}
>
<option value="">None</option>
{sshKeys.map((key) => (
<option key={key.id} value={key.id}>
{key.name}
</option>
))}
</select>
<div className="hint">
Mounts this SSH key into the container at {"~/.ssh"} when a
tool instance is started with this profile.
</div>
</div>
{/* Environment Variables */}
<div className="form-group">
<label>Environment Variables</label>
@@ -1248,6 +1277,7 @@ export const ConfigProfilesPage = () => {
<GitMountEditor
mounts={formData.git_mounts || []}
onChange={(git_mounts) => updateFormField("git_mounts", git_mounts)}
defaultSshKeyId={formData.ssh_key_id || undefined}
/>
</div>