Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:
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:
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
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
Backwards compatibility
All four changes are additive:
🤖 Generated with Claude Code