import { isAbsolute, resolve } from "node:path"; import * as core from "@actions/core"; import { type } from "arktype"; import { AgentName, type AuthorPermission, Effort, type PayloadEvent } from "../external.ts"; import packageJson from "../package.json" with { type: "json" }; import type { RepoSettings } from "./repoSettings.ts"; import { validateCompatibility } from "./versioning.ts"; // tool permission enum types for inputs const ToolPermissionInput = type.enumerated("disabled", "enabled"); const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled"); // 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 export const JsonPayload = type({ "~pullfrog": "true", version: "string", "agent?": AgentName.or("undefined"), "prompt?": "string", "eventInstructions?": "string", "repoInstructions?": "string", "event?": "object", "effort?": Effort.or("undefined"), }); // 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() // note: tool permissions use .or("undefined") because getInput() || undefined // explicitly sets the property to undefined when empty, which is different from // the property being absent. arktype's "prop?" means "optional to include" but // if included, must match the type - so we need to explicitly allow undefined. export const Inputs = type({ prompt: "string", "effort?": Effort.or("undefined"), "agent?": AgentName.or("undefined"), "web?": ToolPermissionInput.or("undefined"), "search?": ToolPermissionInput.or("undefined"), "write?": ToolPermissionInput.or("undefined"), "bash?": BashPermissionInput.or("undefined"), "cwd?": type.string.or("undefined"), }); export type Inputs = typeof Inputs.infer; function isAgentName(value: unknown): value is AgentName { return typeof value === "string" && AgentName(value) instanceof type.errors === false; } function isPayloadEvent(value: unknown): value is PayloadEvent { return typeof value === "object" && value !== null && "trigger" in value; } function resolveCwd(cwd: string | undefined): string | undefined { const workspace = process.env.GITHUB_WORKSPACE; if (!cwd) return workspace; if (isAbsolute(cwd)) return cwd; return workspace ? resolve(workspace, cwd) : cwd; } export function resolvePayload(repoSettings: RepoSettings) { const inputs = Inputs.assert({ prompt: core.getInput("prompt", { required: true }), effort: core.getInput("effort") || undefined, agent: core.getInput("agent") || undefined, cwd: core.getInput("cwd") || undefined, web: core.getInput("web") || undefined, search: core.getInput("search") || undefined, write: core.getInput("write") || undefined, bash: core.getInput("bash") || undefined, }); // validate agent name const agent: AgentName | undefined = inputs.agent !== undefined && isAgentName(inputs.agent) ? inputs.agent : undefined; // try to parse prompt as JSON payload (internal invocation) let jsonPayload: typeof JsonPayload.infer | null = null; try { const parsed = JSON.parse(inputs.prompt); // if it looks like a pullfrog payload but fails validation, that's an error if (parsed && typeof parsed === "object" && "~pullfrog" in parsed) { jsonPayload = JsonPayload.assert(parsed); } } catch (error) { // JSON parse error is fine (plain text prompt), but validation error should propagate if (error instanceof type.errors) { throw new Error(`invalid pullfrog payload: ${error.summary}`); } // not JSON, treat as plain string prompt } // validate version compatibility from jsonPayload if (jsonPayload) validateCompatibility(jsonPayload.version, packageJson.version); // resolve event - use type guard for jsonPayload.event, fallback to unknown trigger const rawEvent = jsonPayload?.event; const event: PayloadEvent = isPayloadEvent(rawEvent) ? rawEvent : { trigger: "unknown" }; // resolve agent from jsonPayload with type guard const jsonAgent = jsonPayload?.agent; const resolvedAgent: AgentName | undefined = agent ?? (jsonAgent !== undefined && isAgentName(jsonAgent) ? jsonAgent : undefined); // determine bash permission - strictest setting wins // precedence: disabled > restricted > enabled // non-collaborators always get at least "restricted" const isNonCollaborator = !isCollaborator(event); const repoBash = repoSettings.bash ?? "restricted"; const inputBash = inputs.bash; // resolve bash: start with repo setting, then apply restrictions let resolvedBash = repoBash; // input can only make it stricter (disabled > restricted > enabled) if (inputBash === "disabled") { resolvedBash = "disabled"; } else if (inputBash === "restricted" && resolvedBash === "enabled") { resolvedBash = "restricted"; } // non-collaborators get at least "restricted" (can't have "enabled") if (isNonCollaborator && resolvedBash === "enabled") { resolvedBash = "restricted"; } // build payload - precedence: inputs > repoSettings > fallbacks // note: modes are NOT in payload - they come from repoSettings in main() return { "~pullfrog": true as const, version: jsonPayload?.version ?? packageJson.version, agent: resolvedAgent, // inverted: jsonPayload.prompt extracts the text from the JSON payload, // whereas inputs.prompt IS the raw JSON string when internally dispatched prompt: jsonPayload?.prompt ?? inputs.prompt, eventInstructions: jsonPayload?.eventInstructions, repoInstructions: jsonPayload?.repoInstructions, event, effort: inputs.effort ?? jsonPayload?.effort ?? "auto", cwd: resolveCwd(inputs.cwd), // permissions: inputs > repoSettings > fallbacks web: inputs.web ?? repoSettings.web ?? "enabled", search: inputs.search ?? repoSettings.search ?? "enabled", write: inputs.write ?? repoSettings.write ?? "enabled", bash: resolvedBash, }; } export type ResolvedPayload = ReturnType;