diff --git a/src/api/routes.ts b/src/api/routes.ts index 9c7b2e0726..6503d18098 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1086,6 +1086,11 @@ export function createApp() { if (!parsed.success) return c.json({ error: "invalid_command_feedback", issues: parsed.error.issues }, 400); const answer = await getAgentCommandAnswer(c.env, parsed.data.answerId); if (!answer) return c.json({ error: "command_answer_not_found" }, 404); + const repo = await getRepository(c.env, answer.repoFullName); + if (identity.kind === "session") { + const repoForbidden = await requireSessionRepoAccess(c, identity, answer.repoFullName, repo); + if (repoForbidden) return repoForbidden; + } const actorLogin = identity.actor; await recordAgentCommandFeedback(c.env, { answerId: answer.id, diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index d3495b248c..3dff912ba6 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -1803,6 +1803,53 @@ describe("api routes", () => { ); expect(forbiddenVictimPreview.status).toBe(403); await expect(forbiddenVictimPreview.json()).resolves.toMatchObject({ error: "forbidden_repo" }); + await upsertAgentCommandAnswer(ownerEnv, { + id: "owned-app-feedback", + repoFullName: "repo-owner/owned-repo", + issueNumber: 7, + command: "plan-next-work", + requestCommentId: 700, + responseCommentId: 701, + responseUrl: "https://github.com/repo-owner/owned-repo/pull/7#issuecomment-701", + actorKind: "maintainer", + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + }); + await upsertAgentCommandAnswer(ownerEnv, { + id: "victim-app-feedback", + repoFullName: "victim-org/secret-repo", + issueNumber: 42, + command: "plan-next-work", + requestCommentId: 4200, + responseCommentId: 4201, + responseUrl: "https://github.com/victim-org/secret-repo/pull/42#issuecomment-4201", + actorKind: "maintainer", + createdAt: "2026-05-28T00:00:00.000Z", + updatedAt: "2026-05-28T00:00:00.000Z", + metadata: {}, + }); + const ownerRepoFeedback = await app.request( + "/v1/app/commands/feedback", + { + method: "POST", + headers: ownerHeaders, + body: JSON.stringify({ answerId: "owned-app-feedback", vote: "useful" }), + }, + ownerEnv, + ); + expect(ownerRepoFeedback.status).toBe(200); + const forbiddenVictimFeedback = await app.request( + "/v1/app/commands/feedback", + { + method: "POST", + headers: ownerHeaders, + body: JSON.stringify({ answerId: "victim-app-feedback", vote: "not_useful" }), + }, + ownerEnv, + ); + expect(forbiddenVictimFeedback.status).toBe(403); + await expect(forbiddenVictimFeedback.json()).resolves.toMatchObject({ error: "forbidden_repo" }); const { token: operatorToken } = await createSessionForGitHubUser(ownerEnv, { login: "jsonbored", id: 1 }); const operatorVictimPreview = await app.request( "/v1/app/commands/preview",