Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/server/src/github/GitHubPrBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/github/githubActorTrust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ 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,
actorId: 1,
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", () => {
Expand Down
11 changes: 6 additions & 5 deletions apps/server/src/github/githubActorTrust.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
*/
Expand All @@ -61,7 +62,7 @@ export function classifyGitHubActorTrust(input: {
readonly people: ReadonlyArray<IdentityMapPerson>;
}): 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 =
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/jira/jiraActorTrust.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
10 changes: 5 additions & 5 deletions apps/server/src/jira/jiraActorTrust.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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"
Expand All @@ -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
*/
Expand All @@ -39,7 +39,7 @@ export function classifyJiraActorTrust(input: {
readonly people: ReadonlyArray<IdentityMapPerson>;
}): 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) {
Expand Down
Loading