a18c8c630c
* surface gateway timeout for tailscale * talescale fix #2 - attempt 1 * luke findings fix#1 * add narrow log for clientId * prod safe proxy log * fix log visibility * LAN connection & subagent SOUL|IDENTITY fixes * Initialize missing files for subagent SOUL|IDENTITY * surface missing files in UI * capturing agent - runtime,identity,session * plugin-install fix * fix: recover agent workspace for marketplace installs * fix: recover agent workspace and identity name from file provenance * fix: tolerate webchat session patch blocks during permission updates
250 lines
8.0 KiB
TypeScript
250 lines
8.0 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
isPermissionsCustom,
|
|
resolveAgentPermissionsDraft,
|
|
resolveCommandModeFromRole,
|
|
resolvePresetDefaultsForRole,
|
|
resolveRoleForCommandMode,
|
|
resolveToolGroupOverrides,
|
|
resolveToolGroupStateFromConfigEntry,
|
|
updateAgentPermissionsViaStudio,
|
|
updateExecutionRoleViaStudio,
|
|
} from "@/features/agents/operations/agentPermissionsOperation";
|
|
import { syncGatewaySessionSettings } from "@/lib/gateway/GatewayClient";
|
|
import { updateGatewayAgentOverrides } from "@/lib/gateway/agentConfig";
|
|
import {
|
|
readGatewayAgentExecApprovals,
|
|
upsertGatewayAgentExecApprovals,
|
|
} from "@/lib/gateway/execApprovals";
|
|
import { GatewayResponseError } from "@/lib/gateway/errors";
|
|
|
|
vi.mock("@/lib/gateway/GatewayClient", async () => {
|
|
const actual = await vi.importActual<typeof import("@/lib/gateway/GatewayClient")>(
|
|
"@/lib/gateway/GatewayClient"
|
|
);
|
|
return {
|
|
...actual,
|
|
syncGatewaySessionSettings: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/gateway/agentConfig", async () => {
|
|
const actual = await vi.importActual<typeof import("@/lib/gateway/agentConfig")>(
|
|
"@/lib/gateway/agentConfig"
|
|
);
|
|
return {
|
|
...actual,
|
|
updateGatewayAgentOverrides: vi.fn(async () => undefined),
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/gateway/execApprovals", () => ({
|
|
readGatewayAgentExecApprovals: vi.fn(async () => null),
|
|
upsertGatewayAgentExecApprovals: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
const createWebchatBlockedPatchError = () =>
|
|
new GatewayResponseError({
|
|
code: "INVALID_REQUEST",
|
|
message: "webchat clients cannot patch sessions; use chat.send for session-scoped updates",
|
|
});
|
|
|
|
describe("agentPermissionsOperation", () => {
|
|
const mockedSyncGatewaySessionSettings = vi.mocked(syncGatewaySessionSettings);
|
|
const mockedUpdateGatewayAgentOverrides = vi.mocked(updateGatewayAgentOverrides);
|
|
const mockedReadGatewayAgentExecApprovals = vi.mocked(readGatewayAgentExecApprovals);
|
|
const mockedUpsertGatewayAgentExecApprovals = vi.mocked(upsertGatewayAgentExecApprovals);
|
|
|
|
beforeEach(() => {
|
|
mockedSyncGatewaySessionSettings.mockReset();
|
|
mockedUpdateGatewayAgentOverrides.mockClear();
|
|
mockedReadGatewayAgentExecApprovals.mockReset();
|
|
mockedUpsertGatewayAgentExecApprovals.mockReset();
|
|
mockedReadGatewayAgentExecApprovals.mockResolvedValue(null);
|
|
mockedUpsertGatewayAgentExecApprovals.mockResolvedValue(undefined);
|
|
mockedUpdateGatewayAgentOverrides.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it("maps command mode and preset role in both directions", () => {
|
|
expect(resolveRoleForCommandMode("off")).toBe("conservative");
|
|
expect(resolveRoleForCommandMode("ask")).toBe("collaborative");
|
|
expect(resolveRoleForCommandMode("auto")).toBe("autonomous");
|
|
|
|
expect(resolveCommandModeFromRole("conservative")).toBe("off");
|
|
expect(resolveCommandModeFromRole("collaborative")).toBe("ask");
|
|
expect(resolveCommandModeFromRole("autonomous")).toBe("auto");
|
|
});
|
|
|
|
it("resolves autonomous preset defaults to permissive capabilities", () => {
|
|
expect(resolvePresetDefaultsForRole("autonomous")).toEqual({
|
|
commandMode: "auto",
|
|
webAccess: true,
|
|
fileTools: true,
|
|
});
|
|
});
|
|
|
|
it("derives tool-group state from allow and deny with deny precedence", () => {
|
|
const state = resolveToolGroupStateFromConfigEntry({
|
|
allow: ["group:web", "group:runtime"],
|
|
deny: ["group:web"],
|
|
});
|
|
|
|
expect(state.usesAllow).toBe(true);
|
|
expect(state.runtime).toBe(true);
|
|
expect(state.web).toBe(false);
|
|
expect(state.fs).toBeNull();
|
|
});
|
|
|
|
it("merges group toggles while preserving allow mode", () => {
|
|
const overrides = resolveToolGroupOverrides({
|
|
existingTools: {
|
|
allow: ["group:web", "custom:tool"],
|
|
deny: ["group:runtime", "group:fs"],
|
|
},
|
|
runtimeEnabled: true,
|
|
webEnabled: false,
|
|
fsEnabled: true,
|
|
});
|
|
|
|
expect(overrides.tools.allow).toEqual(
|
|
expect.arrayContaining(["custom:tool", "group:runtime", "group:fs"])
|
|
);
|
|
expect(overrides.tools.allow).not.toEqual(expect.arrayContaining(["group:web"]));
|
|
expect(overrides.tools.deny).toEqual(expect.arrayContaining(["group:web"]));
|
|
expect(overrides.tools.deny).not.toEqual(
|
|
expect.arrayContaining(["group:runtime", "group:fs"])
|
|
);
|
|
});
|
|
|
|
it("merges group toggles while preserving alsoAllow mode", () => {
|
|
const overrides = resolveToolGroupOverrides({
|
|
existingTools: {
|
|
alsoAllow: ["group:web"],
|
|
deny: [],
|
|
},
|
|
runtimeEnabled: true,
|
|
webEnabled: true,
|
|
fsEnabled: false,
|
|
});
|
|
|
|
expect(overrides.tools).not.toHaveProperty("allow");
|
|
expect(overrides.tools.alsoAllow).toEqual(
|
|
expect.arrayContaining(["group:web", "group:runtime"])
|
|
);
|
|
expect(overrides.tools.deny).toEqual(expect.arrayContaining(["group:fs"]));
|
|
});
|
|
|
|
it("resolves draft from session role and config group overrides", () => {
|
|
const draft = resolveAgentPermissionsDraft({
|
|
agent: {
|
|
sessionExecSecurity: "allowlist",
|
|
sessionExecAsk: "always",
|
|
},
|
|
existingTools: {
|
|
allow: ["group:web"],
|
|
deny: ["group:fs"],
|
|
},
|
|
});
|
|
|
|
expect(draft).toEqual({
|
|
commandMode: "ask",
|
|
webAccess: true,
|
|
fileTools: false,
|
|
});
|
|
});
|
|
|
|
it("flags custom draft when advanced values diverge from preset baseline", () => {
|
|
expect(
|
|
isPermissionsCustom({
|
|
role: "autonomous",
|
|
draft: {
|
|
commandMode: "auto",
|
|
webAccess: false,
|
|
fileTools: true,
|
|
},
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("does not fail permission updates when webchat blocks sessions.patch after config writes", async () => {
|
|
mockedSyncGatewaySessionSettings.mockRejectedValue(createWebchatBlockedPatchError());
|
|
const client = {
|
|
call: vi.fn(async (method: string) => {
|
|
if (method === "config.get") {
|
|
return {
|
|
config: {
|
|
agents: [
|
|
{
|
|
id: "agent-1",
|
|
sandbox: { mode: "workspace-write" },
|
|
tools: { allow: ["group:web"], deny: [] },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`Unexpected method: ${method}`);
|
|
}),
|
|
} as never;
|
|
const loadAgents = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
updateAgentPermissionsViaStudio({
|
|
client,
|
|
agentId: "agent-1",
|
|
sessionKey: "session-1",
|
|
draft: {
|
|
commandMode: "ask",
|
|
webAccess: true,
|
|
fileTools: false,
|
|
},
|
|
loadAgents,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(mockedUpsertGatewayAgentExecApprovals).toHaveBeenCalledTimes(1);
|
|
expect(mockedUpdateGatewayAgentOverrides).toHaveBeenCalledTimes(1);
|
|
expect(mockedSyncGatewaySessionSettings).toHaveBeenCalledTimes(1);
|
|
expect(loadAgents).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not fail execution-role updates when webchat blocks sessions.patch after config writes", async () => {
|
|
mockedSyncGatewaySessionSettings.mockRejectedValue(createWebchatBlockedPatchError());
|
|
const client = {
|
|
call: vi.fn(async (method: string) => {
|
|
if (method === "config.get") {
|
|
return {
|
|
config: {
|
|
agents: [
|
|
{
|
|
id: "agent-1",
|
|
sandbox: { mode: "workspace-write" },
|
|
tools: { allow: ["group:web"], deny: [] },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`Unexpected method: ${method}`);
|
|
}),
|
|
} as never;
|
|
const loadAgents = vi.fn(async () => undefined);
|
|
|
|
await expect(
|
|
updateExecutionRoleViaStudio({
|
|
client,
|
|
agentId: "agent-1",
|
|
sessionKey: "session-1",
|
|
role: "autonomous",
|
|
loadAgents,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
|
|
expect(mockedUpsertGatewayAgentExecApprovals).toHaveBeenCalledTimes(1);
|
|
expect(mockedUpdateGatewayAgentOverrides).toHaveBeenCalledTimes(1);
|
|
expect(mockedSyncGatewaySessionSettings).toHaveBeenCalledTimes(1);
|
|
expect(loadAgents).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|