Fix configuration section composition and model fallback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-22 15:25:45 +00:00
1 parent 8818747b3a
commit 3d1fa75b60
3 files changed
+110 -115

No files matched your search

@@ -13,7 +13,7 @@ describe("agent configuration shell", () => {
expect([...filterAgentConfigurationSections("sandbox")]).toEqual(["danger"]);
});
it("resolves adapter defaults to a concrete model label", () => {
it("does not guess an inherited adapter model from available choices", () => {
const agent = {
adapterType: "claude_local",
adapterConfig: { modelReasoningEffort: "high" },
@@ -22,8 +22,8 @@ describe("agent configuration shell", () => {
defaultEnvironmentId: null,
} as unknown as Agent;
expect(resolveEffectiveConfiguration(agent, [{ id: "claude-fable-5", label: "Claude Fable 5" }], 2)).toMatchObject({
model: "Claude Fable 5 · high",
expect(resolveEffectiveConfiguration(agent, 2)).toMatchObject({
model: "Adapter default · high",
modelInherited: true,
environmentInherited: true,
apiKeyCount: 2,
@@ -1,7 +1,6 @@
import type { ReactNode } from "react";
import { Search, Zap } from "lucide-react";
import type { Agent, AgentEnvConfig } from "@paperclipai/shared";
import type { AdapterModel } from "../api/agents";
import { adapterLabels } from "./agent-config-primitives";
import { Badge } from "@/components/ui/badge";
import { cn } from "../lib/utils";
@@ -44,7 +43,7 @@ function readString(value: unknown): string {
return typeof value === "string" ? value : "";
}
export function resolveEffectiveConfiguration(agent: Agent, adapterModels: AdapterModel[] = [], apiKeyCount = 0) {
export function resolveEffectiveConfiguration(agent: Agent, apiKeyCount = 0) {
const adapterConfig = (agent.adapterConfig ?? {}) as Record<string, unknown>;
const runtimeConfig = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
const heartbeat = (runtimeConfig.heartbeat ?? {}) as Record<string, unknown>;
@@ -52,7 +51,7 @@ export function resolveEffectiveConfiguration(agent: Agent, adapterModels: Adapt
const cheap = (profiles.cheap ?? {}) as Record<string, unknown>;
const cheapConfig = (cheap.adapterConfig ?? {}) as Record<string, unknown>;
const explicitModel = readString(adapterConfig.model);
const resolvedModel = explicitModel || adapterModels[0]?.label || adapterModels[0]?.id || "Adapter default";
const resolvedModel = explicitModel || "Adapter default";
const effort = readString(adapterConfig.modelReasoningEffort || adapterConfig.reasoningEffort || adapterConfig.effort || adapterConfig.variant || adapterConfig.mode);
const env = (adapterConfig.env ?? {}) as AgentEnvConfig;
return {
+105 -109
View File
@@ -1758,14 +1758,9 @@ function AgentConfigurePage({
queryKey: queryKeys.agents.keys(agentId),
queryFn: () => agentsApi.listKeys(agentId, companyId),
});
const { data: adapterModels = [] } = useQuery({
queryKey: companyId ? queryKeys.agents.adapterModels(companyId, agent.adapterType) : ["agents", "none", "adapter-models", agent.adapterType],
queryFn: () => agentsApi.adapterModels(companyId!, agent.adapterType),
enabled: Boolean(companyId),
});
const effectiveConfig = useMemo(
() => resolveEffectiveConfiguration(agent, adapterModels, keys.filter((key) => !key.revokedAt).length),
[adapterModels, agent, keys],
() => resolveEffectiveConfiguration(agent, keys.filter((key) => !key.revokedAt).length),
[agent, keys],
);
const handleDirtyDetailsChange = useCallback((details: { count: number; sections: string[] }) => {
setDirtyDetails(details);
@@ -1817,20 +1812,6 @@ function AgentConfigurePage({
hideInstructionsFile
visibleSections={visibleSections}
/>
{visibleSections.has("keys") ? (
<ConfigurationSection id="keys" title="API Keys" instant>
<p className="text-xs text-muted-foreground">Long-lived keys for external processes acting as this agent. Runs receive short-lived tokens automatically.</p>
<KeysTab agentId={agentId} companyId={companyId} />
</ConfigurationSection>
) : null}
{visibleSections.has("danger") ? (
<ConfigurationSection id="danger" title="Danger & Legacy">
<div className="rounded-lg border border-border p-4 text-sm text-muted-foreground">
Adapter-specific sandbox bypass, permission skipping, and deprecated values remain draftable in the runtime form. They are grouped here for navigation while the field internals stay unchanged in P1.
</div>
</ConfigurationSection>
) : null}
{/* Configuration Revisions — collapsible at the bottom */}
{visibleSections.has("history") ? <ConfigurationSection id="history" title="History" instant>
<button
@@ -2000,6 +1981,108 @@ function ConfigurationTab({
? "Enabled by simple company-wide task assignment defaults."
: "Disabled unless explicitly granted.";
const configurationShellAfterSchedule = (
<>
{visibleSections.has("access") ? (
<ConfigurationSection id="access" title="Access & Governance" instant>
<TrustPresetSection
permissions={agent.permissions}
disabled={updatePermissions.isPending}
companyId={companyId}
projectCandidates={(boundaryProjects ?? []).map((project) => ({
id: project.id,
label: project.name,
}))}
issueCandidates={(boundaryIssues ?? []).map((issue) => ({
id: issue.id,
label: `${issue.identifier ?? issue.id.slice(0, 8)} · ${issue.title}`,
}))}
candidatesLoading={boundaryProjectsLoading || boundaryIssuesLoading}
onChange={(nextPermissions) =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills,
canAssignTasks,
...buildPermissionsForTrustPreset(
nextPermissions,
nextPermissions.trustPreset === "low_trust_review" ? "low_trust_review" : "standard",
),
})
}
/>
<div>
<h3 className="text-sm font-medium mb-3">Permissions</h3>
<div className="border border-border rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can create new agents</div>
<p className="text-xs text-muted-foreground">
Lets this agent create or hire agents. This also grants task assignment authority.
</p>
</div>
<ToggleSwitch
checked={canCreateAgents}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents: !canCreateAgents,
canCreateSkills,
canAssignTasks: !canCreateAgents ? true : canAssignTasks,
})
}
disabled={updatePermissions.isPending}
/>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can create/import skills</div>
<p className="text-xs text-muted-foreground">
Lets this agent install, import, create, and scan company skills without creating agents.
</p>
</div>
<ToggleSwitch
checked={canCreateSkills}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills: !canCreateSkills,
canAssignTasks,
})
}
disabled={updatePermissions.isPending}
/>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can assign tasks</div>
<p className="text-xs text-muted-foreground">{taskAssignHint}</p>
</div>
<ToggleSwitch
checked={canAssignTasks}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills,
canAssignTasks: !canAssignTasks,
})
}
disabled={updatePermissions.isPending || taskAssignLocked}
/>
</div>
</div>
</div>
</ConfigurationSection>
) : null}
{visibleSections.has("keys") ? (
<ConfigurationSection id="keys" title="API Keys" instant>
<p className="text-xs text-muted-foreground">
Long-lived keys for external processes acting as this agent. Runs receive short-lived tokens automatically.
</p>
<KeysTab agentId={agent.id} companyId={companyId} />
</ConfigurationSection>
) : null}
</>
);
return (
<div className="space-y-6">
<AgentConfigForm
@@ -2018,99 +2101,12 @@ function ConfigurationTab({
hideIdentity
configurationShell
visibleConfigurationSections={visibleSections}
configurationShellAfterSchedule={configurationShellAfterSchedule}
sectionLayout="cards"
/>
<p className="text-xs text-muted-foreground">
Saved adapter config affects the next run. Active runs keep the config they started with, and config changes may start a fresh adapter session.
</p>
{visibleSections.has("access") ? <ConfigurationSection id="access" title="Access & Governance" instant>
<TrustPresetSection
permissions={agent.permissions}
disabled={updatePermissions.isPending}
companyId={companyId}
projectCandidates={(boundaryProjects ?? []).map((project) => ({
id: project.id,
label: project.name,
}))}
issueCandidates={(boundaryIssues ?? []).map((issue) => ({
id: issue.id,
label: `${issue.identifier ?? issue.id.slice(0, 8)} · ${issue.title}`,
}))}
candidatesLoading={boundaryProjectsLoading || boundaryIssuesLoading}
onChange={(nextPermissions) =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills,
canAssignTasks,
...buildPermissionsForTrustPreset(nextPermissions, nextPermissions.trustPreset === "low_trust_review" ? "low_trust_review" : "standard"),
})
}
/>
<div>
<h3 className="text-sm font-medium mb-3">Permissions</h3>
<div className="border border-border rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can create new agents</div>
<p className="text-xs text-muted-foreground">
Lets this agent create or hire agents. This also grants task assignment authority.
</p>
</div>
<ToggleSwitch
checked={canCreateAgents}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents: !canCreateAgents,
canCreateSkills,
canAssignTasks: !canCreateAgents ? true : canAssignTasks,
})
}
disabled={updatePermissions.isPending}
/>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can create/import skills</div>
<p className="text-xs text-muted-foreground">
Lets this agent install, import, create, and scan company skills without creating agents.
</p>
</div>
<ToggleSwitch
checked={canCreateSkills}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills: !canCreateSkills,
canAssignTasks,
})
}
disabled={updatePermissions.isPending}
/>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<div className="space-y-1">
<div>Can assign tasks</div>
<p className="text-xs text-muted-foreground">
{taskAssignHint}
</p>
</div>
<ToggleSwitch
checked={canAssignTasks}
onCheckedChange={() =>
updatePermissions.mutate({
canCreateAgents,
canCreateSkills,
canAssignTasks: !canAssignTasks,
})
}
disabled={updatePermissions.isPending || taskAssignLocked}
/>
</div>
</div>
</div>
</ConfigurationSection> : null}
</div>
);
}