rafter-cli 0.10.0__tar.gz → 0.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/PKG-INFO +1 -1
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/pyproject.toml +1 -1
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/agent.py +95 -11
- rafter_cli-0.10.2/rafter_cli/commands/backend.py +520 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/issues/issues_app.py +38 -5
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/mcp_server.py +5 -5
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/notify.py +4 -2
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/sites.py +6 -4
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/config_manager.py +111 -6
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/config_schema.py +7 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/risk_rules.py +318 -19
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/rafter-security-skill.md +1 -1
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/betterleaks.py +38 -21
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/api.py +67 -0
- rafter_cli-0.10.0/rafter_cli/commands/backend.py +0 -288
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/README.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/__main__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/agent_components.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/brief.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/ci.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/docs.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/hook.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/issues/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/issues/dedup.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/issues/github_client.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/issues/issue_builder.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/policy.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/report.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/scan.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/skill.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/commands/skill_remote.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/audit_logger.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/command_interceptor.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/custom_patterns.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/docs_loader.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/hook_control.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/pattern_engine.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/policy_loader.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/core/suppression_writer.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/agents/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/agents/rafter.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/continue-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/continue-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/continue-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/continue-rules/rafter.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/cursor-rules/rafter.mdc +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/pre-commit-hook.sh +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/pre-push-hook.sh +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/SKILL.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/docs/backend.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/docs/cli-reference.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/docs/finding-triage.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/docs/guardrails.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter/docs/shift-left.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/SKILL.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/api.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/cwe-top25.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/investigation-playbook.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/api-design.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/data-storage.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/dependencies.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/deployment.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/ingestion.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/standards-pointers.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-secure-design/docs/threat-modeling.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/authorship-provenance.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/changelog-review.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/data-practices.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/malware-indicators.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/prompt-injection.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/skills/rafter-skill-review/docs/telemetry.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/windsurf-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/resources/windsurf-rules/rafter.md +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/git_diff_scan.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/regex_scanner.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/secret_patterns.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/skill_scanner.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/scanners/union.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/__init__.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/binary_manager.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/formatter.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/git.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/git_diff.py +0 -0
- {rafter_cli-0.10.0 → rafter_cli-0.10.2}/rafter_cli/utils/skill_manager.py +0 -0
|
@@ -1711,6 +1711,31 @@ def _scan_file(file_path: str, engine: str, custom_patterns=None) -> list[ScanRe
|
|
|
1711
1711
|
return run_patterns()
|
|
1712
1712
|
|
|
1713
1713
|
|
|
1714
|
+
def _output_empty_diff_scan(
|
|
1715
|
+
empty_message: str,
|
|
1716
|
+
json_output: bool,
|
|
1717
|
+
quiet: bool,
|
|
1718
|
+
context_label: str,
|
|
1719
|
+
format: str,
|
|
1720
|
+
suppressions,
|
|
1721
|
+
) -> None:
|
|
1722
|
+
"""Emit the result of a diff scan that had no files to scan.
|
|
1723
|
+
|
|
1724
|
+
``empty_message`` ("No files changed since <ref>") is a *status* line, so it
|
|
1725
|
+
goes to stderr — stdout has to stay parseable as JSON under ``--json``, and
|
|
1726
|
+
``_output_scan_results`` owns the single stdout success line. Mirrors the
|
|
1727
|
+
empty branches of ``runGitAddedLineScan`` in
|
|
1728
|
+
node/src/commands/agent/scan.ts — keep the two in sync.
|
|
1729
|
+
"""
|
|
1730
|
+
if not quiet:
|
|
1731
|
+
# rprint, not print — fmt.success returns rich markup that plain print
|
|
1732
|
+
# would emit literally as "[green]...[/green]".
|
|
1733
|
+
rprint(fmt.success(empty_message), file=sys.stderr)
|
|
1734
|
+
_output_scan_results(
|
|
1735
|
+
[], json_output, quiet, context_label, format=format, suppressions=suppressions
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
|
|
1714
1739
|
def _run_git_added_line_scan(
|
|
1715
1740
|
git_args: list[str],
|
|
1716
1741
|
git_cwd: str | None,
|
|
@@ -1743,14 +1768,16 @@ def _run_git_added_line_scan(
|
|
|
1743
1768
|
raise typer.Exit(code=2)
|
|
1744
1769
|
|
|
1745
1770
|
if not patch.strip():
|
|
1746
|
-
|
|
1747
|
-
|
|
1771
|
+
_output_empty_diff_scan(
|
|
1772
|
+
empty_message, json_output, quiet, context_label, format, suppressions
|
|
1773
|
+
)
|
|
1748
1774
|
raise typer.Exit(code=0)
|
|
1749
1775
|
|
|
1750
1776
|
added = parse_unified_diff_added_lines(patch)
|
|
1751
1777
|
if not added:
|
|
1752
|
-
|
|
1753
|
-
|
|
1778
|
+
_output_empty_diff_scan(
|
|
1779
|
+
empty_message, json_output, quiet, context_label, format, suppressions
|
|
1780
|
+
)
|
|
1754
1781
|
raise typer.Exit(code=0)
|
|
1755
1782
|
|
|
1756
1783
|
try:
|
|
@@ -2397,16 +2424,65 @@ def _audit_share() -> None:
|
|
|
2397
2424
|
# ── exec ─────────────────────────────────────────────────────────────
|
|
2398
2425
|
|
|
2399
2426
|
|
|
2427
|
+
# Approval model (rf-ss67): the only party who can approve a command that the
|
|
2428
|
+
# policy says needs approval is a person at an interactive terminal. No flag,
|
|
2429
|
+
# env var or stdin trick stands in for that, because every one of those can be
|
|
2430
|
+
# produced by the agent whose command is being gated:
|
|
2431
|
+
# * `--force` used to skip the prompt. Combined with the PreToolUse hook
|
|
2432
|
+
# treating a quoted argument as prose, `rafter agent exec --force "<cmd>"`
|
|
2433
|
+
# ran any HIGH-tier command unprompted with the hook blind. The flag is
|
|
2434
|
+
# kept only so old invocations parse; it changes nothing.
|
|
2435
|
+
# * A piped "yes" is not a person. Approval is offered only when stdin is a
|
|
2436
|
+
# TTY; otherwise the command is denied and says why.
|
|
2437
|
+
# The machine owner widens policy in ~/.rafter/config.json, not per call.
|
|
2438
|
+
|
|
2439
|
+
_DRY_RUN_EXIT = {"allowed": 0, "blocked": 1, "approval": 2}
|
|
2440
|
+
|
|
2441
|
+
|
|
2442
|
+
def _join_command_parts(parts: list[str]) -> str:
|
|
2443
|
+
"""One quoted argument is the command verbatim; several (the `-- rm -rf x`
|
|
2444
|
+
form the docs show) are re-joined with shell quoting so the classifier sees
|
|
2445
|
+
what will run — `-- echo "a b"` becomes `echo 'a b'`, not `echo a b`."""
|
|
2446
|
+
import shlex
|
|
2447
|
+
|
|
2448
|
+
if len(parts) == 1:
|
|
2449
|
+
return parts[0]
|
|
2450
|
+
return shlex.join(parts)
|
|
2451
|
+
|
|
2452
|
+
|
|
2400
2453
|
@agent_app.command("exec")
|
|
2401
2454
|
def exec_cmd(
|
|
2402
|
-
command: str = typer.Argument(..., help="Command to execute"),
|
|
2455
|
+
command: list[str] = typer.Argument(..., help="Command to execute (quote it, or pass it after --)"),
|
|
2403
2456
|
skip_scan: bool = typer.Option(False, "--skip-scan", help="Skip pre-execution file scanning"),
|
|
2404
|
-
|
|
2457
|
+
dry_run: bool = typer.Option(
|
|
2458
|
+
False, "--dry-run",
|
|
2459
|
+
help="Classify the command and exit without running it (exit 0 allowed, 1 blocked, 2 needs approval)",
|
|
2460
|
+
),
|
|
2461
|
+
force: bool = typer.Option(False, "--force", hidden=True, help="Deprecated: no longer skips approval (rf-ss67)"),
|
|
2405
2462
|
):
|
|
2406
2463
|
"""Execute command with security validation."""
|
|
2464
|
+
command = _join_command_parts(command)
|
|
2407
2465
|
interceptor = CommandInterceptor()
|
|
2408
2466
|
evaluation = interceptor.evaluate(command)
|
|
2409
2467
|
|
|
2468
|
+
# --dry-run reports the classification and stops. Nothing runs, nothing is
|
|
2469
|
+
# scanned, nothing is logged as executed.
|
|
2470
|
+
if dry_run:
|
|
2471
|
+
blocked = not evaluation.allowed and not evaluation.requires_approval
|
|
2472
|
+
verdict = "BLOCKED" if blocked else ("REQUIRES APPROVAL" if evaluation.requires_approval else "ALLOWED")
|
|
2473
|
+
print(f"Dry run: {verdict}")
|
|
2474
|
+
print(f"Risk Level: {evaluation.risk_level.upper()}")
|
|
2475
|
+
print(f"Requires approval: {'yes' if evaluation.requires_approval else 'no'}")
|
|
2476
|
+
if evaluation.reason:
|
|
2477
|
+
print(f"Reason: {evaluation.reason}")
|
|
2478
|
+
print(f"Command: {command}")
|
|
2479
|
+
print("Not executed (--dry-run).")
|
|
2480
|
+
raise typer.Exit(
|
|
2481
|
+
code=_DRY_RUN_EXIT["blocked"] if blocked
|
|
2482
|
+
else _DRY_RUN_EXIT["approval"] if evaluation.requires_approval
|
|
2483
|
+
else _DRY_RUN_EXIT["allowed"]
|
|
2484
|
+
)
|
|
2485
|
+
|
|
2410
2486
|
# Blocked
|
|
2411
2487
|
if not evaluation.allowed and not evaluation.requires_approval:
|
|
2412
2488
|
rprint(f"\n{fmt.error('Command BLOCKED')}\n")
|
|
@@ -2446,8 +2522,10 @@ def exec_cmd(
|
|
|
2446
2522
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
2447
2523
|
pass
|
|
2448
2524
|
|
|
2449
|
-
# Requires approval
|
|
2450
|
-
if evaluation.requires_approval
|
|
2525
|
+
# Requires approval — only a person at a terminal can give it.
|
|
2526
|
+
if evaluation.requires_approval:
|
|
2527
|
+
if force:
|
|
2528
|
+
rprint(f"\n{fmt.warning('--force no longer skips approval (rf-ss67); approval needs a person at an interactive terminal')}\n")
|
|
2451
2529
|
rprint(f"\n{fmt.warning('Command requires approval')}\n")
|
|
2452
2530
|
print(f"Risk Level: {evaluation.risk_level.upper()}")
|
|
2453
2531
|
print(f"Command: {command}")
|
|
@@ -2455,6 +2533,15 @@ def exec_cmd(
|
|
|
2455
2533
|
print(f"Reason: {evaluation.reason}")
|
|
2456
2534
|
print()
|
|
2457
2535
|
|
|
2536
|
+
if not sys.stdin.isatty():
|
|
2537
|
+
rprint(fmt.error("Command denied: approval needs an interactive terminal, and stdin is not one"))
|
|
2538
|
+
print(
|
|
2539
|
+
"Run the command yourself at a terminal, or have the machine owner adjust "
|
|
2540
|
+
"commandPolicy in ~/.rafter/config.json.\n"
|
|
2541
|
+
)
|
|
2542
|
+
interceptor.log_evaluation(evaluation, "blocked")
|
|
2543
|
+
raise typer.Exit(code=1)
|
|
2544
|
+
|
|
2458
2545
|
answer = input("Approve this command? (yes/no): ").strip().lower()
|
|
2459
2546
|
if answer not in ("yes", "y"):
|
|
2460
2547
|
rprint(f"\n{fmt.error('Command cancelled')}\n")
|
|
@@ -2463,9 +2550,6 @@ def exec_cmd(
|
|
|
2463
2550
|
|
|
2464
2551
|
rprint(f"\n{fmt.success('Command approved by user')}\n")
|
|
2465
2552
|
interceptor.log_evaluation(evaluation, "overridden")
|
|
2466
|
-
elif force and evaluation.requires_approval:
|
|
2467
|
-
rprint(f"\n{fmt.warning('Forcing execution (--force flag)')}\n")
|
|
2468
|
-
interceptor.log_evaluation(evaluation, "overridden")
|
|
2469
2553
|
else:
|
|
2470
2554
|
interceptor.log_evaluation(evaluation, "allowed")
|
|
2471
2555
|
|