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:
committed by
pullfrog[bot]
parent
b8a0d799ee
commit
a7bd746f21
+2
-2
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user