Skip to content

feat: prerequisites for overnight-burndown automation (file cmd, policy overlay, env-var logging)#7

Merged
jdfalk merged 4 commits intomainfrom
feat/burndown-prereqs
Apr 25, 2026
Merged

feat: prerequisites for overnight-burndown automation (file cmd, policy overlay, env-var logging)#7
jdfalk merged 4 commits intomainfrom
feat/burndown-prereqs

Conversation

@jdfalk
Copy link
Copy Markdown
Owner

@jdfalk jdfalk commented Apr 25, 2026

Summary

Four additive improvements to safe-ai-util that unblock building an overnight-automation driver (jdfalk/overnight-burndown) on top of it. None of the changes affect existing callers when the new opt-in features (Config.allowlist, --policy-overlay, SAFE_AI_UTIL_{LOG_DIR,AUDIT_PATH,QUIET}) are unused.

1. `commands/file.rs` — full read/write/glob/list/exists implementation

Replaces the `println!("not yet implemented")` stub with a policy-gated file subcommand:

  • `file read --path` / `file write --path [--content | --content-stdin] [--create-dirs]` / `file glob --pattern` / `file list --path` / `file exists --path`
  • Refuses `/etc`, `/bin`, `/sys`, `/proc`, `/dev`, etc. (same set as the existing validator)
  • When `SAFE_AI_UTIL_REPO_ROOT` is set, every path must canonicalize inside it; glob results that escape are filtered
  • 10 MiB read / 5 MiB write byte ceilings (overridable via env)
  • 5,000-result glob cap

This lets agents work with files entirely through audited subcommands instead of calling `open()` from a wrapping language.

2. `Config.allowlist` field — rich allowlist loadable from TOML

Adds an optional `allowlist: Option` to `Config`. When set, `Executor::new` builds a `SecurityManager` via the new `with_policy()` constructor that:

  • Replaces the legacy hardcoded allowed_commands hashset
  • Adds `policy.validate_command()` to the validate_arguments pipeline (so per-command `forbidden_args`, `forbidden_patterns`, `max_args` caps are enforced)

When `Config.allowlist` is `None`, behavior is bit-for-bit identical to pre-1.1.

3. `--policy-overlay` flag — narrow-only overlays

New `AllowlistOverlay` struct (all-optional fields). `AllowlistConfig::apply_overlay` returns an error if the overlay would widen access:

  • Adds a command to `always_allowed` not reachable in the base
  • Introduces a `conditionally_allowed` key the base lacks
  • Sets `permissive_mode = true` against a strict base

Conversely it can drop entries from `always_allowed`, add bans to `blocked`, and tighten conditional restrictions (forbidden lists unioned, max_args min'd).

Also fixes a pre-existing latent bug: `--config` was previously parsed but ignored — `Config::load()` always read from default discovery paths. Now `main.rs` parses CLI before loading config so both `--config` and `--policy-overlay` actually influence which files are read.

4. `SAFE_AI_UTIL_{LOG_DIR,AUDIT_PATH,QUIET}` env vars

  • `SAFE_AI_UTIL_LOG_DIR` — pin per-invocation logs to a chosen directory instead of `./logs/` in cwd
  • `SAFE_AI_UTIL_AUDIT_PATH` — namespaced replacement for the legacy `COPILOT_AUDIT_DIR` (still honored as fallback)
  • `SAFE_AI_UTIL_QUIET=1` — disables file logging entirely and routes diagnostics to stderr at warn level; avoids polluting cwd from headless callers (MCP subprocesses, automation) and keeps stdout JSON-pipeline-clean

Motivation

Building jdfalk/overnight-burndown — a launchd-driven nightly automation that drains a queue of small, safe work items across configured repos — required these capabilities so every agent action runs through a single audited trust boundary. But each change is independently useful for any AI-tooling consumer of safe-ai-util.

Test plan

  • `cargo build --bin safe-ai-util` — clean
  • `cargo test --lib` — 39 passed (8 new for overlay narrowing, 6 new for file path policy, plus existing)
  • CLI smoke: `safe-ai-util file write/read/glob/exists` end-to-end with sandboxed `SAFE_AI_UTIL_REPO_ROOT`
  • CLI smoke: `/etc/passwd` read attempt blocked (no contents leaked)
  • CLI smoke: `SAFE_AI_UTIL_QUIET=1` produces silent stdout, no `./logs/` directory
  • CLI smoke: `--policy-overlay` banning git rejects `git status`
  • CLI smoke: `--policy-overlay` widening (adds curl) fails at startup before any command runs
  • Reviewer to manually exercise on their own machine if desired

Backwards compatibility

All four changes are additive:

  • `Config.allowlist = None` (the default) → legacy SecurityManager behavior
  • `SAFE_AI_UTIL_*` env vars unset → legacy paths/loggers
  • No `--policy-overlay` flag → no overlay applied
  • `commands/file.rs` was previously an unimplemented stub, so wiring it up cannot break callers

🤖 Generated with Claude Code

jdfalk and others added 4 commits April 25, 2026 00:37
Replaces the stubbed `commands/file.rs` placeholder with a full implementation
of the `file` subcommand tree. All five operations are policy-gated:

  - Sensitive system prefixes (/etc, /bin, /sys, ...) are rejected.
  - When SAFE_AI_UTIL_REPO_ROOT is set, paths must canonicalize inside it.
    Glob results that escape the root are filtered.
  - Read and write enforce byte ceilings (default 10 MiB read / 5 MiB write,
    overridable via SAFE_AI_UTIL_MAX_READ_BYTES / _WRITE_BYTES).
  - Write supports inline --content or --content-stdin (mutually exclusive).
  - Write --create-dirs creates parent directories if missing.
  - Glob results are capped at 5000 by default.

Motivation: this unblocks the safe-ai-util-mcp `fs_read` / `fs_write` /
`fs_glob` tools that the overnight-burndown driver needs in order to give
LLM agents a fully-audited filesystem surface (no Python `open()` bypass).

Tests: 6 new unit tests covering sensitive-path rejection, repo-root
sandboxing (allow inside, deny outside), parent-traversal handling, write
to non-existent paths, and glob pattern resolution. All pass.

CLI smoke: write/read/glob/exists all work end-to-end against a sandboxed
SAFE_AI_UTIL_REPO_ROOT; `/etc/passwd` read attempt is rejected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
logger:
  - SAFE_AI_UTIL_LOG_DIR sets where the per-invocation log file goes
    (defaults to ./logs/ — preserves legacy behavior).
  - SAFE_AI_UTIL_QUIET=1 disables file logging entirely and routes
    diagnostics to stderr at warn level. Lets headless callers (MCP
    subprocesses, overnight automation) avoid polluting cwd with a logs/
    directory and avoid corrupting JSON pipelines via stdout.

audit:
  - SAFE_AI_UTIL_AUDIT_PATH preferred (project-namespaced).
  - COPILOT_AUDIT_DIR retained as fallback for backwards compatibility.
  - When unset, audit logs land under SAFE_AI_UTIL_LOG_DIR/security if that
    is set, then ./logs/security otherwise.

Together these let an overnight burndown driver pin a single repo's
per-night artifacts to ~/.burndown/{logs,audit}/ instead of having
every worktree inherit the legacy cwd-relative paths.

Backwards compatible: no env vars set yields the original behavior.
27 lib tests still pass; CLI smoke confirms QUIET mode is silent and
AUDIT_PATH is honored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an optional `allowlist: Option<AllowlistConfig>` field to the
top-level Config struct. When set (e.g. loaded from a TOML config file),
the Executor builds a SecurityManager via the new
`SecurityManager::with_policy(...)` constructor, which:

  * Replaces the legacy hardcoded allowed_commands hashset with one
    derived from `policy.always_allowed ∪ policy.conditionally_allowed`.
  * Adds `policy.validate_command()` to the validate_arguments pipeline,
    so per-command restrictions (forbidden_args, forbidden_patterns,
    max_args caps, custom validators) are enforced.

When Config.allowlist is None (default), behavior is bit-for-bit
identical to pre-1.1 — the hardcoded allowed list and existing
sanitizer/validator chain run unchanged.

This is the schema half of the policy-overlay work. The CLI plumbing
(--config wiring + --policy-overlay flag with narrow-only enforcement)
follows in the next commit.

Tests:
  * Config::default() omits the [allowlist] section in TOML output.
  * Round-trip parsing of a TOML with an explicit [allowlist] section.
  * with_policy() ignores the legacy hardcoded list when the policy
    set is tighter (cargo/npm rejected when only `git` is allowed).
  * with_policy() enforces forbidden_args on a custom `make clean` ban
    that the legacy validator has no awareness of.

31 lib tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the AllowlistOverlay struct: a TOML document with all-optional
fields that, when applied to a base AllowlistConfig, can only ever
TIGHTEN the policy. The new `AllowlistConfig::apply_overlay` method
returns an error if the overlay would widen access in any of these ways:

  * Adds a command to always_allowed that the base does not already
    have reachable (in always or conditionally_allowed).
  * Introduces a conditionally_allowed key the base lacks.
  * Sets permissive_mode = true when the base has it false.

Conversely the overlay CAN:

  * Drop entries from always_allowed (intersection semantics).
  * Add entries to blocked (union; bans win against any allow).
  * Tighten existing conditional restrictions:
      - forbidden_args / forbidden_patterns / required_args are unioned.
      - max_args is min'd.
      - allowed_patterns is intersected when both sides populate it.

CLI plumbing:

  * `--policy-overlay <FILE>` flag added.
  * main.rs reordered to parse CLI BEFORE loading config so both
    `--config` and `--policy-overlay` actually influence which files
    are read. The pre-existing `--config` flag was previously parsed
    but ignored (Config::load() always read from default discovery
    paths) — fixed as part of this change.
  * Config::load_with_paths(config?, overlay?) is the new entry point;
    Config::load() preserves the zero-arg default behavior.
  * If the loaded config has no [allowlist] section but an overlay is
    supplied, the overlay narrows the secure_default baseline.

Tests added (8 new):

  * Narrowing always_allowed succeeds.
  * Widening always_allowed is rejected.
  * Adding to blocked succeeds and overrides allowed sets.
  * Tightening conditional restrictions unions/mins as documented.
  * Introducing an unknown conditional key is rejected.
  * Enabling permissive_mode against a strict base is rejected.
  * Disabling permissive_mode against a permissive base succeeds.

CLI smoke verified end-to-end: a --policy-overlay that bans `git`
makes `safe-ai-util git status` fail with a security violation; a
widening overlay that tries to add `curl` fails at config-load time
before any command runs.

39 lib tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the module:config Configuration management label Apr 25, 2026
@jdfalk jdfalk merged commit cfeca78 into main Apr 25, 2026
31 of 42 checks passed
@jdfalk jdfalk deleted the feat/burndown-prereqs branch April 25, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module:config Configuration management

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant