diff --git a/server/src/__tests__/board-access-denial.test.ts b/server/src/__tests__/board-access-denial.test.ts index 8372b1825f0..34fcf9e4e76 100644 --- a/server/src/__tests__/board-access-denial.test.ts +++ b/server/src/__tests__/board-access-denial.test.ts @@ -35,6 +35,7 @@ describe("board-only denial", () => { ["post", `/api/companies/${COMPANY_ID}/secrets`, "[Open company secrets](//company/settings/secrets)"], ["post", `/api/secrets/${AGENT_ID}/rotate`, "[Open company secrets](//company/settings/secrets)"], ["post", `/api/companies/${COMPANY_ID}/budget-incidents/i-1/resolve?x=1`, "[Open budgets](//activity/budgets)"], + ["post", `/api/companies/${COMPANY_ID}/costs/retry-accounting`, "[Open costs](//activity/costs)"], ["put", `/api/companies/${COMPANY_ID}/prompt-injection-prevention`, "[Open connection settings](//company/settings/connections)"], ["patch", `/api/agents/${AGENT_ID}/permissions`, `[Open the agent's permissions](//agents/${AGENT_ID}/permissions)`], ["post", `/api/agents/${AGENT_ID}/keys`, `[Open the agent's API keys](//agents/${AGENT_ID}/api-keys)`], diff --git a/server/src/routes/authz.ts b/server/src/routes/authz.ts index 4862ca61593..833f3554ca9 100644 --- a/server/src/routes/authz.ts +++ b/server/src/routes/authz.ts @@ -49,6 +49,8 @@ export type BoardControlPage = [label: string, path: string]; /** Budgets are board-only. These pages hold the budget controls behind any route that writes one. */ export const COMPANY_BUDGETS_PAGE: BoardControlPage = ["budgets", "//activity/budgets"]; +/** Cost recording controls (such as retrying a run's saved-usage cost) are on the Costs page, not Budgets. */ +export const COMPANY_COSTS_PAGE: BoardControlPage = ["costs", "//activity/costs"]; export function agentBudgetPage(agentId: string): BoardControlPage { return [`the agent's ${AGENT_PROFILE_TABS.budgets![0]}`, `//agents/${agentId}/${AGENT_PROFILE_TABS.budgets![1]}`]; } @@ -59,7 +61,8 @@ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{ const BOARD_CONTROL_PAGES: Array<[RegExp, (match: RegExpExecArray, req: Request) => [string, string] | null]> = [ [/^\/(?:companies\/[^/]+\/)?secrets?(?:[-/]|$)/, () => ["company secrets", "//company/settings/secrets"]], - [/^\/companies\/[^/]+\/(?:budgets|budget-incidents|costs)(?:\/|$)/, () => COMPANY_BUDGETS_PAGE], + [/^\/companies\/[^/]+\/(?:budgets|budget-incidents)(?:\/|$)/, () => COMPANY_BUDGETS_PAGE], + [/^\/companies\/[^/]+\/costs(?:\/|$)/, () => COMPANY_COSTS_PAGE], [/^\/companies\/[^/]+\/(?:prompt-injection-prevention|decision-model)(?:\/|$)/, () => ["connection settings", "//company/settings/connections"]], [/^\/companies\/[^/]+\/(?:members|invites|join-requests)(?:\/|$)/, () => ["company members", "//company/settings/members"]], [/^\/companies\/[^/]+\/(?:tools\/)?gateways(?:\/|$)|^\/tool-gateway\//, () => ["tool gateways", "//apps/gateways"]], diff --git a/server/src/routes/openapi.ts b/server/src/routes/openapi.ts index e9e9a8c92c6..5aab5b9265f 100644 --- a/server/src/routes/openapi.ts +++ b/server/src/routes/openapi.ts @@ -6123,7 +6123,7 @@ for (const segment of costSummaryPaths) { registry.registerPath({ method: "post", path: "/api/companies/{companyId}/costs/retry-accounting", tags: ["costs"], summary: "Retry recording a run's saved cost data", - description: "Requires company cost-read and board write access. Viewers and agents cannot retry. Replays accounting evidence only; never starts provider work or invents missing usage.", + description: "Requires company cost-read and board write access. Viewers and agents cannot retry. Replays accounting evidence only; never starts provider work or invents missing usage. A board user retries with Retry cost recording on the run's row on the [Costs page](//activity/costs).", request: { params: z.object({ companyId: z.string() }), body: jsonBody(retryAccountingSchema) }, responses: { 200: r.ok(), 400: r.badRequest, 401: r.unauthorized, 403: r.forbidden, 404: r.notFound }, });