Skip to content

refactor: migrate flag-block groups to ports (PR 2/6 of #2)#4

Open
stevehansen wants to merge 2 commits intomainfrom
refactor/issue-2-phase-3-flag-block-groups
Open

refactor: migrate flag-block groups to ports (PR 2/6 of #2)#4
stevehansen wants to merge 2 commits intomainfrom
refactor/issue-2-phase-3-flag-block-groups

Conversation

@stevehansen
Copy link
Copy Markdown
Member

Summary

Phase 3 of #2 (PR 2 of 6). Migrates the flag-block groups to the ports-and-adapters seam landed in PR #3 and extends the structured Blocked JSON envelope contract to five more groups.

  • Migrated: dotnet, docker, npm, pnpm, db — all now use Func<Ports, string[], int> handlers and emit structured { blocked, command, reason, suggestion } JSON envelopes under --json.
  • New safety primitives:
    • Policy.DenyFlags(params string[]) — exact-match (case-insensitive) flag denial. Powers --force, -v, --volumes, --rmi, --accept-data-loss, --force-reset, --skip-seed, --skip-generate, -f.
    • Policy.DenyArgsContaining(params string[]) — substring denial for embedded forms like migrate:fresh. Reach for it only when exact DenyFlags won't catch the intended pattern.
  • Shared allowlist: Safety.NodeScripts.AllowedScripts replaces three copies of the same 22-script HashSet in Bun/Npm/Pnpm.
  • Tests: +52 new (96 total). Covers each migrated group's safety-critical paths (force flags, script allowlist, artisan substring, django zero, docker volume block, npm postinstall warnings) plus the new policy primitives.

Per-group rollout status (from #2)

Group Migrated Structured Blocked JSON
bun ✅ 0.4.0
dotnet, docker, npm, pnpm, db ✅ this PR
git, file, process, env, proxy, generate ❌ (still Spectre markup)

The remaining six groups will roll over in Phases 4–5 of #2.

Notable implementation choices

  • Run.Tool now accepts Policy? mirroring Run.Bun. This is the workhorse for the new groups; Run.Bun stays as a thin facade.
  • DbCommands.CheckDestructive preserves the project's "drops tables/data" wording rather than using DenyFlags' generic message — destructive flags carry security context worth keeping. The same pattern is used inline in DockerCommands.RunComposeDown and DbCommands.RunArtisanMigrate/RunDjangoMigrate for cases where the original message conveyed something DenyFlags alone wouldn't.
  • DockerCommands.FilterFlags retains its silent-filter semantics for build and compose-up (drops unknown flags rather than blocking). This is a known security smell from the issue description but changing the contract is out of scope for this PR.

Smoke tests

$ safe npm audit-fix --force --json
{
  "blocked": true,
  "command": "npm audit fix --force",
  "reason": "Force audit fix can install breaking major version changes",
  "suggestion": "safe npm audit-fix (without --force) for safe semver-compatible fixes"
}

$ safe docker compose-down -v --json
{ "blocked": true, "command": "docker compose down -v", "reason": "Removing volumes/images during compose down is not allowed", ... }

$ safe db prisma-migrate-deploy --force --json
{ "blocked": true, "command": "prisma migrate deploy --force", "reason": "Flag '--force' can cause irreversible data loss (drops tables/data)", ... }

$ safe git push --force --json
# unmigrated group — still Spectre markup, as documented in CHANGELOG
[Blocked: git push --force ...]

Test plan

  • dotnet build SafeCommands.slnx clean
  • dotnet test 96/96 pass
  • Smoke: structured Blocked envelope under --json for migrated groups
  • Smoke: human-mode markup unchanged for migrated groups
  • Smoke: unmigrated git group still emits Spectre markup under --json (regression check)

🤖 Generated with Claude Code

Phase 3 of the ports-and-adapters refactor (issue #2). Migrates 5 command
groups (dotnet, docker, npm, pnpm, db) to the direct-port handler shape and
extends the structured Blocked JSON envelope contract to those groups.

Adds two new safety primitives:
  - Policy.DenyFlags - exact-match flag denial (--force, -v, --volumes, ...)
  - Policy.DenyArgsContaining - substring denial for embedded forms like
    'migrate:fresh'

Collapses the duplicated AllowedScripts HashSet (previously copy-pasted in
BunCommands/NpmCommands/PnpmCommands) into a single Safety.NodeScripts source.

Six of twelve groups have now rolled over to the structured Blocked envelope
under --json. The remaining six (git, file, process, env, proxy, generate)
still emit Spectre markup until each migrates in subsequent phases.

Tests: 96 total (52 new), 0 failing. Build: clean.

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

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates SafeCommands to version 0.5.0, introducing new safety primitives DenyFlags and DenyArgsContaining and centralizing the node script allowlist. It migrates the dotnet, docker, npm, pnpm, and db command handlers to a unified structure that supports structured JSON output for blocked operations. Feedback focuses on performance optimizations within the new policy rules to reduce allocations and processing overhead, addresses a lookup performance regression in DbCommands where a HashSet was replaced by an array, and suggests consolidating duplicated logic in the Artisan migration handler.

Comment thread src/SafeCommands/Safety/Policy.cs
Comment thread src/SafeCommands/Safety/Policy.cs
Comment thread src/SafeCommands/Commands/DbCommands.cs Outdated
Comment thread src/SafeCommands/Commands/DbCommands.cs Outdated
Comment thread src/SafeCommands/Commands/DockerCommands.cs Outdated
- Policy.DenyFlagsRule: pre-compute lowercased HashSet in ctor (was per-call alloc)
- Policy.DenyArgsContainingRule: pre-lowercase needles in ctor (was per-arg alloc in inner loop)
- DbCommands.DestructiveFlags: restore HashSet for O(1) lookup in CheckDestructive's offending-arg search
- DbCommands.RunArtisanMigrate: extract ArtisanDestructiveTerms shared between Policy and offending-arg search; cache ArtisanDestructivePolicy
- DbCommands.DestructivePolicy: cache as static field instead of per-call expression
- DockerCommands.ComposeDownPolicy: cache as static field instead of allocating per call

All 96 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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