diff --git a/server/src/__tests__/agent-cross-tenant-authz-routes.test.ts b/server/src/__tests__/agent-cross-tenant-authz-routes.test.ts index ac5c9313aee..1df94d90352 100644 --- a/server/src/__tests__/agent-cross-tenant-authz-routes.test.ts +++ b/server/src/__tests__/agent-cross-tenant-authz-routes.test.ts @@ -60,6 +60,7 @@ const mockAgentService = vi.hoisted(() => ({ const mockAccessService = vi.hoisted(() => ({ canUser: vi.fn(), + decide: vi.fn(), hasPermission: vi.fn(), getMembership: vi.fn(), ensureMembership: vi.fn(), @@ -293,6 +294,17 @@ function resetMockDefaults() { revokedAt: new Date("2026-04-11T00:05:00.000Z"), })); mockAccessService.canUser.mockImplementation(async () => currentAccessCanUser); + mockAccessService.decide.mockImplementation(async (input: { actor?: { type?: string; source?: string }; action?: string }) => { + const allowed = input.actor?.type === "board" && input.actor.source === "local_implicit" + ? true + : currentAccessCanUser; + return { + allowed, + action: input.action, + reason: allowed ? "allow_explicit_grant" : "deny_missing_grant", + explanation: allowed ? "Allowed by test grant." : `Missing permission: ${input.action ?? "action"}`, + }; + }); mockAccessService.hasPermission.mockImplementation(async () => false); mockAccessService.getMembership.mockImplementation(async () => null); mockAccessService.listPrincipalGrants.mockImplementation(async () => []); diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts index 0eeebe524f1..d0549059a30 100644 --- a/server/src/__tests__/agent-live-run-routes.test.ts +++ b/server/src/__tests__/agent-live-run-routes.test.ts @@ -51,7 +51,16 @@ function registerModuleMocks() { vi.doMock("../services/index.js", () => ({ agentService: () => mockAgentService, agentInstructionsService: () => ({}), - accessService: () => ({}), + accessService: () => ({ + canUser: vi.fn(async () => true), + decide: vi.fn(async (input: { action?: string }) => ({ + allowed: true, + action: input.action, + reason: "allow_explicit_grant", + explanation: "Allowed by test grant.", + })), + hasPermission: vi.fn(async () => true), + }), approvalService: () => ({}), companySkillService: () => ({ listRuntimeSkillEntries: vi.fn() }), budgetService: () => ({}), diff --git a/server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts b/server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts index 2dc70626322..bd196a60a03 100644 --- a/server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts +++ b/server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts @@ -22,6 +22,12 @@ const mockIssueService = vi.hoisted(() => ({ vi.mock("../services/index.js", () => ({ accessService: () => ({ canUser: vi.fn(async () => true), + decide: vi.fn(async (input: { action?: string }) => ({ + allowed: true, + action: input.action, + reason: "allow_explicit_grant", + explanation: "Allowed by test grant.", + })), hasPermission: vi.fn(async () => true), }), agentService: () => ({ diff --git a/server/src/__tests__/issue-comment-reopen-routes.test.ts b/server/src/__tests__/issue-comment-reopen-routes.test.ts index ecb3db9bf8f..ad4ee770498 100644 --- a/server/src/__tests__/issue-comment-reopen-routes.test.ts +++ b/server/src/__tests__/issue-comment-reopen-routes.test.ts @@ -16,6 +16,7 @@ const mockIssueService = vi.hoisted(() => ({ const mockAccessService = vi.hoisted(() => ({ canUser: vi.fn(), + decide: vi.fn(), hasPermission: vi.fn(), })); @@ -229,6 +230,7 @@ describe.sequential("issue comment reopen routes", () => { mockIssueService.listWakeableBlockedDependents.mockReset(); mockIssueService.getWakeableParentAfterChildCompletion.mockReset(); mockAccessService.canUser.mockReset(); + mockAccessService.decide.mockReset(); mockAccessService.hasPermission.mockReset(); mockHeartbeatService.wakeup.mockReset(); mockHeartbeatService.reportRunActivity.mockReset(); @@ -307,6 +309,15 @@ describe.sequential("issue comment reopen routes", () => { mockIssueService.getWakeableParentAfterChildCompletion.mockResolvedValue(null); mockIssueService.assertCheckoutOwner.mockResolvedValue({ adoptedFromRunId: null }); mockAccessService.canUser.mockResolvedValue(false); + mockAccessService.decide.mockImplementation(async (input: { action?: string }) => { + const allowed = input.action !== "tasks:manage_active_checkouts"; + return { + allowed, + action: input.action, + reason: allowed ? "allow_explicit_grant" : "deny_missing_grant", + explanation: allowed ? "Allowed by test grant." : "Missing active checkout override.", + }; + }); mockAccessService.hasPermission.mockResolvedValue(false); mockAgentService.getById.mockResolvedValue(null); mockAgentService.list.mockResolvedValue([ diff --git a/server/src/__tests__/issue-execution-policy-routes.test.ts b/server/src/__tests__/issue-execution-policy-routes.test.ts index 1b0db770a19..0b7392d2e2e 100644 --- a/server/src/__tests__/issue-execution-policy-routes.test.ts +++ b/server/src/__tests__/issue-execution-policy-routes.test.ts @@ -26,6 +26,7 @@ const mockHeartbeatService = vi.hoisted(() => ({ const mockAccessService = vi.hoisted(() => ({ canUser: vi.fn(async () => false), + decide: vi.fn(), hasPermission: vi.fn(async () => false), })); @@ -160,6 +161,17 @@ describe("issue execution policy routes", () => { parentBlockerAdded: false, }); mockAccessService.canUser.mockResolvedValue(false); + mockAccessService.decide.mockImplementation(async (input: { actor?: { type?: string; source?: string }; action?: string }) => { + const allowed = input.actor?.type === "board" && input.actor.source === "local_implicit" + ? true + : Boolean(await mockAccessService.canUser() || await mockAccessService.hasPermission()); + return { + allowed, + action: input.action, + reason: allowed ? "allow_explicit_grant" : "deny_missing_grant", + explanation: allowed ? "Allowed by test grant." : `Missing permission: ${input.action ?? "action"}`, + }; + }); mockAccessService.hasPermission.mockResolvedValue(false); }); diff --git a/server/src/__tests__/issue-thread-interaction-routes.test.ts b/server/src/__tests__/issue-thread-interaction-routes.test.ts index 818c5d7d9ac..785dbf182cf 100644 --- a/server/src/__tests__/issue-thread-interaction-routes.test.ts +++ b/server/src/__tests__/issue-thread-interaction-routes.test.ts @@ -43,6 +43,12 @@ function registerModuleMocks() { }), accessService: () => ({ canUser: vi.fn(async () => true), + decide: vi.fn(async (input: { action?: string }) => ({ + allowed: true, + action: input.action, + reason: "allow_explicit_grant", + explanation: "Allowed by test grant.", + })), hasPermission: vi.fn(async () => true), }), agentService: () => ({ diff --git a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts index 167037fbafd..42539b218bc 100644 --- a/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts +++ b/server/src/__tests__/issue-update-comment-wakeup-routes.test.ts @@ -33,6 +33,12 @@ vi.mock("../services/index.js", () => ({ }), accessService: () => ({ canUser: vi.fn(async () => true), + decide: vi.fn(async (input: { action?: string }) => ({ + allowed: true, + action: input.action, + reason: "allow_explicit_grant", + explanation: "Allowed by test grant.", + })), hasPermission: vi.fn(async () => true), }), agentService: () => ({ @@ -95,6 +101,12 @@ function registerModuleMocks() { }), accessService: () => ({ canUser: vi.fn(async () => true), + decide: vi.fn(async (input: { action?: string }) => ({ + allowed: true, + action: input.action, + reason: "allow_explicit_grant", + explanation: "Allowed by test grant.", + })), hasPermission: vi.fn(async () => true), }), agentService: () => ({