Files
istosandClaude Fable 5 ead190ac03 Shebang'd files ship executable; update.sh heals existing installs (task 21 hotfix)
First field bug of v0.1-alpha: install.py was committed 100644, so
every install shipped it permission-denied for direct ./install.py use
(start.sh's python3 invocation masked it). Exec bit set on all four
shebang'd files (install.py, board.py, hook_settings.py,
permission_config.py) and install.py added to update.sh's post-update
chmod line so already-broken installs heal on their next update. The
artifact-side invariant test — every shipped #! file executable — is
card 21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 08:50:08 +02:00

97 lines
4.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Print the opencode config JSON for one headless launch: the permission
rules for the launch's intent, plus the model when the board configured one.
Usage: permission_config.py [work|act-pr|review]
Same three stances as every adapter (the contract is in
core/adapters/README.md), rendered in opencode's native rule language:
glob patterns over the whole command line, last match wins, so "*" deny
comes first and the specific allows override it. Headless runs have no
human at a prompt — "ask" would hang — so every rule is allow or deny,
and never a blanket allow: the worktree is isolated, the shell is not.
work "edit": "allow" + git bookkeeping (add/commit/status/diff) and
the project's test/check commands. No push.
act-pr the work stance + `git push` + reading the PR's reviews and
line comments through gh + `git fetch`/`git merge` so a
conflicted PR can be resolved by merging main into the
branch. The branch is public, so resolution is additive
only: rebase and the force-push spellings get explicit deny
rules, placed last so they win over the `git push *` allow.
review "edit": "deny" + reading the PR it judges + posting the
verdict with gh pr review/comment. Everything else denied.
The project's test/check commands arrive in AGENT_COMMANDS as comma-
separated neutral command prefixes (set BOARD_AGENT_COMMANDS in
local/.env); here each becomes "<prefix>" and "<prefix> *" allow rules.
AGENT_MODEL, when set, becomes the config's top-level "model" key —
opencode's "provider/model-id" form (opencode.ai/docs/config), passed
through untranslated. Absent = no key, opencode's own resolution applies.
"""
import json
import os
import sys
# Universal git/gh prefixes per intent; project commands are appended.
MODE_PREFIXES = {
"work": ["git add", "git commit", "git status", "git diff"],
"act-pr": ["git add", "git commit", "git status", "git diff",
"git fetch", "git merge",
"git push", "gh pr view", "gh pr diff", "gh api"],
"review": ["git status", "git diff", "git log", "git show",
"gh pr view", "gh pr diff", "gh pr review", "gh pr comment"],
}
# History must never rewrite under a public PR: deny the force and rebase
# spellings even though nothing allows them — "git push *" would otherwise
# cover them. Globs run over the whole command line, so the flag is caught
# wherever it sits.
MODE_DENY_PATTERNS = {
"act-pr": ["git rebase", "git rebase *",
"git push --force*", "git push * --force*",
"git push -f", "git push -f *", "git push * -f *"],
}
# Which intents run the project's own test/check commands.
MODES_WITH_PROJECT_COMMANDS = {"work", "act-pr"}
def split_commands(raw: str) -> list[str]:
"""AGENT_COMMANDS: comma-separated neutral command prefixes."""
return [p.strip() for p in (raw or "").split(",") if p.strip()]
def bash_rules(mode: str, commands: list[str]) -> dict:
"""Deny everything, then allow each prefix exactly and any longer
command starting with it. Insertion order is the rule order."""
prefixes = list(MODE_PREFIXES.get(mode, []))
if mode in MODES_WITH_PROJECT_COMMANDS:
prefixes += [c for c in commands if c not in prefixes]
rules = {"*": "deny"}
for prefix in prefixes:
rules[prefix] = "allow"
rules[f"{prefix} *"] = "allow"
for pattern in MODE_DENY_PATTERNS.get(mode, []):
rules[pattern] = "deny" # last, so it wins over the allows
return rules
def build_config(mode: str, commands: list[str], model: str = "") -> dict:
config = {
"$schema": "https://opencode.ai/config.json",
"permission": {
"edit": "deny" if mode == "review" else "allow",
"bash": bash_rules(mode, commands),
},
}
if model:
config["model"] = model
return config
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "work"
commands = split_commands(os.environ.get("AGENT_COMMANDS", ""))
model = os.environ.get("AGENT_MODEL", "").strip()
print(json.dumps(build_config(mode, commands, model)))