vibesafu 0.1.26 → 0.1.27
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 +132 -147
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -109,11 +109,7 @@ var DEFAULT_CONFIG = {
|
|
|
109
109
|
block: [],
|
|
110
110
|
allow: []
|
|
111
111
|
},
|
|
112
|
-
allowedMCPTools: []
|
|
113
|
-
logging: {
|
|
114
|
-
enabled: true,
|
|
115
|
-
path: join2(CONFIG_DIR, "logs")
|
|
116
|
-
}
|
|
112
|
+
allowedMCPTools: []
|
|
117
113
|
};
|
|
118
114
|
function mergeConfig(defaults, user) {
|
|
119
115
|
return {
|
|
@@ -121,8 +117,7 @@ function mergeConfig(defaults, user) {
|
|
|
121
117
|
models: { ...defaults.models, ...user.models },
|
|
122
118
|
trustedDomains: user.trustedDomains ?? defaults.trustedDomains,
|
|
123
119
|
customPatterns: { ...defaults.customPatterns, ...user.customPatterns },
|
|
124
|
-
allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools
|
|
125
|
-
logging: { ...defaults.logging, ...user.logging }
|
|
120
|
+
allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools
|
|
126
121
|
};
|
|
127
122
|
}
|
|
128
123
|
async function readConfig() {
|
|
@@ -1562,19 +1557,21 @@ var PROMPT_INJECTION_PATTERNS = [
|
|
|
1562
1557
|
/pretend\s+(to\s+be|you\s+are)/i,
|
|
1563
1558
|
/new\s+instructions?:/i,
|
|
1564
1559
|
/updated?\s+instructions?:/i,
|
|
1565
|
-
// Context/role markers (
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1560
|
+
// Context/role markers (require injection-like context to avoid false positives)
|
|
1561
|
+
// "system:" alone is too broad (matches "operating system: linux")
|
|
1562
|
+
// Require either line-start or preceded by newline to indicate role-marker usage
|
|
1563
|
+
/^\s*system\s*:/im,
|
|
1564
|
+
/^\s*assistant\s*:/im,
|
|
1565
|
+
/^\s*human\s*:/im,
|
|
1566
|
+
/^\s*user\s*:/im,
|
|
1570
1567
|
/<\s*system\s*>/i,
|
|
1571
1568
|
/<\s*\/?\s*instructions?\s*>/i,
|
|
1572
|
-
// Emphasis markers
|
|
1573
|
-
/\bIMPORTANT\s
|
|
1574
|
-
/\bNOTE\s
|
|
1575
|
-
/\bWARNING\s
|
|
1576
|
-
/\bCRITICAL\s
|
|
1577
|
-
/\bURGENT\s
|
|
1569
|
+
// Emphasis markers - only flag when combined with directive language
|
|
1570
|
+
/\bIMPORTANT\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
|
|
1571
|
+
/\bNOTE\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
|
|
1572
|
+
/\bWARNING\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
|
|
1573
|
+
/\bCRITICAL\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
|
|
1574
|
+
/\bURGENT\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
|
|
1578
1575
|
// Output manipulation
|
|
1579
1576
|
/respond\s+with\s+(this\s+)?(exact\s+)?json/i,
|
|
1580
1577
|
/return\s+(only\s+)?["']?ALLOW["']?/i,
|
|
@@ -1634,10 +1631,10 @@ var FORCE_ESCALATE_PATTERNS = [
|
|
|
1634
1631
|
// su commands
|
|
1635
1632
|
/chmod\s+[0-7]*[7][0-7]*/i,
|
|
1636
1633
|
// chmod with executable permissions
|
|
1637
|
-
/\.env/i,
|
|
1638
|
-
// env file access
|
|
1639
|
-
/\/(etc|root
|
|
1640
|
-
// System directory access
|
|
1634
|
+
/\.env(\s|$|\.local|\.production|\.development|\.staging|\.test)/i,
|
|
1635
|
+
// .env file access (not .envoy, .environment, etc.)
|
|
1636
|
+
/\/(etc|root)\//i
|
|
1637
|
+
// System directory access (/etc/, /root/ - not /home/ which is too broad)
|
|
1641
1638
|
];
|
|
1642
1639
|
function shouldForceEscalate(command) {
|
|
1643
1640
|
if (containsPromptInjection(command)) {
|
|
@@ -1702,6 +1699,40 @@ function extractJsonFromText(text) {
|
|
|
1702
1699
|
return null;
|
|
1703
1700
|
}
|
|
1704
1701
|
|
|
1702
|
+
// src/utils/llm-call.ts
|
|
1703
|
+
async function callLLM(options) {
|
|
1704
|
+
const { client, model, systemPrompt, userPrompt, maxTokens, timeoutMs } = options;
|
|
1705
|
+
try {
|
|
1706
|
+
const controller = new AbortController();
|
|
1707
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1708
|
+
const response = await client.messages.create(
|
|
1709
|
+
{
|
|
1710
|
+
model,
|
|
1711
|
+
max_tokens: maxTokens,
|
|
1712
|
+
system: systemPrompt,
|
|
1713
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
1714
|
+
},
|
|
1715
|
+
{ signal: controller.signal }
|
|
1716
|
+
);
|
|
1717
|
+
clearTimeout(timeoutId);
|
|
1718
|
+
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
1719
|
+
if (!text) {
|
|
1720
|
+
return { ok: false, error: "empty_response", message: "Empty response from LLM" };
|
|
1721
|
+
}
|
|
1722
|
+
const extracted = extractJsonFromText(text);
|
|
1723
|
+
if (!extracted) {
|
|
1724
|
+
return { ok: false, error: "parse_error", message: "Could not parse JSON response" };
|
|
1725
|
+
}
|
|
1726
|
+
return { ok: true, data: extracted };
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1729
|
+
if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
|
|
1730
|
+
return { ok: false, error: "timeout", message: "API timeout" };
|
|
1731
|
+
}
|
|
1732
|
+
return { ok: false, error: "api_error", message: errorMessage };
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1705
1736
|
// src/guard/haiku-triage.ts
|
|
1706
1737
|
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
|
|
1707
1738
|
var API_TIMEOUT_MS = 3e4;
|
|
@@ -1760,70 +1791,42 @@ async function triageWithHaiku(client, checkpoint, model) {
|
|
|
1760
1791
|
}
|
|
1761
1792
|
const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
|
|
1762
1793
|
const userPrompt = TRIAGE_USER_PROMPT.replace("{command}", escapeXml(sanitizedCommand)).replace("{checkpoint_type}", escapeXml(checkpoint.type)).replace("{context}", escapeXml(checkpoint.description));
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
{ signal: controller.signal }
|
|
1774
|
-
);
|
|
1775
|
-
clearTimeout(timeoutId);
|
|
1776
|
-
const text = response.content[0]?.type === "text" ? response.content[0].text : "";
|
|
1777
|
-
if (!text) {
|
|
1778
|
-
return {
|
|
1779
|
-
classification: "ESCALATE",
|
|
1780
|
-
reason: "Triage failed: Empty response from Haiku",
|
|
1781
|
-
riskIndicators: ["triage_error"]
|
|
1782
|
-
};
|
|
1783
|
-
}
|
|
1784
|
-
const extracted = extractJsonFromText(text);
|
|
1785
|
-
if (!extracted) {
|
|
1786
|
-
return {
|
|
1787
|
-
classification: "ESCALATE",
|
|
1788
|
-
reason: "Triage failed: Could not parse JSON response",
|
|
1789
|
-
riskIndicators: ["triage_error"]
|
|
1790
|
-
};
|
|
1791
|
-
}
|
|
1792
|
-
const parsed = extracted;
|
|
1793
|
-
if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
|
|
1794
|
-
return {
|
|
1795
|
-
classification: "ESCALATE",
|
|
1796
|
-
reason: "Triage failed: Invalid classification in response",
|
|
1797
|
-
riskIndicators: ["triage_error"]
|
|
1798
|
-
};
|
|
1799
|
-
}
|
|
1800
|
-
if (parsed.classification === "SELF_HANDLE" && shouldForceEscalate(checkpoint.command)) {
|
|
1801
|
-
return {
|
|
1802
|
-
classification: "ESCALATE",
|
|
1803
|
-
reason: "Auto-escalated: Command contains patterns requiring deeper review",
|
|
1804
|
-
riskIndicators: ["forced_escalation", ...parsed.risk_indicators ?? []]
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1794
|
+
const result = await callLLM({
|
|
1795
|
+
client,
|
|
1796
|
+
model: model ?? DEFAULT_HAIKU_MODEL,
|
|
1797
|
+
systemPrompt: TRIAGE_SYSTEM_PROMPT,
|
|
1798
|
+
userPrompt,
|
|
1799
|
+
maxTokens: 500,
|
|
1800
|
+
timeoutMs: API_TIMEOUT_MS
|
|
1801
|
+
});
|
|
1802
|
+
if (!result.ok) {
|
|
1803
|
+
const tag = result.error === "timeout" ? "triage_timeout" : "triage_error";
|
|
1807
1804
|
return {
|
|
1808
|
-
classification:
|
|
1809
|
-
reason:
|
|
1810
|
-
riskIndicators:
|
|
1805
|
+
classification: "ESCALATE",
|
|
1806
|
+
reason: `Triage failed: ${result.message}`,
|
|
1807
|
+
riskIndicators: [tag]
|
|
1811
1808
|
};
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
return {
|
|
1816
|
-
classification: "ESCALATE",
|
|
1817
|
-
reason: "Triage failed: API timeout",
|
|
1818
|
-
riskIndicators: ["triage_timeout"]
|
|
1819
|
-
};
|
|
1820
|
-
}
|
|
1809
|
+
}
|
|
1810
|
+
const parsed = result.data;
|
|
1811
|
+
if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
|
|
1821
1812
|
return {
|
|
1822
1813
|
classification: "ESCALATE",
|
|
1823
|
-
reason:
|
|
1814
|
+
reason: "Triage failed: Invalid classification in response",
|
|
1824
1815
|
riskIndicators: ["triage_error"]
|
|
1825
1816
|
};
|
|
1826
1817
|
}
|
|
1818
|
+
if (parsed.classification === "SELF_HANDLE" && shouldForceEscalate(checkpoint.command)) {
|
|
1819
|
+
return {
|
|
1820
|
+
classification: "ESCALATE",
|
|
1821
|
+
reason: "Auto-escalated: Command contains patterns requiring deeper review",
|
|
1822
|
+
riskIndicators: ["forced_escalation", ...parsed.risk_indicators ?? []]
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
return {
|
|
1826
|
+
classification: parsed.classification,
|
|
1827
|
+
reason: parsed.reason ?? "No reason provided",
|
|
1828
|
+
riskIndicators: parsed.risk_indicators ?? []
|
|
1829
|
+
};
|
|
1827
1830
|
}
|
|
1828
1831
|
|
|
1829
1832
|
// src/guard/sonnet-review.ts
|
|
@@ -1902,73 +1905,43 @@ BLOCK - Do not allow:
|
|
|
1902
1905
|
async function reviewWithSonnet(client, checkpoint, triage, model) {
|
|
1903
1906
|
const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
|
|
1904
1907
|
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"));
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
verdict: "ASK_USER",
|
|
1922
|
-
riskLevel: "medium",
|
|
1923
|
-
reason: "Review failed: Empty response from Sonnet",
|
|
1924
|
-
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1925
|
-
};
|
|
1926
|
-
}
|
|
1927
|
-
const extracted = extractJsonFromText(text);
|
|
1928
|
-
if (!extracted) {
|
|
1929
|
-
return {
|
|
1930
|
-
verdict: "ASK_USER",
|
|
1931
|
-
riskLevel: "medium",
|
|
1932
|
-
reason: "Review failed: Could not parse JSON response",
|
|
1933
|
-
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1934
|
-
};
|
|
1935
|
-
}
|
|
1936
|
-
const parsed = extracted;
|
|
1937
|
-
const verdict = parsed.verdict ?? "ASK_USER";
|
|
1938
|
-
if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
|
|
1939
|
-
return {
|
|
1940
|
-
verdict: "ASK_USER",
|
|
1941
|
-
riskLevel: "medium",
|
|
1942
|
-
reason: "Review failed: Invalid verdict in response",
|
|
1943
|
-
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
const result = {
|
|
1947
|
-
verdict,
|
|
1948
|
-
riskLevel: parsed.risk_level ?? "medium",
|
|
1949
|
-
reason: parsed.analysis?.intent ?? "Review completed"
|
|
1908
|
+
const FALLBACK_MSG = "Automated security review failed. Please review this operation manually.";
|
|
1909
|
+
const result = await callLLM({
|
|
1910
|
+
client,
|
|
1911
|
+
model: model ?? DEFAULT_SONNET_MODEL,
|
|
1912
|
+
systemPrompt: REVIEW_SYSTEM_PROMPT,
|
|
1913
|
+
userPrompt,
|
|
1914
|
+
maxTokens: 1e3,
|
|
1915
|
+
timeoutMs: API_TIMEOUT_MS2
|
|
1916
|
+
});
|
|
1917
|
+
if (!result.ok) {
|
|
1918
|
+
const msg = result.error === "timeout" ? "Security review timed out. Please review this operation manually." : FALLBACK_MSG;
|
|
1919
|
+
return {
|
|
1920
|
+
verdict: "ASK_USER",
|
|
1921
|
+
riskLevel: "medium",
|
|
1922
|
+
reason: `Review failed: ${result.message}`,
|
|
1923
|
+
userMessage: msg
|
|
1950
1924
|
};
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
} catch (error) {
|
|
1956
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1957
|
-
if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
|
|
1958
|
-
return {
|
|
1959
|
-
verdict: "ASK_USER",
|
|
1960
|
-
riskLevel: "medium",
|
|
1961
|
-
reason: "Review failed: API timeout",
|
|
1962
|
-
userMessage: "Security review timed out. Please review this operation manually."
|
|
1963
|
-
};
|
|
1964
|
-
}
|
|
1925
|
+
}
|
|
1926
|
+
const parsed = result.data;
|
|
1927
|
+
const verdict = parsed.verdict ?? "ASK_USER";
|
|
1928
|
+
if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
|
|
1965
1929
|
return {
|
|
1966
1930
|
verdict: "ASK_USER",
|
|
1967
1931
|
riskLevel: "medium",
|
|
1968
|
-
reason:
|
|
1969
|
-
userMessage:
|
|
1932
|
+
reason: "Review failed: Invalid verdict in response",
|
|
1933
|
+
userMessage: FALLBACK_MSG
|
|
1970
1934
|
};
|
|
1971
1935
|
}
|
|
1936
|
+
const reviewResult = {
|
|
1937
|
+
verdict,
|
|
1938
|
+
riskLevel: parsed.risk_level ?? "medium",
|
|
1939
|
+
reason: parsed.analysis?.intent ?? "Review completed"
|
|
1940
|
+
};
|
|
1941
|
+
if (parsed.user_message) {
|
|
1942
|
+
reviewResult.userMessage = parsed.user_message;
|
|
1943
|
+
}
|
|
1944
|
+
return reviewResult;
|
|
1972
1945
|
}
|
|
1973
1946
|
|
|
1974
1947
|
// src/hook.ts
|
|
@@ -1976,12 +1949,22 @@ var TIMEOUT_SECONDS = 7;
|
|
|
1976
1949
|
var REGEX_TIMEOUT_MS = 50;
|
|
1977
1950
|
function safeRegexTest(pattern, input) {
|
|
1978
1951
|
try {
|
|
1979
|
-
const regex = new RegExp(pattern, "i");
|
|
1980
1952
|
if (/(\(.+[+*]\))[+*]|\(\?:[^)]+[+*]\)[+*]/.test(pattern)) {
|
|
1981
1953
|
process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
|
|
1982
1954
|
`);
|
|
1983
1955
|
return false;
|
|
1984
1956
|
}
|
|
1957
|
+
if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
|
|
1958
|
+
process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
|
|
1959
|
+
`);
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
|
|
1963
|
+
process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
|
|
1964
|
+
`);
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const regex = new RegExp(pattern, "i");
|
|
1985
1968
|
const testInput = input.length > REGEX_TIMEOUT_MS * 40 ? input.slice(0, REGEX_TIMEOUT_MS * 40) : input;
|
|
1986
1969
|
return regex.test(testInput);
|
|
1987
1970
|
} catch {
|
|
@@ -2098,6 +2081,13 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
|
|
|
2098
2081
|
};
|
|
2099
2082
|
}
|
|
2100
2083
|
const command = input.tool_input.command;
|
|
2084
|
+
if (typeof command !== "string" || !command.trim()) {
|
|
2085
|
+
return {
|
|
2086
|
+
decision: "deny",
|
|
2087
|
+
reason: `Invalid input: Bash tool requires a non-empty string command, got ${typeof command}`,
|
|
2088
|
+
source: "instant-block"
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2101
2091
|
for (const pattern of config2.customPatterns.allow) {
|
|
2102
2092
|
if (safeRegexTest(pattern, command)) {
|
|
2103
2093
|
return {
|
|
@@ -2280,11 +2270,6 @@ If this was intentional, re-run the command and click "Allow".`;
|
|
|
2280
2270
|
console.log(JSON.stringify(output));
|
|
2281
2271
|
}
|
|
2282
2272
|
|
|
2283
|
-
// src/cli/check.ts
|
|
2284
|
-
async function check() {
|
|
2285
|
-
await runHook();
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
2273
|
// src/index.ts
|
|
2289
2274
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
2290
2275
|
var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
|
|
@@ -2323,7 +2308,7 @@ async function main() {
|
|
|
2323
2308
|
await uninstall();
|
|
2324
2309
|
break;
|
|
2325
2310
|
case "check":
|
|
2326
|
-
await
|
|
2311
|
+
await runHook();
|
|
2327
2312
|
break;
|
|
2328
2313
|
case "config":
|
|
2329
2314
|
await config();
|
package/package.json
CHANGED