fix(permissions): validate network access flow

This commit is contained in:
2026-07-26 10:52:46 +02:00
parent 3047dc9195
commit 13fb538880
70 changed files with 4784 additions and 158 deletions

View File

@ -122,7 +122,8 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
wanted: "allow",
effective: "deny",
runtimeLock: { state: "locked", reason: "Runtime locked." },
control: { mode: "readOnly", reason: "Runtime locked." },
control: { mode: "editable" },
runtimeControl: { mode: "readOnly", reason: "Runtime locked." },
});
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", undefined, permission);

View File

@ -344,7 +344,9 @@ function NetworkPermissionEditor({
const wanted = resolved?.wanted ?? source?.network ?? inherited?.network ?? null;
const effective = resolved?.effective ?? null;
const runtimeLocked = resolved?.runtimeLock.state === "locked";
const lockReason = resolved?.runtimeLock.reason ?? resolved?.control.reason ?? null;
const runtimeReadOnly = resolved?.runtimeControl.mode === "readOnly";
const runtimeReason = resolved?.runtimeLock.reason ?? resolved?.runtimeControl.reason ?? null;
const runtimeUnobserved = resolved?.runtimeLock.state === "none" && runtimeReadOnly;
return (
<section className="rounded-md border border-border bg-surface">
@ -371,7 +373,15 @@ function NetworkPermissionEditor({
/>
<StatusLine
label="Runtime"
value={runtimeLocked ? "Verrouillé par le runtime" : "Aucun verrou connu"}
value={
runtimeLocked
? "Runtime verrouillé"
: runtimeUnobserved
? "Aucun runtime actif"
: runtimeReadOnly
? "Runtime en lecture seule"
: "Runtime modifiable"
}
/>
<StatusLine
label="Control"
@ -381,7 +391,19 @@ function NetworkPermissionEditor({
{readOnly && (
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
{lockReason ?? "Le runtime actif ne permet pas de modifier cette permission."}
{resolved?.control.reason ?? "Cette configuration réseau est en lecture seule."}
</p>
)}
{!readOnly && runtimeUnobserved && (
<p className="rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-xs text-primary">
Aucun runtime actif. La configuration sera appliquée au prochain lancement.
</p>
)}
{!readOnly && runtimeLocked && (
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
{runtimeReason ?? "Le runtime actif expose un état réseau en lecture seule."}
</p>
)}

View File

@ -102,7 +102,7 @@ describe("PermissionsPanel", () => {
expect(screen.getAllByText("Réseau autorisé").length).toBeGreaterThan(0);
});
it("renders the network control read-only when the resolved runtime says so", async () => {
it("renders the network control read-only when persisted control says so", async () => {
const agent = new MockAgentGateway();
const permission = new MockPermissionGateway();
const created = await agent.createAgent(PROJECT_ID, {
@ -118,6 +118,7 @@ describe("PermissionsPanel", () => {
reason: "Runtime network is locked.",
},
control: { mode: "readOnly", reason: "Runtime network is locked." },
runtimeControl: { mode: "readOnly", reason: "Runtime network is locked." },
});
const gateways = {
agent,
@ -132,7 +133,7 @@ describe("PermissionsPanel", () => {
);
await waitFor(() => {
expect(screen.getByText("Verrouillé par le runtime")).toBeTruthy();
expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
});
expect(screen.getByText("Runtime network is locked.")).toBeTruthy();
expect(
@ -140,4 +141,95 @@ describe("PermissionsPanel", () => {
.disabled,
).toBe(true);
});
it("keeps wanted network editable when no runtime is active", async () => {
const { permission } = await renderPanel();
await waitFor(() => {
expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
});
expect(
screen.getByText(
"Aucun runtime actif. La configuration sera appliquée au prochain lancement.",
),
).toBeTruthy();
expect(
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
.disabled,
).toBe(false);
fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), {
target: { value: "deny" },
});
fireEvent.click(screen.getByRole("button", { name: "Save network" }));
await waitFor(async () => {
const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
expect(doc.projectDefault?.network).toBe("deny");
});
});
it("keeps an idle agent editable by default and only enables Save after a change", async () => {
await renderPanel();
fireEvent.click(screen.getByText("Builder"));
await waitFor(() => {
expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
});
expect(screen.queryByText("Lecture seule")).toBeNull();
expect(screen.queryByText("Runtime verrouillé")).toBeNull();
const select = screen.getByLabelText("Réseau — Builder network") as HTMLSelectElement;
const save = screen.getByRole("button", { name: "Save network" });
expect(select.disabled).toBe(false);
expect(save).toHaveProperty("disabled", true);
fireEvent.change(select, { target: { value: "allow" } });
expect(save).toHaveProperty("disabled", false);
});
it("keeps wanted network editable when only runtime control is read-only", 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",
source: "external-runtime",
reason: "IdeA cannot change this active runtime.",
},
control: { mode: "editable" },
runtimeControl: {
mode: "readOnly",
reason: "IdeA cannot change this active runtime.",
},
});
const gateways = {
agent,
permission,
profile: new MockProfileGateway(),
} as unknown as Gateways;
render(
<DIProvider gateways={gateways}>
<PermissionsPanel projectId={PROJECT_ID} />
</DIProvider>,
);
await waitFor(() => {
expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
});
expect(
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
.disabled,
).toBe(false);
});
});

View File

@ -61,7 +61,8 @@ describe("TerminalView (with MockTerminalGateway)", () => {
wanted: "allow",
effective: "deny",
runtimeLock: { state: "locked", reason: "Runtime network locked." },
control: { mode: "readOnly", reason: "Runtime network locked." },
control: { mode: "editable" },
runtimeControl: { mode: "readOnly", reason: "Runtime network locked." },
},
});