Preserve consent gate service failures

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-07-09 20:50:34 +00:00
1 parent 9ec4f2a25a
commit badd887aad
2 files changed
+34 -3

No files matched your search

@@ -1234,6 +1234,33 @@ describe("company skill mutation permissions", () => {
expect(mockCompanySkillService.createLocalSkill).not.toHaveBeenCalled();
});
it("does not convert consent gate service failures into authorization denials", async () => {
mockAccessService.decide.mockResolvedValue(denySkillChangeDecision(
"deny_missing_consent",
"Permission skills:suggest-changes requires accepted change consent before applying this mutation.",
));
mockReflectionCoachMutationGate.assertConsented.mockRejectedValue(new Error("database unavailable"));
const res = await request(await createApp({
type: "agent",
agentId: "reflection-coach",
companyId: "company-1",
runId: "run-apply",
}))
.post("/api/companies/company-1/skills")
.send({ name: "Reflection Draft", slug: "reflection-draft", markdown: "# Draft" });
expect(res.status, JSON.stringify(res.body)).toBe(500);
expect(res.body.error).toBe("Internal server error");
expect(mockReflectionCoachMutationGate.assertConsented).toHaveBeenCalledWith({
companyId: "company-1",
actorAgentId: "reflection-coach",
actorRunId: "run-apply",
targetKeys: ["skill-slug:reflection-draft"],
});
expect(mockCompanySkillService.createLocalSkill).not.toHaveBeenCalled();
});
it("allows suggest-tier skill mutations after accepted change consent", async () => {
mockAccessService.decide
.mockResolvedValueOnce(denySkillChangeDecision(
+7 -3
View File
@@ -31,9 +31,10 @@ import {
listCatalogSkillsOrEmpty,
readCatalogSkillFile,
} from "../services/skills-catalog.js";
import { forbidden } from "../errors.js";
import { forbidden, HttpError } from "../errors.js";
import { assertAuthenticated, assertCompanyAccess, getActorInfo } from "./authz.js";
import { getTelemetryClient } from "../telemetry.js";
import { authorizationDeniedDetails } from "../services/authorization.js";
import {
changeConsentGateService,
skillChangeTargetKey,
@@ -139,8 +140,11 @@ export function companySkillRoutes(db: Db) {
actorRunId: req.actor.runId ?? null,
targetKeys,
});
} catch {
throw forbidden(decision.explanation, { reason: decision.reason });
} catch (err) {
if (err instanceof HttpError && err.status === 403) {
throw forbidden(decision.explanation, authorizationDeniedDetails(decision));
}
throw err;
}
const consentedDecision = await access.decide({