diff --git a/apps/server/src/github/GitHubPrBridge.ts b/apps/server/src/github/GitHubPrBridge.ts index 9a8f6db0e62..60fb20b82df 100644 --- a/apps/server/src/github/GitHubPrBridge.ts +++ b/apps/server/src/github/GitHubPrBridge.ts @@ -1218,9 +1218,9 @@ export const make = Effect.gen(function* () { return; } - // When the closed-set identity map is on, map membership is required in - // addition to the GitHub permission floor. Public-repo write / outside - // collaborators cannot drive the host unless listed. + // Closed-set identity map is required for agent turns (fail-closed when the + // map is off/empty). Public-repo write / outside collaborators cannot drive + // the host unless listed; permission floor alone is never enough. const mapEnabled = yield* identity.isMapEnabled(); const mapPeople = yield* identity.listMapPeople(); const trust = classifyGitHubActorTrust({ diff --git a/apps/server/src/github/githubActorTrust.test.ts b/apps/server/src/github/githubActorTrust.test.ts index 0f6696609e1..9932ec88f92 100644 --- a/apps/server/src/github/githubActorTrust.test.ts +++ b/apps/server/src/github/githubActorTrust.test.ts @@ -43,7 +43,7 @@ describe("resolvePersonByGitHubActor", () => { }); describe("classifyGitHubActorTrust", () => { - it("allows full access when the identity map is disabled", () => { + it("denies agent access when the identity map is disabled (fail-closed)", () => { expect( classifyGitHubActorTrust({ identityMapEnabled: false, @@ -51,7 +51,7 @@ describe("classifyGitHubActorTrust", () => { actorLogin: "stranger", people: [], }), - ).toEqual({ mode: "full", person: null, reason: "identity_map_disabled" }); + ).toEqual({ mode: "denied", person: null, reason: "identity_map_disabled" }); }); it("trusts mapped github accounts for full agent turns", () => { diff --git a/apps/server/src/github/githubActorTrust.ts b/apps/server/src/github/githubActorTrust.ts index 9a2dd150ba1..fef05c57129 100644 --- a/apps/server/src/github/githubActorTrust.ts +++ b/apps/server/src/github/githubActorTrust.ts @@ -1,10 +1,11 @@ /** * GitHub actor trust relative to the closed-set identity map. * - * When the map is off, collaborator permission alone gates agent turns - * (legacy behaviour). When the map is on, the actor must also resolve to a + * Fail-closed: no configured map (or empty map) is treated like an unmapped + * actor — no agent turns. When the map is on, the actor must resolve to a * mapped person (github id or login) — so public-repo write access or - * outside collaborators cannot drive the host unless listed. + * outside collaborators cannot drive the host unless listed. Collaborator + * permission floor is still enforced separately before this gate. */ import { findPersonByGithubId, @@ -50,7 +51,7 @@ export function resolvePersonByGitHubActor( /** * Classify a GitHub mention actor for agent execution. * - * - Map off → full (permission floor still enforced separately) + * - Map off / empty → denied (no agent; same as unmapped) * - Map on + id/login in map → full * - Map on + unmapped/missing → denied (no agent turn) */ @@ -61,7 +62,7 @@ export function classifyGitHubActorTrust(input: { readonly people: ReadonlyArray; }): GitHubActorTrustDecision { if (!input.identityMapEnabled) { - return { mode: "full", person: null, reason: "identity_map_disabled" }; + return { mode: "denied", person: null, reason: "identity_map_disabled" }; } const login = input.actorLogin?.trim() ?? ""; const hasId = diff --git a/apps/server/src/jira/jiraActorTrust.test.ts b/apps/server/src/jira/jiraActorTrust.test.ts index b0955aa2a13..e699ac40565 100644 --- a/apps/server/src/jira/jiraActorTrust.test.ts +++ b/apps/server/src/jira/jiraActorTrust.test.ts @@ -47,14 +47,14 @@ describe("resolvePersonByJiraAccountId", () => { }); describe("classifyJiraActorTrust", () => { - it("allows full access when the identity map is disabled", () => { + it("denies agent access when the identity map is disabled (fail-closed)", () => { expect( classifyJiraActorTrust({ identityMapEnabled: false, actorAccountId: "stranger", people: [], }), - ).toEqual({ mode: "full", person: null, reason: "identity_map_disabled" }); + ).toEqual({ mode: "context-only", person: null, reason: "identity_map_disabled" }); }); it("trusts mapped Jira account ids for full agent turns", () => { diff --git a/apps/server/src/jira/jiraActorTrust.ts b/apps/server/src/jira/jiraActorTrust.ts index 454d2026386..9d7808cfe5b 100644 --- a/apps/server/src/jira/jiraActorTrust.ts +++ b/apps/server/src/jira/jiraActorTrust.ts @@ -1,8 +1,8 @@ /** * Jira actor trust relative to the closed-set identity map. * - * When the map is off, all actors keep full agent turns (legacy behaviour). - * When the map is on, only people with a mapped Jira accountId may run the + * Fail-closed: no configured map (or empty map) is treated like an unmapped + * actor — no agent turns. Only people with a mapped Jira accountId may run the * agent; everyone else may only append context to an already-linked thread. */ import { @@ -15,7 +15,7 @@ export type JiraActorTrustMode = "full" | "context-only"; export type JiraActorTrustDecision = { readonly mode: JiraActorTrustMode; - /** Mapped person when trusted; null when map is off or actor is unmapped. */ + /** Mapped person when trusted; null when untrusted. */ readonly person: IdentityMapPerson | null; readonly reason: | "identity_map_disabled" @@ -29,7 +29,7 @@ export { normalizeJiraAccountId, resolvePersonByJiraAccountId }; /** * Classify a Jira mention actor for agent execution. * - * - Map off → full (backward compatible) + * - Map off / empty → context-only (no agent; same as unmapped) * - Map on + accountId in map → full * - Map on + missing/unmapped accountId → context-only */ @@ -39,7 +39,7 @@ export function classifyJiraActorTrust(input: { readonly people: ReadonlyArray; }): JiraActorTrustDecision { if (!input.identityMapEnabled) { - return { mode: "full", person: null, reason: "identity_map_disabled" }; + return { mode: "context-only", person: null, reason: "identity_map_disabled" }; } const normalized = normalizeJiraAccountId(input.actorAccountId); if (normalized === null) {