feat(permissions): expose network permission state (#103)
This commit is contained in:
@ -14,7 +14,7 @@
|
||||
* the existing `createAgent` path with the selected profile.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button, Input, Panel, Spinner, cn } from "@/shared";
|
||||
import { TerminalView } from "@/features/terminals/TerminalView";
|
||||
@ -25,6 +25,7 @@ import { useAgents } from "./useAgents";
|
||||
import { correlateModelServerStatus } from "./modelServerLaunch";
|
||||
import { AgentLimitBadge } from "./AgentLimitBadge";
|
||||
import { ModelServerLaunchBadge } from "./ModelServerLaunchBadge";
|
||||
import type { ResolvedAgentSystemPermissions } from "@/domain";
|
||||
|
||||
export interface AgentsPanelProps {
|
||||
/** The project whose agents to manage. */
|
||||
@ -41,6 +42,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
const drift = useDrift(projectId);
|
||||
const gateways = useGateways();
|
||||
const templateGw = gateways.template ?? null;
|
||||
const permissionGw = gateways.permission ?? null;
|
||||
|
||||
// Create form state
|
||||
const [newName, setNewName] = useState("");
|
||||
@ -130,6 +132,45 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
const [agentSessions, setAgentSessions] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [networkByAgent, setNetworkByAgent] = useState<
|
||||
Record<string, ResolvedAgentSystemPermissions>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (!permissionGw || vm.agents.length === 0) {
|
||||
setNetworkByAgent({});
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void Promise.all(
|
||||
vm.agents.map(async (candidate) => {
|
||||
try {
|
||||
return [
|
||||
candidate.id,
|
||||
await permissionGw.resolveAgentSystemPermissions(
|
||||
projectId,
|
||||
candidate.id,
|
||||
),
|
||||
] as const;
|
||||
} catch {
|
||||
return [candidate.id, null] as const;
|
||||
}
|
||||
}),
|
||||
).then((pairs) => {
|
||||
if (cancelled) return;
|
||||
setNetworkByAgent(
|
||||
Object.fromEntries(
|
||||
pairs.filter(
|
||||
(pair): pair is readonly [string, ResolvedAgentSystemPermissions] =>
|
||||
pair[1] !== null,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [permissionGw, projectId, vm.agents]);
|
||||
|
||||
/**
|
||||
* Pending profile change awaiting confirmation: the target agent + the chosen
|
||||
@ -342,6 +383,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
vm.modelServerStatusByServer,
|
||||
);
|
||||
const launchFailure = vm.launchFailureByAgent[a.id];
|
||||
const network = networkByAgent[a.id] ?? null;
|
||||
return (
|
||||
<li
|
||||
key={a.id}
|
||||
@ -379,6 +421,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
{delegationSource}
|
||||
</span>
|
||||
)}
|
||||
{network && <NetworkPermissionBadge state={network} />}
|
||||
</span>
|
||||
<span className="text-xs text-muted">{profileName}</span>
|
||||
{live && (
|
||||
@ -513,6 +556,7 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
onSessionId={(sid) =>
|
||||
setAgentSessions((prev) => ({ ...prev, [activeAgentId]: sid }))
|
||||
}
|
||||
systemPermissions={networkByAgent[activeAgentId] ?? null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -667,3 +711,40 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function NetworkPermissionBadge({
|
||||
state,
|
||||
}: {
|
||||
state: ResolvedAgentSystemPermissions;
|
||||
}) {
|
||||
const locked = state.runtimeLock.state === "locked";
|
||||
const label = locked
|
||||
? "Verrouillé"
|
||||
: state.effective === "allow"
|
||||
? "Autorisé"
|
||||
: state.effective === "deny"
|
||||
? "Interdit"
|
||||
: "Demande";
|
||||
return (
|
||||
<span
|
||||
aria-label={`network ${label}`}
|
||||
title={
|
||||
locked
|
||||
? (state.runtimeLock.reason ?? "Réseau verrouillé par le runtime")
|
||||
: `État effectif : ${label}`
|
||||
}
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
locked
|
||||
? "bg-warning/15 text-warning"
|
||||
: state.effective === "allow"
|
||||
? "bg-success/15 text-success"
|
||||
: state.effective === "deny"
|
||||
? "bg-danger/15 text-danger"
|
||||
: "bg-primary/15 text-primary",
|
||||
)}
|
||||
>
|
||||
Réseau: {label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import {
|
||||
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockPermissionGateway,
|
||||
MockProfileGateway,
|
||||
MockSystemGateway,
|
||||
MockTemplateGateway,
|
||||
@ -44,13 +45,15 @@ function renderPanel(
|
||||
profile: MockProfileGateway = new MockProfileGateway(),
|
||||
projectRoot = "/home/me/proj",
|
||||
template?: MockTemplateGateway,
|
||||
permission: MockPermissionGateway = new MockPermissionGateway(),
|
||||
) {
|
||||
const tmpl = template ?? new MockTemplateGateway(agent);
|
||||
const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
|
||||
const gateways = { agent, profile, template: tmpl, permission } as unknown as Gateways;
|
||||
return {
|
||||
agent,
|
||||
profile,
|
||||
template: tmpl,
|
||||
permission,
|
||||
...render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<AgentsPanel projectId={PROJECT_ID} projectRoot={projectRoot} />
|
||||
@ -108,6 +111,27 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
expect(agent).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the resolved network permission badge for an agent", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const permission = new MockPermissionGateway();
|
||||
const created = await agent.createAgent(PROJECT_ID, {
|
||||
name: "Builder",
|
||||
profileId: "p1",
|
||||
});
|
||||
permission.setResolvedAgentSystemPermissions(created.id, {
|
||||
wanted: "allow",
|
||||
effective: "deny",
|
||||
runtimeLock: { state: "locked", reason: "Runtime locked." },
|
||||
control: { mode: "readOnly", reason: "Runtime locked." },
|
||||
});
|
||||
|
||||
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", undefined, permission);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Réseau: Verrouillé")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("the Create button is disabled when the name is empty", async () => {
|
||||
renderPanel();
|
||||
await waitForIdle();
|
||||
|
||||
Reference in New Issue
Block a user