feat(permissions): expose network permission state (#103)

This commit is contained in:
2026-07-25 23:05:55 +02:00
parent e8731834f4
commit 3047dc9195
31 changed files with 2080 additions and 94 deletions

View File

@ -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>
);
}