Restructure dash (#372)

* Restructure dash

* WIP

* WIP

* refactor trigger UI: extract PR summary card, add mentions section, rename labels

Co-authored-by: Cursor <cursoragent@cursor.com>

* clean up console UI: remove info icons from section descriptions, rename mentions trigger

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix review feedback: layout, terminology, form scope

- extract console sidebar sections to module-level constant
- align three-column layout breakpoints to xl (match sidebar visibility)
- fix mixed shell/bash terminology in beta page
- scope FormProvider to trigger sections only, restore autoComplete="off"

Co-authored-by: Cursor <cursoragent@cursor.com>

* Bump

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Colin McDonnell
2026-02-23 23:34:29 +00:00
committed by pullfrog[bot]
parent b8a0d799ee
commit a7bd746f21
53 changed files with 672 additions and 672 deletions
+2 -2
View File
@@ -42,7 +42,7 @@ type GitAuthOptions = {
cwd?: string;
// when true, disables hooks during authenticated git operations to prevent
// token exfiltration via malicious hooks reading GIT_CONFIG_PARAMETERS.
// should be true whenever bash is not "enabled" (both restricted and disabled).
// should be true whenever shell is not "enabled" (both restricted and disabled).
restricted?: boolean;
};
@@ -126,7 +126,7 @@ export function $git(
const cwd = options.cwd ?? process.cwd();
// SECURITY: disable hooks during authenticated operations to prevent token exfiltration.
// in restricted mode, agents can write .git/hooks/ via bash; in disabled mode, defense-in-depth.
// in restricted mode, agents can write .git/hooks/ via shell; in disabled mode, defense-in-depth.
if (options.restricted) {
const hasHooksOverride = args.some(
(arg) => arg.toLowerCase().includes("hookspath") || arg.toLowerCase().includes("hooks")
+10 -10
View File
@@ -83,10 +83,10 @@ function buildEventMetadata(event: PayloadEvent): string {
return toonEncode(restWithTrigger);
}
function getShellInstructions(bash: ResolvedPayload["bash"]): string {
const backgroundInstructions = `For long-running processes (dev servers, watchers), use \`bash({ command, background: true })\` which returns a handle. Use \`${ghPullfrogMcpName}/kill_background\` to stop background processes by handle.`;
function getShellInstructions(shell: ResolvedPayload["shell"]): string {
const backgroundInstructions = `For long-running processes (dev servers, watchers), use \`shell({ command, background: true })\` which returns a handle. Use \`${ghPullfrogMcpName}/kill_background\` to stop background processes by handle.`;
switch (bash) {
switch (shell) {
case "disabled":
return `### Shell commands
@@ -94,13 +94,13 @@ Shell command execution is DISABLED. Do not attempt to run shell commands.`;
case "restricted":
return `### Shell commands
Use the \`${ghPullfrogMcpName}/bash\` MCP tool for all shell command execution. This tool provides a secure environment with filtered credentials. Do NOT use any native shell/bash tool - it is disabled for security. ${backgroundInstructions}`;
Use the \`${ghPullfrogMcpName}/shell\` MCP tool for all shell command execution. This tool provides a secure environment with filtered credentials. Do NOT use any native shell tool - it is disabled for security. ${backgroundInstructions}`;
case "enabled":
return `### Shell commands
Use your native bash/shell tool for shell command execution. ${backgroundInstructions}`;
Use your native shell tool for shell command execution. ${backgroundInstructions}`;
default: {
const _exhaustive: never = bash;
const _exhaustive: never = shell;
return _exhaustive satisfies never;
}
}
@@ -130,7 +130,7 @@ You are running as a step in a user-defined CI workflow. When you complete your
// shared system prompt body used by both orchestrator and subagent instructions.
// the priority order and YOUR TASK section differ — callers compose those separately.
interface SystemPromptContext {
bash: ResolvedPayload["bash"];
shell: ResolvedPayload["shell"];
trigger: string;
priorityOrder: string;
taskSection: string;
@@ -188,7 +188,7 @@ Rules:
Use MCP tools from ${ghPullfrogMcpName} for all GitHub operations. Never use the \`gh\` CLI — it is not authenticated and will fail. The MCP tools handle authentication, enforce permissions, and integrate with the delegation system.
${getShellInstructions(ctx.bash)}
${getShellInstructions(ctx.shell)}
${getFileInstructions()}
@@ -202,7 +202,7 @@ Trust the tools — do not repeatedly verify file contents or git status after o
### Command execution
Never use \`sleep\` to wait for commands to complete. Commands run synchronously — when the bash tool returns, the command has finished.
Never use \`sleep\` to wait for commands to complete. Commands run synchronously — when the shell tool returns, the command has finished.
### Commenting style
@@ -399,7 +399,7 @@ When all delegations are complete, call \`${ghPullfrogMcpName}/set_output\` with
If the task clearly requires no work (e.g., irrelevant event, duplicate request), skip delegation entirely. Call \`${ghPullfrogMcpName}/report_progress\` directly to explain why no action is needed.`;
const system = buildSystemPrompt({
bash: ctx.payload.bash,
shell: ctx.payload.shell,
trigger: ctx.payload.event.trigger,
priorityOrder: orchestratorPriorityOrder,
taskSection: orchestratorTaskSection,
+5 -5
View File
@@ -17,10 +17,10 @@ describe("Inputs schema", () => {
["push", "enabled"],
["push", "disabled"],
["push", undefined],
["bash", "enabled"],
["bash", "restricted"],
["bash", "disabled"],
["bash", undefined],
["shell", "enabled"],
["shell", "restricted"],
["shell", "disabled"],
["shell", undefined],
["effort", "mini"],
["effort", "auto"],
["effort", "max"],
@@ -39,7 +39,7 @@ describe("Inputs schema", () => {
expect(() => Inputs.assert(input)).not.toThrow();
});
it.each([["web"], ["search"], ["push"], ["bash"], ["effort"], ["agent"]] as const)(
it.each([["web"], ["search"], ["push"], ["shell"], ["effort"], ["agent"]] as const)(
"should reject invalid %s values",
(prop) => {
const input = { prompt: "test", [prop]: "invalid" as any };
+15 -15
View File
@@ -8,7 +8,7 @@ import { validateCompatibility } from "./versioning.ts";
// tool permission enum types for inputs
const ToolPermissionInput = type.enumerated("disabled", "enabled");
const BashPermissionInput = type.enumerated("disabled", "restricted", "enabled");
const ShellPermissionInput = type.enumerated("disabled", "restricted", "enabled");
const PushPermissionInput = type.enumerated("disabled", "restricted", "enabled");
// schema for JSON payload passed via prompt (internal dispatch invocation)
@@ -51,7 +51,7 @@ export const Inputs = type({
"web?": ToolPermissionInput.or("undefined"),
"search?": ToolPermissionInput.or("undefined"),
"push?": PushPermissionInput.or("undefined"),
"bash?": BashPermissionInput.or("undefined"),
"shell?": ShellPermissionInput.or("undefined"),
"cwd?": type.string.or("undefined"),
});
@@ -105,7 +105,7 @@ function resolveNonPromptInputs() {
web: core.getInput("web") || undefined,
search: core.getInput("search") || undefined,
push: core.getInput("push") || undefined,
bash: core.getInput("bash") || undefined,
shell: core.getInput("shell") || undefined,
});
}
@@ -138,26 +138,26 @@ export function resolvePayload(
const resolvedAgent: AgentName | undefined =
agent ?? (jsonAgent !== undefined && isAgentName(jsonAgent) ? jsonAgent : undefined);
// determine bash permission - strictest setting wins
// determine shell 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;
const repoShell = repoSettings.shell ?? "restricted";
const inputShell = inputs.shell;
// resolve bash: start with repo setting, then apply restrictions
let resolvedBash = repoBash;
// resolve shell: start with repo setting, then apply restrictions
let resolvedShell = repoShell;
// input can only make it stricter (disabled > restricted > enabled)
if (inputBash === "disabled") {
resolvedBash = "disabled";
} else if (inputBash === "restricted" && resolvedBash === "enabled") {
resolvedBash = "restricted";
if (inputShell === "disabled") {
resolvedShell = "disabled";
} else if (inputShell === "restricted" && resolvedShell === "enabled") {
resolvedShell = "restricted";
}
// non-collaborators get at least "restricted" (can't have "enabled")
if (isNonCollaborator && resolvedBash === "enabled") {
resolvedBash = "restricted";
if (isNonCollaborator && resolvedShell === "enabled") {
resolvedShell = "restricted";
}
// build payload - precedence: inputs > repoSettings > fallbacks
@@ -183,7 +183,7 @@ export function resolvePayload(
web: inputs.web ?? repoSettings.web ?? "enabled",
search: inputs.search ?? repoSettings.search ?? "enabled",
push: inputs.push ?? repoSettings.push ?? "restricted",
bash: resolvedBash,
shell: resolvedShell,
};
}
+3 -3
View File
@@ -1,4 +1,4 @@
import type { AgentName, BashPermission, PushPermission, ToolPermission } from "../external.ts";
import type { AgentName, PushPermission, ShellPermission, ToolPermission } from "../external.ts";
import { apiFetch } from "./apiFetch.ts";
import type { RepoContext } from "./github.ts";
@@ -18,7 +18,7 @@ export interface RepoSettings {
web: ToolPermission;
search: ToolPermission;
push: PushPermission;
bash: BashPermission;
shell: ShellPermission;
}
export interface RunContext {
@@ -35,7 +35,7 @@ const defaultSettings: RepoSettings = {
web: "enabled",
search: "enabled",
push: "restricted",
bash: "restricted",
shell: "restricted",
};
const defaultRunContext: RunContext = {
+10 -10
View File
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { BashPermission, PayloadEvent } from "../external.ts";
import type { PayloadEvent, ShellPermission } from "../external.ts";
import { checkoutPrBranch } from "../mcp/checkout.ts";
import type { ToolState } from "../mcp/server.ts";
import { log } from "./cli.ts";
@@ -51,11 +51,11 @@ export interface GitContext {
name: string;
octokit: OctokitWithPlugins;
toolState: ToolState;
// bash permission level — controls hook and security behavior:
// enabled: full bash, hooks run, no restrictions
// restricted: MCP bash in stripped env, hooks run, token protection on auth ops
// disabled: no bash, hooks disabled globally, all code execution paths blocked
bash: BashPermission;
// shell permission level — controls hook and security behavior:
// enabled: full shell, hooks run, no restrictions
// restricted: MCP shell in stripped env, hooks run, token protection on auth ops
// disabled: no shell, hooks disabled globally, all code execution paths blocked
shell: ShellPermission;
postCheckoutScript: string | null;
}
@@ -107,16 +107,16 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
log.debug(`» git user already configured (${currentEmail}), skipping`);
}
// SECURITY: disable git hooks when bash is disabled to prevent code execution.
// SECURITY: disable git hooks when shell is disabled to prevent code execution.
// in restricted mode, hooks run in the stripped sandbox — that's fine.
// in enabled mode, the agent has full bash anyway.
// in enabled mode, the agent has full shell anyway.
// in disabled mode, hooks are the primary code-execution escape vector.
if (params.bash === "disabled") {
if (params.shell === "disabled") {
execSync("git config --local core.hooksPath /dev/null", {
cwd: repoDir,
stdio: "pipe",
});
log.debug("» git hooks disabled (bash=disabled)");
log.debug("» git hooks disabled (shell=disabled)");
}
} catch (error) {
// If git config fails, log warning but don't fail the action