diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index cc7518e725b..73ecd39e3b1 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -1285,7 +1285,7 @@ describe.sequential("agent permission routes", () => { })); }); - it("rejects creating an agent with an environment from another company", async () => { + it("allows creating an agent with an instance-scoped environment referenced from another company", async () => { const environmentId = "33333333-3333-4333-8333-333333333333"; mockEnvironmentService.getById.mockResolvedValue({ id: environmentId, @@ -1312,9 +1312,13 @@ describe.sequential("agent permission routes", () => { defaultEnvironmentId: environmentId, })); - expect(res.status).toBe(422); - expect(res.body.error).toContain("Environment not found"); - expect(mockAgentService.create).not.toHaveBeenCalled(); + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockAgentService.create).toHaveBeenCalledWith( + companyId, + expect.objectContaining({ + defaultEnvironmentId: environmentId, + }), + ); }); it("rejects creating an agent with an unsupported default environment driver", async () => { diff --git a/server/src/__tests__/environment-routes.test.ts b/server/src/__tests__/environment-routes.test.ts index 6ac1b2b47d7..a475a7eca57 100644 --- a/server/src/__tests__/environment-routes.test.ts +++ b/server/src/__tests__/environment-routes.test.ts @@ -229,7 +229,7 @@ describe("environment routes", () => { }); }); - it("lists company-scoped environments", async () => { + it("lists instance-scoped environments through the company route alias", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); const app = createApp({ type: "board", @@ -241,7 +241,7 @@ describe("environment routes", () => { expect(res.status).toBe(200); expect(res.body).toHaveLength(1); - expect(mockEnvironmentService.list).toHaveBeenCalledWith("company-1", { + expect(mockEnvironmentService.list).toHaveBeenCalledWith({ status: undefined, driver: "local", }); @@ -308,7 +308,7 @@ describe("environment routes", () => { .toBe("supported"); }); - it("redacts config and metadata for unprivileged agent list reads", async () => { + it("rejects agent list reads for instance-scoped environments", async () => { mockEnvironmentService.list.mockResolvedValue([createEnvironment()]); mockAgentService.getById.mockResolvedValue({ id: "agent-1", @@ -327,19 +327,12 @@ describe("environment routes", () => { const res = await request(app).get("/api/companies/company-1/environments"); - expect(res.status).toBe(200); - expect(res.body).toEqual([ - expect.objectContaining({ - id: "env-1", - config: {}, - metadata: null, - configRedacted: true, - metadataRedacted: true, - }), - ]); + expect(res.status).toBe(403); + expect(res.body.error).toContain("board operators"); + expect(mockEnvironmentService.list).not.toHaveBeenCalled(); }); - it("returns full config for privileged environment readers", async () => { + it("rejects agent detail reads for instance-scoped environments", async () => { mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); mockAgentService.getById.mockResolvedValue({ id: "agent-1", @@ -358,59 +351,17 @@ describe("environment routes", () => { const res = await request(app).get("/api/environments/env-1"); - expect(res.status).toBe(200); - expect(res.body.config).toEqual({ shell: "zsh" }); - expect(res.body.metadata).toEqual({ source: "manual" }); - expect(res.body.configRedacted).toBeUndefined(); - }); - - it("redacts config and metadata for unprivileged agent detail reads", async () => { - mockEnvironmentService.getById.mockResolvedValue(createEnvironment()); - mockAgentService.getById.mockResolvedValue({ - id: "agent-1", - companyId: "company-1", - role: "engineer", - permissions: { canCreateAgents: false }, - }); - mockAccessService.hasPermission.mockResolvedValue(false); - const app = createApp({ - type: "agent", - agentId: "agent-1", - companyId: "company-1", - source: "agent_key", - runId: "run-1", - }); - - const res = await request(app).get("/api/environments/env-1"); - - expect(res.status).toBe(200); - expect(res.body).toEqual( - expect.objectContaining({ - id: "env-1", - config: {}, - metadata: null, - configRedacted: true, - metadataRedacted: true, - }), - ); + expect(res.status).toBe(403); + expect(res.body.error).toContain("board operators"); }); it("creates an environment and logs activity", async () => { const environment = createEnvironment(); - mockAgentService.getById.mockResolvedValue({ - id: "agent-1", - companyId: "company-1", - role: "cto", - permissions: { canCreateAgents: true }, - }); - mockAccessService.hasPermission.mockResolvedValue(false); mockEnvironmentService.create.mockResolvedValue(environment); const app = createApp({ - type: "agent", - agentId: "agent-1", - companyId: "company-1", - source: "agent_key", - runId: "run-1", + type: "board", + userId: "user-1", + source: "local_implicit", }); const res = await request(app) @@ -423,21 +374,22 @@ describe("environment routes", () => { }); expect(res.status).toBe(201); - expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", { + expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Local", driver: "local", description: "Current development machine", status: "active", config: { shell: "zsh" }, + envVars: {}, }); expect(mockLogActivity).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ companyId: "company-1", - actorType: "agent", - actorId: "agent-1", - agentId: "agent-1", - runId: "run-1", + actorType: "user", + actorId: "user-1", + agentId: null, + runId: null, action: "environment.created", entityType: "environment", entityId: environment.id, @@ -462,11 +414,11 @@ describe("environment routes", () => { }); expect(res.status).toBe(409); - expect(res.body.error).toBe("A local environment already exists for this company."); + expect(res.body.error).toBe("A local environment already exists for this instance."); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); - it("allows non-admin board users with environments:manage to create environments", async () => { + it("rejects non-admin board users even when they have company environment permissions", async () => { const environment = createEnvironment(); mockAccessService.canUser.mockResolvedValue(true); mockEnvironmentService.create.mockResolvedValue(environment); @@ -486,15 +438,12 @@ describe("environment routes", () => { config: {}, }); - expect(res.status).toBe(201); - expect(mockAccessService.canUser).toHaveBeenCalledWith( - "company-1", - "user-1", - "environments:manage", - ); + expect(res.status).toBe(403); + expect(res.body.error).toContain("Instance admin access required"); + expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); - it("rejects non-admin board users without environments:manage", async () => { + it("rejects non-admin board users without instance admin access", async () => { mockAccessService.canUser.mockResolvedValue(false); const app = createApp({ type: "board", @@ -513,11 +462,11 @@ describe("environment routes", () => { }); expect(res.status).toBe(403); - expect(res.body.error).toContain("environments:manage"); + expect(res.body.error).toContain("Instance admin access required"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); - it("allows agents with explicit environments:manage grants to create environments", async () => { + it("rejects agent environment creation even with explicit company grants", async () => { const environment = createEnvironment(); mockAgentService.getById.mockResolvedValue({ id: "agent-1", @@ -543,13 +492,9 @@ describe("environment routes", () => { config: {}, }); - expect(res.status).toBe(201); - expect(mockAccessService.hasPermission).toHaveBeenCalledWith( - "company-1", - "agent", - "agent-1", - "environments:manage", - ); + expect(res.status).toBe(403); + expect(res.body.error).toContain("board operators"); + expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); it("rejects invalid SSH config on create", async () => { @@ -618,7 +563,7 @@ describe("environment routes", () => { }); expect(res.status).toBe(201); - expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ + expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({ config: expect.objectContaining({ privateKey: null, privateKeySecretRef: { @@ -627,8 +572,9 @@ describe("environment routes", () => { version: "latest", }, }), + envVars: {}, })); - expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][1])).not.toContain("super-secret-key"); + expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("super-secret-key"); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ @@ -760,7 +706,7 @@ describe("environment routes", () => { reuseLease: true, }, }); - expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", { + expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Fake plugin Sandbox", driver: "sandbox", status: "active", @@ -770,6 +716,7 @@ describe("environment routes", () => { timeoutMs: 450000, reuseLease: true, }, + envVars: {}, }); expect(mockSecretService.create).not.toHaveBeenCalled(); }); @@ -846,7 +793,7 @@ describe("environment routes", () => { reuseLease: true, }, }); - expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", { + expect(mockEnvironmentService.create).toHaveBeenCalledWith({ name: "Secure Sandbox", driver: "sandbox", status: "active", @@ -857,8 +804,9 @@ describe("environment routes", () => { timeoutMs: 450000, reuseLease: true, }, + envVars: {}, }); - expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][1])).not.toContain("test-provider-key"); + expect(JSON.stringify(mockEnvironmentService.create.mock.calls[0][0])).not.toContain("test-provider-key"); expect(mockSecretService.create).toHaveBeenCalledWith( "company-1", expect.objectContaining({ @@ -990,12 +938,13 @@ describe("environment routes", () => { }, }, }); - expect(mockEnvironmentService.create).toHaveBeenCalledWith("company-1", expect.objectContaining({ + expect(mockEnvironmentService.create).toHaveBeenCalledWith(expect.objectContaining({ config: environment.config, + envVars: {}, })); }); - it("rejects unprivileged agent mutations for shared environments", async () => { + it("rejects agent mutations for instance-scoped environments", async () => { mockAgentService.getById.mockResolvedValue({ id: "agent-1", companyId: "company-1", @@ -1019,7 +968,7 @@ describe("environment routes", () => { }); expect(res.status).toBe(403); - expect(res.body.error).toContain("environments:manage"); + expect(res.body.error).toContain("board operators"); expect(mockEnvironmentService.create).not.toHaveBeenCalled(); }); @@ -1094,7 +1043,7 @@ describe("environment routes", () => { expect(mockEnvironmentService.getLeaseById).toHaveBeenCalledWith("lease-1"); }); - it("rejects cross-company agent access", async () => { + it("rejects agent access regardless of company when environment management is instance-scoped", async () => { mockEnvironmentService.list.mockResolvedValue([]); const app = createApp({ type: "agent", @@ -1107,7 +1056,7 @@ describe("environment routes", () => { const res = await request(app).get("/api/companies/company-1/environments"); expect(res.status).toBe(403); - expect(res.body.error).toContain("another company"); + expect(res.body.error).toContain("board operators"); expect(mockEnvironmentService.list).not.toHaveBeenCalled(); }); @@ -1125,7 +1074,7 @@ describe("environment routes", () => { }); const res = await request(app) - .patch(`/api/environments/${environment.id}`) + .patch(`/api/environments/${environment.id}?companyId=company-1`) .send({ status: "archived", config: { @@ -1184,7 +1133,7 @@ describe("environment routes", () => { }); const res = await request(app) - .patch(`/api/environments/${environment.id}`) + .patch(`/api/environments/${environment.id}?companyId=company-1`) .send({ driver: "local", }); @@ -1207,7 +1156,7 @@ describe("environment routes", () => { }); const res = await request(app) - .patch("/api/environments/env-1") + .patch("/api/environments/env-1?companyId=company-1") .send({ driver: "ssh", }); @@ -1226,7 +1175,7 @@ describe("environment routes", () => { }); const res = await request(app) - .patch("/api/environments/env-1") + .patch("/api/environments/env-1?companyId=company-1") .send({ driver: "sandbox", config: { @@ -1295,6 +1244,7 @@ describe("environment routes", () => { expect(res.status).toBe(200); expect(res.body.ok).toBe(true); expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, { + companyId: null, pluginWorkerManager: undefined, }); expect(mockLogActivity).toHaveBeenCalledWith( @@ -1349,6 +1299,7 @@ describe("environment routes", () => { expect(res.status).toBe(200); expect(res.body.driver).toBe("sandbox"); expect(mockProbeEnvironment).toHaveBeenCalledWith(expect.anything(), environment, { + companyId: null, pluginWorkerManager: undefined, }); expect(mockLogActivity).toHaveBeenCalledWith( @@ -1509,7 +1460,7 @@ describe("environment routes", () => { expect(JSON.stringify(mockLogActivity.mock.calls[0][1].details)).not.toContain("resolved-provider-key"); }); - it("rejects sandbox draft probes when the actor cannot read company secrets", async () => { + it("rejects sandbox draft probes for non-admin board users", async () => { mockValidatePluginSandboxProviderConfig.mockResolvedValue({ normalizedConfig: { template: "base", @@ -1557,7 +1508,7 @@ describe("environment routes", () => { }); expect(res.status).toBe(403); - expect(res.body.error).toContain("secrets:read"); + expect(res.body.error).toContain("Instance admin access required"); expect(mockSecretService.resolveSecretValueForEphemeralAccess).not.toHaveBeenCalled(); expect(mockProbeEnvironment).not.toHaveBeenCalled(); }); diff --git a/server/src/__tests__/environment-runtime-driver-contract.test.ts b/server/src/__tests__/environment-runtime-driver-contract.test.ts index 53665ed9afc..373eec43e3b 100644 --- a/server/src/__tests__/environment-runtime-driver-contract.test.ts +++ b/server/src/__tests__/environment-runtime-driver-contract.test.ts @@ -87,7 +87,6 @@ describeEmbeddedPostgres("environment runtime driver contract", () => { }) { const companyId = randomUUID(); const agentId = randomUUID(); - const environmentId = randomUUID(); const runId = randomUUID(); const now = new Date(); let config = input.config; @@ -135,16 +134,26 @@ describeEmbeddedPostgres("environment runtime driver contract", () => { }, }; } - await db.insert(environments).values({ - id: environmentId, - companyId, - name: `${input.driver} contract`, - driver: input.driver, - status: "active", - config, - createdAt: now, - updatedAt: now, - }); + const existingLocal = + input.driver === "local" + ? await db.query.environments.findFirst({ + where: (environment, { eq }) => eq(environment.driver, "local"), + }) + : null; + const environmentId = existingLocal?.id ?? randomUUID(); + if (!existingLocal) { + await db.insert(environments).values({ + id: environmentId, + name: `${input.driver} contract`, + driver: input.driver, + status: "active", + config, + createdAt: now, + updatedAt: now, + }); + } else { + config = (existingLocal.config as Record | null) ?? {}; + } await db.insert(heartbeatRuns).values({ id: runId, companyId,