@privacyscrubber/mcp-server 2.4.5 → 2.4.6
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 +14 -7
- package/index.js +542 -155
- package/install.js +93 -2
- package/manifest.json +8 -10
- package/package.json +1 -3
- package/pii-masking-run.js +202 -39
- package/ps-license-manager.cjs +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/ps-license-manager.js +0 -11
- package/ps-pii-engine.js +0 -11
package/index.js
CHANGED
|
@@ -82,6 +82,9 @@ if (rawCliArgs.includes('--memo') || rawCliArgs.includes('memo')) {
|
|
|
82
82
|
console.log(generateProcurementMemo({ targetTier: tierArg, format: 'text' }));
|
|
83
83
|
process.exit(0);
|
|
84
84
|
}
|
|
85
|
+
if (rawCliArgs.includes('--hook')) {
|
|
86
|
+
await import('./pii-masking-run.js');
|
|
87
|
+
}
|
|
85
88
|
if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup', 'uninstall', '--uninstall', '--remove'].includes(arg))) {
|
|
86
89
|
const { runInstaller } = await import('./install.js');
|
|
87
90
|
const isRemove = rawCliArgs.some(arg => ['uninstall', '--uninstall', '--remove'].includes(arg));
|
|
@@ -1364,6 +1367,15 @@ We request approval to acquire a **PrivacyScrubber TEAMS License** (\$99/month f
|
|
|
1364
1367
|
`;
|
|
1365
1368
|
}
|
|
1366
1369
|
|
|
1370
|
+
const MCP_SYSTEM_INSTRUCTIONS = [
|
|
1371
|
+
"Zero-Trust Data Sanitization (ZTDS) Protocol Active (100% local RAM, 0 network egress).",
|
|
1372
|
+
"AUTOMATIC INTERCEPTION RULES FOR AI AGENTS:",
|
|
1373
|
+
"1. FILE READS: Before reading .env, configs, logs, database dumps, or documents (DOCX/PDF/XLSX/CSV/JSON), call 'guard_read_file' (or 'sanitize_file') instead of native file read tools.",
|
|
1374
|
+
"2. SHELL & GIT: Route terminal commands that may output secrets or PII through 'guard_exec', and inspect diffs via 'guard_git_diff'.",
|
|
1375
|
+
"3. RAW TEXT & RAG: Sanitize raw user PII/secrets in prompts via 'sanitize_text' (or 'guard_rag_chunk' for vector chunks) before external reasoning.",
|
|
1376
|
+
"4. LOCAL RESTORATION: Keep placeholders like [EMAIL_1], [API_KEY_1], [NAME_1] intact during reasoning. Call 'reveal_text' to restore final answers, 'guard_apply_patch' to write unmasked code/configs to disk, or 'guard_unmask_args' for local tool calls."
|
|
1377
|
+
].join("\n");
|
|
1378
|
+
|
|
1367
1379
|
// Create the MCP server
|
|
1368
1380
|
const server = new Server(
|
|
1369
1381
|
{
|
|
@@ -1377,6 +1389,7 @@ const server = new Server(
|
|
|
1377
1389
|
logging: {},
|
|
1378
1390
|
resources: {},
|
|
1379
1391
|
},
|
|
1392
|
+
instructions: MCP_SYSTEM_INSTRUCTIONS,
|
|
1380
1393
|
}
|
|
1381
1394
|
);
|
|
1382
1395
|
|
|
@@ -1387,6 +1400,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1387
1400
|
{
|
|
1388
1401
|
name: "audit_context",
|
|
1389
1402
|
description: "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity (CLEAN, LOW, MODERATE, CRITICAL), and triggered regulatory frameworks (GDPR, HIPAA, SOC 2, PCI DSS) with zero text mutation.",
|
|
1403
|
+
annotations: {
|
|
1404
|
+
title: "Audit Context for PII & Secrets",
|
|
1405
|
+
readOnlyHint: true,
|
|
1406
|
+
destructiveHint: false,
|
|
1407
|
+
idempotentHint: true,
|
|
1408
|
+
openWorldHint: false
|
|
1409
|
+
},
|
|
1390
1410
|
inputSchema: {
|
|
1391
1411
|
type: "object",
|
|
1392
1412
|
properties: {
|
|
@@ -1414,6 +1434,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1414
1434
|
{
|
|
1415
1435
|
name: "sanitize_text",
|
|
1416
1436
|
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
1437
|
+
annotations: {
|
|
1438
|
+
title: "Sanitize Text in Local RAM",
|
|
1439
|
+
readOnlyHint: true,
|
|
1440
|
+
destructiveHint: false,
|
|
1441
|
+
idempotentHint: true,
|
|
1442
|
+
openWorldHint: false
|
|
1443
|
+
},
|
|
1417
1444
|
inputSchema: {
|
|
1418
1445
|
type: "object",
|
|
1419
1446
|
properties: {
|
|
@@ -1446,44 +1473,16 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1446
1473
|
required: ["text"]
|
|
1447
1474
|
}
|
|
1448
1475
|
},
|
|
1449
|
-
{
|
|
1450
|
-
name: "scrub_text",
|
|
1451
|
-
description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
|
|
1452
|
-
inputSchema: {
|
|
1453
|
-
type: "object",
|
|
1454
|
-
properties: {
|
|
1455
|
-
text: {
|
|
1456
|
-
type: "string",
|
|
1457
|
-
description: "The raw text, code, or logs to sanitize."
|
|
1458
|
-
},
|
|
1459
|
-
profile: {
|
|
1460
|
-
type: "string",
|
|
1461
|
-
description: PROFILE_PARAM_DESCRIPTION
|
|
1462
|
-
},
|
|
1463
|
-
sector: {
|
|
1464
|
-
type: "string",
|
|
1465
|
-
description: SECTOR_PARAM_DESCRIPTION
|
|
1466
|
-
},
|
|
1467
|
-
compact: {
|
|
1468
|
-
type: "boolean",
|
|
1469
|
-
description: "Optional. Compact 1-line audit summary."
|
|
1470
|
-
},
|
|
1471
|
-
ignore_list: {
|
|
1472
|
-
type: "array",
|
|
1473
|
-
items: { type: "string" },
|
|
1474
|
-
description: "Optional list of plaintext values to skip during detection."
|
|
1475
|
-
},
|
|
1476
|
-
session_id: {
|
|
1477
|
-
type: "string",
|
|
1478
|
-
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1479
|
-
}
|
|
1480
|
-
},
|
|
1481
|
-
required: ["text"]
|
|
1482
|
-
}
|
|
1483
|
-
},
|
|
1484
1476
|
{
|
|
1485
1477
|
name: "reveal_text",
|
|
1486
1478
|
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user. Supports both standard batch mode and real-time streaming mode (stream: true).",
|
|
1479
|
+
annotations: {
|
|
1480
|
+
title: "Restore Tokens in Local RAM",
|
|
1481
|
+
readOnlyHint: true,
|
|
1482
|
+
destructiveHint: false,
|
|
1483
|
+
idempotentHint: true,
|
|
1484
|
+
openWorldHint: false
|
|
1485
|
+
},
|
|
1487
1486
|
inputSchema: {
|
|
1488
1487
|
type: "object",
|
|
1489
1488
|
properties: {
|
|
@@ -1510,6 +1509,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1510
1509
|
{
|
|
1511
1510
|
name: "stream_reveal_text",
|
|
1512
1511
|
description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
|
|
1512
|
+
annotations: {
|
|
1513
|
+
title: "Streaming Token Detokenizer",
|
|
1514
|
+
readOnlyHint: true,
|
|
1515
|
+
destructiveHint: false,
|
|
1516
|
+
idempotentHint: false,
|
|
1517
|
+
openWorldHint: false
|
|
1518
|
+
},
|
|
1513
1519
|
inputSchema: {
|
|
1514
1520
|
type: "object",
|
|
1515
1521
|
properties: {
|
|
@@ -1535,43 +1541,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1535
1541
|
},
|
|
1536
1542
|
{
|
|
1537
1543
|
name: "sanitize_file",
|
|
1538
|
-
description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
profile: {
|
|
1547
|
-
type: "string",
|
|
1548
|
-
description: PROFILE_PARAM_DESCRIPTION
|
|
1549
|
-
},
|
|
1550
|
-
sector: {
|
|
1551
|
-
type: "string",
|
|
1552
|
-
description: SECTOR_PARAM_DESCRIPTION
|
|
1553
|
-
},
|
|
1554
|
-
compact: {
|
|
1555
|
-
type: "boolean",
|
|
1556
|
-
description: "Optional. Compact 1-line audit summary."
|
|
1557
|
-
},
|
|
1558
|
-
session_id: {
|
|
1559
|
-
type: "string",
|
|
1560
|
-
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1561
|
-
}
|
|
1562
|
-
},
|
|
1563
|
-
required: ["file_path"]
|
|
1564
|
-
}
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
name: "scrub_file",
|
|
1568
|
-
description: "Alias for 'sanitize_file'. Reads and sanitizes a local file.",
|
|
1544
|
+
description: "Reads a local file (including plain text, source code, .eml, .rtf, .docx, .pdf, .xlsx), sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
1545
|
+
annotations: {
|
|
1546
|
+
title: "Sanitize Local File in RAM",
|
|
1547
|
+
readOnlyHint: true,
|
|
1548
|
+
destructiveHint: false,
|
|
1549
|
+
idempotentHint: true,
|
|
1550
|
+
openWorldHint: false
|
|
1551
|
+
},
|
|
1569
1552
|
inputSchema: {
|
|
1570
1553
|
type: "object",
|
|
1571
1554
|
properties: {
|
|
1572
1555
|
file_path: {
|
|
1573
1556
|
type: "string",
|
|
1574
|
-
description: "Absolute path to the file to sanitize."
|
|
1557
|
+
description: "Absolute or workspace-relative path to the file to sanitize."
|
|
1575
1558
|
},
|
|
1576
1559
|
profile: {
|
|
1577
1560
|
type: "string",
|
|
@@ -1596,6 +1579,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1596
1579
|
{
|
|
1597
1580
|
name: "audit_directory_for_pii",
|
|
1598
1581
|
description: "Scans a local directory for leaks of secrets, keys, and PII. Returns a summary report. Use this tool for Security Auditing.",
|
|
1582
|
+
annotations: {
|
|
1583
|
+
title: "Audit Directory for PII & Secrets",
|
|
1584
|
+
readOnlyHint: true,
|
|
1585
|
+
destructiveHint: false,
|
|
1586
|
+
idempotentHint: true,
|
|
1587
|
+
openWorldHint: false
|
|
1588
|
+
},
|
|
1599
1589
|
inputSchema: {
|
|
1600
1590
|
type: "object",
|
|
1601
1591
|
properties: {
|
|
@@ -1627,6 +1617,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1627
1617
|
{
|
|
1628
1618
|
name: "redact_file",
|
|
1629
1619
|
description: "Action/Redact: In-place redaction of a local file. Replaces PII and secrets with tokens and saves the file. By default, creates a .bak backup. Use dry_run=true to test without modifying.",
|
|
1620
|
+
annotations: {
|
|
1621
|
+
title: "In-Place File Redaction",
|
|
1622
|
+
readOnlyHint: false,
|
|
1623
|
+
destructiveHint: true,
|
|
1624
|
+
idempotentHint: false,
|
|
1625
|
+
openWorldHint: false
|
|
1626
|
+
},
|
|
1630
1627
|
inputSchema: {
|
|
1631
1628
|
type: "object",
|
|
1632
1629
|
properties: {
|
|
@@ -1657,6 +1654,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1657
1654
|
{
|
|
1658
1655
|
name: "create_default_config",
|
|
1659
1656
|
description: "Creates a default 'privacyscrubber.json' configuration file in the active workspace root directory if one does not exist. Includes template structures for custom regex rules and exclusion bypass patterns.",
|
|
1657
|
+
annotations: {
|
|
1658
|
+
title: "Create Default Workspace Config",
|
|
1659
|
+
readOnlyHint: false,
|
|
1660
|
+
destructiveHint: false,
|
|
1661
|
+
idempotentHint: true,
|
|
1662
|
+
openWorldHint: false
|
|
1663
|
+
},
|
|
1660
1664
|
inputSchema: {
|
|
1661
1665
|
type: "object",
|
|
1662
1666
|
properties: {},
|
|
@@ -1666,6 +1670,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1666
1670
|
{
|
|
1667
1671
|
name: "generate_compliance_report",
|
|
1668
1672
|
description: "Generates an official Zero-Trust Compliance Audit Certificate (GDPR, HIPAA, EU AI Act, SOC 2) for the current MCP session. Returns cryptographic session hash, masked entities breakdown, and compliance certification.",
|
|
1673
|
+
annotations: {
|
|
1674
|
+
title: "Generate Compliance Audit Certificate",
|
|
1675
|
+
readOnlyHint: true,
|
|
1676
|
+
destructiveHint: false,
|
|
1677
|
+
idempotentHint: true,
|
|
1678
|
+
openWorldHint: false
|
|
1679
|
+
},
|
|
1669
1680
|
inputSchema: {
|
|
1670
1681
|
type: "object",
|
|
1671
1682
|
properties: {
|
|
@@ -1693,6 +1704,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1693
1704
|
{
|
|
1694
1705
|
name: "mark_false_positive",
|
|
1695
1706
|
description: "Marks a previously detected token as a false positive. The original plaintext value will be excluded from all future sanitize_text calls in this session. Returns the restored original value and updated ignore list size.",
|
|
1707
|
+
annotations: {
|
|
1708
|
+
title: "Mark Token as False Positive",
|
|
1709
|
+
readOnlyHint: true,
|
|
1710
|
+
destructiveHint: false,
|
|
1711
|
+
idempotentHint: true,
|
|
1712
|
+
openWorldHint: false
|
|
1713
|
+
},
|
|
1696
1714
|
inputSchema: {
|
|
1697
1715
|
type: "object",
|
|
1698
1716
|
properties: {
|
|
@@ -1711,6 +1729,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1711
1729
|
{
|
|
1712
1730
|
name: "check_status",
|
|
1713
1731
|
description: "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
|
|
1732
|
+
annotations: {
|
|
1733
|
+
title: "Check MCP License & Session Status",
|
|
1734
|
+
readOnlyHint: true,
|
|
1735
|
+
destructiveHint: false,
|
|
1736
|
+
idempotentHint: true,
|
|
1737
|
+
openWorldHint: false
|
|
1738
|
+
},
|
|
1714
1739
|
inputSchema: {
|
|
1715
1740
|
type: "object",
|
|
1716
1741
|
properties: {},
|
|
@@ -1720,6 +1745,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1720
1745
|
{
|
|
1721
1746
|
name: "guard_exec",
|
|
1722
1747
|
description: "Zero-Trust Agentic Guard: Safely executes a shell/terminal command locally in isolated process, sanitizes stdout and stderr in volatile RAM, tokenizes any API keys, credentials, database URIs, or PII before sending output to LLM. Maps original values in memory for safe local restoration.",
|
|
1748
|
+
annotations: {
|
|
1749
|
+
title: "Guard Shell Command Execution",
|
|
1750
|
+
readOnlyHint: false,
|
|
1751
|
+
destructiveHint: true,
|
|
1752
|
+
idempotentHint: false,
|
|
1753
|
+
openWorldHint: false
|
|
1754
|
+
},
|
|
1723
1755
|
inputSchema: {
|
|
1724
1756
|
type: "object",
|
|
1725
1757
|
properties: {
|
|
@@ -1742,6 +1774,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1742
1774
|
timeout_ms: {
|
|
1743
1775
|
type: "number",
|
|
1744
1776
|
description: "Execution timeout in milliseconds. Defaults to 15000 (15s)."
|
|
1777
|
+
},
|
|
1778
|
+
session_id: {
|
|
1779
|
+
type: "string",
|
|
1780
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1745
1781
|
}
|
|
1746
1782
|
},
|
|
1747
1783
|
required: ["command"]
|
|
@@ -1749,7 +1785,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1749
1785
|
},
|
|
1750
1786
|
{
|
|
1751
1787
|
name: "guard_read_file",
|
|
1752
|
-
description: "Zero-Trust Agentic Guard: Reads
|
|
1788
|
+
description: "Zero-Trust Agentic Guard: Preferred over native file read tools. Reads any sensitive local file (.env, config, credentials, source code, database dumps, .docx, .pdf, .xlsx, .eml, .rtf), sanitizes all credentials and PII in volatile RAM, and returns safe tokenized content for AI agent context.",
|
|
1789
|
+
annotations: {
|
|
1790
|
+
title: "Guard Local File Read",
|
|
1791
|
+
readOnlyHint: true,
|
|
1792
|
+
destructiveHint: false,
|
|
1793
|
+
idempotentHint: true,
|
|
1794
|
+
openWorldHint: false
|
|
1795
|
+
},
|
|
1753
1796
|
inputSchema: {
|
|
1754
1797
|
type: "object",
|
|
1755
1798
|
properties: {
|
|
@@ -1768,6 +1811,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1768
1811
|
max_lines: {
|
|
1769
1812
|
type: "number",
|
|
1770
1813
|
description: "Optional line cap for large files. Defaults to 500."
|
|
1814
|
+
},
|
|
1815
|
+
session_id: {
|
|
1816
|
+
type: "string",
|
|
1817
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1771
1818
|
}
|
|
1772
1819
|
},
|
|
1773
1820
|
required: ["file_path"]
|
|
@@ -1776,6 +1823,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1776
1823
|
{
|
|
1777
1824
|
name: "guard_git_diff",
|
|
1778
1825
|
description: "Zero-Trust Agentic Guard: Inspects unstaged or staged git diffs locally, sanitizes any newly added or modified credentials, tokens, or PII in RAM, and returns safe diff for AI commit generation or code review.",
|
|
1826
|
+
annotations: {
|
|
1827
|
+
title: "Guard Git Diff Inspection",
|
|
1828
|
+
readOnlyHint: true,
|
|
1829
|
+
destructiveHint: false,
|
|
1830
|
+
idempotentHint: true,
|
|
1831
|
+
openWorldHint: false
|
|
1832
|
+
},
|
|
1779
1833
|
inputSchema: {
|
|
1780
1834
|
type: "object",
|
|
1781
1835
|
properties: {
|
|
@@ -1806,6 +1860,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1806
1860
|
{
|
|
1807
1861
|
name: "guard_apply_patch",
|
|
1808
1862
|
description: "Zero-Trust Agentic Guard: Reverses token placeholders ([API_KEY_1], [SECRET_1]) in AI-generated code or text, restores authentic values from local volatile RAM, and writes real content directly to target file on disk. Remote LLM never learns actual secrets.",
|
|
1863
|
+
annotations: {
|
|
1864
|
+
title: "Guard Apply Unmasked Patch to Disk",
|
|
1865
|
+
readOnlyHint: false,
|
|
1866
|
+
destructiveHint: false,
|
|
1867
|
+
idempotentHint: true,
|
|
1868
|
+
openWorldHint: false
|
|
1869
|
+
},
|
|
1809
1870
|
inputSchema: {
|
|
1810
1871
|
type: "object",
|
|
1811
1872
|
properties: {
|
|
@@ -1820,6 +1881,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1820
1881
|
create_backup: {
|
|
1821
1882
|
type: "boolean",
|
|
1822
1883
|
description: "Whether to create a .bak backup if target file already exists. Defaults to true."
|
|
1884
|
+
},
|
|
1885
|
+
session_id: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1823
1888
|
}
|
|
1824
1889
|
},
|
|
1825
1890
|
required: ["file_path", "content"]
|
|
@@ -1827,7 +1892,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1827
1892
|
},
|
|
1828
1893
|
{
|
|
1829
1894
|
name: "create_agent_rules",
|
|
1830
|
-
description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursorrules, .windsurfrules, CLAUDE.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
|
|
1895
|
+
description: "Zero-Trust Agentic Guard: Scaffolds CISO-grade AI Agent Rules in workspace (.cursor/rules/privacyscrubber.mdc, .cursorrules, .windsurfrules, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, .clinerules). Guides autonomous agents to route all file reads, git diffs, and shell commands through PrivacyScrubber Guard tools.",
|
|
1896
|
+
annotations: {
|
|
1897
|
+
title: "Scaffold AI Agent Security Rules",
|
|
1898
|
+
readOnlyHint: false,
|
|
1899
|
+
destructiveHint: false,
|
|
1900
|
+
idempotentHint: true,
|
|
1901
|
+
openWorldHint: false
|
|
1902
|
+
},
|
|
1831
1903
|
inputSchema: {
|
|
1832
1904
|
type: "object",
|
|
1833
1905
|
properties: {
|
|
@@ -1835,7 +1907,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1835
1907
|
type: "array",
|
|
1836
1908
|
items: {
|
|
1837
1909
|
type: "string",
|
|
1838
|
-
enum: ["all", "cursor", "windsurf", "claude_code", "copilot", "cline"]
|
|
1910
|
+
enum: ["all", "cursor", "windsurf", "claude_code", "agents_md", "codex", "copilot", "cline"]
|
|
1839
1911
|
},
|
|
1840
1912
|
description: "List of agent targets to generate rules for. Use ['all'] to generate rules for all supported AI agents."
|
|
1841
1913
|
},
|
|
@@ -1850,6 +1922,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1850
1922
|
{
|
|
1851
1923
|
name: "guard_unmask_args",
|
|
1852
1924
|
description: "Zero-Trust Agentic Guard: Detokenizes parameters, JSON objects, or arguments in local RAM before dispatching to local tools or APIs without sending cleartext secrets back to the LLM.",
|
|
1925
|
+
annotations: {
|
|
1926
|
+
title: "Unmask Tool Arguments in Local RAM",
|
|
1927
|
+
readOnlyHint: true,
|
|
1928
|
+
destructiveHint: false,
|
|
1929
|
+
idempotentHint: true,
|
|
1930
|
+
openWorldHint: false
|
|
1931
|
+
},
|
|
1853
1932
|
inputSchema: {
|
|
1854
1933
|
type: "object",
|
|
1855
1934
|
properties: {
|
|
@@ -1867,6 +1946,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1867
1946
|
{
|
|
1868
1947
|
name: "guard_session_info",
|
|
1869
1948
|
description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
|
|
1949
|
+
annotations: {
|
|
1950
|
+
title: "Inspect RAM Session Diagnostics",
|
|
1951
|
+
readOnlyHint: true,
|
|
1952
|
+
destructiveHint: false,
|
|
1953
|
+
idempotentHint: true,
|
|
1954
|
+
openWorldHint: false
|
|
1955
|
+
},
|
|
1870
1956
|
inputSchema: {
|
|
1871
1957
|
type: "object",
|
|
1872
1958
|
properties: {
|
|
@@ -1881,6 +1967,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1881
1967
|
{
|
|
1882
1968
|
name: "guard_session_reset",
|
|
1883
1969
|
description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
|
|
1970
|
+
annotations: {
|
|
1971
|
+
title: "Reset RAM Session Token Map",
|
|
1972
|
+
readOnlyHint: true,
|
|
1973
|
+
destructiveHint: false,
|
|
1974
|
+
idempotentHint: true,
|
|
1975
|
+
openWorldHint: false
|
|
1976
|
+
},
|
|
1884
1977
|
inputSchema: {
|
|
1885
1978
|
type: "object",
|
|
1886
1979
|
properties: {
|
|
@@ -1899,6 +1992,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1899
1992
|
{
|
|
1900
1993
|
name: "guard_rag_chunk",
|
|
1901
1994
|
description: "Zero-Trust Agentic Guard: Sanitizes an array of document chunks or knowledge base records before sending to embedding models or vector databases (Chroma, Pinecone, Qdrant). Keeps authentic data in local RAM with zero network leakage.",
|
|
1995
|
+
annotations: {
|
|
1996
|
+
title: "Sanitize RAG Document Chunks",
|
|
1997
|
+
readOnlyHint: true,
|
|
1998
|
+
destructiveHint: false,
|
|
1999
|
+
idempotentHint: true,
|
|
2000
|
+
openWorldHint: false
|
|
2001
|
+
},
|
|
1902
2002
|
inputSchema: {
|
|
1903
2003
|
type: "object",
|
|
1904
2004
|
properties: {
|
|
@@ -1939,6 +2039,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1939
2039
|
{
|
|
1940
2040
|
name: "guard_rag_restore",
|
|
1941
2041
|
description: "Zero-Trust Agentic Guard: Re-hydrates retrieved vector search results or chunks by restoring token placeholders with authentic data from the volatile RAM session before presenting to the user.",
|
|
2042
|
+
annotations: {
|
|
2043
|
+
title: "Restore RAG Vector Search Results",
|
|
2044
|
+
readOnlyHint: true,
|
|
2045
|
+
destructiveHint: false,
|
|
2046
|
+
idempotentHint: true,
|
|
2047
|
+
openWorldHint: false
|
|
2048
|
+
},
|
|
1942
2049
|
inputSchema: {
|
|
1943
2050
|
type: "object",
|
|
1944
2051
|
properties: {
|
|
@@ -1972,6 +2079,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1972
2079
|
{
|
|
1973
2080
|
name: "export_audit_log",
|
|
1974
2081
|
description: "Zero-Trust Enterprise Compliance: Exports an immutable, tamper-evident audit log in SIEM standard formats (CEF / ArcSight, RFC 5424 Syslog, JSONL, or JSON) with zero plaintext PII leakage. Can write directly to a local log file or stream.",
|
|
2082
|
+
annotations: {
|
|
2083
|
+
title: "Export SIEM Compliance Audit Log",
|
|
2084
|
+
readOnlyHint: false,
|
|
2085
|
+
destructiveHint: false,
|
|
2086
|
+
idempotentHint: true,
|
|
2087
|
+
openWorldHint: false
|
|
2088
|
+
},
|
|
1975
2089
|
inputSchema: {
|
|
1976
2090
|
type: "object",
|
|
1977
2091
|
properties: {
|
|
@@ -2006,6 +2120,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2006
2120
|
{
|
|
2007
2121
|
name: "generate_procurement_memo",
|
|
2008
2122
|
description: "Zero-Trust Enterprise Procurement: Generates an executive-ready Procurement & ROI Memorandum for Chief Information Security Officers (CISO), Data Protection Officers (DPO), and VP of Engineering. Compares TCO of traditional Cloud DLP ($1.5k–$4k/mo) vs PrivacyScrubber TEAMS ($99/mo flat) or Developer SDK ($299/mo flat), citing statutory frameworks (GDPR Art. 4/28, HIPAA §164.514, SOC 2 Type II, PCI DSS v4.0, NIST SP 800-53, EU AI Act Art. 50).",
|
|
2123
|
+
annotations: {
|
|
2124
|
+
title: "Generate CISO Procurement & ROI Memo",
|
|
2125
|
+
readOnlyHint: true,
|
|
2126
|
+
destructiveHint: false,
|
|
2127
|
+
idempotentHint: true,
|
|
2128
|
+
openWorldHint: false
|
|
2129
|
+
},
|
|
2009
2130
|
inputSchema: {
|
|
2010
2131
|
type: "object",
|
|
2011
2132
|
properties: {
|
|
@@ -2178,11 +2299,51 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
2178
2299
|
throw new Error(`Resource not found: ${uri}`);
|
|
2179
2300
|
});
|
|
2180
2301
|
|
|
2302
|
+
function getEffectiveWorkspaceRoots() {
|
|
2303
|
+
const roots = [];
|
|
2304
|
+
const addCandidate = (dir) => {
|
|
2305
|
+
if (!dir || typeof dir !== 'string') return;
|
|
2306
|
+
const trimmed = dir.trim();
|
|
2307
|
+
if (!trimmed) return;
|
|
2308
|
+
try {
|
|
2309
|
+
const resolved = fs.existsSync(trimmed) ? fs.realpathSync(trimmed) : path.resolve(trimmed);
|
|
2310
|
+
if (!roots.includes(resolved)) roots.push(resolved);
|
|
2311
|
+
} catch (_) {
|
|
2312
|
+
const fallback = path.resolve(trimmed);
|
|
2313
|
+
if (!roots.includes(fallback)) roots.push(fallback);
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
|
|
2317
|
+
addCandidate(process.cwd());
|
|
2318
|
+
addCandidate(process.env.PRIVACYSCRUBBER_WORKSPACE);
|
|
2319
|
+
addCandidate(process.env.CURSOR_WORKSPACE_PATH);
|
|
2320
|
+
addCandidate(process.env.VSCODE_CWD);
|
|
2321
|
+
addCandidate(process.env.PROJECT_ROOT);
|
|
2322
|
+
addCandidate(process.env.INIT_CWD);
|
|
2323
|
+
if (process.env.WORKSPACE_FOLDER_PATHS) {
|
|
2324
|
+
for (const part of process.env.WORKSPACE_FOLDER_PATHS.split(path.delimiter)) {
|
|
2325
|
+
addCandidate(part);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return roots;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function isWithinAnyWorkspaceRoot(targetPath, roots) {
|
|
2332
|
+
for (const root of roots) {
|
|
2333
|
+
const rel = path.relative(root, targetPath);
|
|
2334
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
2335
|
+
return { ok: true, matchedRoot: root };
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
return { ok: false, matchedRoot: roots[0] || process.cwd() };
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2181
2341
|
function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
|
|
2182
|
-
const
|
|
2183
|
-
const
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2342
|
+
const roots = getEffectiveWorkspaceRoots();
|
|
2343
|
+
const primaryRoot = roots[0];
|
|
2344
|
+
const resolvedPath = path.resolve(primaryRoot, inputPath);
|
|
2345
|
+
const boundaryCheck = isWithinAnyWorkspaceRoot(resolvedPath, roots);
|
|
2346
|
+
if (!boundaryCheck.ok) {
|
|
2186
2347
|
return {
|
|
2187
2348
|
ok: false,
|
|
2188
2349
|
error: `Error: Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`
|
|
@@ -2196,15 +2357,15 @@ function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
|
|
|
2196
2357
|
}
|
|
2197
2358
|
if (fs.existsSync(checkCursor)) {
|
|
2198
2359
|
const realCheck = fs.realpathSync(checkCursor);
|
|
2199
|
-
const
|
|
2200
|
-
if (
|
|
2360
|
+
const realBoundary = isWithinAnyWorkspaceRoot(realCheck, roots);
|
|
2361
|
+
if (!realBoundary.ok) {
|
|
2201
2362
|
return {
|
|
2202
2363
|
ok: false,
|
|
2203
2364
|
error: `Error: Security policy violation — target path symlink escapes the active workspace directory.`
|
|
2204
2365
|
};
|
|
2205
2366
|
}
|
|
2206
2367
|
}
|
|
2207
|
-
return { ok: true, resolvedPath, workspaceRoot };
|
|
2368
|
+
return { ok: true, resolvedPath, workspaceRoot: boundaryCheck.matchedRoot };
|
|
2208
2369
|
}
|
|
2209
2370
|
|
|
2210
2371
|
// Handle tool execution calls
|
|
@@ -2239,18 +2400,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2239
2400
|
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
|
|
2240
2401
|
const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
|
|
2241
2402
|
|
|
2403
|
+
const structuredAudit = {
|
|
2404
|
+
riskLevel: telemetry.riskLevel,
|
|
2405
|
+
totalDetected: telemetry.totalDetected,
|
|
2406
|
+
entities: telemetry.entities,
|
|
2407
|
+
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
2408
|
+
profile: targetProfile,
|
|
2409
|
+
executionEngine: "ZTDS Client-Side RAM"
|
|
2410
|
+
};
|
|
2411
|
+
|
|
2242
2412
|
if (format === "json") {
|
|
2243
2413
|
return {
|
|
2414
|
+
structuredContent: structuredAudit,
|
|
2244
2415
|
content: [{
|
|
2245
2416
|
type: "text",
|
|
2246
|
-
text: JSON.stringify(
|
|
2247
|
-
riskLevel: telemetry.riskLevel,
|
|
2248
|
-
totalDetected: telemetry.totalDetected,
|
|
2249
|
-
entities: telemetry.entities,
|
|
2250
|
-
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
2251
|
-
profile: targetProfile,
|
|
2252
|
-
executionEngine: "ZTDS Client-Side RAM"
|
|
2253
|
-
}, null, 2)
|
|
2417
|
+
text: JSON.stringify(structuredAudit, null, 2)
|
|
2254
2418
|
}]
|
|
2255
2419
|
};
|
|
2256
2420
|
}
|
|
@@ -2275,6 +2439,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2275
2439
|
}
|
|
2276
2440
|
|
|
2277
2441
|
return {
|
|
2442
|
+
structuredContent: structuredAudit,
|
|
2278
2443
|
content: [{ type: "text", text: report }]
|
|
2279
2444
|
};
|
|
2280
2445
|
}
|
|
@@ -2335,6 +2500,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2335
2500
|
const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
|
|
2336
2501
|
|
|
2337
2502
|
return {
|
|
2503
|
+
structuredContent: {
|
|
2504
|
+
scrubbedText,
|
|
2505
|
+
tokensRedacted: telemetry.totalCount,
|
|
2506
|
+
riskLevel: telemetry.riskLevel,
|
|
2507
|
+
entities: telemetry.entities,
|
|
2508
|
+
frameworks: telemetry.frameworksList,
|
|
2509
|
+
profile: finalProfile,
|
|
2510
|
+
sessionId: session_id,
|
|
2511
|
+
wasTruncated: Boolean(wasTruncated)
|
|
2512
|
+
},
|
|
2338
2513
|
content: [
|
|
2339
2514
|
{ type: "text", text: scrubbedText + receiptMd }
|
|
2340
2515
|
]
|
|
@@ -2404,6 +2579,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2404
2579
|
restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
|
|
2405
2580
|
|
|
2406
2581
|
return {
|
|
2582
|
+
structuredContent: {
|
|
2583
|
+
restoredText,
|
|
2584
|
+
restoredCount: restored.restoredCount || 0,
|
|
2585
|
+
sessionId: session_id
|
|
2586
|
+
},
|
|
2407
2587
|
content: [
|
|
2408
2588
|
{
|
|
2409
2589
|
type: "text",
|
|
@@ -2584,9 +2764,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2584
2764
|
const dataStart = localOffset + 30 + lNameLen + lExtraLen;
|
|
2585
2765
|
const compData = docxBuf.subarray(dataStart, dataStart + compSize);
|
|
2586
2766
|
if (method === 0) {
|
|
2587
|
-
|
|
2767
|
+
if (compData.length <= 20 * 1024 * 1024) {
|
|
2768
|
+
zipEntries[entryName] = compData.toString('utf8');
|
|
2769
|
+
}
|
|
2588
2770
|
} else if (method === 8) {
|
|
2589
|
-
zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
|
|
2771
|
+
zipEntries[entryName] = zlib.inflateRawSync(compData, { maxOutputLength: 20 * 1024 * 1024 }).toString('utf8');
|
|
2590
2772
|
}
|
|
2591
2773
|
}
|
|
2592
2774
|
pos += 46 + nameLen + extraLen + commentLen;
|
|
@@ -2910,6 +3092,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2910
3092
|
const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
|
|
2911
3093
|
|
|
2912
3094
|
return {
|
|
3095
|
+
structuredContent: {
|
|
3096
|
+
scrubbedText,
|
|
3097
|
+
tokensRedacted: telemetry.totalCount,
|
|
3098
|
+
riskLevel: telemetry.riskLevel,
|
|
3099
|
+
entities: telemetry.entities,
|
|
3100
|
+
frameworks: telemetry.frameworksList,
|
|
3101
|
+
profile: finalProfile,
|
|
3102
|
+
sessionId: session_id,
|
|
3103
|
+
wasTruncated: Boolean(wasTruncated)
|
|
3104
|
+
},
|
|
2913
3105
|
content: [
|
|
2914
3106
|
{ type: "text", text: scrubbedText + receiptMd }
|
|
2915
3107
|
]
|
|
@@ -2931,7 +3123,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2931
3123
|
}
|
|
2932
3124
|
|
|
2933
3125
|
try {
|
|
2934
|
-
const
|
|
3126
|
+
const lstat = await fs.promises.lstat(resolvedRedactPath);
|
|
3127
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
3128
|
+
return { isError: true, content: [{ type: "text", text: `Error: Target must be a regular non-symlink file.` }] };
|
|
3129
|
+
}
|
|
3130
|
+
const stat = lstat;
|
|
2935
3131
|
if (stat.size > 50 * 1024 * 1024) {
|
|
2936
3132
|
return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
|
|
2937
3133
|
}
|
|
@@ -3229,20 +3425,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3229
3425
|
async function walkDir(dir) {
|
|
3230
3426
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
3231
3427
|
for (const entry of entries) {
|
|
3428
|
+
if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
|
|
3232
3429
|
const fullPath = path.join(dir, entry.name);
|
|
3430
|
+
const safeChild = resolveWorkspaceSafePath(fullPath, "directory_path");
|
|
3431
|
+
if (!safeChild.ok) continue;
|
|
3233
3432
|
if (entry.isDirectory()) {
|
|
3234
|
-
if (!ignoreDirs.includes(entry.name)) await walkDir(
|
|
3433
|
+
if (!ignoreDirs.includes(entry.name)) await walkDir(safeChild.resolvedPath);
|
|
3235
3434
|
} else if (entry.isFile()) {
|
|
3236
3435
|
if (extensions && extensions.length > 0) {
|
|
3237
3436
|
const ext = path.extname(entry.name);
|
|
3238
3437
|
if (!extensions.includes(ext)) continue;
|
|
3239
3438
|
}
|
|
3240
|
-
filesToScan.push(
|
|
3439
|
+
filesToScan.push(safeChild.resolvedPath);
|
|
3241
3440
|
}
|
|
3242
3441
|
}
|
|
3243
3442
|
}
|
|
3244
3443
|
|
|
3245
|
-
await walkDir(
|
|
3444
|
+
await walkDir(safeDir.resolvedPath);
|
|
3246
3445
|
|
|
3247
3446
|
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
3248
3447
|
let filesWithIssues = 0;
|
|
@@ -3443,23 +3642,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3443
3642
|
};
|
|
3444
3643
|
}
|
|
3445
3644
|
const rawBuffer = fs.readFileSync(resolvedPath);
|
|
3446
|
-
const
|
|
3447
|
-
for (let i = 0; i < checkLen; i++) {
|
|
3448
|
-
if (rawBuffer[i] === 0) {
|
|
3449
|
-
return {
|
|
3450
|
-
isError: true,
|
|
3451
|
-
content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
|
|
3452
|
-
};
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3645
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
3455
3646
|
const license = checkLicenseStatus();
|
|
3456
3647
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3457
3648
|
if (limitStatus.blocked) {
|
|
3458
3649
|
return buildDailyLimitBlockedResponse();
|
|
3459
3650
|
}
|
|
3460
3651
|
|
|
3461
|
-
let rawContent =
|
|
3462
|
-
|
|
3652
|
+
let rawContent = '';
|
|
3653
|
+
if (ext === '.docx') {
|
|
3654
|
+
try {
|
|
3655
|
+
const mammoth = require('mammoth');
|
|
3656
|
+
const result = await mammoth.extractRawText({ buffer: rawBuffer });
|
|
3657
|
+
rawContent = result.value || '';
|
|
3658
|
+
} catch (docxErr) {
|
|
3659
|
+
return {
|
|
3660
|
+
isError: true,
|
|
3661
|
+
content: [{ type: "text", text: `Error: Failed to parse DOCX file: ${docxErr.message}` }]
|
|
3662
|
+
};
|
|
3663
|
+
}
|
|
3664
|
+
} else if (ext === '.pdf' || ext === '.xlsx' || ext === '.xls') {
|
|
3665
|
+
if (!license.isPro) {
|
|
3666
|
+
return {
|
|
3667
|
+
isError: true,
|
|
3668
|
+
content: [{
|
|
3669
|
+
type: "text",
|
|
3670
|
+
text: `🔒 [PrivacyScrubber PRO / TEAMS] PDF and Excel file sanitization is a PRO feature (${ext === '.pdf' ? 'PDF' : 'Excel/XLSX'} detected).\n\n👉 7-Day Free B2B Trial (No card): https://privacyscrubber.com/pricing/?tier=teams&trial=true&utm_source=mcp_cli&utm_medium=file_type&utm_campaign=b2b_trial`
|
|
3671
|
+
}]
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
if (ext === '.pdf') {
|
|
3675
|
+
try {
|
|
3676
|
+
const pdf = require('pdf-parse');
|
|
3677
|
+
const data = await pdf(rawBuffer);
|
|
3678
|
+
rawContent = data.text || '';
|
|
3679
|
+
} catch (pdfErr) {
|
|
3680
|
+
return {
|
|
3681
|
+
isError: true,
|
|
3682
|
+
content: [{ type: "text", text: `Error: Failed to parse PDF file: ${pdfErr.message}` }]
|
|
3683
|
+
};
|
|
3684
|
+
}
|
|
3685
|
+
} else {
|
|
3686
|
+
try {
|
|
3687
|
+
const XLSX = require('xlsx');
|
|
3688
|
+
const workbook = XLSX.read(rawBuffer, { type: 'buffer' });
|
|
3689
|
+
workbook.SheetNames.forEach(sheetName => {
|
|
3690
|
+
rawContent += `--- Sheet: ${sheetName} ---\n` + XLSX.utils.sheet_to_csv(workbook.Sheets[sheetName]) + '\n';
|
|
3691
|
+
});
|
|
3692
|
+
} catch (xlsxErr) {
|
|
3693
|
+
return {
|
|
3694
|
+
isError: true,
|
|
3695
|
+
content: [{ type: "text", text: `Error: Failed to parse Excel file: ${xlsxErr.message}` }]
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
} else {
|
|
3700
|
+
const checkLen = Math.min(rawBuffer.length, 8000);
|
|
3701
|
+
for (let i = 0; i < checkLen; i++) {
|
|
3702
|
+
if (rawBuffer[i] === 0) {
|
|
3703
|
+
return {
|
|
3704
|
+
isError: true,
|
|
3705
|
+
content: [{ type: "text", text: `Error: Binary file format detected. Only plain text, source code, and document formats (.docx, .pdf, .xlsx, .eml, .rtf) are supported.` }]
|
|
3706
|
+
};
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
rawContent = rawBuffer.toString('utf8');
|
|
3710
|
+
}
|
|
3463
3711
|
if (ext === '.eml') {
|
|
3464
3712
|
const decodeQpUtf8 = (str) =>
|
|
3465
3713
|
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
@@ -3599,28 +3847,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3599
3847
|
};
|
|
3600
3848
|
}
|
|
3601
3849
|
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
const relToRoot = path.relative(workspaceRoot, resolvedPath);
|
|
3605
|
-
if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
|
|
3850
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
3851
|
+
if (!safeTarget.ok) {
|
|
3606
3852
|
return {
|
|
3607
3853
|
isError: true,
|
|
3608
|
-
content: [{ type: "text", text:
|
|
3854
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
3609
3855
|
};
|
|
3610
3856
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3857
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
3858
|
+
|
|
3859
|
+
const license = checkLicenseStatus();
|
|
3860
|
+
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3861
|
+
if (limitStatus.blocked) {
|
|
3862
|
+
return buildDailyLimitBlockedResponse();
|
|
3616
3863
|
}
|
|
3617
|
-
if (fs.existsSync(
|
|
3618
|
-
const
|
|
3619
|
-
|
|
3620
|
-
if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
|
|
3864
|
+
if (fs.existsSync(resolvedPath)) {
|
|
3865
|
+
const existingLstat = fs.lstatSync(resolvedPath);
|
|
3866
|
+
if (existingLstat.isSymbolicLink() || !existingLstat.isFile()) {
|
|
3621
3867
|
return {
|
|
3622
3868
|
isError: true,
|
|
3623
|
-
content: [{ type: "text", text: "Error: Security policy violation — target
|
|
3869
|
+
content: [{ type: "text", text: "Error: Security policy violation — target must be a regular non-symlink file." }]
|
|
3624
3870
|
};
|
|
3625
3871
|
}
|
|
3626
3872
|
}
|
|
@@ -3665,7 +3911,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3665
3911
|
}
|
|
3666
3912
|
targetDir = safeWorkspace.resolvedPath;
|
|
3667
3913
|
}
|
|
3668
|
-
const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
|
|
3914
|
+
const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline", "agents_md"] : agent_types;
|
|
3669
3915
|
|
|
3670
3916
|
const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
|
|
3671
3917
|
# 100% Local In-Memory Data Sanitization. Zero Network Egress.
|
|
@@ -3689,6 +3935,14 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3689
3935
|
const p = path.join(targetDir, '.cursorrules');
|
|
3690
3936
|
fs.writeFileSync(p, ruleText, 'utf8');
|
|
3691
3937
|
generatedFiles.push('.cursorrules');
|
|
3938
|
+
|
|
3939
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
3940
|
+
try {
|
|
3941
|
+
if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
3942
|
+
const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) Agentic Guard\nglobs: *\nalwaysApply: true\n---\n\n${ruleText}`;
|
|
3943
|
+
fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
|
|
3944
|
+
generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
|
|
3945
|
+
} catch (_) {}
|
|
3692
3946
|
}
|
|
3693
3947
|
if (targets.includes("windsurf")) {
|
|
3694
3948
|
const p = path.join(targetDir, '.windsurfrules');
|
|
@@ -3708,6 +3962,19 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3708
3962
|
generatedFiles.push('CLAUDE.md');
|
|
3709
3963
|
}
|
|
3710
3964
|
}
|
|
3965
|
+
if (targets.includes("agents_md") || targets.includes("codex")) {
|
|
3966
|
+
const p = path.join(targetDir, 'AGENTS.md');
|
|
3967
|
+
if (fs.existsSync(p)) {
|
|
3968
|
+
const existing = fs.readFileSync(p, 'utf8');
|
|
3969
|
+
if (!existing.includes("Zero-Trust Agentic Guard")) {
|
|
3970
|
+
fs.writeFileSync(p, existing + "\n\n" + ruleText, 'utf8');
|
|
3971
|
+
generatedFiles.push('AGENTS.md (appended)');
|
|
3972
|
+
}
|
|
3973
|
+
} else {
|
|
3974
|
+
fs.writeFileSync(p, ruleText, 'utf8');
|
|
3975
|
+
generatedFiles.push('AGENTS.md');
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3711
3978
|
if (targets.includes("copilot")) {
|
|
3712
3979
|
const copilotDir = path.join(targetDir, '.github');
|
|
3713
3980
|
if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
|
|
@@ -3721,7 +3988,57 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3721
3988
|
generatedFiles.push('.clinerules');
|
|
3722
3989
|
}
|
|
3723
3990
|
|
|
3991
|
+
if (args?.include_hooks !== false && (targets.includes("cursor") || targets.includes("claude_code") || agent_types.includes("all"))) {
|
|
3992
|
+
const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
|
|
3993
|
+
if (targets.includes("cursor") || agent_types.includes("all")) {
|
|
3994
|
+
try {
|
|
3995
|
+
const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
|
|
3996
|
+
fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
|
|
3997
|
+
let cursorHooks = { version: 1, hooks: {} };
|
|
3998
|
+
if (fs.existsSync(cursorHooksPath)) {
|
|
3999
|
+
try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
|
|
4000
|
+
}
|
|
4001
|
+
if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
|
|
4002
|
+
for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
|
|
4003
|
+
if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
|
|
4004
|
+
if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
|
|
4005
|
+
cursorHooks.hooks[ev].push({ command: hookCmd });
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
|
|
4009
|
+
generatedFiles.push('.cursor/hooks.json');
|
|
4010
|
+
} catch (_) {}
|
|
4011
|
+
}
|
|
4012
|
+
if (targets.includes("claude_code") || agent_types.includes("all")) {
|
|
4013
|
+
try {
|
|
4014
|
+
const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
4015
|
+
fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
|
|
4016
|
+
let claudeSettings = {};
|
|
4017
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
4018
|
+
try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
|
|
4019
|
+
}
|
|
4020
|
+
if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
|
|
4021
|
+
if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
|
|
4022
|
+
const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
|
|
4023
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
|
|
4024
|
+
);
|
|
4025
|
+
if (!hasClaudeHook) {
|
|
4026
|
+
claudeSettings.hooks.PreToolUse.push({
|
|
4027
|
+
matcher: "Read|Bash",
|
|
4028
|
+
hooks: [{ type: "command", command: hookCmd }]
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
|
|
4032
|
+
generatedFiles.push('.claude/settings.json');
|
|
4033
|
+
} catch (_) {}
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
|
|
3724
4037
|
return {
|
|
4038
|
+
structuredContent: {
|
|
4039
|
+
workspace: targetDir,
|
|
4040
|
+
generatedFiles
|
|
4041
|
+
},
|
|
3725
4042
|
content: [{
|
|
3726
4043
|
type: "text",
|
|
3727
4044
|
text: `[Zero-Trust Agentic Guard: Agent Rules Generated]\n` +
|
|
@@ -3744,22 +4061,38 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3744
4061
|
}
|
|
3745
4062
|
|
|
3746
4063
|
const targetSession = getSessionMap(session_id);
|
|
4064
|
+
const seenUnmask = new WeakMap();
|
|
3747
4065
|
|
|
3748
4066
|
function unmaskRecursive(val) {
|
|
3749
4067
|
if (typeof val === 'string') {
|
|
3750
4068
|
return reverseTokens(val, targetSession);
|
|
3751
4069
|
}
|
|
3752
|
-
if (
|
|
3753
|
-
|
|
4070
|
+
if (val === null || typeof val !== 'object') return val;
|
|
4071
|
+
if (
|
|
4072
|
+
val instanceof Date ||
|
|
4073
|
+
val instanceof RegExp ||
|
|
4074
|
+
val instanceof Error ||
|
|
4075
|
+
ArrayBuffer.isView(val) ||
|
|
4076
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
4077
|
+
) {
|
|
4078
|
+
return val;
|
|
3754
4079
|
}
|
|
3755
|
-
if (val
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
4080
|
+
if (seenUnmask.has(val)) return seenUnmask.get(val);
|
|
4081
|
+
if (Array.isArray(val)) {
|
|
4082
|
+
const outArr = [];
|
|
4083
|
+
seenUnmask.set(val, outArr);
|
|
4084
|
+
for (let i = 0; i < val.length; i++) {
|
|
4085
|
+
outArr.push(unmaskRecursive(val[i]));
|
|
3759
4086
|
}
|
|
3760
|
-
return
|
|
4087
|
+
return outArr;
|
|
3761
4088
|
}
|
|
3762
|
-
|
|
4089
|
+
const res = {};
|
|
4090
|
+
seenUnmask.set(val, res);
|
|
4091
|
+
for (const [k, v] of Object.entries(val)) {
|
|
4092
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
4093
|
+
res[k] = unmaskRecursive(v);
|
|
4094
|
+
}
|
|
4095
|
+
return res;
|
|
3763
4096
|
}
|
|
3764
4097
|
|
|
3765
4098
|
const unmasked = unmaskRecursive(targetArgs);
|
|
@@ -3772,17 +4105,20 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3772
4105
|
}
|
|
3773
4106
|
}
|
|
3774
4107
|
|
|
4108
|
+
const unmaskPayload = {
|
|
4109
|
+
status: "success",
|
|
4110
|
+
success: true,
|
|
4111
|
+
session_id,
|
|
4112
|
+
tokens_restored: tokensRestored,
|
|
4113
|
+
unmasked_count: tokensRestored,
|
|
4114
|
+
unmasked_args: unmasked
|
|
4115
|
+
};
|
|
4116
|
+
|
|
3775
4117
|
return {
|
|
4118
|
+
structuredContent: unmaskPayload,
|
|
3776
4119
|
content: [{
|
|
3777
4120
|
type: "text",
|
|
3778
|
-
text: JSON.stringify(
|
|
3779
|
-
status: "success",
|
|
3780
|
-
success: true,
|
|
3781
|
-
session_id,
|
|
3782
|
-
tokens_restored: tokensRestored,
|
|
3783
|
-
unmasked_count: tokensRestored,
|
|
3784
|
-
unmasked_args: unmasked
|
|
3785
|
-
}, null, 2)
|
|
4121
|
+
text: JSON.stringify(unmaskPayload, null, 2)
|
|
3786
4122
|
}]
|
|
3787
4123
|
};
|
|
3788
4124
|
}
|
|
@@ -3796,6 +4132,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3796
4132
|
const session = agentSessions.get(sid);
|
|
3797
4133
|
if (!session) {
|
|
3798
4134
|
return {
|
|
4135
|
+
structuredContent: { sessionId: sid, exists: false, totalTokens: 0 },
|
|
3799
4136
|
content: [{
|
|
3800
4137
|
type: "text",
|
|
3801
4138
|
text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
|
|
@@ -3806,6 +4143,16 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3806
4143
|
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
3807
4144
|
const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
|
|
3808
4145
|
return {
|
|
4146
|
+
structuredContent: {
|
|
4147
|
+
sessionId: sid,
|
|
4148
|
+
exists: true,
|
|
4149
|
+
createdAt: new Date(session.createdAt).toISOString(),
|
|
4150
|
+
lastActive: new Date(session.lastActive).toISOString(),
|
|
4151
|
+
totalTokens: tokenEntries.length,
|
|
4152
|
+
streamBufferBytes: streamBufferLen,
|
|
4153
|
+
entities: telemetry.entities,
|
|
4154
|
+
riskLevel: telemetry.riskLevel
|
|
4155
|
+
},
|
|
3809
4156
|
content: [{
|
|
3810
4157
|
type: "text",
|
|
3811
4158
|
text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
|
|
@@ -3830,6 +4177,13 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3830
4177
|
}
|
|
3831
4178
|
|
|
3832
4179
|
return {
|
|
4180
|
+
structuredContent: {
|
|
4181
|
+
activeSessions: agentSessions.size,
|
|
4182
|
+
totalTokens,
|
|
4183
|
+
heapUsedMb: Number((memUsage.heapUsed / (1024 * 1024)).toFixed(2)),
|
|
4184
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
4185
|
+
sessions: sessionList
|
|
4186
|
+
},
|
|
3833
4187
|
content: [{
|
|
3834
4188
|
type: "text",
|
|
3835
4189
|
text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
|
|
@@ -3928,19 +4282,21 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3928
4282
|
|
|
3929
4283
|
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
3930
4284
|
const auditReceipt = formatAuditReceipt(telemetry);
|
|
4285
|
+
const ragChunkPayload = {
|
|
4286
|
+
status: "success",
|
|
4287
|
+
session_id,
|
|
4288
|
+
total_chunks: chunks.length,
|
|
4289
|
+
tokens_redacted_in_batch: totalTokensRedacted,
|
|
4290
|
+
total_session_tokens: Object.keys(targetSession).length,
|
|
4291
|
+
risk_level: telemetry.riskLevel,
|
|
4292
|
+
sanitized_chunks: sanitizedChunks
|
|
4293
|
+
};
|
|
3931
4294
|
|
|
3932
4295
|
return {
|
|
4296
|
+
structuredContent: ragChunkPayload,
|
|
3933
4297
|
content: [{
|
|
3934
4298
|
type: "text",
|
|
3935
|
-
text: JSON.stringify({
|
|
3936
|
-
status: "success",
|
|
3937
|
-
session_id,
|
|
3938
|
-
total_chunks: chunks.length,
|
|
3939
|
-
tokens_redacted_in_batch: totalTokensRedacted,
|
|
3940
|
-
total_session_tokens: Object.keys(targetSession).length,
|
|
3941
|
-
risk_level: telemetry.riskLevel,
|
|
3942
|
-
sanitized_chunks: sanitizedChunks
|
|
3943
|
-
}, null, 2) + `\n\n${auditReceipt}`
|
|
4299
|
+
text: JSON.stringify(ragChunkPayload, null, 2) + `\n\n${auditReceipt}`
|
|
3944
4300
|
}]
|
|
3945
4301
|
};
|
|
3946
4302
|
}
|
|
@@ -3988,16 +4344,19 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
3988
4344
|
}
|
|
3989
4345
|
}
|
|
3990
4346
|
|
|
4347
|
+
const ragRestorePayload = {
|
|
4348
|
+
status: "success",
|
|
4349
|
+
session_id,
|
|
4350
|
+
total_results: results.length,
|
|
4351
|
+
tokens_restored: totalTokensRestored,
|
|
4352
|
+
restored_results: restoredResults
|
|
4353
|
+
};
|
|
4354
|
+
|
|
3991
4355
|
return {
|
|
4356
|
+
structuredContent: ragRestorePayload,
|
|
3992
4357
|
content: [{
|
|
3993
4358
|
type: "text",
|
|
3994
|
-
text: JSON.stringify(
|
|
3995
|
-
status: "success",
|
|
3996
|
-
session_id,
|
|
3997
|
-
total_results: results.length,
|
|
3998
|
-
tokens_restored: totalTokensRestored,
|
|
3999
|
-
restored_results: restoredResults
|
|
4000
|
-
}, null, 2)
|
|
4359
|
+
text: JSON.stringify(ragRestorePayload, null, 2)
|
|
4001
4360
|
}]
|
|
4002
4361
|
};
|
|
4003
4362
|
}
|
|
@@ -4026,6 +4385,9 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4026
4385
|
const sessionHash = receipt.receiptSha256;
|
|
4027
4386
|
|
|
4028
4387
|
let outputText = "";
|
|
4388
|
+
const safeCompanyName = String(company_name || "Organization").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
|
|
4389
|
+
const safeDepartment = String(department || "Security & Compliance").replace(/[\r\n\x00-\x1f]/g, " ").trim().slice(0, 120);
|
|
4390
|
+
|
|
4029
4391
|
const eventData = {
|
|
4030
4392
|
vendor: "PrivacyScrubber",
|
|
4031
4393
|
product: "ZTDS",
|
|
@@ -4036,8 +4398,8 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4036
4398
|
entities: telemetry.entities,
|
|
4037
4399
|
frameworks: telemetry.frameworksList,
|
|
4038
4400
|
sessionHash,
|
|
4039
|
-
organization:
|
|
4040
|
-
department
|
|
4401
|
+
organization: safeCompanyName,
|
|
4402
|
+
department: safeDepartment
|
|
4041
4403
|
};
|
|
4042
4404
|
|
|
4043
4405
|
if (format === "cef") {
|
|
@@ -4045,19 +4407,35 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4045
4407
|
} else if (format === "syslog") {
|
|
4046
4408
|
outputText = formatSyslogEvent(eventData);
|
|
4047
4409
|
} else if (format === "json" || format === "markdown") {
|
|
4048
|
-
outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
|
|
4410
|
+
outputText = formatPsr2Report({ format, receipt, telemetry, company_name: safeCompanyName, department: safeDepartment, utmSource: 'mcp_audit_export', utmMedium: format });
|
|
4049
4411
|
} else {
|
|
4050
4412
|
// Default: jsonl
|
|
4051
4413
|
outputText = formatJsonlEvent(eventData);
|
|
4052
4414
|
}
|
|
4053
4415
|
|
|
4054
4416
|
if (typeof destination_file === 'string' && destination_file.trim()) {
|
|
4417
|
+
const safeDest = resolveWorkspaceSafePath(destination_file.trim(), "destination_file");
|
|
4418
|
+
if (!safeDest.ok) {
|
|
4419
|
+
return {
|
|
4420
|
+
isError: true,
|
|
4421
|
+
content: [{ type: "text", text: safeDest.error }]
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4055
4424
|
try {
|
|
4056
|
-
|
|
4425
|
+
if (fs.existsSync(safeDest.resolvedPath)) {
|
|
4426
|
+
const st = fs.lstatSync(safeDest.resolvedPath);
|
|
4427
|
+
if (st.isSymbolicLink() || !st.isFile()) {
|
|
4428
|
+
return {
|
|
4429
|
+
isError: true,
|
|
4430
|
+
content: [{ type: "text", text: "Error: Security policy violation — destination_file must be a regular non-symlink file." }]
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
const dir = path.dirname(safeDest.resolvedPath);
|
|
4057
4435
|
if (!fs.existsSync(dir)) {
|
|
4058
4436
|
fs.mkdirSync(dir, { recursive: true });
|
|
4059
4437
|
}
|
|
4060
|
-
fs.appendFileSync(
|
|
4438
|
+
fs.appendFileSync(safeDest.resolvedPath, outputText + '\n', 'utf8');
|
|
4061
4439
|
} catch (e) {
|
|
4062
4440
|
return {
|
|
4063
4441
|
isError: true,
|
|
@@ -4067,6 +4445,15 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
4067
4445
|
}
|
|
4068
4446
|
|
|
4069
4447
|
return {
|
|
4448
|
+
structuredContent: {
|
|
4449
|
+
format,
|
|
4450
|
+
riskLevel: telemetry.riskLevel,
|
|
4451
|
+
tokensMasked: telemetry.totalCount,
|
|
4452
|
+
entities: telemetry.entities,
|
|
4453
|
+
frameworks: telemetry.frameworksList,
|
|
4454
|
+
receiptSha256: sessionHash,
|
|
4455
|
+
timestamp
|
|
4456
|
+
},
|
|
4070
4457
|
content: [{
|
|
4071
4458
|
type: "text",
|
|
4072
4459
|
text: outputText
|