rafter-cli 0.8.2__tar.gz → 0.8.4__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.8.2 → rafter_cli-0.8.4}/PKG-INFO +1 -1
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/pyproject.toml +1 -1
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/agent.py +277 -11
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/agent_components.py +57 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/scan.py +7 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/policy_loader.py +45 -4
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/rafter-security-skill.md +1 -1
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/SKILL.md +2 -2
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/scanners/secret_patterns.py +7 -0
- rafter_cli-0.8.4/rafter_cli/utils/skill_manager.py +45 -0
- rafter_cli-0.8.2/rafter_cli/utils/skill_manager.py +0 -22
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/README.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/__main__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/backend.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/brief.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/ci.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/docs.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/hook.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/issues/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/issues/dedup.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/issues/github_client.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/issues/issue_builder.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/issues/issues_app.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/mcp_server.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/notify.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/policy.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/report.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/skill.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/commands/skill_remote.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/audit_logger.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/command_interceptor.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/config_manager.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/config_schema.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/custom_patterns.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/docs_loader.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/pattern_engine.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/core/risk_rules.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/agents/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/agents/rafter.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter.mdc +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/pre-commit-hook.sh +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/pre-push-hook.sh +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/backend.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/cli-reference.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/finding-triage.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/guardrails.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/shift-left.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/SKILL.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/api.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/cwe-top25.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/investigation-playbook.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/api-design.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/data-storage.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/dependencies.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/deployment.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/ingestion.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/standards-pointers.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/threat-modeling.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/authorship-provenance.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/changelog-review.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/data-practices.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/malware-indicators.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/prompt-injection.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/docs/telemetry.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter.md +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/scanners/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/scanners/betterleaks.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/scanners/regex_scanner.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/utils/__init__.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/utils/api.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/utils/binary_manager.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/utils/formatter.py +0 -0
- {rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/utils/git.py +0 -0
|
@@ -248,6 +248,7 @@ def _print_dry_run_plan(
|
|
|
248
248
|
want_windsurf: bool,
|
|
249
249
|
want_continue: bool,
|
|
250
250
|
want_aider: bool,
|
|
251
|
+
want_hermes: bool,
|
|
251
252
|
want_betterleaks: bool,
|
|
252
253
|
risk_level: str,
|
|
253
254
|
) -> None:
|
|
@@ -355,6 +356,11 @@ def _print_dry_run_plan(
|
|
|
355
356
|
W(root / "RAFTER.md", "rafter:start/end marker block")
|
|
356
357
|
W(root / ".aider.conf.yml", "appends RAFTER.md to read: list; strips legacy mcp-server-command line")
|
|
357
358
|
|
|
359
|
+
if want_hermes:
|
|
360
|
+
print()
|
|
361
|
+
print("Hermes (--with-hermes):")
|
|
362
|
+
W(root / ".hermes" / "config.yaml", "mcp_servers.rafter entry merged into existing YAML")
|
|
363
|
+
|
|
358
364
|
if want_openclaw:
|
|
359
365
|
print()
|
|
360
366
|
print("OpenClaw (--with-openclaw):")
|
|
@@ -987,6 +993,44 @@ def _install_aider_read(root: Path) -> bool:
|
|
|
987
993
|
return True
|
|
988
994
|
|
|
989
995
|
|
|
996
|
+
def _install_hermes_mcp(root: Path) -> bool:
|
|
997
|
+
"""Install MCP server config for Hermes (<root>/.hermes/config.yaml).
|
|
998
|
+
|
|
999
|
+
Hermes uses a YAML config with an ``mcp_servers:`` block (snake_case,
|
|
1000
|
+
unlike Cursor/Windsurf/Claude Code which use ``mcpServers`` camelCase).
|
|
1001
|
+
Schema per server is ``{command, args, env}``. We use PyYAML (already
|
|
1002
|
+
imported) to merge in the rafter entry while preserving any existing
|
|
1003
|
+
servers.
|
|
1004
|
+
|
|
1005
|
+
Hooks (preToolUse/postToolUse equivalents) deferred to a follow-on bead
|
|
1006
|
+
pending confirmation Hermes exposes a hook surface — landing MCP-only as
|
|
1007
|
+
v0 mirrors how Gemini and Continue.dev were initially shipped (sable-gyw).
|
|
1008
|
+
"""
|
|
1009
|
+
hermes_dir = root / ".hermes"
|
|
1010
|
+
config_path = hermes_dir / "config.yaml"
|
|
1011
|
+
|
|
1012
|
+
hermes_dir.mkdir(parents=True, exist_ok=True)
|
|
1013
|
+
|
|
1014
|
+
config: dict[str, Any] = {}
|
|
1015
|
+
if config_path.exists():
|
|
1016
|
+
try:
|
|
1017
|
+
loaded = yaml.safe_load(config_path.read_text())
|
|
1018
|
+
if isinstance(loaded, dict):
|
|
1019
|
+
config = loaded
|
|
1020
|
+
except yaml.YAMLError:
|
|
1021
|
+
rprint(fmt.warning("Existing Hermes config.yaml was not valid YAML, creating new one"))
|
|
1022
|
+
|
|
1023
|
+
mcp_servers = config.get("mcp_servers")
|
|
1024
|
+
if not isinstance(mcp_servers, dict):
|
|
1025
|
+
mcp_servers = {}
|
|
1026
|
+
config["mcp_servers"] = mcp_servers
|
|
1027
|
+
mcp_servers["rafter"] = {**_RAFTER_MCP_ENTRY}
|
|
1028
|
+
|
|
1029
|
+
config_path.write_text(yaml.safe_dump(config, sort_keys=False))
|
|
1030
|
+
rprint(fmt.success(f"Installed Rafter MCP server to {config_path}"))
|
|
1031
|
+
return True
|
|
1032
|
+
|
|
1033
|
+
|
|
990
1034
|
@agent_app.command()
|
|
991
1035
|
def init(
|
|
992
1036
|
risk_level: str = typer.Option("moderate", "--risk-level", help="minimal, moderate, or aggressive"),
|
|
@@ -999,6 +1043,7 @@ def init(
|
|
|
999
1043
|
with_cursor: bool = typer.Option(False, "--with-cursor", help="Install Cursor integration"),
|
|
1000
1044
|
with_windsurf: bool = typer.Option(False, "--with-windsurf", help="Install Windsurf integration"),
|
|
1001
1045
|
with_continue: bool = typer.Option(False, "--with-continue", help="Install Continue.dev integration"),
|
|
1046
|
+
with_hermes: bool = typer.Option(False, "--with-hermes", help="Install Hermes integration"),
|
|
1002
1047
|
all_integrations: bool = typer.Option(False, "--all", help="Install all detected integrations and download Betterleaks"),
|
|
1003
1048
|
update: bool = typer.Option(False, "--update", help="Re-download betterleaks and reinstall integrations without resetting config"),
|
|
1004
1049
|
local: bool = typer.Option(
|
|
@@ -1037,6 +1082,7 @@ def init(
|
|
|
1037
1082
|
has_windsurf = scope == "user" and (home / ".codeium" / "windsurf").exists()
|
|
1038
1083
|
has_continue_dev = scope == "user" and (home / ".continue").exists()
|
|
1039
1084
|
has_aider = scope == "user" and (home / ".aider.conf.yml").exists()
|
|
1085
|
+
has_hermes = scope == "user" and (home / ".hermes").exists()
|
|
1040
1086
|
|
|
1041
1087
|
# Resolve opt-in flags. In --local scope, --all is restricted to platforms with
|
|
1042
1088
|
# a project-local config story (claudeCode, codex, gemini, cursor).
|
|
@@ -1057,6 +1103,10 @@ def init(
|
|
|
1057
1103
|
# Aider can install at --local scope (writes RAFTER.md + .aider.conf.yml
|
|
1058
1104
|
# in cwd) since rf-du2o.
|
|
1059
1105
|
want_aider = with_aider or all_integrations
|
|
1106
|
+
# Hermes: MCP-only v0 (no hooks confirmed yet). User scope only — Hermes
|
|
1107
|
+
# reads ~/.hermes/config.yaml; project-local install story isn't
|
|
1108
|
+
# established. Excluded from --all in --local for the same reason (sable-gyw).
|
|
1109
|
+
want_hermes = with_hermes or (all_integrations and not local)
|
|
1060
1110
|
want_betterleaks = with_betterleaks or (all_integrations and not local)
|
|
1061
1111
|
|
|
1062
1112
|
# Show detected environments
|
|
@@ -1077,6 +1127,8 @@ def init(
|
|
|
1077
1127
|
detected.append("Continue.dev")
|
|
1078
1128
|
if has_aider:
|
|
1079
1129
|
detected.append("Aider")
|
|
1130
|
+
if has_hermes:
|
|
1131
|
+
detected.append("Hermes")
|
|
1080
1132
|
|
|
1081
1133
|
if detected:
|
|
1082
1134
|
rprint(fmt.info(f"Detected environments: {', '.join(detected)}"))
|
|
@@ -1102,6 +1154,8 @@ def init(
|
|
|
1102
1154
|
rprint(fmt.warning("Continue.dev requested but not detected (~/.continue not found)"))
|
|
1103
1155
|
if want_aider and not has_aider:
|
|
1104
1156
|
rprint(fmt.warning("Aider requested but not detected (~/.aider.conf.yml not found)"))
|
|
1157
|
+
if want_hermes and not has_hermes:
|
|
1158
|
+
rprint(fmt.warning("Hermes requested but not detected (~/.hermes not found)"))
|
|
1105
1159
|
|
|
1106
1160
|
# --dry-run: print every file path the command would touch, then exit
|
|
1107
1161
|
# before any filesystem write happens (rf-hrtd). Built from the same
|
|
@@ -1118,6 +1172,7 @@ def init(
|
|
|
1118
1172
|
want_windsurf=want_windsurf and (has_windsurf or local),
|
|
1119
1173
|
want_continue=want_continue and (has_continue_dev or local),
|
|
1120
1174
|
want_aider=want_aider and (has_aider or local),
|
|
1175
|
+
want_hermes=want_hermes and has_hermes,
|
|
1121
1176
|
want_betterleaks=want_betterleaks,
|
|
1122
1177
|
risk_level=risk_level,
|
|
1123
1178
|
)
|
|
@@ -1312,6 +1367,18 @@ def init(
|
|
|
1312
1367
|
except Exception as e:
|
|
1313
1368
|
rprint(fmt.error(f"Failed to install Aider integration: {e}"))
|
|
1314
1369
|
|
|
1370
|
+
# Install Hermes integration if opted in (sable-gyw).
|
|
1371
|
+
# User scope only — Hermes reads ~/.hermes/config.yaml. MCP-only v0;
|
|
1372
|
+
# hooks deferred pending confirmation Hermes exposes a hook surface.
|
|
1373
|
+
hermes_ok = False
|
|
1374
|
+
if want_hermes and has_hermes:
|
|
1375
|
+
try:
|
|
1376
|
+
hermes_ok = _install_hermes_mcp(root)
|
|
1377
|
+
if hermes_ok:
|
|
1378
|
+
manager.set("agent.environments.hermes.enabled", True)
|
|
1379
|
+
except Exception as e:
|
|
1380
|
+
rprint(fmt.error(f"Failed to install Hermes integration: {e}"))
|
|
1381
|
+
|
|
1315
1382
|
# Install global instruction files for platforms that support them
|
|
1316
1383
|
_install_global_instructions(
|
|
1317
1384
|
claude_code=claude_code_ok,
|
|
@@ -1327,7 +1394,7 @@ def init(
|
|
|
1327
1394
|
rprint(fmt.success("Agent security initialized!"))
|
|
1328
1395
|
rprint()
|
|
1329
1396
|
|
|
1330
|
-
any_integration = openclaw_ok or claude_code_ok or codex_ok or gemini_ok or cursor_ok or windsurf_ok or continue_ok or aider_ok
|
|
1397
|
+
any_integration = openclaw_ok or claude_code_ok or codex_ok or gemini_ok or cursor_ok or windsurf_ok or continue_ok or aider_ok or hermes_ok
|
|
1331
1398
|
|
|
1332
1399
|
if any_integration:
|
|
1333
1400
|
rprint("Next steps:")
|
|
@@ -1372,6 +1439,8 @@ def init(
|
|
|
1372
1439
|
rprint(" rafter agent init --with-continue # Continue.dev only")
|
|
1373
1440
|
if has_aider:
|
|
1374
1441
|
rprint(" rafter agent init --with-aider # Aider only")
|
|
1442
|
+
if has_hermes:
|
|
1443
|
+
rprint(" rafter agent init --with-hermes # Hermes only")
|
|
1375
1444
|
else:
|
|
1376
1445
|
rprint("No agent environments detected. Install an agent tool and re-run with --with-<tool>.")
|
|
1377
1446
|
|
|
@@ -1424,6 +1493,68 @@ def _scan_file(file_path: str, engine: str, custom_patterns=None) -> list[ScanRe
|
|
|
1424
1493
|
return [r] if r.matches else []
|
|
1425
1494
|
|
|
1426
1495
|
|
|
1496
|
+
def _path_matches_exclude_pattern(rel_path: str, pattern: str) -> bool:
|
|
1497
|
+
"""Mirror of Node ``pathMatchesExcludePattern`` (sable-yz0).
|
|
1498
|
+
|
|
1499
|
+
Rules (any one matches → exclude):
|
|
1500
|
+
1. Exact match: ``rel_path == pattern``.
|
|
1501
|
+
2. Directory-prefix: ``rel_path`` starts with ``pattern + "/"``.
|
|
1502
|
+
Trailing ``/`` on the pattern is ignored.
|
|
1503
|
+
3. Dir-name anywhere: any segment of ``rel_path`` equals ``pattern``.
|
|
1504
|
+
Preserves the RegexScanner walker's existing dir-name behavior.
|
|
1505
|
+
4. Glob: if pattern has ``* ? [``, use fnmatch with auto-anchor
|
|
1506
|
+
(try ``pattern`` and ``**/pattern``) — mirrors Node ``matchGlob``.
|
|
1507
|
+
"""
|
|
1508
|
+
import fnmatch as _fnmatch
|
|
1509
|
+
rel = rel_path.replace("\\", "/")
|
|
1510
|
+
p = pattern.replace("\\", "/").rstrip("/")
|
|
1511
|
+
if not p:
|
|
1512
|
+
return False
|
|
1513
|
+
if rel == p:
|
|
1514
|
+
return True
|
|
1515
|
+
if rel.startswith(p + "/"):
|
|
1516
|
+
return True
|
|
1517
|
+
if p in rel.split("/"):
|
|
1518
|
+
return True
|
|
1519
|
+
if any(c in p for c in "*?["):
|
|
1520
|
+
if _fnmatch.fnmatch(rel, p):
|
|
1521
|
+
return True
|
|
1522
|
+
if not p.startswith("/") and not p.startswith("**"):
|
|
1523
|
+
if _fnmatch.fnmatch(rel, "**/" + p) or _fnmatch.fnmatch(rel, "*/" + p):
|
|
1524
|
+
return True
|
|
1525
|
+
return False
|
|
1526
|
+
|
|
1527
|
+
|
|
1528
|
+
def _apply_exclude_paths(
|
|
1529
|
+
results: list[ScanResult],
|
|
1530
|
+
exclude_paths: list[str] | None,
|
|
1531
|
+
scan_root: str,
|
|
1532
|
+
) -> list[ScanResult]:
|
|
1533
|
+
"""Strip findings whose path matches any ``scan.exclude_paths`` entry.
|
|
1534
|
+
|
|
1535
|
+
Chokepoint that fixes sable-yz0 across both scan engines AND the staged
|
|
1536
|
+
/ diff modes. See the Node ``applyExcludePaths`` docstring for the full
|
|
1537
|
+
rationale — both engines previously bypassed the policy on the
|
|
1538
|
+
happy-path, and the existing walker-level filter only matched
|
|
1539
|
+
directory NAMES, missing every multi-segment path users wrote.
|
|
1540
|
+
"""
|
|
1541
|
+
if not exclude_paths:
|
|
1542
|
+
return results
|
|
1543
|
+
root = os.path.abspath(scan_root).replace("\\", "/")
|
|
1544
|
+
kept: list[ScanResult] = []
|
|
1545
|
+
for r in results:
|
|
1546
|
+
abs_p = os.path.abspath(r.file).replace("\\", "/")
|
|
1547
|
+
if abs_p == root:
|
|
1548
|
+
rel = ""
|
|
1549
|
+
elif abs_p.startswith(root + "/"):
|
|
1550
|
+
rel = abs_p[len(root) + 1 :]
|
|
1551
|
+
else:
|
|
1552
|
+
rel = abs_p
|
|
1553
|
+
if not any(_path_matches_exclude_pattern(rel, pat) for pat in exclude_paths):
|
|
1554
|
+
kept.append(r)
|
|
1555
|
+
return kept
|
|
1556
|
+
|
|
1557
|
+
|
|
1427
1558
|
def _scan_directory(
|
|
1428
1559
|
dir_path: str,
|
|
1429
1560
|
engine: str,
|
|
@@ -1442,13 +1573,15 @@ def _scan_directory(
|
|
|
1442
1573
|
try:
|
|
1443
1574
|
bl = BetterleaksScanner()
|
|
1444
1575
|
results = bl.scan_directory(dir_path, use_git=history)
|
|
1445
|
-
|
|
1576
|
+
results = [ScanResult(file=r.file, matches=r.matches) for r in results]
|
|
1446
1577
|
except Exception:
|
|
1447
1578
|
scanner = RegexScanner(custom)
|
|
1448
|
-
|
|
1579
|
+
results = scanner.scan_directory(dir_path, exclude_paths=exclude, respect_gitignore=respect_gitignore)
|
|
1449
1580
|
else:
|
|
1450
1581
|
scanner = RegexScanner(custom)
|
|
1451
|
-
|
|
1582
|
+
results = scanner.scan_directory(dir_path, exclude_paths=exclude, respect_gitignore=respect_gitignore)
|
|
1583
|
+
# sable-yz0 — post-filter chokepoint. See _apply_exclude_paths docstring.
|
|
1584
|
+
return _apply_exclude_paths(results, exclude, dir_path)
|
|
1452
1585
|
|
|
1453
1586
|
|
|
1454
1587
|
def _output_scan_results(
|
|
@@ -2449,6 +2582,34 @@ def _check_aider() -> _CheckResult:
|
|
|
2449
2582
|
return _CheckResult(name, True, f"RAFTER.md + read: entry in {conf}")
|
|
2450
2583
|
|
|
2451
2584
|
|
|
2585
|
+
def _check_hermes() -> _CheckResult:
|
|
2586
|
+
"""Check if Hermes integration is healthy (sable-gyw).
|
|
2587
|
+
|
|
2588
|
+
Hermes uses ~/.hermes/config.yaml with a snake_case ``mcp_servers:`` block
|
|
2589
|
+
(MCP-only v0 — no hook surface confirmed yet).
|
|
2590
|
+
"""
|
|
2591
|
+
name = "Hermes"
|
|
2592
|
+
home = Path.home()
|
|
2593
|
+
hermes_dir = home / ".hermes"
|
|
2594
|
+
|
|
2595
|
+
if not hermes_dir.exists():
|
|
2596
|
+
return _CheckResult(name, False, "Not detected — run 'rafter agent init --with-hermes' to enable", optional=True)
|
|
2597
|
+
|
|
2598
|
+
config_path = hermes_dir / "config.yaml"
|
|
2599
|
+
if not config_path.exists():
|
|
2600
|
+
return _CheckResult(name, False, f"Config not found: {config_path} — run 'rafter agent init --with-hermes'", optional=True)
|
|
2601
|
+
|
|
2602
|
+
try:
|
|
2603
|
+
loaded = yaml.safe_load(config_path.read_text()) or {}
|
|
2604
|
+
except (OSError, yaml.YAMLError) as e:
|
|
2605
|
+
return _CheckResult(name, False, f"Cannot read config: {e}", optional=True)
|
|
2606
|
+
|
|
2607
|
+
servers = loaded.get("mcp_servers") if isinstance(loaded, dict) else None
|
|
2608
|
+
if not (isinstance(servers, dict) and servers.get("rafter")):
|
|
2609
|
+
return _CheckResult(name, False, "Rafter MCP server not configured — run 'rafter agent init --with-hermes'", optional=True)
|
|
2610
|
+
return _CheckResult(name, True, "MCP server configured")
|
|
2611
|
+
|
|
2612
|
+
|
|
2452
2613
|
def _probe_claude_code() -> _CheckResult:
|
|
2453
2614
|
"""Runtime probe of the Claude Code hook integration (rf-65zg).
|
|
2454
2615
|
|
|
@@ -2549,6 +2710,7 @@ def verify(
|
|
|
2549
2710
|
_check_windsurf(),
|
|
2550
2711
|
_check_continue_dev(),
|
|
2551
2712
|
_check_aider(),
|
|
2713
|
+
_check_hermes(),
|
|
2552
2714
|
]
|
|
2553
2715
|
|
|
2554
2716
|
if probe:
|
|
@@ -2882,18 +3044,24 @@ def update_betterleaks(
|
|
|
2882
3044
|
# ── agent status ─────────────────────────────────────────────────────────
|
|
2883
3045
|
|
|
2884
3046
|
@agent_app.command("status")
|
|
2885
|
-
def status(
|
|
3047
|
+
def status(
|
|
3048
|
+
json_output: bool = typer.Option(False, "--json", help="Output status as JSON"),
|
|
3049
|
+
):
|
|
2886
3050
|
"""Show agent security status dashboard."""
|
|
2887
3051
|
from ..core.config_schema import get_audit_log_path, get_rafter_dir
|
|
2888
3052
|
|
|
2889
3053
|
rafter_dir = get_rafter_dir()
|
|
2890
3054
|
audit_path = get_audit_log_path()
|
|
3055
|
+
config_path = rafter_dir / "config.json"
|
|
3056
|
+
|
|
3057
|
+
if json_output:
|
|
3058
|
+
print(json.dumps(_agent_status_json(config_path, audit_path), indent=2))
|
|
3059
|
+
return
|
|
2891
3060
|
|
|
2892
3061
|
print("Rafter Agent Status")
|
|
2893
3062
|
print("=" * 50)
|
|
2894
3063
|
|
|
2895
3064
|
# --- Config ---
|
|
2896
|
-
config_path = rafter_dir / "config.json"
|
|
2897
3065
|
if config_path.exists():
|
|
2898
3066
|
try:
|
|
2899
3067
|
cfg = ConfigManager().load()
|
|
@@ -2946,11 +3114,18 @@ def status():
|
|
|
2946
3114
|
print(f"PostToolUse: {posttool_status}")
|
|
2947
3115
|
|
|
2948
3116
|
# --- OpenClaw skill ---
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3117
|
+
# rf-zgwj moved the skill to the canonical ClawHub workspace path
|
|
3118
|
+
# (~/.openclaw/workspace/skills/rafter-security/SKILL.md) and strips the
|
|
3119
|
+
# legacy flat file. Detect via SkillManager so this matches `agent verify`
|
|
3120
|
+
# and the installer — checking the legacy path here is a false negative.
|
|
3121
|
+
skill_manager = SkillManager()
|
|
3122
|
+
if skill_manager.is_rafter_skill_installed():
|
|
3123
|
+
print(f"OpenClaw: skill installed ({skill_manager.get_rafter_skill_path()})")
|
|
3124
|
+
elif skill_manager.is_openclaw_installed():
|
|
3125
|
+
if skill_manager.has_legacy_rafter_skill():
|
|
3126
|
+
print(f"OpenClaw: legacy skill at {skill_manager.get_legacy_rafter_skill_path()} (not loaded) — run: rafter agent init --with-openclaw to migrate")
|
|
3127
|
+
else:
|
|
3128
|
+
print("OpenClaw: detected but skill missing — run: rafter agent init --with-openclaw")
|
|
2954
3129
|
else:
|
|
2955
3130
|
print("OpenClaw: not detected (optional)")
|
|
2956
3131
|
|
|
@@ -2971,6 +3146,7 @@ def status():
|
|
|
2971
3146
|
{"name": "Cursor", "flag": "--with-cursor", "config_dir": home / ".cursor", "config_file": home / ".cursor" / "mcp.json", "needle": "rafter"},
|
|
2972
3147
|
{"name": "Windsurf", "flag": "--with-windsurf", "config_dir": home / ".codeium" / "windsurf", "config_file": home / ".codeium" / "windsurf" / "mcp_config.json", "needle": "rafter"},
|
|
2973
3148
|
{"name": "Continue.dev", "flag": "--with-continue", "config_dir": home / ".continue", "config_file": home / ".continue" / "config.json", "needle": "rafter"},
|
|
3149
|
+
{"name": "Hermes", "flag": "--with-hermes", "config_dir": home / ".hermes", "config_file": home / ".hermes" / "config.yaml", "needle": "rafter"},
|
|
2974
3150
|
]
|
|
2975
3151
|
|
|
2976
3152
|
for agent in mcp_agents:
|
|
@@ -3030,6 +3206,96 @@ def status():
|
|
|
3030
3206
|
print()
|
|
3031
3207
|
|
|
3032
3208
|
|
|
3209
|
+
def _agent_status_json(config_path: Path, audit_path: Path) -> dict[str, Any]:
|
|
3210
|
+
return {
|
|
3211
|
+
"installed": config_path.exists(),
|
|
3212
|
+
"version": __version__,
|
|
3213
|
+
"agents_detected": _detect_agent_platforms(),
|
|
3214
|
+
"hooks_installed": _detect_git_hooks(),
|
|
3215
|
+
"betterleaks_available": _betterleaks_available(),
|
|
3216
|
+
"config_path": _format_home_path(config_path),
|
|
3217
|
+
"audit_log_path": _format_home_path(audit_path),
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
|
|
3221
|
+
def _detect_agent_platforms() -> list[str]:
|
|
3222
|
+
home = Path.home()
|
|
3223
|
+
candidates = [
|
|
3224
|
+
("claude-code", home / ".claude"),
|
|
3225
|
+
("openclaw", home / ".openclaw"),
|
|
3226
|
+
("codex", home / ".codex"),
|
|
3227
|
+
("gemini", home / ".gemini"),
|
|
3228
|
+
("cursor", home / ".cursor"),
|
|
3229
|
+
("windsurf", home / ".codeium" / "windsurf"),
|
|
3230
|
+
("continue", home / ".continue"),
|
|
3231
|
+
("aider", home / ".aider.conf.yml"),
|
|
3232
|
+
("hermes", home / ".hermes"),
|
|
3233
|
+
]
|
|
3234
|
+
return [name for name, path in candidates if path.exists()]
|
|
3235
|
+
|
|
3236
|
+
|
|
3237
|
+
def _detect_git_hooks() -> list[str]:
|
|
3238
|
+
hooks: set[str] = set()
|
|
3239
|
+
specs = [
|
|
3240
|
+
("pre-commit", "Rafter Security Pre-Commit Hook"),
|
|
3241
|
+
("pre-push", "Rafter Security Pre-Push Hook"),
|
|
3242
|
+
]
|
|
3243
|
+
home = Path.home()
|
|
3244
|
+
|
|
3245
|
+
for hook_name, marker in specs:
|
|
3246
|
+
if _file_contains(home / ".rafter" / "git-hooks" / hook_name, marker):
|
|
3247
|
+
hooks.add(hook_name)
|
|
3248
|
+
|
|
3249
|
+
try:
|
|
3250
|
+
git_dir = subprocess.run(
|
|
3251
|
+
["git", "rev-parse", "--git-dir"],
|
|
3252
|
+
capture_output=True,
|
|
3253
|
+
text=True,
|
|
3254
|
+
timeout=5,
|
|
3255
|
+
check=True,
|
|
3256
|
+
).stdout.strip()
|
|
3257
|
+
hooks_dir = Path(git_dir).resolve() / "hooks"
|
|
3258
|
+
for hook_name, marker in specs:
|
|
3259
|
+
if _file_contains(hooks_dir / hook_name, marker):
|
|
3260
|
+
hooks.add(hook_name)
|
|
3261
|
+
except Exception:
|
|
3262
|
+
pass
|
|
3263
|
+
|
|
3264
|
+
return sorted(hooks)
|
|
3265
|
+
|
|
3266
|
+
|
|
3267
|
+
def _betterleaks_available() -> bool:
|
|
3268
|
+
bm = BinaryManager()
|
|
3269
|
+
if shutil.which("betterleaks"):
|
|
3270
|
+
try:
|
|
3271
|
+
result = subprocess.run(
|
|
3272
|
+
["betterleaks", "version"],
|
|
3273
|
+
capture_output=True,
|
|
3274
|
+
text=True,
|
|
3275
|
+
timeout=5,
|
|
3276
|
+
)
|
|
3277
|
+
if result.returncode == 0:
|
|
3278
|
+
return True
|
|
3279
|
+
except Exception:
|
|
3280
|
+
pass
|
|
3281
|
+
return bm.get_betterleaks_path().exists() or bool(bm.find_legacy_gitleaks())
|
|
3282
|
+
|
|
3283
|
+
|
|
3284
|
+
def _file_contains(path: Path, needle: str) -> bool:
|
|
3285
|
+
try:
|
|
3286
|
+
return needle in path.read_text(encoding="utf-8")
|
|
3287
|
+
except Exception:
|
|
3288
|
+
return False
|
|
3289
|
+
|
|
3290
|
+
|
|
3291
|
+
def _format_home_path(path: Path) -> str:
|
|
3292
|
+
home = Path.home()
|
|
3293
|
+
try:
|
|
3294
|
+
return f"~/{path.relative_to(home).as_posix()}"
|
|
3295
|
+
except ValueError:
|
|
3296
|
+
return str(path)
|
|
3297
|
+
|
|
3298
|
+
|
|
3033
3299
|
# ── baseline ─────────────────────────────────────────────────────────
|
|
3034
3300
|
|
|
3035
3301
|
_BASELINE_PATH = Path.home() / ".rafter" / "baseline.json"
|
|
@@ -839,6 +839,62 @@ def _continue_mcp() -> ComponentSpec:
|
|
|
839
839
|
)
|
|
840
840
|
|
|
841
841
|
|
|
842
|
+
def _hermes_mcp() -> ComponentSpec:
|
|
843
|
+
"""Hermes MCP server entry (~/.hermes/config.yaml).
|
|
844
|
+
|
|
845
|
+
Hermes uses a YAML config with a snake_case ``mcp_servers:`` block (unlike
|
|
846
|
+
the camelCase ``mcpServers`` of Cursor/Windsurf/Claude Code). MCP-only v0 —
|
|
847
|
+
hooks deferred pending confirmation Hermes exposes a hook surface (sable-gyw).
|
|
848
|
+
"""
|
|
849
|
+
home = Path.home()
|
|
850
|
+
detect_dir = home / ".hermes"
|
|
851
|
+
config_path = detect_dir / "config.yaml"
|
|
852
|
+
|
|
853
|
+
def read_yaml() -> dict[str, Any]:
|
|
854
|
+
if not config_path.exists():
|
|
855
|
+
return {}
|
|
856
|
+
try:
|
|
857
|
+
loaded = yaml.safe_load(config_path.read_text())
|
|
858
|
+
except (OSError, yaml.YAMLError):
|
|
859
|
+
return {}
|
|
860
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
861
|
+
|
|
862
|
+
def is_installed() -> bool:
|
|
863
|
+
servers = read_yaml().get("mcp_servers")
|
|
864
|
+
return isinstance(servers, dict) and bool(servers.get("rafter"))
|
|
865
|
+
|
|
866
|
+
def install() -> None:
|
|
867
|
+
detect_dir.mkdir(parents=True, exist_ok=True)
|
|
868
|
+
cfg = read_yaml()
|
|
869
|
+
servers = cfg.get("mcp_servers")
|
|
870
|
+
if not isinstance(servers, dict):
|
|
871
|
+
servers = {}
|
|
872
|
+
cfg["mcp_servers"] = servers
|
|
873
|
+
servers["rafter"] = dict(RAFTER_MCP_ENTRY)
|
|
874
|
+
config_path.write_text(yaml.safe_dump(cfg))
|
|
875
|
+
|
|
876
|
+
def uninstall() -> None:
|
|
877
|
+
if not config_path.exists():
|
|
878
|
+
return
|
|
879
|
+
cfg = read_yaml()
|
|
880
|
+
servers = cfg.get("mcp_servers")
|
|
881
|
+
if isinstance(servers, dict) and "rafter" in servers:
|
|
882
|
+
del servers["rafter"]
|
|
883
|
+
config_path.write_text(yaml.safe_dump(cfg))
|
|
884
|
+
|
|
885
|
+
return ComponentSpec(
|
|
886
|
+
id="hermes.mcp",
|
|
887
|
+
platform="hermes",
|
|
888
|
+
kind="mcp",
|
|
889
|
+
description="Hermes MCP server entry (~/.hermes/config.yaml)",
|
|
890
|
+
detect_dir=detect_dir,
|
|
891
|
+
path=config_path,
|
|
892
|
+
is_installed=is_installed,
|
|
893
|
+
install=install,
|
|
894
|
+
uninstall=uninstall,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
|
|
842
898
|
_AIDER_LEGACY_MCP_BLOCK_RE = re.compile(
|
|
843
899
|
r"\n?#\s*Rafter security MCP server\s*\nmcp-server-command:\s*rafter\s+mcp\s+serve\s*\n?",
|
|
844
900
|
)
|
|
@@ -997,6 +1053,7 @@ def get_registry() -> list[ComponentSpec]:
|
|
|
997
1053
|
_continue_rules(),
|
|
998
1054
|
_continue_mcp(),
|
|
999
1055
|
_aider_read(),
|
|
1056
|
+
_hermes_mcp(),
|
|
1000
1057
|
_openclaw_skill(),
|
|
1001
1058
|
]
|
|
1002
1059
|
return _REGISTRY
|
|
@@ -107,6 +107,7 @@ def scan_local(
|
|
|
107
107
|
_output_sarif,
|
|
108
108
|
_watch_and_scan,
|
|
109
109
|
_apply_baseline,
|
|
110
|
+
_apply_exclude_paths,
|
|
110
111
|
_load_baseline_entries,
|
|
111
112
|
)
|
|
112
113
|
from ..core.config_manager import ConfigManager
|
|
@@ -164,6 +165,9 @@ def scan_local(
|
|
|
164
165
|
resolved = os.path.join(repo_root, f)
|
|
165
166
|
if os.path.isfile(resolved):
|
|
166
167
|
all_results.extend(_scan_file(resolved, eng, custom_patterns))
|
|
168
|
+
# sable-yz0 — honor scan.exclude_paths in --diff mode too.
|
|
169
|
+
exclude = scan_cfg.exclude_paths if scan_cfg else None
|
|
170
|
+
all_results = _apply_exclude_paths(all_results, exclude, repo_root)
|
|
167
171
|
filtered = _apply_baseline(all_results, baseline_entries)
|
|
168
172
|
_output_scan_results(filtered, json_output, quiet, f"files changed since {diff}", format=format, suppressions=suppressions)
|
|
169
173
|
return
|
|
@@ -201,6 +205,9 @@ def scan_local(
|
|
|
201
205
|
resolved = os.path.join(repo_root, f)
|
|
202
206
|
if os.path.isfile(resolved):
|
|
203
207
|
all_results.extend(_scan_file(resolved, eng, custom_patterns))
|
|
208
|
+
# sable-yz0 — honor scan.exclude_paths in --staged mode too.
|
|
209
|
+
exclude = scan_cfg.exclude_paths if scan_cfg else None
|
|
210
|
+
all_results = _apply_exclude_paths(all_results, exclude, repo_root)
|
|
204
211
|
filtered = _apply_baseline(all_results, baseline_entries)
|
|
205
212
|
_output_scan_results(filtered, json_output, quiet, "staged files", format=format, suppressions=suppressions)
|
|
206
213
|
return
|
|
@@ -9,20 +9,41 @@ from pathlib import Path
|
|
|
9
9
|
from ..utils.git import get_git_root
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
#: Policy file candidates, in precedence order (sable-c1c).
|
|
13
|
+
#:
|
|
14
|
+
#: The cloud scanner (rafter-backend) reads ``.rafter/config.yml`` (subdir
|
|
15
|
+
#: + ``config.yml``), while the CLI canonical is ``.rafter.yml``. The CLI
|
|
16
|
+
#: reads both indefinitely so customers writing either shape get the same
|
|
17
|
+
#: behavior locally. Canonical dotfile takes precedence; backend file is
|
|
18
|
+
#: the fallback. Schema compatibility (top-level ``exclude_paths`` /
|
|
19
|
+
#: ``custom_patterns`` vs nested ``scan.*``) is handled in ``_map_policy``.
|
|
20
|
+
POLICY_FILE_CANDIDATES: list[Path] = [
|
|
21
|
+
Path(".rafter.yml"),
|
|
22
|
+
Path(".rafter.yaml"),
|
|
23
|
+
Path(".rafter") / "config.yml",
|
|
24
|
+
Path(".rafter") / "config.yaml",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Back-compat alias for code outside this module that imported the previous
|
|
28
|
+
# constant. Resolves to the canonical-dotfile names only (subset).
|
|
12
29
|
POLICY_FILENAMES = [".rafter.yml", ".rafter.yaml"]
|
|
13
30
|
_KNOWN_DOC_KEYS = {"id", "path", "url", "description", "tags", "cache"}
|
|
14
31
|
|
|
15
32
|
|
|
16
33
|
def find_policy_file() -> Path | None:
|
|
17
|
-
"""Walk from cwd up to git root looking for a policy file.
|
|
34
|
+
"""Walk from cwd up to git root looking for a policy file.
|
|
35
|
+
|
|
36
|
+
Returns the first candidate that exists, in the precedence order
|
|
37
|
+
declared by ``POLICY_FILE_CANDIDATES``.
|
|
38
|
+
"""
|
|
18
39
|
cwd = Path.cwd()
|
|
19
40
|
root = get_git_root()
|
|
20
41
|
stop = Path(root) if root else cwd.anchor and Path(cwd.anchor)
|
|
21
42
|
|
|
22
43
|
current = cwd
|
|
23
44
|
while True:
|
|
24
|
-
for
|
|
25
|
-
candidate = current /
|
|
45
|
+
for candidate_rel in POLICY_FILE_CANDIDATES:
|
|
46
|
+
candidate = current / candidate_rel
|
|
26
47
|
if candidate.exists():
|
|
27
48
|
return candidate
|
|
28
49
|
parent = current.parent
|
|
@@ -85,6 +106,22 @@ def _map_policy(raw: dict) -> dict:
|
|
|
85
106
|
for p in scan["custom_patterns"]
|
|
86
107
|
]
|
|
87
108
|
|
|
109
|
+
# sable-c1c — backend flat-shape compat. rafter-backend reads
|
|
110
|
+
# exclude_paths / custom_patterns at the top level (no `scan:` nesting),
|
|
111
|
+
# so customers writing the backend shape get the same behavior locally.
|
|
112
|
+
# Nested form takes precedence if both are present in the same file.
|
|
113
|
+
if "scan" not in policy or not policy["scan"].get("exclude_paths"):
|
|
114
|
+
if isinstance(raw.get("exclude_paths"), list):
|
|
115
|
+
policy.setdefault("scan", {})
|
|
116
|
+
policy["scan"]["exclude_paths"] = raw["exclude_paths"]
|
|
117
|
+
if "scan" not in policy or not policy["scan"].get("custom_patterns"):
|
|
118
|
+
if isinstance(raw.get("custom_patterns"), list):
|
|
119
|
+
policy.setdefault("scan", {})
|
|
120
|
+
policy["scan"]["custom_patterns"] = [
|
|
121
|
+
{"name": p.get("name", ""), "regex": p.get("regex", ""), "severity": p.get("severity", "high")}
|
|
122
|
+
for p in raw["custom_patterns"]
|
|
123
|
+
]
|
|
124
|
+
|
|
88
125
|
ignore = raw.get("ignore")
|
|
89
126
|
if isinstance(ignore, list):
|
|
90
127
|
rules: list[dict] = []
|
|
@@ -179,7 +216,11 @@ def _derive_doc_id(source: str, kind: str) -> str:
|
|
|
179
216
|
return hashlib.sha256(source.encode("utf-8")).hexdigest()[:8]
|
|
180
217
|
|
|
181
218
|
|
|
182
|
-
_VALID_TOP_LEVEL_KEYS = {
|
|
219
|
+
_VALID_TOP_LEVEL_KEYS = {
|
|
220
|
+
"version", "risk_level", "command_policy", "scan", "ignore", "audit", "docs",
|
|
221
|
+
# sable-c1c — backend flat-shape compat keys.
|
|
222
|
+
"exclude_paths", "custom_patterns",
|
|
223
|
+
}
|
|
183
224
|
_VALID_RISK_LEVELS = {"minimal", "moderate", "aggressive"}
|
|
184
225
|
_VALID_COMMAND_MODES = {"allow-all", "approve-dangerous", "deny-list"}
|
|
185
226
|
_VALID_LOG_LEVELS = {"debug", "info", "warn", "error"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rafter-security
|
|
3
3
|
description: Security toolkit for AI workflows. Use when scanning code or repos for vulnerabilities, auditing third-party skills/MCPs/agent configs before installing, evaluating shell commands before running them, or generating secure design questions for new features. Provides `rafter run` (remote SAST + SCA, needs RAFTER_API_KEY), `rafter secrets` (offline secrets-only), `rafter agent exec --dry-run` (command-risk classification), and `rafter skill review`.
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.4
|
|
5
5
|
homepage: https://rafter.so
|
|
6
6
|
metadata:
|
|
7
7
|
openclaw:
|
|
@@ -114,5 +114,5 @@ export RAFTER_API_KEY="..." # or put it in .env
|
|
|
114
114
|
|
|
115
115
|
Without a key, only `rafter secrets` works — that's secret-hygiene, not code review. If security matters for the task, flag the missing key to the user rather than silently accepting the narrower scan.
|
|
116
116
|
|
|
117
|
-
##
|
|
118
|
-
Not wired
|
|
117
|
+
## Setup
|
|
118
|
+
Not installed? `npm i -g @rafter-security/cli` (Node) or `pip install rafter-cli` (Python). `npx` form is `npx @rafter-security/cli` — the bare `npx rafter-cli` resolves to an unrelated package. Inside Cursor's sandbox or any session where writing `~/.rafter` triggers a prompt, prefer `rafter agent init --local --with-<platform>` — writes `./.rafter/` + `./.<platform>/` instead of `$HOME`. Not wired yet? `rafter agent install-hook` (pre-commit), `rafter ci init` (CI), `.rafter.yml` (policy). Per-platform: `rafter brief setup/<platform>`.
|
|
@@ -88,6 +88,13 @@ DEFAULT_SECRET_PATTERNS: list[Pattern] = [
|
|
|
88
88
|
severity="critical",
|
|
89
89
|
description="Twilio API Key detected",
|
|
90
90
|
),
|
|
91
|
+
# HashiCorp Vault
|
|
92
|
+
Pattern(
|
|
93
|
+
name="HashiCorp Vault Token",
|
|
94
|
+
regex=r"hvs\.[a-zA-Z0-9_-]{90,}",
|
|
95
|
+
severity="critical",
|
|
96
|
+
description="HashiCorp Vault service token detected",
|
|
97
|
+
),
|
|
91
98
|
# Generic
|
|
92
99
|
Pattern(
|
|
93
100
|
name="Generic API Key",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Skill manager for OpenClaw integration — Python port of Node skill-manager.ts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SkillManager:
|
|
8
|
+
"""Manage OpenClaw skill installation and detection.
|
|
9
|
+
|
|
10
|
+
OpenClaw auto-discovers ClawHub-shaped skills from
|
|
11
|
+
``<workspace>/skills/<skill>/SKILL.md`` (default workspace
|
|
12
|
+
``~/.openclaw/workspace/``). rafter ≤ 0.7.7 wrote a loose
|
|
13
|
+
``~/.openclaw/skills/<name>.md`` file that OpenClaw never read; the canonical
|
|
14
|
+
path was adopted in rf-zgwj. Detection here must match the installer and
|
|
15
|
+
``agent verify`` — checking the legacy path is a false negative.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def get_openclaw_root(self) -> Path:
|
|
19
|
+
return Path.home() / ".openclaw"
|
|
20
|
+
|
|
21
|
+
def get_openclaw_skills_dir(self) -> Path:
|
|
22
|
+
return self.get_openclaw_root() / "workspace" / "skills"
|
|
23
|
+
|
|
24
|
+
def get_rafter_skill_dir(self) -> Path:
|
|
25
|
+
return self.get_openclaw_skills_dir() / "rafter-security"
|
|
26
|
+
|
|
27
|
+
def get_rafter_skill_path(self) -> Path:
|
|
28
|
+
return self.get_rafter_skill_dir() / "SKILL.md"
|
|
29
|
+
|
|
30
|
+
def get_legacy_rafter_skill_path(self) -> Path:
|
|
31
|
+
"""Legacy install path used by rafter ≤ 0.7.7. Removed on reinstall."""
|
|
32
|
+
return self.get_openclaw_root() / "skills" / "rafter-security.md"
|
|
33
|
+
|
|
34
|
+
def is_openclaw_installed(self) -> bool:
|
|
35
|
+
"""Detect the platform root (~/.openclaw). A fresh OpenClaw install has
|
|
36
|
+
no workspace skills dir yet, so checking the skills dir gives a
|
|
37
|
+
false-negative until at least one skill is written."""
|
|
38
|
+
return self.get_openclaw_root().exists()
|
|
39
|
+
|
|
40
|
+
def has_legacy_rafter_skill(self) -> bool:
|
|
41
|
+
"""True when the rafter ≤ 0.7.7 flat file is present (migration note)."""
|
|
42
|
+
return self.get_legacy_rafter_skill_path().exists()
|
|
43
|
+
|
|
44
|
+
def is_rafter_skill_installed(self) -> bool:
|
|
45
|
+
return self.get_rafter_skill_path().exists()
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"""Skill manager for OpenClaw integration — Python port of Node skill-manager.ts."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import importlib.resources
|
|
5
|
-
import re
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class SkillManager:
|
|
10
|
-
"""Manage OpenClaw skill installation and detection."""
|
|
11
|
-
|
|
12
|
-
def get_openclaw_skills_dir(self) -> Path:
|
|
13
|
-
return Path.home() / ".openclaw" / "skills"
|
|
14
|
-
|
|
15
|
-
def get_rafter_skill_path(self) -> Path:
|
|
16
|
-
return self.get_openclaw_skills_dir() / "rafter-security.md"
|
|
17
|
-
|
|
18
|
-
def is_openclaw_installed(self) -> bool:
|
|
19
|
-
return self.get_openclaw_skills_dir().exists()
|
|
20
|
-
|
|
21
|
-
def is_rafter_skill_installed(self) -> bool:
|
|
22
|
-
return self.get_rafter_skill_path().exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-code-review.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-secure-design.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/continue-rules/rafter-skill-review.md
RENAMED
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/cli-reference.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter/docs/finding-triage.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/SKILL.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/api.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-code-review.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md
RENAMED
|
File without changes
|
{rafter_cli-0.8.2 → rafter_cli-0.8.4}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|