fullcourtdefense-cli 1.36.3 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actionPolicyEngine.d.ts +19 -0
- package/dist/actionPolicyEngine.js +147 -0
- package/dist/askDialog.d.ts +1 -0
- package/dist/askDialog.js +20 -3
- package/dist/callerEnv.d.ts +29 -2
- package/dist/callerEnv.js +48 -2
- package/dist/commandNarrative.js +87 -7
- package/dist/commands/daemon.d.ts +10 -0
- package/dist/commands/daemon.js +348 -170
- package/dist/commands/desktopChatGuard.js +2 -0
- package/dist/commands/deterministicGuard.d.ts +11 -0
- package/dist/commands/deterministicGuard.js +445 -28
- package/dist/commands/discoverPaths.d.ts +12 -9
- package/dist/commands/discoverPaths.js +30 -0
- package/dist/commands/ephemeralStack.js +7 -0
- package/dist/commands/flushSpool.d.ts +2 -0
- package/dist/commands/flushSpool.js +9 -2
- package/dist/commands/hook.d.ts +14 -0
- package/dist/commands/hook.js +302 -101
- package/dist/commands/httpGatewayCommand.d.ts +1 -0
- package/dist/commands/httpGatewayCommand.js +7 -1
- package/dist/commands/installClaudeHook.js +32 -10
- package/dist/commands/installCursorHook.js +20 -9
- package/dist/commands/login.js +8 -1
- package/dist/commands/mcpGateway.d.ts +21 -0
- package/dist/commands/mcpGateway.js +90 -32
- package/dist/commands/refreshPolicyCache.d.ts +28 -0
- package/dist/commands/refreshPolicyCache.js +59 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +30 -5
- package/dist/config.d.ts +36 -0
- package/dist/config.js +78 -12
- package/dist/daemonDetectorBoot.d.ts +1 -0
- package/dist/daemonDetectorBoot.js +12 -0
- package/dist/daemonForensics.d.ts +14 -1
- package/dist/daemonForensics.js +32 -0
- package/dist/detectorInProcess.d.ts +18 -0
- package/dist/detectorInProcess.js +234 -0
- package/dist/detectorInThread.d.ts +15 -0
- package/dist/detectorInThread.js +129 -0
- package/dist/distress.d.ts +2 -0
- package/dist/distress.js +2 -0
- package/dist/fileWriteCanon.d.ts +17 -0
- package/dist/fileWriteCanon.js +141 -0
- package/dist/gateReports.d.ts +144 -0
- package/dist/gateReports.js +488 -0
- package/dist/hookIo.d.ts +59 -0
- package/dist/hookIo.js +319 -9
- package/dist/hookSlim.d.ts +15 -8
- package/dist/hookSlim.js +187 -25
- package/dist/housekeeping.d.ts +64 -0
- package/dist/housekeeping.js +384 -0
- package/dist/housekeepingOps.d.ts +150 -0
- package/dist/housekeepingOps.js +218 -0
- package/dist/housekeepingWorker.d.ts +1 -0
- package/dist/housekeepingWorker.js +113 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -0
- package/dist/localDetectionData.d.ts +10 -0
- package/dist/localDetectionData.js +48 -0
- package/dist/localDetectionUpdates.d.ts +2 -2
- package/dist/localDetectionUpdates.js +4 -4
- package/dist/localSafetySnapshot.d.ts +48 -2
- package/dist/localSafetySnapshot.js +241 -25
- package/dist/machineIdentity.d.ts +7 -0
- package/dist/machineIdentity.js +20 -2
- package/dist/monitorFastPath.d.ts +191 -0
- package/dist/monitorFastPath.js +1198 -0
- package/dist/nativeHook.js +9 -2
- package/dist/pendingDrain.d.ts +82 -0
- package/dist/pendingDrain.js +215 -0
- package/dist/pendingDrainer.d.ts +30 -0
- package/dist/pendingDrainer.js +217 -0
- package/dist/perfSnapshot.d.ts +1 -1
- package/dist/perfSnapshot.js +28 -4
- package/dist/policyCacheFiles.d.ts +73 -0
- package/dist/policyCacheFiles.js +320 -0
- package/dist/policyGateHealth.d.ts +8 -0
- package/dist/policyGateHealth.js +17 -0
- package/dist/policyRefresh.d.ts +28 -0
- package/dist/policyRefresh.js +69 -0
- package/dist/protectionMaintenance.d.ts +15 -0
- package/dist/protectionMaintenance.js +43 -1
- package/dist/runtimeConfig.d.ts +77 -20
- package/dist/runtimeConfig.js +278 -57
- package/dist/selfTest.d.ts +6 -1
- package/dist/selfTest.js +11 -4
- package/dist/telemetry.d.ts +136 -4
- package/dist/telemetry.js +493 -96
- package/dist/verdictIpc.d.ts +60 -0
- package/dist/verdictIpc.js +504 -19
- package/dist/verdictIpcClient.d.ts +175 -18
- package/dist/verdictIpcClient.js +277 -59
- package/dist/version.json +1 -1
- package/package.json +2 -2
|
@@ -460,6 +460,8 @@ function startDesktopChatGuard(runtime) {
|
|
|
460
460
|
shieldKey: runtime.shieldKey,
|
|
461
461
|
developerName: snapshotIdentity.developerName,
|
|
462
462
|
machineName: snapshotIdentity.machineName,
|
|
463
|
+
// Read what the daemon poll keeps current — no re-download per tick.
|
|
464
|
+
hotPath: true,
|
|
463
465
|
});
|
|
464
466
|
scanOptions = (0, localSafetySnapshot_1.snapshotToScanOptions)(snapshot);
|
|
465
467
|
}
|
|
@@ -215,6 +215,17 @@ export declare function isPowerShellLine(text: string): boolean;
|
|
|
215
215
|
* still carries fixture strings and docs.
|
|
216
216
|
*/
|
|
217
217
|
export declare function unquotedCode(text: string): string;
|
|
218
|
+
/** POSIX heredocs of a command line, in order: body span, delimiter-line end, and receiver. */
|
|
219
|
+
export declare function posixHeredocs(text: string): Array<{
|
|
220
|
+
bodyStart: number;
|
|
221
|
+
bodyEnd: number;
|
|
222
|
+
end: number;
|
|
223
|
+
body: string;
|
|
224
|
+
receiver: {
|
|
225
|
+
kind: 'shell' | 'language' | 'data';
|
|
226
|
+
program: string;
|
|
227
|
+
};
|
|
228
|
+
}>;
|
|
218
229
|
export declare function stripInertDataSegments(text: string): string;
|
|
219
230
|
export declare function scanDeterministicToolCall(toolName: string, toolArgs: Record<string, unknown>, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
|
|
220
231
|
export declare function scanDeterministicTextResponse(text: string, options?: LocalSafetyScanOptions): DeterministicFinding | undefined;
|
|
@@ -48,6 +48,7 @@ exports.getShellAstMode = getShellAstMode;
|
|
|
48
48
|
exports.takeShellAstComparison = takeShellAstComparison;
|
|
49
49
|
exports.isPowerShellLine = isPowerShellLine;
|
|
50
50
|
exports.unquotedCode = unquotedCode;
|
|
51
|
+
exports.posixHeredocs = posixHeredocs;
|
|
51
52
|
exports.stripInertDataSegments = stripInertDataSegments;
|
|
52
53
|
exports.scanDeterministicToolCall = scanDeterministicToolCall;
|
|
53
54
|
exports.scanDeterministicTextResponse = scanDeterministicTextResponse;
|
|
@@ -1056,9 +1057,10 @@ function destructiveCommandDecision(value, commandContext) {
|
|
|
1056
1057
|
if (astHit)
|
|
1057
1058
|
return astHit;
|
|
1058
1059
|
// The AST parsed the literal command only. A hit that exists only in a
|
|
1059
|
-
// decoded or de-obfuscated surface (`echo <base64> | base64 -d | sh`)
|
|
1060
|
-
// outside what it saw, so it
|
|
1061
|
-
|
|
1060
|
+
// decoded or de-obfuscated surface (`echo <base64> | base64 -d | sh`) or in
|
|
1061
|
+
// a heredoc body a shell runs (`bash <<'EOF'`) is outside what it saw, so it
|
|
1062
|
+
// cannot clear it.
|
|
1063
|
+
if (regexRm && !RM_SCOPE_ITEMS.has(destructiveCommandReason(withoutActingHeredocBodies(value), { literalOnly: true })?.itemId || ''))
|
|
1062
1064
|
return regexHit;
|
|
1063
1065
|
if (regexRm) {
|
|
1064
1066
|
// The AST cleared the rm-scope hit; other destructive rules still apply.
|
|
@@ -1519,7 +1521,7 @@ function isSensitivePathContext(toolName, candidate) {
|
|
|
1519
1521
|
// remote executor earlier in the same statement) are kept.
|
|
1520
1522
|
// 5. Secret-literal rules and honeypot/custom rules never use this view —
|
|
1521
1523
|
// a token pasted into a commit message still leaks.
|
|
1522
|
-
const DATA_TO_INTERPRETER = /\b(?:iex|invoke-expression|invoke-command)\b|\beval\b|\bxargs\b|\bbase64\b[^\n]*(?:-d\b|--decode\b)|frombase64string|-encodedcommand\b|\s-enc\s|(?<!\|)\|(?!\|)\s*&?\s*(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\.exe)?|cmd(?:\.exe)?|node|python[0-9.]*|perl|ruby)\b|\b(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\.exe)?|cmd(?:\.exe)?)\s+(?:-c|\/c|-e|-command|-scriptblock)\b|\b(?:ssh|wsl(?:\.exe)?|chroot)\b|\b(?:docker|kubectl|podman|nerdctl)(?:\.exe)?\s+(?:exec|run)\b/i;
|
|
1524
|
+
const DATA_TO_INTERPRETER = /\b(?:iex|icm|invoke-expression|invoke-command)\b|\[scriptblock\]::create\b|\beval\b|\bxargs\b|\bbase64\b[^\n]*(?:-d\b|--decode\b)|frombase64string|-encodedcommand\b|\s-enc\s|(?<!\|)\|(?!\|)\s*&?\s*(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\.exe)?|cmd(?:\.exe)?|node|python[0-9.]*|perl|ruby)\b|\b(?:sh|bash|zsh|dash|ksh|csh|pwsh|powershell(?:\.exe)?|cmd(?:\.exe)?)\s+(?:-c|\/c|-e|-command|-scriptblock)\b|\b(?:ssh|wsl(?:\.exe)?|chroot)\b|\b(?:docker|kubectl|podman|nerdctl)(?:\.exe)?\s+(?:exec|run)\b/i;
|
|
1523
1525
|
// A LANGUAGE interpreter's `-c` / `-e` argument is CODE in another language, not
|
|
1524
1526
|
// a shell command line. `node -e "console.log('rm -rf /')"` prints a string;
|
|
1525
1527
|
// `python -c "print('DROP TABLE')"` prints a string. That code only reaches the
|
|
@@ -1544,33 +1546,42 @@ function interpreterFileArguments(text) {
|
|
|
1544
1546
|
if (text.length > 32_000)
|
|
1545
1547
|
return '';
|
|
1546
1548
|
const paths = [];
|
|
1547
|
-
|
|
1549
|
+
const budget = { calls: 0 };
|
|
1548
1550
|
text.replace(/"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'/g, (whole, dq, sq, offset) => {
|
|
1549
1551
|
if (!LANG_INTERPRETER_CODE_BEFORE.test(text.slice(Math.max(0, offset - 80), offset)))
|
|
1550
1552
|
return whole;
|
|
1551
|
-
|
|
1552
|
-
// Preserve offsets while masking string contents: a call printed inside a
|
|
1553
|
-
// string is not an invocation; a sensitive path in another argument is data.
|
|
1554
|
-
const instructions = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, literal => ' '.repeat(literal.length));
|
|
1555
|
-
for (const call of instructions.matchAll(/\b(?:open|fopen|readFile(?:Sync)?|createReadStream|ReadAllText|ReadAllBytes)\s*\(/g)) {
|
|
1556
|
-
if (++calls > 48)
|
|
1557
|
-
break;
|
|
1558
|
-
const start = (call.index || 0) + call[0].length;
|
|
1559
|
-
let depth = 1;
|
|
1560
|
-
for (let end = start; end < instructions.length; end++) {
|
|
1561
|
-
const ch = instructions[end];
|
|
1562
|
-
if ((ch === ')' && --depth === 0) || (ch === ',' && depth === 1)) {
|
|
1563
|
-
paths.push(code.slice(start, end));
|
|
1564
|
-
break;
|
|
1565
|
-
}
|
|
1566
|
-
if (ch === '(')
|
|
1567
|
-
depth++;
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1553
|
+
collectFileCallArguments(dq !== undefined ? dq : (sq || ''), paths, budget);
|
|
1570
1554
|
return whole;
|
|
1571
1555
|
});
|
|
1556
|
+
// Heredoc bodies a language interpreter runs (`python3 - <<'EOF'`, or
|
|
1557
|
+
// `cat > x.js <<'EOF' … node x.js`) are the same inline code.
|
|
1558
|
+
for (const heredoc of heredocView(text)?.heredocs || []) {
|
|
1559
|
+
if (heredoc.receiver.kind === 'language')
|
|
1560
|
+
collectFileCallArguments(heredoc.body, paths, budget);
|
|
1561
|
+
}
|
|
1572
1562
|
return paths.join('\n');
|
|
1573
1563
|
}
|
|
1564
|
+
const FILE_CALL_RE = /\b(?:open|fopen|readFile(?:Sync)?|createReadStream|ReadAllText|ReadAllBytes|readTextFile|file_get_contents|copyFile(?:Sync)?|copyfile|copy2?|Path|File\.read)\s*\(/g;
|
|
1565
|
+
function collectFileCallArguments(code, paths, budget) {
|
|
1566
|
+
// Preserve offsets while masking string contents: a call printed inside a
|
|
1567
|
+
// string is not an invocation; a sensitive path in another argument is data.
|
|
1568
|
+
const instructions = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, literal => ' '.repeat(literal.length));
|
|
1569
|
+
for (const call of instructions.matchAll(FILE_CALL_RE)) {
|
|
1570
|
+
if (++budget.calls > 48)
|
|
1571
|
+
break;
|
|
1572
|
+
const start = (call.index || 0) + call[0].length;
|
|
1573
|
+
let depth = 1;
|
|
1574
|
+
for (let end = start; end < instructions.length; end++) {
|
|
1575
|
+
const ch = instructions[end];
|
|
1576
|
+
if ((ch === ')' && --depth === 0) || (ch === ',' && depth === 1)) {
|
|
1577
|
+
paths.push(code.slice(start, end));
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
if (ch === '(')
|
|
1581
|
+
depth++;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1574
1585
|
// $( … ) always executes; backtick pairs only with ≥2 chars of content
|
|
1575
1586
|
// (PowerShell escapes like `n / `t are single-char and inert).
|
|
1576
1587
|
const INTERPOLATES = /\$\(|`[^`]{2,}`/;
|
|
@@ -1717,16 +1728,414 @@ function replaceQuotedSpans(src, fn) {
|
|
|
1717
1728
|
}
|
|
1718
1729
|
return out + src.slice(from);
|
|
1719
1730
|
}
|
|
1731
|
+
// Stdin of these is executed as shell/SQL code; `done`/`fi`/`esac`/`}`/`)`
|
|
1732
|
+
// close a compound command whose body acts on what it reads (`while read c; do $c; done <<EOF`).
|
|
1733
|
+
const SHELL_STDIN_PROGRAMS = /^(?:sh|bash|zsh|dash|ksh|mksh|ash|csh|tcsh|fish|busybox|pwsh|powershell|cmd|ssh|wsl|chroot|su|eval|source|\.|xargs|parallel|at|batch|crontab|expect|tclsh|wish|osascript|script|psql|mysql|mariadb|sqlite3|sqlcmd|mongosh|mongo|redis-cli|cqlsh|clickhouse-client|duckdb|snowsql|done|fi|esac|\}|\))$/;
|
|
1734
|
+
const LANGUAGE_STDIN_PROGRAMS = /^(?:node|nodejs|deno|bun|python[0-9.]*|py|pypy[0-9.]*|perl|ruby|php|lua|luajit|tsx|ts-node|rscript|julia|groovy)$/;
|
|
1735
|
+
const CONTAINER_CLI_PROGRAMS = /^(?:docker|podman|nerdctl|kubectl|oc)$/;
|
|
1736
|
+
const RECEIVER_RANK = { data: 0, language: 1, shell: 2 };
|
|
1737
|
+
function programName(token) {
|
|
1738
|
+
return token.toLowerCase().replace(/^.*[\\/]/, '').replace(/\.(?:exe|cmd|bat)$/, '');
|
|
1739
|
+
}
|
|
1740
|
+
/** Words of one stage without assignments and redirections (`> f`, `2>&1`, `<<'EOF'`). */
|
|
1741
|
+
function stageWords(stage) {
|
|
1742
|
+
const words = [];
|
|
1743
|
+
const tokens = segmentTokens(stage.trim());
|
|
1744
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1745
|
+
const token = tokens[i];
|
|
1746
|
+
if (/^(?:\d*>{1,2}|&>{1,2}|<{1,3}-?|\d*<)$/.test(token)) {
|
|
1747
|
+
i++;
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
if (/^(?:\d*>|&>|<)/.test(token))
|
|
1751
|
+
continue;
|
|
1752
|
+
if (!words.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(token))
|
|
1753
|
+
continue;
|
|
1754
|
+
words.push(token);
|
|
1755
|
+
}
|
|
1756
|
+
return words;
|
|
1757
|
+
}
|
|
1758
|
+
/** How one pipeline stage treats the bytes on its stdin. */
|
|
1759
|
+
function stdinReceiver(stage) {
|
|
1760
|
+
const words = stageWords(stage);
|
|
1761
|
+
for (let i = 0; i < words.length; i++) {
|
|
1762
|
+
const program = programName(words[i]);
|
|
1763
|
+
if (SHELL_STDIN_PROGRAMS.test(program))
|
|
1764
|
+
return { kind: 'shell', program };
|
|
1765
|
+
if (LANGUAGE_STDIN_PROGRAMS.test(program))
|
|
1766
|
+
return { kind: 'language', program };
|
|
1767
|
+
if (CONTAINER_CLI_PROGRAMS.test(program)) {
|
|
1768
|
+
return words.slice(i + 1).some(word => /^(?:exec|run|attach|debug)$/i.test(word)) ? { kind: 'shell', program } : { kind: 'data', program };
|
|
1769
|
+
}
|
|
1770
|
+
if (AST_TRANSPARENT_WRAPPERS.has(program)) {
|
|
1771
|
+
// `sudo -u root bash`, `env X=1 node`, `timeout 5 sh`: the reader is a later word, past the wrapper's own options.
|
|
1772
|
+
let reader = { kind: 'data', program };
|
|
1773
|
+
for (const word of words.slice(i + 1))
|
|
1774
|
+
reader = strongerReceiver(reader, stdinReceiver(word));
|
|
1775
|
+
return reader;
|
|
1776
|
+
}
|
|
1777
|
+
return { kind: 'data', program };
|
|
1778
|
+
}
|
|
1779
|
+
return { kind: 'data', program: '' };
|
|
1780
|
+
}
|
|
1781
|
+
function strongerReceiver(a, b) {
|
|
1782
|
+
return RECEIVER_RANK[b.kind] > RECEIVER_RANK[a.kind] ? b : a;
|
|
1783
|
+
}
|
|
1784
|
+
/** Files the opener stage writes its stdin to (`cat > f`, `tee f`). */
|
|
1785
|
+
function stageWriteTargets(stage) {
|
|
1786
|
+
const targets = redirectTargets(stage);
|
|
1787
|
+
const words = stageWords(stage);
|
|
1788
|
+
if (programName(words[0] || '') === 'tee')
|
|
1789
|
+
targets.push(...words.slice(1).filter(word => !word.startsWith('-')));
|
|
1790
|
+
return targets.map(target => programName(target)).filter(Boolean);
|
|
1791
|
+
}
|
|
1792
|
+
/** The strongest program among `statements` that runs one of `files` (`node x.js`, `bash x.sh`, `./x.sh`). */
|
|
1793
|
+
function laterRunnerOf(files, statements) {
|
|
1794
|
+
let runner = { kind: 'data', program: '' };
|
|
1795
|
+
if (!files.length)
|
|
1796
|
+
return runner;
|
|
1797
|
+
for (const stages of statements) {
|
|
1798
|
+
for (const stage of stages) {
|
|
1799
|
+
const words = stageWords(stage);
|
|
1800
|
+
if (!words.some(word => files.includes(programName(word))))
|
|
1801
|
+
continue;
|
|
1802
|
+
const direct = files.includes(programName(words[0] || ''));
|
|
1803
|
+
runner = strongerReceiver(runner, direct ? { kind: 'shell', program: programName(words[0]) } : stdinReceiver(stage));
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return runner;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Every heredoc of a POSIX command line, with its exact body and its stages.
|
|
1810
|
+
* `undefined` when the line does not lex (an unclosed quote or substitution):
|
|
1811
|
+
* callers then keep every body as code.
|
|
1812
|
+
*/
|
|
1813
|
+
function lexHeredocs(text) {
|
|
1814
|
+
const spans = [];
|
|
1815
|
+
const pending = [];
|
|
1816
|
+
const frame = (closer, stageStart) => ({ closer, quote: '', depth: 0, afterPipe: false, stageStart, stages: [], open: [] });
|
|
1817
|
+
const stack = [frame('', 0)];
|
|
1818
|
+
/** `i` at the first `(` of `((`: index of the closing `))` (arithmetic holds no heredoc; `<<` there is a shift). */
|
|
1819
|
+
const arithmeticEnd = (i) => {
|
|
1820
|
+
let depth = 0;
|
|
1821
|
+
for (let k = i; k < text.length; k++) {
|
|
1822
|
+
if (text[k] === '(')
|
|
1823
|
+
depth++;
|
|
1824
|
+
else if (text[k] === ')' && --depth === 0)
|
|
1825
|
+
return k;
|
|
1826
|
+
}
|
|
1827
|
+
return -1;
|
|
1828
|
+
};
|
|
1829
|
+
const codeSlice = (from, to) => {
|
|
1830
|
+
let out = '';
|
|
1831
|
+
let at = from;
|
|
1832
|
+
for (const span of spans) {
|
|
1833
|
+
if (span.end <= at || span.bodyStart >= to)
|
|
1834
|
+
continue;
|
|
1835
|
+
out += text.slice(at, Math.max(at, span.bodyStart));
|
|
1836
|
+
at = Math.max(at, span.end);
|
|
1837
|
+
}
|
|
1838
|
+
return out + text.slice(at, Math.max(at, to));
|
|
1839
|
+
};
|
|
1840
|
+
const endStage = (ctx, at) => { ctx.stages.push(codeSlice(ctx.stageStart, at)); ctx.stageStart = at + 1; };
|
|
1841
|
+
const endStatement = (ctx, at) => {
|
|
1842
|
+
endStage(ctx, at);
|
|
1843
|
+
for (const { span, stage } of ctx.open)
|
|
1844
|
+
span.stages = ctx.stages.slice(stage);
|
|
1845
|
+
ctx.stages = [];
|
|
1846
|
+
ctx.open = [];
|
|
1847
|
+
};
|
|
1848
|
+
const readBodies = (newline) => {
|
|
1849
|
+
let pos = newline + 1;
|
|
1850
|
+
for (const { span, delimiter, stripTabs } of pending.splice(0)) {
|
|
1851
|
+
span.bodyStart = pos;
|
|
1852
|
+
span.bodyEnd = span.end = text.length;
|
|
1853
|
+
while (pos < text.length) {
|
|
1854
|
+
const lineEnd = text.indexOf('\n', pos);
|
|
1855
|
+
const stop = lineEnd < 0 ? text.length : lineEnd;
|
|
1856
|
+
let line = text.slice(pos, stop).replace(/\r$/, '');
|
|
1857
|
+
if (stripTabs)
|
|
1858
|
+
line = line.replace(/^\t+/, '');
|
|
1859
|
+
if (line === delimiter) {
|
|
1860
|
+
span.bodyEnd = pos;
|
|
1861
|
+
span.end = stop;
|
|
1862
|
+
pos = stop + 1;
|
|
1863
|
+
break;
|
|
1864
|
+
}
|
|
1865
|
+
pos = stop + 1;
|
|
1866
|
+
}
|
|
1867
|
+
span.body = text.slice(span.bodyStart, span.bodyEnd < text.length ? Math.max(span.bodyStart, span.bodyEnd - 1) : text.length);
|
|
1868
|
+
spans.push(span);
|
|
1869
|
+
}
|
|
1870
|
+
return pos;
|
|
1871
|
+
};
|
|
1872
|
+
for (let i = 0; i < text.length; i++) {
|
|
1873
|
+
const ctx = stack[stack.length - 1];
|
|
1874
|
+
const c = text[i];
|
|
1875
|
+
const next = text[i + 1];
|
|
1876
|
+
if (ctx.quote === "'") {
|
|
1877
|
+
if (c === "'")
|
|
1878
|
+
ctx.quote = '';
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (c === '\\') {
|
|
1882
|
+
i++;
|
|
1883
|
+
continue;
|
|
1884
|
+
}
|
|
1885
|
+
if (ctx.quote === '"') {
|
|
1886
|
+
if (c === '"')
|
|
1887
|
+
ctx.quote = '';
|
|
1888
|
+
else if (c === '$' && next === '(' && text[i + 2] === '(') {
|
|
1889
|
+
const e = arithmeticEnd(i + 1);
|
|
1890
|
+
if (e < 0)
|
|
1891
|
+
return undefined;
|
|
1892
|
+
i = e;
|
|
1893
|
+
}
|
|
1894
|
+
else if (c === '$' && next === '(') {
|
|
1895
|
+
stack.push(frame(')', i + 2));
|
|
1896
|
+
i++;
|
|
1897
|
+
}
|
|
1898
|
+
else if (c === '`')
|
|
1899
|
+
stack.push(frame('`', i + 1));
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
if (c !== '|' && c !== '\n' && !/\s/.test(c))
|
|
1903
|
+
ctx.afterPipe = false;
|
|
1904
|
+
if (c === '#' && (i === 0 || /[\s;&|(]/.test(text[i - 1]))) {
|
|
1905
|
+
const nl = text.indexOf('\n', i);
|
|
1906
|
+
i = (nl < 0 ? text.length : nl) - 1;
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (c === "'" || c === '"') {
|
|
1910
|
+
ctx.quote = c;
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if ((c === '$' && next === '(' && text[i + 2] === '(') || (c === '(' && next === '(')) {
|
|
1914
|
+
const e = arithmeticEnd(c === '$' ? i + 1 : i);
|
|
1915
|
+
if (e < 0)
|
|
1916
|
+
return undefined;
|
|
1917
|
+
i = e;
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
if (c === '$' && next === '(') {
|
|
1921
|
+
stack.push(frame(')', i + 2));
|
|
1922
|
+
i++;
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
if (c === '`') {
|
|
1926
|
+
if (ctx.closer === '`') {
|
|
1927
|
+
endStatement(ctx, i);
|
|
1928
|
+
stack.pop();
|
|
1929
|
+
}
|
|
1930
|
+
else
|
|
1931
|
+
stack.push(frame('`', i + 1));
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (c === '(') {
|
|
1935
|
+
endStatement(ctx, i);
|
|
1936
|
+
ctx.depth++;
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (c === ')') {
|
|
1940
|
+
endStatement(ctx, i);
|
|
1941
|
+
if (ctx.depth > 0)
|
|
1942
|
+
ctx.depth--;
|
|
1943
|
+
else if (ctx.closer === ')' && stack.length > 1)
|
|
1944
|
+
stack.pop();
|
|
1945
|
+
continue;
|
|
1946
|
+
}
|
|
1947
|
+
if (c === '{' && (next === undefined || /\s/.test(next))) {
|
|
1948
|
+
endStatement(ctx, i);
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (c === ';' || (c === '&' && next !== '>' && text[i - 1] !== '>' && text[i - 1] !== '<')) {
|
|
1952
|
+
endStatement(ctx, i);
|
|
1953
|
+
if ((c === '&' && next === '&') || (c === ';' && next === ';'))
|
|
1954
|
+
i++;
|
|
1955
|
+
continue;
|
|
1956
|
+
}
|
|
1957
|
+
if (c === '|') {
|
|
1958
|
+
if (next === '|') {
|
|
1959
|
+
endStatement(ctx, i);
|
|
1960
|
+
i++;
|
|
1961
|
+
}
|
|
1962
|
+
else {
|
|
1963
|
+
endStage(ctx, i);
|
|
1964
|
+
ctx.afterPipe = true;
|
|
1965
|
+
if (next === '&')
|
|
1966
|
+
i++;
|
|
1967
|
+
}
|
|
1968
|
+
continue;
|
|
1969
|
+
}
|
|
1970
|
+
if (c === '\n') {
|
|
1971
|
+
// `cat <<'EOF' |` continues the pipeline on the line after the body.
|
|
1972
|
+
if (!ctx.afterPipe)
|
|
1973
|
+
endStatement(ctx, i);
|
|
1974
|
+
if (pending.length) {
|
|
1975
|
+
const after = readBodies(i);
|
|
1976
|
+
if (ctx.stageStart <= after)
|
|
1977
|
+
ctx.stageStart = after;
|
|
1978
|
+
i = after - 1;
|
|
1979
|
+
}
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
if (c === '<' && next === '<' && text[i - 1] !== '<') {
|
|
1983
|
+
if (text[i + 2] === '<') {
|
|
1984
|
+
i += 2;
|
|
1985
|
+
continue;
|
|
1986
|
+
} // `<<<` here-string: quoted-span pass
|
|
1987
|
+
let j = i + 2;
|
|
1988
|
+
const stripTabs = text[j] === '-';
|
|
1989
|
+
if (stripTabs)
|
|
1990
|
+
j++;
|
|
1991
|
+
while (text[j] === ' ' || text[j] === '\t')
|
|
1992
|
+
j++;
|
|
1993
|
+
let delimiter = '';
|
|
1994
|
+
let literal = false;
|
|
1995
|
+
while (j < text.length && !/[\s;&|<>()]/.test(text[j])) {
|
|
1996
|
+
const q = text[j];
|
|
1997
|
+
if (q === "'" || q === '"') {
|
|
1998
|
+
const close = text.indexOf(q, j + 1);
|
|
1999
|
+
if (close < 0)
|
|
2000
|
+
return undefined;
|
|
2001
|
+
delimiter += text.slice(j + 1, close);
|
|
2002
|
+
literal = true;
|
|
2003
|
+
j = close + 1;
|
|
2004
|
+
}
|
|
2005
|
+
else if (q === '\\') {
|
|
2006
|
+
delimiter += text[j + 1] || '';
|
|
2007
|
+
literal = true;
|
|
2008
|
+
j += 2;
|
|
2009
|
+
}
|
|
2010
|
+
else {
|
|
2011
|
+
delimiter += q;
|
|
2012
|
+
j++;
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
if (!delimiter) {
|
|
2016
|
+
i = j - 1;
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
const span = { bodyStart: text.length, bodyEnd: text.length, end: text.length, body: '', literal, stages: [] };
|
|
2020
|
+
pending.push({ span, delimiter, stripTabs });
|
|
2021
|
+
ctx.open.push({ span, stage: ctx.stages.length });
|
|
2022
|
+
i = j - 1;
|
|
2023
|
+
continue;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (stack.length > 1 || stack[0].quote)
|
|
2027
|
+
return undefined;
|
|
2028
|
+
if (pending.length)
|
|
2029
|
+
readBodies(text.length - 1);
|
|
2030
|
+
endStatement(stack[0], text.length);
|
|
2031
|
+
return spans;
|
|
2032
|
+
}
|
|
2033
|
+
/** Heredocs of a POSIX line, each with the receiver that decides what its body is. */
|
|
2034
|
+
function heredocView(text) {
|
|
2035
|
+
if (!text.includes('<<') || text.length > 256_000)
|
|
2036
|
+
return undefined;
|
|
2037
|
+
const spans = lexHeredocs(text);
|
|
2038
|
+
if (!spans || !spans.length)
|
|
2039
|
+
return undefined;
|
|
2040
|
+
let skeleton = '';
|
|
2041
|
+
let at = 0;
|
|
2042
|
+
for (const span of spans) {
|
|
2043
|
+
skeleton += text.slice(at, span.bodyStart);
|
|
2044
|
+
at = span.end;
|
|
2045
|
+
}
|
|
2046
|
+
skeleton += text.slice(at);
|
|
2047
|
+
const statements = splitUnquotedPipelines(skeleton);
|
|
2048
|
+
const heredocs = spans.map(span => {
|
|
2049
|
+
let receiver = { kind: 'data', program: '' };
|
|
2050
|
+
for (const stage of span.stages)
|
|
2051
|
+
receiver = strongerReceiver(receiver, stdinReceiver(stage));
|
|
2052
|
+
receiver = strongerReceiver(receiver, laterRunnerOf(stageWriteTargets(span.stages[0] || ''), statements));
|
|
2053
|
+
return { ...span, receiver };
|
|
2054
|
+
});
|
|
2055
|
+
return { heredocs, skeleton };
|
|
2056
|
+
}
|
|
2057
|
+
/** POSIX heredocs of a command line, in order: body span, delimiter-line end, and receiver. */
|
|
2058
|
+
function posixHeredocs(text) {
|
|
2059
|
+
return (heredocView(text)?.heredocs || []).map(({ bodyStart, bodyEnd, end, body, receiver }) => ({ bodyStart, bodyEnd, end, body, receiver }));
|
|
2060
|
+
}
|
|
2061
|
+
/** Does this heredoc body run as code where it lands? */
|
|
2062
|
+
function heredocBodyActs(heredoc) {
|
|
2063
|
+
// Unquoted delimiter: `$( … )` / backticks in the body run before any receiver sees it.
|
|
2064
|
+
if (!heredoc.literal && INTERPOLATES.test(heredoc.body))
|
|
2065
|
+
return true;
|
|
2066
|
+
if (heredoc.receiver.kind === 'shell')
|
|
2067
|
+
return true;
|
|
2068
|
+
if (heredoc.receiver.kind === 'language') {
|
|
2069
|
+
return CODE_SPAWNS_PROCESS.test(heredoc.body)
|
|
2070
|
+
|| (/^(?:perl|ruby)$/.test(heredoc.receiver.program) && /`[^`]*`|%x[\[{(]/.test(heredoc.body))
|
|
2071
|
+
|| interpreterCodeRequestsMetadata(heredoc.body);
|
|
2072
|
+
}
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Each body becomes `inert` when it is data, or `acting(index)` when it runs
|
|
2077
|
+
* (left as is without `acting`). Delimiter lines stay, so the result still
|
|
2078
|
+
* lexes. Returns the bodies that run so a caller can put them back verbatim.
|
|
2079
|
+
*/
|
|
2080
|
+
function replaceHeredocBodies(text, view, inert, acting) {
|
|
2081
|
+
let out = '';
|
|
2082
|
+
let at = 0;
|
|
2083
|
+
const kept = [];
|
|
2084
|
+
for (const heredoc of view.heredocs) {
|
|
2085
|
+
const acts = heredocBodyActs(heredoc);
|
|
2086
|
+
if (acts && !acting)
|
|
2087
|
+
continue;
|
|
2088
|
+
out += text.slice(at, heredoc.bodyStart) + (acts ? acting(kept.push(heredoc.body) - 1) : inert);
|
|
2089
|
+
at = heredoc.bodyStart + heredoc.body.length;
|
|
2090
|
+
}
|
|
2091
|
+
return { text: out + text.slice(at), acting: kept };
|
|
2092
|
+
}
|
|
2093
|
+
/** The line as a bash parser sees it: bodies that RUN are opaque text there, so blank them. */
|
|
2094
|
+
function withoutActingHeredocBodies(text) {
|
|
2095
|
+
const view = heredocView(text);
|
|
2096
|
+
if (!view)
|
|
2097
|
+
return text;
|
|
2098
|
+
let out = '';
|
|
2099
|
+
let at = 0;
|
|
2100
|
+
for (const heredoc of view.heredocs) {
|
|
2101
|
+
if (!heredocBodyActs(heredoc))
|
|
2102
|
+
continue;
|
|
2103
|
+
out += text.slice(at, heredoc.bodyStart);
|
|
2104
|
+
at = heredoc.bodyStart + heredoc.body.length;
|
|
2105
|
+
}
|
|
2106
|
+
return out + text.slice(at);
|
|
2107
|
+
}
|
|
2108
|
+
const ACTING_BODY_MARK = (index) => `fcd_acting_heredoc_${index}_`;
|
|
2109
|
+
/** Receiver of a `<<<` here-string whose quoted word starts at `offset`. */
|
|
2110
|
+
function hereStringReceiver(src, offset) {
|
|
2111
|
+
const head = src.slice(src.lastIndexOf('\n', offset - 1) + 1, offset).replace(/<<<\s*$/, '');
|
|
2112
|
+
const statements = splitUnquotedPipelines(head);
|
|
2113
|
+
const stages = statements[statements.length - 1] || [];
|
|
2114
|
+
return stdinReceiver(stages[stages.length - 1] || '');
|
|
2115
|
+
}
|
|
1720
2116
|
function stripInertDataSegments(text) {
|
|
1721
|
-
|
|
2117
|
+
const heredocs = heredocView(text);
|
|
2118
|
+
// A data body (`cat > notes.md <<'EOF' … ssh … EOF`) cannot make the line an
|
|
2119
|
+
// interpreter feed; only the code around it, and bodies that run, can.
|
|
2120
|
+
const executable = heredocs ? replaceHeredocBodies(text, heredocs, '').text : text;
|
|
2121
|
+
if (DATA_TO_INTERPRETER.test(unquotedCode(executable)))
|
|
1722
2122
|
return text;
|
|
2123
|
+
// A body that runs is code line for line: park it so the quoted-span pass
|
|
2124
|
+
// below cannot read `os.system('rm -rf ~')` as prose, then put it back.
|
|
2125
|
+
const parked = heredocs ? replaceHeredocBodies(text, heredocs, INERT_DATA, ACTING_BODY_MARK) : { text, acting: [] };
|
|
2126
|
+
let out = stripQuotedData(parked.text);
|
|
2127
|
+
parked.acting.forEach((body, index) => {
|
|
2128
|
+
const mark = ACTING_BODY_MARK(index);
|
|
2129
|
+
out = out.includes(mark) ? out.replace(mark, () => body) : `${out}\n${body}`;
|
|
2130
|
+
});
|
|
2131
|
+
return out;
|
|
2132
|
+
}
|
|
2133
|
+
/** Here-strings and quoted prose of a line whose heredoc bodies are already resolved. */
|
|
2134
|
+
function stripQuotedData(text) {
|
|
1723
2135
|
let out = text;
|
|
1724
2136
|
// PowerShell here-strings: @'…'@ is fully literal; @"…"@ interpolates $( ).
|
|
1725
2137
|
out = replaceDelimited(out, "@'", "'@", () => INERT_DATA);
|
|
1726
2138
|
out = replaceDelimited(out, '@"', '"@', m => (m.includes('$(') ? m : INERT_DATA));
|
|
1727
|
-
// POSIX heredocs: quoted delimiter (<<'EOF') is literal; unquoted interpolates.
|
|
1728
|
-
out = out.replace(/<<-?\s*'(\w+)'[\s\S]*?(?:\n\1\b|$)/g, INERT_DATA);
|
|
1729
|
-
out = out.replace(/<<-?\s*"?(\w+)"?[\s\S]*?(?:\n\1\b|$)/g, m => (INTERPOLATES.test(m) ? m : INERT_DATA));
|
|
1730
2139
|
// Generic quoted-span pass: whitespace inside quotes ⇒ free text ⇒ inert,
|
|
1731
2140
|
// unless the span sits in a code position (invariants 2–4 above).
|
|
1732
2141
|
const src = out;
|
|
@@ -1736,6 +2145,14 @@ function stripInertDataSegments(text) {
|
|
|
1736
2145
|
if (!/\s/.test(body))
|
|
1737
2146
|
return whole; // single token: argv, not prose
|
|
1738
2147
|
const before = src.slice(Math.max(0, offset - 80), offset);
|
|
2148
|
+
// `bash <<< "…"` feeds the quoted word to stdin: it is what its reader makes of it.
|
|
2149
|
+
if (/<<<\s*$/.test(before)) {
|
|
2150
|
+
const receiver = hereStringReceiver(src, offset);
|
|
2151
|
+
if (receiver.kind === 'shell')
|
|
2152
|
+
return whole;
|
|
2153
|
+
if (receiver.kind === 'language')
|
|
2154
|
+
return CODE_SPAWNS_PROCESS.test(body) || interpreterCodeRequestsMetadata(body) ? whole : INERT_DATA;
|
|
2155
|
+
}
|
|
1739
2156
|
// Language-interpreter code: data unless it spawns a process (see above).
|
|
1740
2157
|
if (LANG_INTERPRETER_CODE_BEFORE.test(before))
|
|
1741
2158
|
return interpreterCodeSpawnsProcess(before, body) || interpreterCodeRequestsMetadata(body) ? whole : INERT_DATA;
|
|
@@ -38,6 +38,16 @@ export declare function claudeDesktopInstallTargets(configPath?: string): Claude
|
|
|
38
38
|
export declare function candidateConfigPaths(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
39
39
|
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
40
40
|
export declare function defaultScanRoots(): string[];
|
|
41
|
+
type ProjectConfigScan = {
|
|
42
|
+
candidates: ConfigPathCandidate[];
|
|
43
|
+
scannedDirs: number;
|
|
44
|
+
excludedDirs: number;
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
truncatedReason?: ScanTruncationReason;
|
|
47
|
+
maxDepth: number;
|
|
48
|
+
maxDirs: number;
|
|
49
|
+
};
|
|
50
|
+
export declare function setProjectConfigScanPass(key: string | undefined): void;
|
|
41
51
|
/**
|
|
42
52
|
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
43
53
|
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
@@ -48,15 +58,7 @@ export declare function scanRootsForProjectConfigs(roots: string[], options?: {
|
|
|
48
58
|
maxDepth?: number;
|
|
49
59
|
maxDirs?: number;
|
|
50
60
|
excludedRoots?: string[];
|
|
51
|
-
}):
|
|
52
|
-
candidates: ConfigPathCandidate[];
|
|
53
|
-
scannedDirs: number;
|
|
54
|
-
excludedDirs: number;
|
|
55
|
-
truncated: boolean;
|
|
56
|
-
truncatedReason?: ScanTruncationReason;
|
|
57
|
-
maxDepth: number;
|
|
58
|
-
maxDirs: number;
|
|
59
|
-
};
|
|
61
|
+
}): ProjectConfigScan;
|
|
60
62
|
export interface FolderCatalogEntry {
|
|
61
63
|
path: string;
|
|
62
64
|
/** Depth relative to its walk root (home or a dev root) — lets the UI indent. */
|
|
@@ -85,3 +87,4 @@ export declare function discoverScanTargets(cwd: string, extra?: string): Config
|
|
|
85
87
|
* `~/.codex` / `~/.cursor` without Cursor.exe must not count as installed.
|
|
86
88
|
*/
|
|
87
89
|
export declare function detectIdeAppInstalled(clientKey: string, env?: NodeJS.ProcessEnv, home?: string, platform?: NodeJS.Platform): boolean;
|
|
90
|
+
export {};
|
|
@@ -46,6 +46,7 @@ exports.claudeDesktopLikelyInstalled = claudeDesktopLikelyInstalled;
|
|
|
46
46
|
exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
|
|
47
47
|
exports.candidateConfigPaths = candidateConfigPaths;
|
|
48
48
|
exports.defaultScanRoots = defaultScanRoots;
|
|
49
|
+
exports.setProjectConfigScanPass = setProjectConfigScanPass;
|
|
49
50
|
exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
|
|
50
51
|
exports.buildFolderCatalog = buildFolderCatalog;
|
|
51
52
|
exports.resolveScanRoots = resolveScanRoots;
|
|
@@ -297,6 +298,23 @@ function defaultScanRoots() {
|
|
|
297
298
|
}
|
|
298
299
|
});
|
|
299
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Reuse of one walk across a single daemon protection pass. A pass inspects,
|
|
303
|
+
* repairs, rescans and verifies, and each step walked every scan root again
|
|
304
|
+
* (4-7 walks of up to 25,000 folders). Only the daemon's housekeeping sets a
|
|
305
|
+
* pass key; every other caller (and work between a pass's steps) walks fresh.
|
|
306
|
+
*/
|
|
307
|
+
let scanPassKey;
|
|
308
|
+
const scanPassResults = new Map();
|
|
309
|
+
const SCAN_PASSES_KEPT = 4;
|
|
310
|
+
function setProjectConfigScanPass(key) {
|
|
311
|
+
scanPassKey = key;
|
|
312
|
+
if (key === undefined || scanPassResults.has(key))
|
|
313
|
+
return;
|
|
314
|
+
scanPassResults.set(key, new Map());
|
|
315
|
+
while (scanPassResults.size > SCAN_PASSES_KEPT)
|
|
316
|
+
scanPassResults.delete(scanPassResults.keys().next().value);
|
|
317
|
+
}
|
|
300
318
|
/**
|
|
301
319
|
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
302
320
|
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
@@ -304,6 +322,18 @@ function defaultScanRoots() {
|
|
|
304
322
|
* are pruned; only the known project config filenames are checked.
|
|
305
323
|
*/
|
|
306
324
|
function scanRootsForProjectConfigs(roots, options = {}) {
|
|
325
|
+
const pass = scanPassKey === undefined ? undefined : scanPassResults.get(scanPassKey);
|
|
326
|
+
if (!pass)
|
|
327
|
+
return walkProjectConfigs(roots, options);
|
|
328
|
+
const memoKey = JSON.stringify([roots, options.maxDepth, options.maxDirs, options.excludedRoots]);
|
|
329
|
+
const cached = pass.get(memoKey);
|
|
330
|
+
if (cached)
|
|
331
|
+
return { ...cached, candidates: cached.candidates.map(candidate => ({ ...candidate })) };
|
|
332
|
+
const result = walkProjectConfigs(roots, options);
|
|
333
|
+
pass.set(memoKey, result);
|
|
334
|
+
return { ...result, candidates: result.candidates.map(candidate => ({ ...candidate })) };
|
|
335
|
+
}
|
|
336
|
+
function walkProjectConfigs(roots, options) {
|
|
307
337
|
const maxDepth = options.maxDepth ?? 4;
|
|
308
338
|
const maxDirs = options.maxDirs ?? 25000;
|
|
309
339
|
const candidates = [];
|
|
@@ -51,6 +51,7 @@ const config_1 = require("../config");
|
|
|
51
51
|
const child_process_1 = require("child_process");
|
|
52
52
|
const installClaudeHook_1 = require("./installClaudeHook");
|
|
53
53
|
const mcpGateway_1 = require("./mcpGateway");
|
|
54
|
+
const refreshPolicyCache_1 = require("./refreshPolicyCache");
|
|
54
55
|
/**
|
|
55
56
|
* Written by `ci-protect` / `workload-protect` next to the credentials they
|
|
56
57
|
* save. Its presence says "the enrollment on this machine is EPHEMERAL", so a
|
|
@@ -214,6 +215,12 @@ async function startDetachedHttpGateway(input) {
|
|
|
214
215
|
*/
|
|
215
216
|
async function installEphemeralProtectionStack(options, config) {
|
|
216
217
|
const sendLog = options.sendLog || exports.silentLogSink;
|
|
218
|
+
// Seed both policy caches before the agent's first action, as login does:
|
|
219
|
+
// hooks and gateways never fetch policy while a cache exists, and a job or
|
|
220
|
+
// container has no daemon to poll. Callers export the ephemeral identity
|
|
221
|
+
// (FCD_MACHINE_ID) first, so the seed carries it. Best-effort and silent —
|
|
222
|
+
// on failure the hot path's single-flight no-cache attempt still applies.
|
|
223
|
+
await (0, refreshPolicyCache_1.refreshPolicyCachesInBackground)({ apiUrl: options.apiUrl, shieldId: options.shieldId, shieldKey: options.shieldKey });
|
|
217
224
|
// The Claude-format hook covers Claude Code, VS Code agent mode, and
|
|
218
225
|
// Copilot CLI — the common agents on runners and in containers.
|
|
219
226
|
if (options.hooks !== 'false') {
|
|
@@ -4,6 +4,8 @@ export interface FlushSpoolArgs {
|
|
|
4
4
|
shieldId?: string;
|
|
5
5
|
shieldKey?: string;
|
|
6
6
|
heartbeat?: string;
|
|
7
|
+
/** `false`: leave the terminal guard rule files alone (the daemon keeps their 30 s cadence). */
|
|
8
|
+
guardRules?: string;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* `fullcourtdefense flush-spool` — drain the local telemetry spool to the backend
|