Drop permissions from webhook payload, fix potential vuln, simplify dispatch options

This commit is contained in:
Colin McDonnell
2026-01-16 22:22:18 +00:00
committed by pullfrog[bot]
parent 93d74a9bea
commit b64721edcf
3 changed files with 39 additions and 32 deletions
+13 -10
View File
@@ -138179,12 +138179,13 @@ var JsonPayload = type({
"agent?": AgentName.or("null"), "agent?": AgentName.or("null"),
"prompt?": "string", "prompt?": "string",
"event?": "object", "event?": "object",
"effort?": Effort, "effort?": Effort
"web?": ToolPermissionInput,
"search?": ToolPermissionInput,
"write?": ToolPermissionInput,
"bash?": BashPermissionInput
}); });
var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
function isCollaborator(event) {
const perm = event.authorPermission;
return perm !== void 0 && COLLABORATOR_PERMISSIONS.includes(perm);
}
var Inputs = type({ var Inputs = type({
prompt: "string", prompt: "string",
"effort?": Effort, "effort?": Effort,
@@ -138234,6 +138235,7 @@ function resolvePayload(repoSettings) {
const event = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" }; const event = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" };
const jsonAgent = jsonPayload?.agent; const jsonAgent = jsonPayload?.agent;
const resolvedAgent = agent2 ?? (jsonAgent !== void 0 && jsonAgent !== "null" && isAgentName(jsonAgent) ? jsonAgent : null); const resolvedAgent = agent2 ?? (jsonAgent !== void 0 && jsonAgent !== "null" && isAgentName(jsonAgent) ? jsonAgent : null);
const shouldRestrict = !isCollaborator(event);
return { return {
"~pullfrog": true, "~pullfrog": true,
agent: resolvedAgent, agent: resolvedAgent,
@@ -138243,11 +138245,12 @@ function resolvePayload(repoSettings) {
event, event,
effort: inputs.effort ?? jsonPayload?.effort ?? "auto", effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
cwd: resolveCwd(inputs.cwd), cwd: resolveCwd(inputs.cwd),
// permissions: inputs > jsonPayload > repoSettings > fallbacks // permissions: inputs > repoSettings > fallbacks
web: inputs.web ?? jsonPayload?.web ?? repoSettings.web ?? "enabled", // bash is restricted for non-collaborators regardless of repoSettings
search: inputs.search ?? jsonPayload?.search ?? repoSettings.search ?? "enabled", web: inputs.web ?? repoSettings.web ?? "enabled",
write: inputs.write ?? jsonPayload?.write ?? repoSettings.write ?? "enabled", search: inputs.search ?? repoSettings.search ?? "enabled",
bash: inputs.bash ?? jsonPayload?.bash ?? repoSettings.bash ?? "restricted" write: inputs.write ?? repoSettings.write ?? "enabled",
bash: inputs.bash ?? (shouldRestrict ? "restricted" : repoSettings.bash) ?? "restricted"
}; };
} }
+3 -12
View File
@@ -93,6 +93,8 @@ interface BasePayloadEvent {
comment_ids?: number[] | "all"; comment_ids?: number[] | "all";
/** permission level of the user who triggered this event */ /** permission level of the user who triggered this event */
authorPermission?: AuthorPermission; authorPermission?: AuthorPermission;
/** when true, runs silently without progress comments (e.g., auto-labeling) */
silent?: boolean;
[key: string]: any; [key: string]: any;
} }
@@ -239,19 +241,8 @@ export type PayloadEvent =
| ImplementPlanEvent | ImplementPlanEvent
| UnknownEvent; | UnknownEvent;
// | undefined needed on optional props for exactOptionalPropertyTypes
export interface DispatchOptions {
/** when true, disables progress comment (no "leaping into action" comment, no report_progress tool) */
disableProgressComment?: true | undefined;
/** granular tool permissions set server-side for dispatch workflows */
web?: ToolPermission | undefined;
search?: ToolPermission | undefined;
write?: ToolPermission | undefined;
bash?: BashPermission | undefined;
}
// writeable payload type for building payloads // writeable payload type for building payloads
export interface WriteablePayload extends DispatchOptions { export interface WriteablePayload {
"~pullfrog": true; "~pullfrog": true;
/** agent slug identifier (e.g., "claude", "codex", "gemini") */ /** agent slug identifier (e.g., "claude", "codex", "gemini") */
agent: AgentName | null; agent: AgentName | null;
+23 -10
View File
@@ -4,6 +4,7 @@ import { type } from "arktype";
import { import {
AgentName, AgentName,
type AgentName as AgentNameType, type AgentName as AgentNameType,
type AuthorPermission,
Effort, Effort,
type PayloadEvent, type PayloadEvent,
} from "../external.ts"; } from "../external.ts";
@@ -14,18 +15,25 @@ const ToolPermissionInput = type.enumerated("disabled", "enabled");
const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled"); const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled");
// schema for JSON payload passed via prompt (internal dispatch invocation) // schema for JSON payload passed via prompt (internal dispatch invocation)
// note: permissions are intentionally NOT included here to prevent injection attacks
// permissions are derived from event.authorPermission instead
const JsonPayload = type({ const JsonPayload = type({
"~pullfrog": "true", "~pullfrog": "true",
"agent?": AgentName.or("null"), "agent?": AgentName.or("null"),
"prompt?": "string", "prompt?": "string",
"event?": "object", "event?": "object",
"effort?": Effort, "effort?": Effort,
"web?": ToolPermissionInput,
"search?": ToolPermissionInput,
"write?": ToolPermissionInput,
"bash?": BashPermissionInput,
}); });
// permission levels that indicate collaborator status (have push access)
const COLLABORATOR_PERMISSIONS: AuthorPermission[] = ["admin", "maintain", "write"];
// check if the event author has collaborator-level permissions
function isCollaborator(event: PayloadEvent): boolean {
const perm = event.authorPermission;
return perm !== undefined && COLLABORATOR_PERMISSIONS.includes(perm);
}
// inputs schema - action inputs from core.getInput() // inputs schema - action inputs from core.getInput()
// note: tool permissions use .or("undefined") because getInput() || undefined // note: tool permissions use .or("undefined") because getInput() || undefined
// explicitly sets the property to undefined when empty, which is different from // explicitly sets the property to undefined when empty, which is different from
@@ -103,7 +111,11 @@ export function resolvePayload(repoSettings: RepoSettings) {
agent ?? agent ??
(jsonAgent !== undefined && jsonAgent !== "null" && isAgentName(jsonAgent) ? jsonAgent : null); (jsonAgent !== undefined && jsonAgent !== "null" && isAgentName(jsonAgent) ? jsonAgent : null);
// build payload - precedence: inputs > jsonPayload > repoSettings > fallbacks // determine if permissions should be restricted based on event author
// non-collaborators (read, triage, none, or missing) get restricted bash access
const shouldRestrict = !isCollaborator(event);
// build payload - precedence: inputs > repoSettings > fallbacks
// note: modes are NOT in payload - they come from repoSettings in main() // note: modes are NOT in payload - they come from repoSettings in main()
return { return {
"~pullfrog": true as const, "~pullfrog": true as const,
@@ -115,11 +127,12 @@ export function resolvePayload(repoSettings: RepoSettings) {
effort: inputs.effort ?? jsonPayload?.effort ?? "auto", effort: inputs.effort ?? jsonPayload?.effort ?? "auto",
cwd: resolveCwd(inputs.cwd), cwd: resolveCwd(inputs.cwd),
// permissions: inputs > jsonPayload > repoSettings > fallbacks // permissions: inputs > repoSettings > fallbacks
web: inputs.web ?? jsonPayload?.web ?? repoSettings.web ?? "enabled", // bash is restricted for non-collaborators regardless of repoSettings
search: inputs.search ?? jsonPayload?.search ?? repoSettings.search ?? "enabled", web: inputs.web ?? repoSettings.web ?? "enabled",
write: inputs.write ?? jsonPayload?.write ?? repoSettings.write ?? "enabled", search: inputs.search ?? repoSettings.search ?? "enabled",
bash: inputs.bash ?? jsonPayload?.bash ?? repoSettings.bash ?? "restricted", write: inputs.write ?? repoSettings.write ?? "enabled",
bash: inputs.bash ?? (shouldRestrict ? "restricted" : repoSettings.bash) ?? "restricted",
}; };
} }