vibesafu 0.1.24 → 0.1.26
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/dist/index.js +200 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
import { dirname, join as join3 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/cli/install.ts
|
|
10
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
10
|
+
import { readFile, writeFile, mkdir, chmod } from "fs/promises";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
|
|
@@ -24,7 +24,12 @@ async function readClaudeSettings() {
|
|
|
24
24
|
try {
|
|
25
25
|
const content = await readFile(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
26
26
|
return JSON.parse(content);
|
|
27
|
-
} catch {
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
32
|
+
console.error(`Warning: Failed to read Claude settings (${CLAUDE_SETTINGS_PATH}): ${msg}. Starting fresh.`);
|
|
28
33
|
return {};
|
|
29
34
|
}
|
|
30
35
|
}
|
|
@@ -32,6 +37,7 @@ async function writeClaudeSettings(settings) {
|
|
|
32
37
|
const dir = join(homedir(), ".claude");
|
|
33
38
|
await mkdir(dir, { recursive: true });
|
|
34
39
|
await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
40
|
+
await chmod(CLAUDE_SETTINGS_PATH, 384);
|
|
35
41
|
}
|
|
36
42
|
function isHookInstalled(settings) {
|
|
37
43
|
const hooks = settings.hooks?.PermissionRequest ?? [];
|
|
@@ -84,7 +90,7 @@ async function uninstall() {
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
// src/cli/config.ts
|
|
87
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
93
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
|
|
88
94
|
import { homedir as homedir2 } from "os";
|
|
89
95
|
import { join as join2 } from "path";
|
|
90
96
|
import { createInterface } from "readline";
|
|
@@ -109,27 +115,44 @@ var DEFAULT_CONFIG = {
|
|
|
109
115
|
path: join2(CONFIG_DIR, "logs")
|
|
110
116
|
}
|
|
111
117
|
};
|
|
118
|
+
function mergeConfig(defaults, user) {
|
|
119
|
+
return {
|
|
120
|
+
anthropic: { ...defaults.anthropic, ...user.anthropic },
|
|
121
|
+
models: { ...defaults.models, ...user.models },
|
|
122
|
+
trustedDomains: user.trustedDomains ?? defaults.trustedDomains,
|
|
123
|
+
customPatterns: { ...defaults.customPatterns, ...user.customPatterns },
|
|
124
|
+
allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools,
|
|
125
|
+
logging: { ...defaults.logging, ...user.logging }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
112
128
|
async function readConfig() {
|
|
113
129
|
try {
|
|
114
130
|
const content = await readFile2(CONFIG_PATH, "utf-8");
|
|
115
|
-
return
|
|
116
|
-
} catch {
|
|
131
|
+
return mergeConfig(DEFAULT_CONFIG, JSON.parse(content));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
134
|
+
return DEFAULT_CONFIG;
|
|
135
|
+
}
|
|
136
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
137
|
+
process.stderr.write(`[vibesafu] Warning: Failed to read config (${CONFIG_PATH}): ${msg}. Using defaults.
|
|
138
|
+
`);
|
|
117
139
|
return DEFAULT_CONFIG;
|
|
118
140
|
}
|
|
119
141
|
}
|
|
120
142
|
async function writeConfig(config2) {
|
|
121
143
|
await mkdir2(CONFIG_DIR, { recursive: true });
|
|
122
144
|
await writeFile2(CONFIG_PATH, JSON.stringify(config2, null, 2));
|
|
145
|
+
await chmod2(CONFIG_PATH, 384);
|
|
123
146
|
}
|
|
124
147
|
function prompt(question) {
|
|
125
148
|
const rl = createInterface({
|
|
126
149
|
input: process.stdin,
|
|
127
150
|
output: process.stdout
|
|
128
151
|
});
|
|
129
|
-
return new Promise((
|
|
152
|
+
return new Promise((resolve2) => {
|
|
130
153
|
rl.question(question, (answer) => {
|
|
131
154
|
rl.close();
|
|
132
|
-
|
|
155
|
+
resolve2(answer);
|
|
133
156
|
});
|
|
134
157
|
});
|
|
135
158
|
}
|
|
@@ -155,16 +178,6 @@ async function config() {
|
|
|
155
178
|
console.log("Configuration saved!");
|
|
156
179
|
console.log(`Config file: ${CONFIG_PATH}`);
|
|
157
180
|
}
|
|
158
|
-
async function getApiKey() {
|
|
159
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
160
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
161
|
-
}
|
|
162
|
-
const cfg = await readConfig();
|
|
163
|
-
if (cfg.anthropic.apiKey) {
|
|
164
|
-
return cfg.anthropic.apiKey;
|
|
165
|
-
}
|
|
166
|
-
return void 0;
|
|
167
|
-
}
|
|
168
181
|
|
|
169
182
|
// src/hook.ts
|
|
170
183
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -768,6 +781,13 @@ var INSTANT_BLOCK_PATTERNS = [
|
|
|
768
781
|
...DESTRUCTIVE_PATTERNS,
|
|
769
782
|
...SELF_PROTECTION_PATTERNS
|
|
770
783
|
];
|
|
784
|
+
for (const p of INSTANT_BLOCK_PATTERNS) {
|
|
785
|
+
if (p.pattern.global) {
|
|
786
|
+
throw new Error(
|
|
787
|
+
`Security pattern "${p.name}" must not use the global (g) flag. The g flag makes RegExp.test() stateful, causing intermittent bypasses. Remove the g flag from the pattern.`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
771
791
|
var CHECKPOINT_PATTERNS = [
|
|
772
792
|
// Script execution
|
|
773
793
|
{ pattern: /curl\s+.*\|\s*(ba)?sh/i, type: "script_execution", description: "curl piped to shell" },
|
|
@@ -816,6 +836,13 @@ var CHECKPOINT_PATTERNS = [
|
|
|
816
836
|
{ pattern: /(cp|mv)\s+.*\.aws\//i, type: "file_sensitive", description: "Copying/moving AWS credentials" },
|
|
817
837
|
{ pattern: /(cp|mv)\s+.*\.env(\s|$)/i, type: "file_sensitive", description: "Copying/moving .env file" }
|
|
818
838
|
];
|
|
839
|
+
for (const p of CHECKPOINT_PATTERNS) {
|
|
840
|
+
if (p.pattern.global) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`Checkpoint pattern "${p.description}" must not use the global (g) flag. The g flag makes RegExp.test() stateful, causing intermittent bypasses. Remove the g flag from the pattern.`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
819
846
|
|
|
820
847
|
// src/guard/instant-block.ts
|
|
821
848
|
function checkHighRiskPatterns(command) {
|
|
@@ -1022,7 +1049,10 @@ function isTrustedUrl(url) {
|
|
|
1022
1049
|
function extractUrls(command) {
|
|
1023
1050
|
const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
|
|
1024
1051
|
const matches = command.match(urlPattern);
|
|
1025
|
-
|
|
1052
|
+
if (!matches) return [];
|
|
1053
|
+
return matches.map((url) => {
|
|
1054
|
+
return url.replace(/[),;.]+$/, "");
|
|
1055
|
+
});
|
|
1026
1056
|
}
|
|
1027
1057
|
function isUrlShortener(hostname) {
|
|
1028
1058
|
const normalizedHost = hostname.toLowerCase();
|
|
@@ -1046,6 +1076,21 @@ function containsUrlShortener(command) {
|
|
|
1046
1076
|
shortenerUrls
|
|
1047
1077
|
};
|
|
1048
1078
|
}
|
|
1079
|
+
var RISKY_URL_PATTERNS = [
|
|
1080
|
+
// Raw file content from GitHub - can be any user's code
|
|
1081
|
+
/raw\.githubusercontent\.com/i,
|
|
1082
|
+
// GitHub gist raw content
|
|
1083
|
+
/gist\.github\.com\/[^/]+\/[^/]+\/raw/i,
|
|
1084
|
+
// GitHub releases downloads - binary files from any user
|
|
1085
|
+
/github\.com\/[^/]+\/[^/]+\/releases\/download/i,
|
|
1086
|
+
// GitHub objects (blobs, etc.)
|
|
1087
|
+
/objects\.githubusercontent\.com/i,
|
|
1088
|
+
// Installer script patterns (get.*.sh)
|
|
1089
|
+
/\/get\.[^/]+\.sh/i
|
|
1090
|
+
];
|
|
1091
|
+
function isRiskyUrlPattern(url) {
|
|
1092
|
+
return RISKY_URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
1093
|
+
}
|
|
1049
1094
|
|
|
1050
1095
|
// src/guard/checkpoint.ts
|
|
1051
1096
|
function detectCheckpoint(command) {
|
|
@@ -1079,29 +1124,39 @@ function checkTrustedDomains(command) {
|
|
|
1079
1124
|
return {
|
|
1080
1125
|
allTrusted: true,
|
|
1081
1126
|
// No URLs means nothing untrusted
|
|
1127
|
+
hasRiskyUrls: false,
|
|
1082
1128
|
urls: [],
|
|
1083
1129
|
trustedUrls: [],
|
|
1084
|
-
untrustedUrls: []
|
|
1130
|
+
untrustedUrls: [],
|
|
1131
|
+
riskyUrls: []
|
|
1085
1132
|
};
|
|
1086
1133
|
}
|
|
1087
1134
|
const trustedUrls = [];
|
|
1088
1135
|
const untrustedUrls = [];
|
|
1136
|
+
const riskyUrls = [];
|
|
1089
1137
|
for (const url of urls) {
|
|
1090
1138
|
if (isTrustedUrl(url)) {
|
|
1091
1139
|
trustedUrls.push(url);
|
|
1092
1140
|
} else {
|
|
1093
1141
|
untrustedUrls.push(url);
|
|
1094
1142
|
}
|
|
1143
|
+
if (isRiskyUrlPattern(url)) {
|
|
1144
|
+
riskyUrls.push(url);
|
|
1145
|
+
}
|
|
1095
1146
|
}
|
|
1096
1147
|
return {
|
|
1097
1148
|
allTrusted: untrustedUrls.length === 0,
|
|
1149
|
+
hasRiskyUrls: riskyUrls.length > 0,
|
|
1098
1150
|
urls,
|
|
1099
1151
|
trustedUrls,
|
|
1100
|
-
untrustedUrls
|
|
1152
|
+
untrustedUrls,
|
|
1153
|
+
riskyUrls
|
|
1101
1154
|
};
|
|
1102
1155
|
}
|
|
1103
1156
|
|
|
1104
1157
|
// src/guard/file-tools.ts
|
|
1158
|
+
import { resolve } from "path";
|
|
1159
|
+
import { homedir as homedir3 } from "os";
|
|
1105
1160
|
var WRITE_SENSITIVE_PATHS = [
|
|
1106
1161
|
// SSH - Critical (persistent access)
|
|
1107
1162
|
{
|
|
@@ -1431,8 +1486,18 @@ var READ_SENSITIVE_PATHS = [
|
|
|
1431
1486
|
}
|
|
1432
1487
|
];
|
|
1433
1488
|
function normalizePath(filePath) {
|
|
1434
|
-
let normalized = filePath
|
|
1489
|
+
let normalized = filePath;
|
|
1490
|
+
normalized = normalized.replace(/\$HOME/g, "~").replace(/\$\{HOME\}/g, "~");
|
|
1435
1491
|
normalized = normalized.replace(/\/+/g, "/");
|
|
1492
|
+
if (normalized.startsWith("/")) {
|
|
1493
|
+
normalized = resolve(normalized);
|
|
1494
|
+
}
|
|
1495
|
+
const home = homedir3();
|
|
1496
|
+
if (normalized.startsWith(home + "/")) {
|
|
1497
|
+
normalized = "~" + normalized.slice(home.length);
|
|
1498
|
+
} else if (normalized === home) {
|
|
1499
|
+
normalized = "~";
|
|
1500
|
+
}
|
|
1436
1501
|
return normalized;
|
|
1437
1502
|
}
|
|
1438
1503
|
function checkFilePath(filePath, action) {
|
|
@@ -1535,7 +1600,7 @@ function sanitizeForPrompt(command) {
|
|
|
1535
1600
|
if (sanitized.length > MAX_COMMAND_LENGTH) {
|
|
1536
1601
|
sanitized = sanitized.slice(0, MAX_COMMAND_LENGTH) + "... [truncated]";
|
|
1537
1602
|
}
|
|
1538
|
-
sanitized = sanitized.replace(
|
|
1603
|
+
sanitized = sanitized.replace(/]]>/g, "]]>");
|
|
1539
1604
|
sanitized = sanitized.replace(/\n{3,}/g, "\n\n");
|
|
1540
1605
|
return sanitized;
|
|
1541
1606
|
}
|
|
@@ -1580,9 +1645,65 @@ function shouldForceEscalate(command) {
|
|
|
1580
1645
|
}
|
|
1581
1646
|
return FORCE_ESCALATE_PATTERNS.some((pattern) => pattern.test(command));
|
|
1582
1647
|
}
|
|
1648
|
+
function extractJsonFromText(text) {
|
|
1649
|
+
try {
|
|
1650
|
+
const parsed = JSON.parse(text.trim());
|
|
1651
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1652
|
+
return parsed;
|
|
1653
|
+
}
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1656
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
|
|
1657
|
+
if (codeBlockMatch) {
|
|
1658
|
+
try {
|
|
1659
|
+
const parsed = JSON.parse(codeBlockMatch[1].trim());
|
|
1660
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1661
|
+
return parsed;
|
|
1662
|
+
}
|
|
1663
|
+
} catch {
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
const startIdx = text.indexOf("{");
|
|
1667
|
+
if (startIdx === -1) return null;
|
|
1668
|
+
let depth = 0;
|
|
1669
|
+
let inString = false;
|
|
1670
|
+
let escaped = false;
|
|
1671
|
+
for (let i = startIdx; i < text.length; i++) {
|
|
1672
|
+
const ch = text[i];
|
|
1673
|
+
if (escaped) {
|
|
1674
|
+
escaped = false;
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
if (ch === "\\" && inString) {
|
|
1678
|
+
escaped = true;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (ch === '"') {
|
|
1682
|
+
inString = !inString;
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
if (inString) continue;
|
|
1686
|
+
if (ch === "{") depth++;
|
|
1687
|
+
else if (ch === "}") {
|
|
1688
|
+
depth--;
|
|
1689
|
+
if (depth === 0) {
|
|
1690
|
+
const candidate = text.slice(startIdx, i + 1);
|
|
1691
|
+
try {
|
|
1692
|
+
const parsed = JSON.parse(candidate);
|
|
1693
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1694
|
+
return parsed;
|
|
1695
|
+
}
|
|
1696
|
+
} catch {
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1583
1704
|
|
|
1584
1705
|
// src/guard/haiku-triage.ts
|
|
1585
|
-
var
|
|
1706
|
+
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
|
|
1586
1707
|
var API_TIMEOUT_MS = 3e4;
|
|
1587
1708
|
var TRIAGE_SYSTEM_PROMPT = `You are a security triage agent for an autonomous coding system.
|
|
1588
1709
|
Your ONLY job is to classify commands as SELF_HANDLE, ESCALATE, or BLOCK.
|
|
@@ -1629,7 +1750,7 @@ var FORCE_ESCALATE_TYPES = [
|
|
|
1629
1750
|
"package_install"
|
|
1630
1751
|
// Supply chain attacks via postinstall scripts
|
|
1631
1752
|
];
|
|
1632
|
-
async function triageWithHaiku(client, checkpoint) {
|
|
1753
|
+
async function triageWithHaiku(client, checkpoint, model) {
|
|
1633
1754
|
if (FORCE_ESCALATE_TYPES.includes(checkpoint.type)) {
|
|
1634
1755
|
return {
|
|
1635
1756
|
classification: "ESCALATE",
|
|
@@ -1644,7 +1765,7 @@ async function triageWithHaiku(client, checkpoint) {
|
|
|
1644
1765
|
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1645
1766
|
const response = await client.messages.create(
|
|
1646
1767
|
{
|
|
1647
|
-
model:
|
|
1768
|
+
model: model ?? DEFAULT_HAIKU_MODEL,
|
|
1648
1769
|
max_tokens: 500,
|
|
1649
1770
|
system: TRIAGE_SYSTEM_PROMPT,
|
|
1650
1771
|
messages: [{ role: "user", content: userPrompt }]
|
|
@@ -1660,15 +1781,15 @@ async function triageWithHaiku(client, checkpoint) {
|
|
|
1660
1781
|
riskIndicators: ["triage_error"]
|
|
1661
1782
|
};
|
|
1662
1783
|
}
|
|
1663
|
-
const
|
|
1664
|
-
if (!
|
|
1784
|
+
const extracted = extractJsonFromText(text);
|
|
1785
|
+
if (!extracted) {
|
|
1665
1786
|
return {
|
|
1666
1787
|
classification: "ESCALATE",
|
|
1667
1788
|
reason: "Triage failed: Could not parse JSON response",
|
|
1668
1789
|
riskIndicators: ["triage_error"]
|
|
1669
1790
|
};
|
|
1670
1791
|
}
|
|
1671
|
-
const parsed =
|
|
1792
|
+
const parsed = extracted;
|
|
1672
1793
|
if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
|
|
1673
1794
|
return {
|
|
1674
1795
|
classification: "ESCALATE",
|
|
@@ -1706,7 +1827,7 @@ async function triageWithHaiku(client, checkpoint) {
|
|
|
1706
1827
|
}
|
|
1707
1828
|
|
|
1708
1829
|
// src/guard/sonnet-review.ts
|
|
1709
|
-
var
|
|
1830
|
+
var DEFAULT_SONNET_MODEL = "claude-sonnet-4-20250514";
|
|
1710
1831
|
var API_TIMEOUT_MS2 = 6e4;
|
|
1711
1832
|
var REVIEW_SYSTEM_PROMPT = `You are a senior security engineer reviewing potentially risky operations.
|
|
1712
1833
|
Your job is to analyze commands and determine if they are safe to execute.
|
|
@@ -1778,7 +1899,7 @@ BLOCK - Do not allow:
|
|
|
1778
1899
|
"user_message": "Concise message explaining the security risk to the user (2-3 sentences max). Do NOT include timing or instructions - those are added automatically."
|
|
1779
1900
|
}
|
|
1780
1901
|
</response_format>`;
|
|
1781
|
-
async function reviewWithSonnet(client, checkpoint, triage) {
|
|
1902
|
+
async function reviewWithSonnet(client, checkpoint, triage, model) {
|
|
1782
1903
|
const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
|
|
1783
1904
|
const userPrompt = REVIEW_USER_PROMPT.replace("{command}", escapeXml(sanitizedCommand)).replace("{checkpoint_type}", escapeXml(checkpoint.type)).replace("{context}", escapeXml(checkpoint.description)).replace("{triage_reason}", escapeXml(triage.reason)).replace("{risk_indicators}", escapeXml(triage.riskIndicators.join(", ") || "none"));
|
|
1784
1905
|
try {
|
|
@@ -1786,7 +1907,7 @@ async function reviewWithSonnet(client, checkpoint, triage) {
|
|
|
1786
1907
|
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS2);
|
|
1787
1908
|
const response = await client.messages.create(
|
|
1788
1909
|
{
|
|
1789
|
-
model:
|
|
1910
|
+
model: model ?? DEFAULT_SONNET_MODEL,
|
|
1790
1911
|
max_tokens: 1e3,
|
|
1791
1912
|
system: REVIEW_SYSTEM_PROMPT,
|
|
1792
1913
|
messages: [{ role: "user", content: userPrompt }]
|
|
@@ -1803,8 +1924,8 @@ async function reviewWithSonnet(client, checkpoint, triage) {
|
|
|
1803
1924
|
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1804
1925
|
};
|
|
1805
1926
|
}
|
|
1806
|
-
const
|
|
1807
|
-
if (!
|
|
1927
|
+
const extracted = extractJsonFromText(text);
|
|
1928
|
+
if (!extracted) {
|
|
1808
1929
|
return {
|
|
1809
1930
|
verdict: "ASK_USER",
|
|
1810
1931
|
riskLevel: "medium",
|
|
@@ -1812,7 +1933,7 @@ async function reviewWithSonnet(client, checkpoint, triage) {
|
|
|
1812
1933
|
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1813
1934
|
};
|
|
1814
1935
|
}
|
|
1815
|
-
const parsed =
|
|
1936
|
+
const parsed = extracted;
|
|
1816
1937
|
const verdict = parsed.verdict ?? "ASK_USER";
|
|
1817
1938
|
if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
|
|
1818
1939
|
return {
|
|
@@ -1852,9 +1973,25 @@ async function reviewWithSonnet(client, checkpoint, triage) {
|
|
|
1852
1973
|
|
|
1853
1974
|
// src/hook.ts
|
|
1854
1975
|
var TIMEOUT_SECONDS = 7;
|
|
1976
|
+
var REGEX_TIMEOUT_MS = 50;
|
|
1977
|
+
function safeRegexTest(pattern, input) {
|
|
1978
|
+
try {
|
|
1979
|
+
const regex = new RegExp(pattern, "i");
|
|
1980
|
+
if (/(\(.+[+*]\))[+*]|\(\?:[^)]+[+*]\)[+*]/.test(pattern)) {
|
|
1981
|
+
process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
|
|
1982
|
+
`);
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
const testInput = input.length > REGEX_TIMEOUT_MS * 40 ? input.slice(0, REGEX_TIMEOUT_MS * 40) : input;
|
|
1986
|
+
return regex.test(testInput);
|
|
1987
|
+
} catch {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1855
1991
|
var PLAN_MODE_TIMEOUT_SECONDS = 72 * 60 * 60;
|
|
1856
1992
|
var SAFE_NON_BASH_TOOLS = ["WebFetch", "WebSearch", "Task", "Glob", "Grep", "LS", "TodoRead", "TodoWrite", "NotebookRead"];
|
|
1857
|
-
async function processPermissionRequest(input, anthropicClient) {
|
|
1993
|
+
async function processPermissionRequest(input, anthropicClient, preloadedConfig) {
|
|
1994
|
+
const config2 = preloadedConfig ?? await readConfig();
|
|
1858
1995
|
if (input.tool_name === "Write" || input.tool_name === "Edit" || input.tool_name === "Read") {
|
|
1859
1996
|
const fileCheck = checkFileTool(input.tool_name, input.tool_input);
|
|
1860
1997
|
if (fileCheck.blocked) {
|
|
@@ -1917,8 +2054,7 @@ This will auto-reject if not approved.`,
|
|
|
1917
2054
|
};
|
|
1918
2055
|
}
|
|
1919
2056
|
if (input.tool_name.startsWith("mcp__")) {
|
|
1920
|
-
const
|
|
1921
|
-
const isAllowed = config3.allowedMCPTools.some((pattern) => {
|
|
2057
|
+
const isAllowed = config2.allowedMCPTools.some((pattern) => {
|
|
1922
2058
|
if (pattern.endsWith("*")) {
|
|
1923
2059
|
const prefix = pattern.slice(0, -1);
|
|
1924
2060
|
return input.tool_name.startsWith(prefix);
|
|
@@ -1962,34 +2098,27 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
|
|
|
1962
2098
|
};
|
|
1963
2099
|
}
|
|
1964
2100
|
const command = input.tool_input.command;
|
|
1965
|
-
const config2 = await readConfig();
|
|
1966
2101
|
for (const pattern of config2.customPatterns.allow) {
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
};
|
|
1974
|
-
}
|
|
1975
|
-
} catch {
|
|
2102
|
+
if (safeRegexTest(pattern, command)) {
|
|
2103
|
+
return {
|
|
2104
|
+
decision: "allow",
|
|
2105
|
+
reason: `Custom allow pattern: ${pattern}`,
|
|
2106
|
+
source: "instant-allow"
|
|
2107
|
+
};
|
|
1976
2108
|
}
|
|
1977
2109
|
}
|
|
1978
2110
|
for (const pattern of config2.customPatterns.block) {
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
userMessage: `[CUSTOM BLOCK] Matched pattern: ${pattern}
|
|
2111
|
+
if (safeRegexTest(pattern, command)) {
|
|
2112
|
+
return {
|
|
2113
|
+
decision: "needs-review",
|
|
2114
|
+
reason: `Custom block pattern: ${pattern}`,
|
|
2115
|
+
source: "high-risk",
|
|
2116
|
+
userMessage: `[CUSTOM BLOCK] Matched pattern: ${pattern}
|
|
1986
2117
|
|
|
1987
2118
|
This command was blocked by your custom config.
|
|
1988
2119
|
|
|
1989
2120
|
Auto-reject in ${TIMEOUT_SECONDS}s.`
|
|
1990
|
-
|
|
1991
|
-
}
|
|
1992
|
-
} catch {
|
|
2121
|
+
};
|
|
1993
2122
|
}
|
|
1994
2123
|
}
|
|
1995
2124
|
const allowResult = checkInstantAllow(command);
|
|
@@ -2027,6 +2156,14 @@ Only proceed if you know what you're doing.`
|
|
|
2027
2156
|
if (checkpoint.type === "network") {
|
|
2028
2157
|
const domainResult = checkTrustedDomains(command);
|
|
2029
2158
|
if (domainResult.allTrusted && domainResult.urls.length > 0) {
|
|
2159
|
+
if (domainResult.hasRiskyUrls) {
|
|
2160
|
+
return {
|
|
2161
|
+
decision: "needs-review",
|
|
2162
|
+
reason: `Risky URL pattern from trusted domain: ${domainResult.riskyUrls.join(", ")}`,
|
|
2163
|
+
source: "checkpoint",
|
|
2164
|
+
checkpoint
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2030
2167
|
return {
|
|
2031
2168
|
decision: "allow",
|
|
2032
2169
|
reason: `All URLs from trusted domains: ${domainResult.trustedUrls.join(", ")}`,
|
|
@@ -2043,7 +2180,7 @@ Only proceed if you know what you're doing.`
|
|
|
2043
2180
|
};
|
|
2044
2181
|
}
|
|
2045
2182
|
process.stderr.write("\x1B[90m[vibesafu] Assessing security risks...\x1B[0m\n");
|
|
2046
|
-
const triage = await triageWithHaiku(anthropicClient, checkpoint);
|
|
2183
|
+
const triage = await triageWithHaiku(anthropicClient, checkpoint, config2.models.triage);
|
|
2047
2184
|
if (triage.classification === "BLOCK") {
|
|
2048
2185
|
return {
|
|
2049
2186
|
decision: "deny",
|
|
@@ -2059,7 +2196,7 @@ Only proceed if you know what you're doing.`
|
|
|
2059
2196
|
};
|
|
2060
2197
|
}
|
|
2061
2198
|
process.stderr.write("\x1B[90m[vibesafu] Escalating to deep analysis...\x1B[0m\n");
|
|
2062
|
-
const review = await reviewWithSonnet(anthropicClient, checkpoint, triage);
|
|
2199
|
+
const review = await reviewWithSonnet(anthropicClient, checkpoint, triage, config2.models.review);
|
|
2063
2200
|
if (review.verdict === "BLOCK") {
|
|
2064
2201
|
const result2 = {
|
|
2065
2202
|
decision: "deny",
|
|
@@ -2117,12 +2254,13 @@ async function runHook() {
|
|
|
2117
2254
|
console.log(JSON.stringify(output2));
|
|
2118
2255
|
return;
|
|
2119
2256
|
}
|
|
2257
|
+
const config2 = await readConfig();
|
|
2120
2258
|
let anthropicClient;
|
|
2121
|
-
const apiKey =
|
|
2259
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? (config2.anthropic.apiKey || void 0);
|
|
2122
2260
|
if (apiKey) {
|
|
2123
2261
|
anthropicClient = new Anthropic({ apiKey });
|
|
2124
2262
|
}
|
|
2125
|
-
const result = await processPermissionRequest(input, anthropicClient);
|
|
2263
|
+
const result = await processPermissionRequest(input, anthropicClient, config2);
|
|
2126
2264
|
let output;
|
|
2127
2265
|
if (result.decision === "allow") {
|
|
2128
2266
|
output = createHookOutput("allow");
|
|
@@ -2131,7 +2269,7 @@ async function runHook() {
|
|
|
2131
2269
|
}
|
|
2132
2270
|
const warningMessage = result.userMessage ?? result.reason;
|
|
2133
2271
|
const timeout = result.timeoutSeconds ?? TIMEOUT_SECONDS;
|
|
2134
|
-
await new Promise((
|
|
2272
|
+
await new Promise((resolve2) => setTimeout(resolve2, timeout * 1e3));
|
|
2135
2273
|
const timeoutDisplay = timeout >= 3600 ? `${Math.round(timeout / 3600)}h` : `${timeout}s`;
|
|
2136
2274
|
const denyMessage = `\u{1F6E1}\uFE0F [vibesafu] Auto-denied (no response in ${timeoutDisplay})
|
|
2137
2275
|
|
package/package.json
CHANGED