/**
* Amp Permissions Extension
*
* Reads exec permissions from Amp-format settings and intercepts bash tool calls.
*
* Settings are loaded from (in order, merged):
* ~/.config/amp/settings.json (global)
* .agents/settings.json (project-local)
*
* Relevant settings keys:
*
* "amp.commands.allowlist": ["git", "npm", "./test.sh"]
* Base command names that are auto-allowed (checked before permissions rules).
* Also matched after stripping a leading "cd
&&" prefix.
*
* "amp.permissions": [
* { "tool": "Bash", "matches": { "cmd": "/\\brm\\b/" }, "action": "ask" },
* { "tool": "Bash", "matches": { "cmd": "*" }, "action": "allow" }
* ]
* Ordered rules. First matching Bash rule wins. See lib/permissions-core.ts.
*
* Extension settings (~/.pi/agent/amplike.json):
* { "permissions": { "mode": "enabled" | "yolo" } }
* Persisted by the /permissions command across pi invocations.
*
* The matching rules + built-in defaults live in lib/permissions-core.ts so the
* subagent runner can enforce the identical policy (non-interactively).
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { resolve } from "node:path";
import {
GLOBAL_SETTINGS,
loadAmplikeSettings,
loadSettings,
resolveBashAction,
ruleAppliesToBash,
saveAmplikeSettings,
} from "./lib/permissions-core.js";
// Permission mode: "enabled" (default) or "yolo" (all commands allowed without checks)
// Loaded from amplike.json on startup; persisted on /permissions toggle.
let permissionMode: "enabled" | "yolo" = loadAmplikeSettings().permissions?.mode ?? "enabled";
export default function (pi: ExtensionAPI) {
pi.registerCommand("permissions", {
description: "Toggle permission mode between 'enabled' (amp rules) and 'yolo' (all commands allowed)",
handler: async (_args, ctx) => {
if (permissionMode === "enabled") {
permissionMode = "yolo";
ctx.ui.setStatus("permissions", "YOLO mode");
ctx.ui.notify("Permissions: switched to YOLO mode — all bash commands allowed without checks", "warning");
} else {
permissionMode = "enabled";
ctx.ui.setStatus("permissions", undefined);
ctx.ui.notify("Permissions: switched to enabled mode — amp permission rules active", "info");
}
const current = loadAmplikeSettings();
saveAmplikeSettings({ ...current, permissions: { ...current.permissions, mode: permissionMode } });
},
});
pi.on("session_start", async (_event, ctx) => {
// Restore status bar if yolo mode was persisted from a previous session
if (permissionMode === "yolo") {
ctx.ui.setStatus("permissions", "YOLO mode");
}
// Warn about any non-Bash permission rules in the user's config
const settings = loadSettings([GLOBAL_SETTINGS, resolve(ctx.cwd, ".agents", "settings.json")]);
const nonBashRules = (settings["amp.permissions"] ?? []).filter((r) => !ruleAppliesToBash(r));
if (nonBashRules.length > 0) {
const tools = [...new Set(nonBashRules.map((r) => r.tool))].join(", ");
ctx.ui.notify(
`permissions: ignoring ${nonBashRules.length} non-Bash amp.permissions rule(s) (tools: ${tools})`,
"warning",
);
}
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName !== "bash") return undefined;
// YOLO mode: bypass all permission checks
if (permissionMode === "yolo") return undefined;
const command = event.input.command as string;
const action = resolveBashAction(command, ctx.cwd);
if (action === "allow") return undefined;
if (action === "deny" || action === "reject") {
return { block: true, reason: "Denied by amp permissions" };
}
// action === "ask"
if (!ctx.hasUI) return { block: true, reason: "Command requires confirmation (no UI available)" };
const choice = await ctx.ui.select(
`⚠️ Permission required:\n\n ${command}\n\nAllow? (Use /permissions to toggle YOLO mode and skip these checks)`,
["Yes", "No"],
);
if (choice !== "Yes") {
ctx.abort();
return { block: true, reason: "Blocked by user" };
}
return undefined;
});
}