@rafter-security/cli 0.6.5 → 0.7.0
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.
- package/README.md +25 -25
- package/dist/commands/agent/audit-skill.js +20 -19
- package/dist/commands/agent/config.js +2 -1
- package/dist/commands/agent/exec.js +2 -0
- package/dist/commands/agent/index.js +2 -0
- package/dist/commands/agent/init-project.js +164 -0
- package/dist/commands/agent/init.js +276 -20
- package/dist/commands/agent/install-hook.js +15 -14
- package/dist/commands/agent/instruction-block.js +63 -0
- package/dist/commands/agent/scan.js +4 -3
- package/dist/commands/agent/verify.js +1 -1
- package/dist/commands/backend/run.js +12 -3
- package/dist/commands/backend/scan-status.js +3 -2
- package/dist/commands/brief.js +39 -2
- package/dist/commands/ci/init.js +26 -22
- package/dist/commands/completion.js +4 -3
- package/dist/commands/hook/posttool.js +95 -10
- package/dist/commands/hook/pretool.js +105 -10
- package/dist/commands/mcp/server.js +5 -5
- package/dist/commands/notify.js +278 -0
- package/dist/commands/report.js +274 -0
- package/dist/commands/scan/index.js +7 -5
- package/dist/core/risk-rules.js +18 -3
- package/dist/index.js +20 -10
- package/dist/scanners/gitleaks.js +14 -4
- package/dist/scanners/secret-patterns.js +1 -1
- package/package.json +2 -1
- package/resources/pre-commit-hook.sh +0 -5
- package/resources/rafter-security-skill.md +1 -1
- package/resources/skills/rafter/SKILL.md +25 -6
- package/resources/skills/rafter-agent-security/SKILL.md +25 -35
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { API, writePayload, EXIT_GENERAL_ERROR, EXIT_SCAN_NOT_FOUND } from "../../utils/api.js";
|
|
4
|
+
import { fmt as output } from "../../utils/formatter.js";
|
|
4
5
|
export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
5
6
|
// First poll
|
|
6
7
|
let poll;
|
|
@@ -9,10 +10,10 @@ export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
|
9
10
|
}
|
|
10
11
|
catch (e) {
|
|
11
12
|
if (e.response?.status === 404) {
|
|
12
|
-
console.error(`Scan '${scan_id}' not found`);
|
|
13
|
+
console.error(output.error(`Scan '${scan_id}' not found`));
|
|
13
14
|
return EXIT_SCAN_NOT_FOUND;
|
|
14
15
|
}
|
|
15
|
-
console.error(
|
|
16
|
+
console.error(output.error(`${e.response?.data || e.message}`));
|
|
16
17
|
return EXIT_GENERAL_ERROR;
|
|
17
18
|
}
|
|
18
19
|
let status = poll.data.status;
|
package/dist/commands/brief.js
CHANGED
|
@@ -54,7 +54,7 @@ function buildTopics() {
|
|
|
54
54
|
render: () => loadSkill("rafter-agent-security"),
|
|
55
55
|
},
|
|
56
56
|
scanning: {
|
|
57
|
-
description: "Remote SAST/SCA code analysis via
|
|
57
|
+
description: "Remote SAST/SCA code analysis via Rafter API",
|
|
58
58
|
render: () => loadSkill("rafter"),
|
|
59
59
|
},
|
|
60
60
|
commands: {
|
|
@@ -78,7 +78,7 @@ function buildTopics() {
|
|
|
78
78
|
return [
|
|
79
79
|
"# Rafter Command Reference",
|
|
80
80
|
"",
|
|
81
|
-
"##
|
|
81
|
+
"## Remote Code Analysis",
|
|
82
82
|
"",
|
|
83
83
|
backCmds,
|
|
84
84
|
"",
|
|
@@ -128,6 +128,43 @@ function buildTopics() {
|
|
|
128
128
|
description: "Setup instructions for unsupported / generic agents",
|
|
129
129
|
render: () => renderPlatformSetup("generic"),
|
|
130
130
|
},
|
|
131
|
+
pricing: {
|
|
132
|
+
description: "What's free, what's paid, and the philosophy behind it",
|
|
133
|
+
render: () => [
|
|
134
|
+
"# Rafter Pricing",
|
|
135
|
+
"",
|
|
136
|
+
"**Free forever for individuals and open source. No account required. No telemetry.**",
|
|
137
|
+
"",
|
|
138
|
+
"## What's Free",
|
|
139
|
+
"",
|
|
140
|
+
"All local agent security features are free with no limits:",
|
|
141
|
+
"",
|
|
142
|
+
"- Secret scanning (21+ patterns, Gitleaks integration)",
|
|
143
|
+
"- Pre-commit hooks (local and global)",
|
|
144
|
+
"- Command interception with risk-tiered approval",
|
|
145
|
+
"- Skill/extension auditing",
|
|
146
|
+
"- Audit logging",
|
|
147
|
+
"- MCP server for tool integration",
|
|
148
|
+
"- CI/CD pipeline generation",
|
|
149
|
+
"- All supported agent integrations (Claude Code, Codex, Gemini, Cursor, Windsurf, Aider, OpenClaw, Continue.dev)",
|
|
150
|
+
"",
|
|
151
|
+
"No API key. No sign-up. No telemetry. No data collection. No network access required.",
|
|
152
|
+
"Everything runs locally on your machine. MIT licensed.",
|
|
153
|
+
"",
|
|
154
|
+
"## Remote Code Analysis (API)",
|
|
155
|
+
"",
|
|
156
|
+
"Remote SAST/SCA scanning via the Rafter API has a free tier.",
|
|
157
|
+
"Sign up at rafter.so for an API key. Enterprise plans offer higher",
|
|
158
|
+
"limits, dashboards, policy management, and compliance reporting.",
|
|
159
|
+
"",
|
|
160
|
+
"## Philosophy",
|
|
161
|
+
"",
|
|
162
|
+
"Security tooling should be free for the people writing code.",
|
|
163
|
+
"Generous free tiers drive bottom-up adoption. Enterprise value",
|
|
164
|
+
"comes from dashboards, policy, and compliance — not from gating",
|
|
165
|
+
"the tools developers use every day.",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
},
|
|
131
168
|
all: {
|
|
132
169
|
description: "Everything — full security + scanning + setup briefing",
|
|
133
170
|
render: () => {
|
package/dist/commands/ci/init.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
import { fmt, isAgentMode } from "../../utils/formatter.js";
|
|
5
5
|
export function createCiInitCommand() {
|
|
6
6
|
return new Command("init")
|
|
7
7
|
.description("Generate CI/CD pipeline config for secret scanning")
|
|
8
8
|
.option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
|
|
9
9
|
.option("--output <path>", "Output file path (default: platform-specific)")
|
|
10
|
-
.option("--with-
|
|
10
|
+
.option("--with-remote", "Include remote security audit job (requires RAFTER_API_KEY)")
|
|
11
|
+
.option("--with-backend", "Deprecated: use --with-remote")
|
|
11
12
|
.action((opts) => {
|
|
12
13
|
const platform = opts.platform || detectPlatform();
|
|
13
14
|
if (!platform) {
|
|
@@ -21,7 +22,8 @@ export function createCiInitCommand() {
|
|
|
21
22
|
console.error(`Valid options: ${validPlatforms.join(", ")}`);
|
|
22
23
|
process.exit(1);
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
+
const includeRemote = !!(opts.includeRemote || opts.withBackend);
|
|
26
|
+
const { content, defaultPath } = generateTemplate(platform, includeRemote);
|
|
25
27
|
const outputPath = opts.output || defaultPath;
|
|
26
28
|
const outputDir = path.dirname(outputPath);
|
|
27
29
|
if (!fs.existsSync(outputDir)) {
|
|
@@ -29,28 +31,30 @@ export function createCiInitCommand() {
|
|
|
29
31
|
}
|
|
30
32
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
31
33
|
console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (!isAgentMode()) {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log("Next steps:");
|
|
37
|
+
console.log(` 1. Review the generated file: ${outputPath}`);
|
|
38
|
+
if (includeRemote) {
|
|
39
|
+
if (platform === "github") {
|
|
40
|
+
console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
|
|
41
|
+
}
|
|
42
|
+
else if (platform === "gitlab") {
|
|
43
|
+
console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(" 2. Add RAFTER_API_KEY to project environment variables");
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
console.log(` ${includeRemote ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
50
|
+
if (platform === "github") {
|
|
51
|
+
console.log();
|
|
52
|
+
console.log("Alternatives:");
|
|
53
|
+
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
|
|
54
|
+
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
44
55
|
}
|
|
45
|
-
}
|
|
46
|
-
console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
47
|
-
if (platform === "github") {
|
|
48
56
|
console.log();
|
|
49
|
-
console.log("Alternatives:");
|
|
50
|
-
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v0");
|
|
51
|
-
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
52
57
|
}
|
|
53
|
-
console.log();
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
function detectPlatform() {
|
|
@@ -74,7 +78,7 @@ function generateTemplate(platform, withBackend) {
|
|
|
74
78
|
}
|
|
75
79
|
function githubTemplate(withBackend) {
|
|
76
80
|
let yaml = `# Generated by: rafter ci init
|
|
77
|
-
# Alternative: uses: Raftersecurity/rafter-cli@
|
|
81
|
+
# Alternative: uses: Raftersecurity/rafter-cli@v1
|
|
78
82
|
name: Rafter Security
|
|
79
83
|
|
|
80
84
|
on:
|
|
@@ -68,7 +68,7 @@ _rafter_completions() {
|
|
|
68
68
|
if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
|
|
69
69
|
COMPREPLY=( $(compgen -W "--risk-level --with-openclaw --with-claude-code --with-codex --with-gemini --with-aider --with-cursor --with-windsurf --with-continue --with-gitleaks --all --help" -- "\${cur}") )
|
|
70
70
|
elif [[ "\${COMP_WORDS[1]}" == "ci" ]]; then
|
|
71
|
-
COMPREPLY=( $(compgen -W "--platform --output --with-backend --help" -- "\${cur}") )
|
|
71
|
+
COMPREPLY=( $(compgen -W "--platform --output --with-remote --with-backend --help" -- "\${cur}") )
|
|
72
72
|
fi
|
|
73
73
|
return 0
|
|
74
74
|
;;
|
|
@@ -81,7 +81,7 @@ const ZSH_COMPLETION = `#compdef rafter
|
|
|
81
81
|
_rafter() {
|
|
82
82
|
local -a commands
|
|
83
83
|
commands=(
|
|
84
|
-
'run:Submit a security scan
|
|
84
|
+
'run:Submit a remote security scan'
|
|
85
85
|
'scan:Alias for run'
|
|
86
86
|
'get:Retrieve scan results'
|
|
87
87
|
'usage:Check API usage quota'
|
|
@@ -369,7 +369,8 @@ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcom
|
|
|
369
369
|
complete -c rafter -n '__fish_seen_subcommand_from ci' -a init -d 'Initialize CI pipeline'
|
|
370
370
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l platform -d 'CI platform' -ra 'github gitlab circleci'
|
|
371
371
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l output -d 'Output path' -r
|
|
372
|
-
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-
|
|
372
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-remote -d 'Include remote audit'
|
|
373
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Deprecated: use --with-remote'
|
|
373
374
|
|
|
374
375
|
# hook subcommands
|
|
375
376
|
complete -c rafter -n '__fish_seen_subcommand_from hook' -a pretool -d 'PreToolUse hook handler'
|
|
@@ -4,31 +4,71 @@ import { AuditLogger } from "../../core/audit-logger.js";
|
|
|
4
4
|
export function createHookPosttoolCommand() {
|
|
5
5
|
return new Command("posttool")
|
|
6
6
|
.description("PostToolUse hook handler (reads stdin, redacts secrets in output, writes JSON to stdout)")
|
|
7
|
-
.
|
|
7
|
+
.option("--format <format>", "Output format: claude (default, also Codex/Continue), cursor, gemini, windsurf", "claude")
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
const format = (opts.format || "claude");
|
|
8
10
|
try {
|
|
9
11
|
const input = await readStdin();
|
|
10
|
-
let
|
|
12
|
+
let raw;
|
|
11
13
|
try {
|
|
12
|
-
|
|
14
|
+
raw = JSON.parse(input);
|
|
13
15
|
}
|
|
14
16
|
catch {
|
|
15
|
-
writeOutput({ action: "continue" });
|
|
17
|
+
writeOutput({ action: "continue" }, format);
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
18
20
|
// Validate payload is an object with expected shape
|
|
19
|
-
if (!
|
|
20
|
-
writeOutput({ action: "continue" });
|
|
21
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
22
|
+
writeOutput({ action: "continue" }, format);
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
25
|
+
const payload = normalizePostInput(raw, format);
|
|
23
26
|
const output = evaluateToolResponse(payload);
|
|
24
|
-
writeOutput(output);
|
|
27
|
+
writeOutput(output, format);
|
|
25
28
|
}
|
|
26
29
|
catch {
|
|
27
30
|
// Any unexpected error → fail open
|
|
28
|
-
writeOutput({ action: "continue" });
|
|
31
|
+
writeOutput({ action: "continue" }, format);
|
|
29
32
|
}
|
|
30
33
|
});
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize platform-specific PostToolUse stdin into common shape.
|
|
37
|
+
* Windsurf sends { tool_info: { stdout, stderr } }, Cursor sends { output, ... }.
|
|
38
|
+
*/
|
|
39
|
+
function normalizePostInput(raw, format) {
|
|
40
|
+
if (format === "windsurf") {
|
|
41
|
+
const toolInfo = raw.tool_info || {};
|
|
42
|
+
return {
|
|
43
|
+
session_id: raw.trajectory_id,
|
|
44
|
+
tool_name: raw.agent_action_name?.includes("run_command") ? "Bash" : (toolInfo.mcp_tool_name || "unknown"),
|
|
45
|
+
tool_input: {},
|
|
46
|
+
tool_response: {
|
|
47
|
+
output: toolInfo.stdout || toolInfo.output || "",
|
|
48
|
+
error: toolInfo.stderr || "",
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (format === "cursor") {
|
|
53
|
+
return {
|
|
54
|
+
session_id: raw.conversation_id,
|
|
55
|
+
tool_name: raw.hook_event_name === "afterShellExecution" ? "Bash" : (raw.tool_name || "unknown"),
|
|
56
|
+
tool_input: raw.tool_input || {},
|
|
57
|
+
tool_response: {
|
|
58
|
+
output: raw.output || raw.tool_response?.output || "",
|
|
59
|
+
content: raw.content || raw.tool_response?.content || "",
|
|
60
|
+
error: raw.error || raw.tool_response?.error || "",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Claude, Codex, Continue, Gemini — same shape
|
|
65
|
+
return {
|
|
66
|
+
session_id: raw.session_id,
|
|
67
|
+
tool_name: raw.tool_name || "",
|
|
68
|
+
tool_input: raw.tool_input || {},
|
|
69
|
+
tool_response: raw.tool_response,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
32
72
|
function evaluateToolResponse(payload) {
|
|
33
73
|
const { tool_response } = payload;
|
|
34
74
|
// No response body — pass through
|
|
@@ -82,6 +122,51 @@ function readStdin() {
|
|
|
82
122
|
process.stdin.resume();
|
|
83
123
|
});
|
|
84
124
|
}
|
|
85
|
-
function writeOutput(output) {
|
|
86
|
-
|
|
125
|
+
function writeOutput(output, format) {
|
|
126
|
+
const isModify = output.action === "modify" && output.tool_response;
|
|
127
|
+
switch (format) {
|
|
128
|
+
case "cursor": {
|
|
129
|
+
// Cursor: { agentMessage?: string } for post-tool notifications
|
|
130
|
+
if (isModify) {
|
|
131
|
+
process.stdout.write(JSON.stringify({
|
|
132
|
+
agentMessage: "Rafter redacted secrets from tool output",
|
|
133
|
+
}) + "\n");
|
|
134
|
+
}
|
|
135
|
+
// No output for continue (noop)
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case "gemini": {
|
|
139
|
+
// Gemini AfterTool: { systemMessage?: string } or {}
|
|
140
|
+
if (isModify) {
|
|
141
|
+
process.stdout.write(JSON.stringify({
|
|
142
|
+
systemMessage: "Rafter redacted secrets from tool output",
|
|
143
|
+
}) + "\n");
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
process.stdout.write("{}\n");
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
case "windsurf": {
|
|
151
|
+
// Windsurf: exit 0 for continue, stderr for notification
|
|
152
|
+
if (isModify) {
|
|
153
|
+
process.stderr.write("Rafter: secrets redacted from tool output\n");
|
|
154
|
+
}
|
|
155
|
+
// Always exit 0 for post-tool (never block after execution)
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
default: {
|
|
159
|
+
// Claude Code / Codex / Continue.dev: hookSpecificOutput envelope
|
|
160
|
+
const hookOutput = {
|
|
161
|
+
hookSpecificOutput: {
|
|
162
|
+
hookEventName: "PostToolUse",
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
if (isModify) {
|
|
166
|
+
hookOutput.hookSpecificOutput.modifiedToolResult = output.tool_response;
|
|
167
|
+
}
|
|
168
|
+
process.stdout.write(JSON.stringify(hookOutput) + "\n");
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
87
172
|
}
|
|
@@ -29,32 +29,81 @@ function formatApprovalMessage(command, evaluation) {
|
|
|
29
29
|
export function createHookPretoolCommand() {
|
|
30
30
|
return new Command("pretool")
|
|
31
31
|
.description("PreToolUse hook handler (reads stdin, writes JSON decision to stdout)")
|
|
32
|
-
.
|
|
32
|
+
.option("--format <format>", "Output format: claude (default, also Codex/Continue), cursor, gemini, windsurf", "claude")
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
const format = (opts.format || "claude");
|
|
33
35
|
try {
|
|
34
36
|
const input = await readStdin();
|
|
35
|
-
let
|
|
37
|
+
let raw;
|
|
36
38
|
try {
|
|
37
|
-
|
|
39
|
+
raw = JSON.parse(input);
|
|
38
40
|
}
|
|
39
41
|
catch {
|
|
40
42
|
// Can't parse → fail open
|
|
41
|
-
writeDecision({ decision: "allow" });
|
|
43
|
+
writeDecision({ decision: "allow" }, format);
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
// Validate payload is an object with expected shape
|
|
45
|
-
if (!
|
|
46
|
-
writeDecision({ decision: "allow" });
|
|
47
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
48
|
+
writeDecision({ decision: "allow" }, format);
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
51
|
+
const payload = normalizeInput(raw, format);
|
|
49
52
|
const decision = evaluateToolCall(payload);
|
|
50
|
-
writeDecision(decision);
|
|
53
|
+
writeDecision(decision, format);
|
|
51
54
|
}
|
|
52
55
|
catch {
|
|
53
56
|
// Any unexpected error → fail open
|
|
54
|
-
writeDecision({ decision: "allow" });
|
|
57
|
+
writeDecision({ decision: "allow" }, format);
|
|
55
58
|
}
|
|
56
59
|
});
|
|
57
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Normalize platform-specific stdin JSON into a common HookInput shape.
|
|
63
|
+
*
|
|
64
|
+
* Claude/Codex/Continue: { tool_name, tool_input: { command } }
|
|
65
|
+
* Cursor: { hook_event_name, command, cwd }
|
|
66
|
+
* Gemini: { tool_name, tool_input: { command } } (same as Claude)
|
|
67
|
+
* Windsurf: { agent_action_name, tool_info: { command_line, cwd } }
|
|
68
|
+
*/
|
|
69
|
+
function normalizeInput(raw, format) {
|
|
70
|
+
if (format === "cursor") {
|
|
71
|
+
// Cursor sends { command, cwd, hook_event_name, ... }
|
|
72
|
+
const command = raw.command || "";
|
|
73
|
+
const eventName = raw.hook_event_name || "";
|
|
74
|
+
// beforeShellExecution → Bash, beforeMCPExecution → tool name from payload
|
|
75
|
+
const toolName = eventName === "beforeShellExecution" ? "Bash"
|
|
76
|
+
: eventName === "beforeReadFile" ? "Read"
|
|
77
|
+
: eventName === "afterFileEdit" ? "Write"
|
|
78
|
+
: raw.tool_name || "unknown";
|
|
79
|
+
return {
|
|
80
|
+
session_id: raw.conversation_id,
|
|
81
|
+
tool_name: toolName,
|
|
82
|
+
tool_input: eventName === "beforeShellExecution" ? { command } : (raw.tool_input || {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (format === "windsurf") {
|
|
86
|
+
// Windsurf sends { agent_action_name, tool_info: { command_line, cwd } }
|
|
87
|
+
const toolInfo = raw.tool_info || {};
|
|
88
|
+
const actionName = raw.agent_action_name || "";
|
|
89
|
+
const toolName = actionName.includes("run_command") ? "Bash"
|
|
90
|
+
: actionName.includes("write_code") ? "Write"
|
|
91
|
+
: actionName.includes("read_code") ? "Read"
|
|
92
|
+
: actionName.includes("mcp_tool_use") ? (toolInfo.mcp_tool_name || "unknown")
|
|
93
|
+
: "unknown";
|
|
94
|
+
return {
|
|
95
|
+
session_id: raw.trajectory_id,
|
|
96
|
+
tool_name: toolName,
|
|
97
|
+
tool_input: toolName === "Bash" ? { command: toolInfo.command_line || "" } : toolInfo,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// Claude, Codex, Continue, Gemini — all use { tool_name, tool_input }
|
|
101
|
+
return {
|
|
102
|
+
session_id: raw.session_id,
|
|
103
|
+
tool_name: raw.tool_name || "",
|
|
104
|
+
tool_input: raw.tool_input || {},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
58
107
|
function evaluateToolCall(payload) {
|
|
59
108
|
const { tool_name, tool_input } = payload;
|
|
60
109
|
if (tool_name === "Bash") {
|
|
@@ -154,6 +203,52 @@ function readStdin() {
|
|
|
154
203
|
process.stdin.resume();
|
|
155
204
|
});
|
|
156
205
|
}
|
|
157
|
-
function writeDecision(decision) {
|
|
158
|
-
|
|
206
|
+
function writeDecision(decision, format) {
|
|
207
|
+
const isDeny = decision.decision === "deny";
|
|
208
|
+
const reason = decision.reason ?? "";
|
|
209
|
+
switch (format) {
|
|
210
|
+
case "cursor": {
|
|
211
|
+
// Cursor: { permission: "allow"|"deny"|"ask", agentMessage?, userMessage? }
|
|
212
|
+
const output = {
|
|
213
|
+
permission: isDeny ? "deny" : "allow",
|
|
214
|
+
};
|
|
215
|
+
if (isDeny && reason) {
|
|
216
|
+
output.agentMessage = reason;
|
|
217
|
+
output.userMessage = reason;
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case "gemini": {
|
|
223
|
+
// Gemini: {} for allow, { decision: "deny", reason: "..." } for deny
|
|
224
|
+
if (isDeny) {
|
|
225
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
process.stdout.write("{}\n");
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
case "windsurf": {
|
|
233
|
+
// Windsurf: exit 0 for allow, exit 2 + stderr for deny
|
|
234
|
+
if (isDeny) {
|
|
235
|
+
process.stderr.write(reason + "\n");
|
|
236
|
+
process.exit(2);
|
|
237
|
+
}
|
|
238
|
+
// Allow: exit 0 (no output needed)
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
default: {
|
|
242
|
+
// Claude Code / Codex / Continue.dev: hookSpecificOutput envelope
|
|
243
|
+
const output = {
|
|
244
|
+
hookSpecificOutput: {
|
|
245
|
+
hookEventName: "PreToolUse",
|
|
246
|
+
permissionDecision: isDeny ? "deny" : "allow",
|
|
247
|
+
permissionDecisionReason: reason,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
159
254
|
}
|
|
@@ -27,14 +27,14 @@ function textResult(data) {
|
|
|
27
27
|
function errorResult(message) {
|
|
28
28
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
29
29
|
}
|
|
30
|
-
function createServer() {
|
|
30
|
+
export function createServer() {
|
|
31
31
|
const server = new Server({ name: "rafter", version: CLI_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
32
32
|
// ── Tools ───────────────────────────────────────────────────────────
|
|
33
33
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
34
34
|
tools: [
|
|
35
35
|
{
|
|
36
36
|
name: "scan_secrets",
|
|
37
|
-
description: "Scan files or directories for
|
|
37
|
+
description: "Scan files or directories for leaked secrets, API keys, tokens, passwords, and credentials. Use before pushing code, when handling config files, or when asked 'is this safe to commit?' or 'check for leaked keys'.",
|
|
38
38
|
inputSchema: {
|
|
39
39
|
type: "object",
|
|
40
40
|
properties: {
|
|
@@ -50,7 +50,7 @@ function createServer() {
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
name: "evaluate_command",
|
|
53
|
-
description: "
|
|
53
|
+
description: "Check if a shell command is safe to run per security policy. Use when asked 'is this command safe?' or before running destructive or privileged operations.",
|
|
54
54
|
inputSchema: {
|
|
55
55
|
type: "object",
|
|
56
56
|
properties: {
|
|
@@ -61,7 +61,7 @@ function createServer() {
|
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
name: "read_audit_log",
|
|
64
|
-
description: "Read
|
|
64
|
+
description: "Read security event history — blocked commands, detected secrets, policy overrides. Use when asked 'what happened?' or 'show security events'.",
|
|
65
65
|
inputSchema: {
|
|
66
66
|
type: "object",
|
|
67
67
|
properties: {
|
|
@@ -76,7 +76,7 @@ function createServer() {
|
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
name: "get_config",
|
|
79
|
-
description: "Read Rafter configuration
|
|
79
|
+
description: "Read Rafter security policy and configuration. Use to understand what protections are active and what risk level is configured.",
|
|
80
80
|
inputSchema: {
|
|
81
81
|
type: "object",
|
|
82
82
|
properties: {
|