diff --git a/packages/control-plane/src/router.policy.test.ts b/packages/control-plane/src/router.policy.test.ts index d5e1b4d91..855ffe73d 100644 --- a/packages/control-plane/src/router.policy.test.ts +++ b/packages/control-plane/src/router.policy.test.ts @@ -78,6 +78,8 @@ describe("route policy table", () => { ], ["GET", "/integration-settings/slack/watched-channels", [{ service: "slack-bot" }]], ["GET", "/model-preferences", [{ service: "slack-bot" }]], + ["GET", "/sessions/session-1/events", [{ service: "slack-bot" }, { service: "linear-bot" }]], + ["GET", "/sessions/session-1/artifacts", [{ service: "slack-bot" }, { service: "linear-bot" }]], ])("declares the exact actorless grants for %s %s", (method, path, expected) => { const authorization = routeFor(method, path)?.authorization; expect(["active-user", "active-global"]).toContain(authorization?.kind); @@ -99,6 +101,8 @@ describe("route policy table", () => { routeFor("GET", "/integration-settings/github/resolved/acme/widgets"), routeFor("GET", "/integration-settings/slack/watched-channels"), routeFor("GET", "/model-preferences"), + routeFor("GET", "/sessions/session-1/events"), + routeFor("GET", "/sessions/session-1/artifacts"), routeFor("POST", "/sessions/session-1/stop"), routeFor("GET", "/sessions/session-1/media/artifact-1"), ]); diff --git a/packages/control-plane/src/routes/session-runtime-proxy.ts b/packages/control-plane/src/routes/session-runtime-proxy.ts index 7eb9244a7..ca926fe89 100644 --- a/packages/control-plane/src/routes/session-runtime-proxy.ts +++ b/packages/control-plane/src/routes/session-runtime-proxy.ts @@ -357,7 +357,9 @@ export const sessionRuntimeProxyRoutes: Route[] = [ method: "GET", routePath: "/sessions/:id/events", internalPath: SessionInternalPaths.events, - authorization: requirePermission("sessions.read"), + authorization: requirePermission("sessions.read", { + actorlessGrants: [{ service: "slack-bot" }, { service: "linear-bot" }], + }), forwardSearch: true, }), simpleProxyRoute({ @@ -365,7 +367,9 @@ export const sessionRuntimeProxyRoutes: Route[] = [ method: "GET", routePath: "/sessions/:id/artifacts", internalPath: SessionInternalPaths.artifacts, - authorization: requirePermission("sessions.read"), + authorization: requirePermission("sessions.read", { + actorlessGrants: [{ service: "slack-bot" }, { service: "linear-bot" }], + }), }), simpleProxyRoute({ policy: GITHUB_USER_OR_SERVICE_ROUTE, diff --git a/packages/control-plane/test/integration/service-auth.test.ts b/packages/control-plane/test/integration/service-auth.test.ts index c3fd07e26..c1146490e 100644 --- a/packages/control-plane/test/integration/service-auth.test.ts +++ b/packages/control-plane/test/integration/service-auth.test.ts @@ -327,7 +327,7 @@ describe("sig1 service-credential authentication", () => { }); }); - it("allows only narrow actorless session callbacks", async () => { + it("allows only narrow actorless session callbacks and completion reads", async () => { const created = await signedFetch({ service: "linear-bot", method: "POST", @@ -348,6 +348,17 @@ describe("sig1 service-credential authentication", () => { }); expect(linearStop.status).not.toBe(403); + for (const service of ["slack-bot", "linear-bot"] as const) { + for (const path of ["events", "artifacts"] as const) { + const completionRead = await signedFetch({ + service, + method: "GET", + url: `https://test.local/sessions/${sessionId}/${path}`, + }); + expect(completionRead.status, `${service} GET ${path}`).not.toBe(403); + } + } + const slackMedia = await signedFetch({ service: "slack-bot", method: "GET",