diff --git a/server/src/__tests__/agent-permissions-routes.test.ts b/server/src/__tests__/agent-permissions-routes.test.ts index 73ecd39e3b1..b8399138920 100644 --- a/server/src/__tests__/agent-permissions-routes.test.ts +++ b/server/src/__tests__/agent-permissions-routes.test.ts @@ -1594,6 +1594,25 @@ describe.sequential("agent permission routes", () => { expect(res.body.access.taskAssignSource).toBe("agent_creator"); }); + it("rejects CEO permission updates outside the caller company scope", async () => { + const app = await createApp({ + type: "agent", + agentId: "ceo-agent", + companyId: "33333333-3333-4333-8333-333333333333", + runId: "run-1", + source: "agent_key", + }); + + const res = await requestApp(app, (baseUrl) => request(baseUrl) + .patch(`/api/agents/${agentId}/permissions`) + .send({ canCreateAgents: true, canAssignTasks: true })); + + expect(res.status).toBe(403); + expect(res.body.error).toContain("another company"); + expect(mockAgentService.updatePermissions).not.toHaveBeenCalled(); + expect(mockAccessService.setPrincipalPermission).not.toHaveBeenCalled(); + }); + it("exposes a dedicated agent route for the inbox mine view", async () => { mockIssueService.list.mockResolvedValue([ { diff --git a/server/src/__tests__/authorization-service.test.ts b/server/src/__tests__/authorization-service.test.ts index 09b98af27b9..2c1dd29427d 100644 --- a/server/src/__tests__/authorization-service.test.ts +++ b/server/src/__tests__/authorization-service.test.ts @@ -1021,6 +1021,25 @@ describeEmbeddedPostgres("authorization service", () => { }); }); + it("denies active-checkout management outside the CEO caller company scope", async () => { + const sourceCompany = await createCompany(db, "CheckoutSource"); + const targetCompany = await createCompany(db, "CheckoutTarget"); + const actorAgent = await createAgent(db, sourceCompany.id, { role: "ceo" }); + const targetAgent = await createAgent(db, targetCompany.id, { role: "engineer" }); + + const decision = await authorizationService(db).decide({ + actor: { type: "agent", agentId: actorAgent.id, companyId: sourceCompany.id, source: "agent_jwt" }, + action: "tasks:manage_active_checkouts", + resource: { type: "issue", companyId: targetCompany.id, assigneeAgentId: targetAgent.id }, + }); + + expect(decision).toMatchObject({ + allowed: false, + reason: "deny_company_boundary", + }); + expect(decision.explanation).toContain("another company"); + }); + it("allows scoped assignment inside a granted project and denies other projects", async () => { const company = await createCompany(db, "ProjectScope"); const project = await createProject(db, company.id, "Allowed"); diff --git a/server/src/__tests__/openclaw-invite-prompt-route.test.ts b/server/src/__tests__/openclaw-invite-prompt-route.test.ts index fd556ea176c..7904e275a35 100644 --- a/server/src/__tests__/openclaw-invite-prompt-route.test.ts +++ b/server/src/__tests__/openclaw-invite-prompt-route.test.ts @@ -173,6 +173,28 @@ describe.sequential("POST /companies/:companyId/openclaw/invite-prompt", () => { expect(res.body.error).toContain("Only CEO agents"); }); + it("rejects CEO agent callers outside the target company scope", async () => { + const db = createDbStub(); + const app = createApp( + { + type: "agent", + agentId: "agent-1", + companyId: "company-2", + source: "agent_key", + }, + db, + ); + + const res = await request(app) + .post("/api/companies/company-1/openclaw/invite-prompt") + .send({}); + + expect(res.status).toBe(403); + expect(res.body.error).toContain("another company"); + expect(mockAgentService.getById).not.toHaveBeenCalled(); + expect((db as any).__insertValues).not.toHaveBeenCalled(); + }); + it("allows CEO agent callers and creates an agent-only invite", async () => { const db = createDbStub([companyBranding], [logoAsset]); mockAgentService.getById.mockResolvedValue({