{
	"$schema": "http://json-schema.org/draft-07/schema#",
	"$id": "https://raw.githubusercontent.com/gvanderclay/pi-bouncer/main/schema/bouncer.schema.json",
	"title": "pi-bouncer config",
	"description": "The user config (bouncer.json in the Pi agent dir) or a project config (.pi/extensions/bouncer/config.json). A project config overrides it entry by entry. An untrusted project may only make it stricter: raise levels, add protect paths, and add ask or deny rules. log, auto, mcp, startMode and trashCommand come from the user config only.",
	"type": "object",
	"additionalProperties": false,
	"properties": {
		"$schema": {
			"type": "string"
		},
		"levels": {
			"$ref": "#/definitions/levels"
		},
		"log": {
			"type": "object",
			"additionalProperties": false,
			"properties": {
				"rotateAboveMiB": {
					"type": "number",
					"exclusiveMinimum": 0,
					"default": 5,
					"description": "Rotate the log above this size, in MiB."
				},
				"generations": {
					"type": "integer",
					"minimum": 0,
					"default": 5,
					"description": "How many rotated logs to keep."
				},
				"maxAgeDays": {
					"type": "integer",
					"minimum": 1,
					"description": "Delete rotated logs older than this. Absent: never by age."
				}
			}
		},
		"rules": {
			"$ref": "#/definitions/rules"
		},
		"protect": {
			"$ref": "#/definitions/protect"
		},
		"trashCommand": {
			"type": "string",
			"pattern": "^\\S+$",
			"description": "The trash program a refused recursive rm suggests, as a name or path with no spaces, instead of the first of trash, trash-put or gio trash found on PATH. trash-root also covers it. User config only."
		},
		"trustAgentMade": {
			"type": "boolean",
			"default": true,
			"description": "A recursive rm of a directory the agent made this session (mkdir of a missing path, mktemp -d, or a recursive cp into a missing path) runs without asking. false asks as before."
		},
		"startMode": {
			"enum": ["off", "auto"],
			"default": "off",
			"description": "The mode a session starts in. YOLO mode starts only with pi --yolo or /yolo."
		},
		"auto": {
			"type": "object",
			"additionalProperties": false,
			"required": ["models"],
			"description": "Auto mode: a model judges each ask.",
			"properties": {
				"models": {
					"type": "array",
					"minItems": 1,
					"items": {
						"type": "string",
						"pattern": "^[^/\\s]+/\\S+$",
						"description": "A model as provider/id."
					},
					"description": "The judge list, in priority order."
				},
				"alwaysAsk": {
					"type": "array",
					"items": {
						"type": "string",
						"pattern": "\\S"
					},
					"description": "Command prefixes auto mode always asks about."
				},
				"environment": {
					"type": "array",
					"items": {
						"type": "string",
						"pattern": "\\S"
					},
					"description": "Facts about your environment the judge is told."
				},
				"firstByProvider": {
					"type": "object",
					"additionalProperties": {
						"type": "string",
						"pattern": "^[^/\\s]+/\\S+$",
						"description": "A model as provider/id."
					},
					"description": "The judge to try first when the session model is from this provider. Each value must be in models."
				},
				"jev": {
					"type": "object",
					"additionalProperties": false,
					"description": "A classifier that settles clear cases before the judge list.",
					"properties": {
						"allowAt": {
							"type": "number",
							"exclusiveMinimum": 0.5,
							"maximum": 1,
							"default": 0.9
						},
						"allowFrom": {
							"enum": ["deny-score", "safety"],
							"default": "deny-score",
							"description": "The number allowAt applies to: 1 minus the deny score, or the safety question's probability."
						},
						"denyAt": {
							"type": ["number", "null"],
							"exclusiveMinimum": 0.5,
							"maximum": 1,
							"default": null
						},
						"model": {
							"type": "string",
							"pattern": "^[^/\\s]+/\\S+$",
							"description": "A model as provider/id."
						}
					}
				}
			}
		},
		"mcp": {
			"type": "object",
			"additionalProperties": false,
			"description": "MCP tool calls. A call to a tool on the allow list runs. Any other MCP call asks, and blocks when no one can answer; in auto mode the judge rules on it first, unless judge is false. User config only.",
			"properties": {
				"allow": {
					"type": "array",
					"items": { "type": "string", "pattern": "^mcp__" },
					"description": "Tool names as Pi registers them, mcp__<server>__<tool>; * matches any characters, so mcp__github__* allows a whole server. Characters other than letters, digits and _ match as _, as in Pi's tool names."
				},
				"judge": {
					"type": "boolean",
					"description": "false: in auto mode an MCP call not on the allow list opens the dialog instead of going to the judge. Default true; the judge then gets the tool name, its description and its arguments."
				}
			}
		},
		"profiles": {
			"type": "object",
			"description": "Profiles by name. A session whose agent name the agents map points at one runs that profile on top of the normal rules.",
			"propertyNames": {
				"pattern": "^[a-z0-9][a-z0-9-]*$"
			},
			"additionalProperties": {
				"$ref": "#/definitions/profile"
			}
		},
		"agents": {
			"type": "object",
			"description": "Agent name to profile name. The agent name comes from PI_BOUNCER_AGENT, PI_SUBAGENT_AGENT or PI_DADDY_DEFINITION.",
			"additionalProperties": {
				"type": "string"
			}
		}
	},
	"definitions": {
		"levels": {
			"type": "object",
			"description": "The level of a built-in rule: ask, deny or off. Rules in the always-deny set (rm-root, disk-format, dd-device, power, privilege) cannot be off. A custom rule's level is set in its definition under rules.",
			"additionalProperties": false,
			"properties": {
				"rm-root": {
					"enum": ["ask", "deny"],
					"description": "recursive rm of the filesystem root, a system directory or your home directory. Built-in level: deny."
				},
				"trash-root": {
					"enum": ["ask", "deny", "off"],
					"description": "moving the filesystem root, a system directory or your home directory to the trash (trash, trash-put, gio trash, or trashCommand). Built-in level: deny."
				},
				"recursive-rm": {
					"enum": ["ask", "deny", "off"],
					"description": "recursive rm deletes whole directory trees. Built-in level: ask."
				},
				"find-delete": {
					"enum": ["ask", "deny", "off"],
					"description": "find -delete deletes every file it matches. Built-in level: ask."
				},
				"find-exec": {
					"enum": ["ask", "deny", "off"],
					"description": "find -exec, -execdir, -ok and -okdir run arbitrary commands. Built-in level: ask."
				},
				"fd-exec": {
					"enum": ["ask", "deny", "off"],
					"description": "fd -x and -X run arbitrary commands. Built-in level: ask."
				},
				"rg-pre": {
					"enum": ["ask", "deny", "off"],
					"description": "rg --pre runs an arbitrary preprocessor command. Built-in level: ask."
				},
				"opaque-exec": {
					"enum": ["ask", "deny", "off"],
					"description": "parallel, rush and rust-parallel run commands built from templates, so what runs cannot be judged. Built-in level: ask."
				},
				"disk-format": {
					"enum": ["ask", "deny"],
					"description": "formatting, erasing or repartitioning a disk destroys its data. Built-in level: deny."
				},
				"dd-device": {
					"enum": ["ask", "deny"],
					"description": "dd writing to a /dev path overwrites a raw device. Built-in level: deny."
				},
				"power": {
					"enum": ["ask", "deny"],
					"description": "shutting down or rebooting takes the user's machine down. Built-in level: deny."
				},
				"privilege": {
					"enum": ["ask", "deny"],
					"description": "the agent must not run anything with elevated privileges. Built-in level: deny."
				},
				"git-clean": {
					"enum": ["ask", "deny", "off"],
					"description": "git clean deletes untracked files for good. Built-in level: ask."
				},
				"git-reset-hard": {
					"enum": ["ask", "deny", "off"],
					"description": "git reset --hard discards uncommitted changes. Built-in level: ask."
				},
				"git-checkout-discard": {
					"enum": ["ask", "deny", "off"],
					"description": "checking out paths or forcing a checkout discards working-tree changes. Built-in level: ask."
				},
				"git-restore-worktree": {
					"enum": ["ask", "deny", "off"],
					"description": "git restore of the working tree discards uncommitted changes. Built-in level: ask."
				},
				"git-stash-destroy": {
					"enum": ["ask", "deny", "off"],
					"description": "git stash drop and clear destroy stashed work. Built-in level: ask."
				},
				"git-push-force": {
					"enum": ["ask", "deny", "off"],
					"description": "a force or mirror push overwrites history on the remote. Built-in level: ask."
				},
				"git-push-delete": {
					"enum": ["ask", "deny", "off"],
					"description": "deleting or pruning remote refs destroys branches and tags on the remote. Built-in level: ask."
				},
				"remote-script": {
					"enum": ["ask", "deny", "off"],
					"description": "a downloaded script fed into a shell or interpreter runs unreviewed code. Built-in level: ask."
				},
				"publish": {
					"enum": ["ask", "deny", "off"],
					"description": "publishing releases a package publicly, which cannot be undone. Built-in level: ask."
				},
				"gh-delete": {
					"enum": ["ask", "deny", "off"],
					"description": "deleting a GitHub repository or release destroys it for everyone. Built-in level: ask."
				},
				"bouncer-escape": {
					"enum": ["ask", "deny", "off"],
					"description": "starting pi with --yolo, or setting or clearing an agent variable (PI_BOUNCER_AGENT, PI_SUBAGENT_AGENT, PI_DADDY_DEFINITION), from bash sidesteps the user's rules. Built-in level: deny."
				}
			}
		},
		"rules": {
			"type": "array",
			"description": "Custom rules. Each matches a program and, optionally, words that follow it in order. A project config that is not trusted may not add steer rules (those with instead).",
			"items": {
				"type": "object",
				"additionalProperties": false,
				"required": ["name", "command", "summary"],
				"properties": {
					"name": {
						"type": "string",
						"pattern": "^[a-z0-9][a-z0-9-]*$",
						"not": {
							"enum": [
								"always-ask",
								"parser-unavailable",
								"unparseable",
								"inline-too-deep",
								"rm-root",
								"trash-root",
								"recursive-rm",
								"find-delete",
								"find-exec",
								"fd-exec",
								"rg-pre",
								"opaque-exec",
								"disk-format",
								"dd-device",
								"power",
								"privilege",
								"git-clean",
								"git-reset-hard",
								"git-checkout-discard",
								"git-restore-worktree",
								"git-stash-destroy",
								"git-push-force",
								"git-push-delete",
								"remote-script",
								"publish",
								"gh-delete",
								"bouncer-escape"
							]
						},
						"description": "Shown in messages, dialogs and the log. Lowercase letters, digits and dashes; not a built-in rule's name."
					},
					"command": {
						"description": "A program name, or a list of them. /usr/bin/grep matches grep.",
						"oneOf": [
							{
								"type": "string",
								"pattern": "\\S"
							},
							{
								"type": "array",
								"minItems": 1,
								"items": {
									"type": "string",
									"pattern": "\\S"
								}
							}
						]
					},
					"args": {
						"type": "array",
						"items": {
							"type": "string",
							"pattern": "\\S"
						},
						"description": "Words that must all follow the program, in this order, not necessarily next to each other."
					},
					"level": {
						"enum": ["ask", "deny", "off"],
						"description": "Default: ask, or deny for a steer rule. A steer rule is deny or off."
					},
					"summary": {
						"type": "string",
						"pattern": "\\S",
						"description": "Why the rule exists, in a few words; shown in the deny and the dialog."
					},
					"instead": {
						"type": "string",
						"pattern": "\\S",
						"description": "Makes this a steer rule: denied in every mode, with this text telling the model what to do instead."
					}
				},
				"if": {
					"properties": {
						"instead": true
					},
					"required": ["instead"]
				},
				"then": {
					"properties": {
						"level": {
							"enum": ["deny", "off"]
						}
					}
				}
			}
		},
		"protect": {
			"type": "object",
			"description": "More paths for rm-root and trash-root to deny. Add-only: the built-in paths always stay, and a project config may add too, trusted or not.",
			"additionalProperties": false,
			"properties": {
				"home": {
					"type": "array",
					"description": "Folders below your home directory, such as code for ~/code. The folder itself is protected, not what is inside it.",
					"items": {
						"type": "string",
						"pattern": "^(?![/~])(?!(?:.*/)?\\.\\.(?:/|$))(?=.*\\S)"
					}
				},
				"paths": {
					"type": "array",
					"description": "Absolute paths, such as /srv/data. The path and its direct children are protected.",
					"items": {
						"type": "string",
						"pattern": "^/"
					}
				}
			}
		},
		"profile": {
			"type": "object",
			"description": "A named group of rule changes that starts from the session's normal rules. levels, rules and protect are checked as at the top level; mode replaces startMode and applies only when the session has no --auto or --yolo flag.",
			"additionalProperties": false,
			"properties": {
				"levels": {
					"$ref": "#/definitions/levels"
				},
				"rules": {
					"$ref": "#/definitions/rules"
				},
				"protect": {
					"$ref": "#/definitions/protect"
				},
				"mode": {
					"enum": ["off", "auto"],
					"description": "The mode a session in this profile starts in. An untrusted project's profile may set only off."
				}
			}
		}
	}
}
