Skip to content

security: per-campaign permission policy template (#135)#138

Closed
sriumcp wants to merge 1 commit into
AI-native-Systems-Research:reflectivefrom
sriumcp:security/135-permission-policy
Closed

security: per-campaign permission policy template (#135)#138
sriumcp wants to merge 1 commit into
AI-native-Systems-Research:reflectivefrom
sriumcp:security/135-permission-policy

Conversation

@sriumcp
Copy link
Copy Markdown
Collaborator

@sriumcp sriumcp commented May 24, 2026

Summary

  • Add orchestrator/settings_template.py — pure renderer that builds a per-campaign .claude/settings.json policy from work_dir + repo_path + experiment_plan + hook paths.
  • setup_work_dir() writes the rendered file at init (idempotent — won't clobber hand-edits).
  • CLIDispatcher auto-detects work_dir/.claude/settings.json on construction; when present, passes --settings <path> to claude -p instead of --dangerously-skip-permissions.
  • docs/security.md describes the model end-to-end.

Why

--dangerously-skip-permissions auto-approves every tool call. Appropriate for a sandbox; unsafe for hours-long campaigns operating against real repos. The settings file imposes deny-by-default with a small, explicit allowlist drawn from what the campaign actually needs.

What's in the rendered settings

Key Default behavior
permissions.allowOnly Campaign work-dir + target repo (when set). Everything else denied.
permissions.allow git, python, pytest, grep, rg, etc. + binaries from experiment_plan.yaml.
permissions.deny Outbound https via curl/wget, rm -rf /*.
hooks.Stop Wired to bin/nous-execute-stop (#129) when that script is present.
hooks.PreToolUse Wired when path provided (foundation for #128).

Behavioral tests (14 in tests/test_settings_template.py)

  • Renderer contract — allowOnly/allow/deny/hooks shape under various inputs.
  • Disk write — round-trip equivalence between produced dict and on-disk JSON.
  • Init wiring — setup_work_dir writes the file fresh; doesn't overwrite custom edits.
  • Replacement invariant — rendered settings impose non-empty allowOnly AND non-empty deny. If either were empty, the file would be functionally equivalent to --dangerously and the swap a regression.

All assertions describe externally-visible properties (file contents, paths, JSON shape). None inspect which functions ran or how the renderer organized its work.

Out of scope

  • The "out-of-worktree write is denied" acceptance criterion is an integration test against a live claude session — verified manually.
  • --dangerously-skip-permissions remains available as a fallback when no settings file exists, so emergency / unsandboxed runs still work.

Test plan

  • pytest tests/test_settings_template.py — 14/14 pass
  • pytest (full suite) — 352/352 pass (was 338; 14 new)
  • Manual: run a real campaign, observe claude is invoked with --settings not --dangerously, and confirm an out-of-worktree write fails.

Closes #135.
Refs #120.

🤖 Generated with Claude Code

…I-native-Systems-Research#135)

Replace --dangerously-skip-permissions with a fine-grained, per-campaign
permission policy generated at init.

The orchestrator's pure renderer (orchestrator/settings_template.py) takes
work_dir, repo_path, and an optional experiment_plan, and returns a dict
suitable for serialization as .claude/settings.json. The contents:

  - permissions.allowOnly: campaign work-dir and target repo path. Anything
    else is denied by default.
  - permissions.allow: Bash command allowlist — conservative defaults plus
    any binaries pulled out of experiment_plan.yaml arm conditions, plus
    caller-provided extras.
  - permissions.deny: hard blocks for outbound https (curl/wget) and
    catastrophic shell commands (rm -rf /).
  - hooks.Stop: registered when bin/nous-execute-stop is present (AI-native-Systems-Research#129
    integration).
  - hooks.PreToolUse: registered when caller provides the path (AI-native-Systems-Research#128 hook).

setup_work_dir() now writes the rendered settings file at init time,
idempotently (won't clobber a hand-edited file). CLIDispatcher
auto-detects work_dir/.claude/settings.json on construction, and when
present passes --settings <path> to claude -p instead of
--dangerously-skip-permissions. SDKDispatcher already accepted
settings_path in AI-native-Systems-Research#121 — wire-up matches.

Behavioral tests (tests/test_settings_template.py): 14 cases.

Renderer contract:
  - allowOnly contains work_dir
  - allowOnly contains repo_path when provided
  - default bin allowlist contains python, git, grep
  - plan binaries (./blis, /usr/local/bin/sim) are added by basename
  - extra_bin_allowlist extends defaults
  - deny blocks outbound https
  - hooks section absent unless hook paths provided
  - Stop hook registered with absolute path
  - PreToolUse hook registered with Bash matcher

Disk write contract:
  - write_campaign_settings creates parent dir + writes JSON
  - settings_path_for returns .claude/settings.json under work_dir

Init wiring contract:
  - setup_work_dir writes the file when fresh
  - setup_work_dir does NOT overwrite a user-customized settings file

Replacement invariant (the security property):
  - rendered settings impose non-empty allowOnly AND non-empty deny
    (otherwise the file is functionally equivalent to --dangerously
    and the swap is a regression).

Out of scope: the "out-of-worktree write is denied" criterion is an
integration test against a live claude session and is verified manually.

docs/security.md describes the model end-to-end.

Test suite: 338 baseline + 14 new = 352 passing.

Closes AI-native-Systems-Research#135.
Refs AI-native-Systems-Research#120.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sriumcp
Copy link
Copy Markdown
Collaborator Author

sriumcp commented May 24, 2026

Superseded by #153 — the consolidated tracking-120 PR carrying all 17 commits in merge order. Closing this in favor of that single PR per project owner's request.

@sriumcp sriumcp closed this May 24, 2026
@sriumcp sriumcp deleted the security/135-permission-policy branch May 25, 2026 00:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant