{
	"$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. A path group also takes an object with a read and/or a write level. Rules in the always-deny set (rm-root, disk-format, dd-device, power, privilege, path-bouncer) 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."
				},
				"path-bouncer": {
					"oneOf": [
						{ "enum": ["ask", "deny"] },
						{ "$ref": "#/definitions/bouncerLevels" }
					],
					"description": "path group: the bouncer's config (<agent dir>/bouncer.json, any .pi/extensions/bouncer/config.json), the folder the bouncer was loaded from, and Pi's settings.json (<agent dir>/settings.json, any .pi/settings.json) decide what the bouncer guards. In the always-deny set: denied in YOLO and auto mode, and never off. Sets the write level of Pi's file tools; reads stay allowed. Built-in levels: read allow, write deny."
				},
				"path-system": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: system folders such as /etc, /usr and /Library change software for the whole machine. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read allow, write ask."
				},
				"path-git": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: any .git folder holds the repository's config, hooks and history. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read allow, write ask."
				},
				"path-shell-rc": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: shell startup files (.zshrc, .bashrc, .profile and the like, and ~/.config/fish/config.fish) run on every new shell. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read allow, write ask."
				},
				"path-keys": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: private keys and certificates (*.pem, *.key, id_rsa*, id_ed25519*, except *.pub) anywhere on disk. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-ssh": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.ssh holds SSH keys and their settings. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-gnupg": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.gnupg holds GPG keys and its trust database. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-aws": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.aws holds AWS credentials and settings. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-gcloud": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.config/gcloud holds Google Cloud credentials. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-azure": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.azure holds Azure credentials. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-kube": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.kube holds Kubernetes cluster credentials. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-docker": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: ~/.docker/config.json holds registry logins. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-pi-auth": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: Pi's auth.json in the agent dir holds the credentials Pi uses for you. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write deny."
				},
				"path-env": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: .env files hold a project's secrets, except .env.example, .env.sample and .env.template. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write ask."
				},
				"path-npmrc": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: .npmrc holds registry tokens. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write ask."
				},
				"path-pypirc": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: .pypirc holds package registry tokens. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write ask."
				},
				"path-netrc": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: .netrc holds server logins. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write ask."
				},
				"path-git-credentials": {
					"oneOf": [
						{ "enum": ["ask", "deny", "off"] },
						{ "$ref": "#/definitions/pathLevels" }
					],
					"description": "path group: .git-credentials holds saved git logins. Sets both directions of Pi's file tools, or the directions an object names. Built-in levels: read deny, write ask."
				}
			}
		},
		"pathLevels": {
			"type": "object",
			"description": "The read and/or write level of a path group. A direction left out keeps the group's built-in level; a plain level sets both.",
			"additionalProperties": false,
			"minProperties": 1,
			"properties": {
				"read": {
					"enum": ["allow", "ask", "deny", "off"],
					"description": "Reading or searching the file with read, grep, find or ls."
				},
				"write": {
					"enum": ["allow", "ask", "deny", "off"],
					"description": "Writing or editing the file."
				}
			}
		},
		"bouncerLevels": {
			"type": "object",
			"description": "path-bouncer's write level: ask or deny, never off. It has no read level, because reads stay allowed.",
			"additionalProperties": false,
			"minProperties": 1,
			"properties": {
				"write": { "enum": ["ask", "deny"] }
			}
		},
		"rules": {
			"type": "array",
			"description": "Custom rules. A bash rule matches a program and, optionally, words that follow it in order; a path rule matches the paths of Pi's file tools. A project config that is not trusted may not add steer rules (those with instead) or path rules that allow.",
			"items": {
				"type": "object",
				"additionalProperties": false,
				"required": ["name", "summary"],
				"oneOf": [
					{
						"required": ["command"],
						"properties": {
							"command": true,
							"paths": false,
							"level": { "enum": ["ask", "deny", "off"] }
						}
					},
					{
						"required": ["paths"],
						"properties": {
							"name": { "type": "string", "pattern": "^(?!path-)" },
							"paths": true,
							"command": false,
							"args": false
						}
					}
				],
				"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",
								"path-bouncer",
								"path-system",
								"path-git",
								"path-shell-rc",
								"path-keys",
								"path-ssh",
								"path-gnupg",
								"path-aws",
								"path-gcloud",
								"path-azure",
								"path-kube",
								"path-docker",
								"path-pi-auth",
								"path-env",
								"path-npmrc",
								"path-pypirc",
								"path-netrc",
								"path-git-credentials"
							]
						},
						"description": "Shown in messages, dialogs and the log. Lowercase letters, digits and dashes; not a built-in rule's name; a path rule's name does not start with path-."
					},
					"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."
					},
					"paths": {
						"type": "array",
						"minItems": 1,
						"items": {
							"type": "string",
							"pattern": "^[^!]*[^!\\s][^!]*$"
						},
						"description": "Makes this a path rule: gitignore-style globs (*, ** and ?, no !) for the paths of read, write, edit, grep, find and ls. A pattern starting with / is absolute, with ~/ is in the home folder, with no / is a file name at any depth, and otherwise is relative to the working directory."
					},
					"level": {
						"oneOf": [
							{ "enum": ["allow", "ask", "deny", "off"] },
							{ "$ref": "#/definitions/pathLevels" }
						],
						"description": "Default: ask, or deny for a steer rule. A steer rule is deny or off. allow, and an object with read and/or write (a direction it leaves out is off), are for path rules only."
					},
					"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": {
							"oneOf": [
								{ "enum": ["deny", "off"] },
								{
									"type": "object",
									"additionalProperties": false,
									"minProperties": 1,
									"properties": {
										"read": { "enum": ["deny", "off"] },
										"write": { "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."
				}
			}
		}
	}
}
