log-llm-config 1.3.97 → 1.4.8
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/apply_deferred_vscdb.js +34 -5
- package/dist/compliance_check_runner.js +28 -1
- package/dist/compliance_prompt_gate.js +32 -12
- package/dist/execute_trusted_restarts.js +5 -0
- package/dist/log_config_files/collection/claude_desktop_extensions_collector.js +104 -1
- package/dist/log_config_files/collection/config_collector.js +62 -3
- package/dist/log_config_files/collection/cowork_session_whitelist.js +89 -0
- package/dist/log_config_files/collection/cursor_project_mcp_collector.js +47 -18
- package/dist/log_config_files/collection/skills_cli_collector.js +146 -0
- package/dist/log_config_files/paths/pattern_resolver.js +122 -31
- package/dist/log_config_files/readers/file_readers.js +112 -2
- package/dist/log_config_files/runtime/compliance_check.js +17 -0
- package/dist/log_config_files/runtime/compliance_session_log.js +372 -0
- package/dist/log_config_files/runtime/hook_logger.js +51 -3
- package/dist/log_config_files/runtime/hook_type_for_request.js +3 -1
- package/dist/log_config_files/runtime/main_runner.js +20 -3
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +4 -0
- package/dist/log_config_files/runtime/remediation_sync.js +4 -1
- package/dist/log_config_files/runtime/trusted_restarts.js +7 -0
- package/dist/log_config_files/sender/batch_sender.js +3 -1
- package/dist/log_sensitive_paths_audit.js +22 -3
- package/package.json +4 -4
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* ~/opt-ai-sec/management/optimus_deferred_vscdb_apply.json (see remediation_sync).
|
|
5
|
-
*/
|
|
2
|
+
// Runs after Cursor SIGKILL: applies queued state.vscdb patches, then immediately runs
|
|
3
|
+
// post-restart verification so the server gets `verified` without waiting for the next prompt.
|
|
6
4
|
import { applyDeferredVscdbFromDisk } from './log_config_files/runtime/remediation_sync.js';
|
|
5
|
+
import { runPostApplyVerification } from './log_config_files/runtime/compliance_check.js';
|
|
6
|
+
import { hookRunLog } from './log_config_files/runtime/hook_logger.js';
|
|
7
|
+
import { finalizeAndUploadComplianceSessionLog, setActiveComplianceLogPath, } from './log_config_files/runtime/compliance_session_log.js';
|
|
8
|
+
const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim() || null;
|
|
9
|
+
if (complianceLogPath) {
|
|
10
|
+
setActiveComplianceLogPath(complianceLogPath);
|
|
11
|
+
}
|
|
7
12
|
applyDeferredVscdbFromDisk()
|
|
8
|
-
.then((ok) =>
|
|
13
|
+
.then(async (ok) => {
|
|
14
|
+
if (ok) {
|
|
15
|
+
try {
|
|
16
|
+
const outcomes = await runPostApplyVerification();
|
|
17
|
+
if (outcomes.length > 0) {
|
|
18
|
+
hookRunLog(`apply_deferred_vscdb: post_apply_verification count=${outcomes.length} ` +
|
|
19
|
+
`verified=${outcomes.filter((o) => o.status === 'verified').length}`);
|
|
20
|
+
}
|
|
21
|
+
if (complianceLogPath) {
|
|
22
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
hookRunLog(`apply_deferred_vscdb: post_apply_verification error: ${e instanceof Error ? e.message : String(e)}`);
|
|
27
|
+
if (complianceLogPath) {
|
|
28
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (complianceLogPath) {
|
|
33
|
+
hookRunLog('apply_deferred_vscdb: apply failed');
|
|
34
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
|
|
35
|
+
}
|
|
36
|
+
process.exit(ok ? 0 : 1);
|
|
37
|
+
})
|
|
9
38
|
.catch(() => process.exit(1));
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { complianceRunnerRunnerLine, hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
|
|
3
3
|
import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
|
|
4
|
+
import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForRunner, isFinalizeComplianceSessionArgv, isInflightComplianceLogPath, parseComplianceLogArg, resolveComplianceSessionLogPath, } from './log_config_files/runtime/compliance_session_log.js';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
4
6
|
(async () => {
|
|
5
|
-
|
|
7
|
+
let complianceLogPath = parseComplianceLogArg(process.argv);
|
|
8
|
+
if (isFinalizeComplianceSessionArgv(process.argv)) {
|
|
9
|
+
if (complianceLogPath) {
|
|
10
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
11
|
+
}
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
if (complianceLogPath) {
|
|
15
|
+
const resolved = resolveComplianceSessionLogPath(complianceLogPath);
|
|
16
|
+
if (existsSync(resolved) && isInflightComplianceLogPath(resolved)) {
|
|
17
|
+
complianceLogPath = initComplianceSessionForRunner(resolved);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Gate may have finalized+moved the session before this background runner started.
|
|
21
|
+
complianceLogPath = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
hookLogAppendSection('compliance_check_runner (background sync + check)');
|
|
26
|
+
}
|
|
6
27
|
complianceRunnerRunnerLine('compliance_check_runner: start');
|
|
7
28
|
try {
|
|
8
29
|
await runComplianceCheck();
|
|
@@ -13,6 +34,12 @@ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.
|
|
|
13
34
|
complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
|
|
14
35
|
complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
|
|
15
36
|
process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
37
|
+
if (complianceLogPath) {
|
|
38
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
|
|
39
|
+
}
|
|
16
40
|
process.exit(1);
|
|
17
41
|
}
|
|
42
|
+
if (complianceLogPath) {
|
|
43
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
44
|
+
}
|
|
18
45
|
})();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Synchronous pre-prompt gate: local compliance only (stdout = single JSON line for IDE hooks).
|
|
4
|
-
* stderr must stay clean; logs go via hookRunLog (file).
|
|
4
|
+
* stderr must stay clean; logs go via hookRunLog (compliance session file when --compliance-log is set).
|
|
5
5
|
*
|
|
6
6
|
* When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
|
|
7
7
|
* the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
|
|
@@ -11,6 +11,7 @@ import { isRemediationQuarantined } from './log_config_files/runtime/remediation
|
|
|
11
11
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
12
12
|
import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
|
|
13
13
|
import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
|
|
14
|
+
import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForGate, parseComplianceLogArg, } from './log_config_files/runtime/compliance_session_log.js';
|
|
14
15
|
import { isThisCliModule } from './cli_invocation_match.js';
|
|
15
16
|
import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
|
|
16
17
|
import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
|
|
@@ -19,6 +20,17 @@ import { syncRemediations } from './log_config_files/runtime/remediation_sync.js
|
|
|
19
20
|
import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
|
|
20
21
|
import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
|
|
21
22
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
/** After verified (or failed verify), push enforcement + session log immediately — do not wait for background runner. */
|
|
24
|
+
async function flushRemediationOutcomeToServer(complianceLogPath) {
|
|
25
|
+
if (!complianceLogPath)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
hookRunLog(`compliance_prompt_gate: session finalize/upload failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
22
34
|
function parseIde() {
|
|
23
35
|
const eq = process.argv.find((a) => a.startsWith('--ide='));
|
|
24
36
|
if (eq) {
|
|
@@ -164,10 +176,23 @@ export async function runCompliancePromptGateCli() {
|
|
|
164
176
|
export async function runCompliancePromptGate() {
|
|
165
177
|
const ide = parseIde();
|
|
166
178
|
const agent = parseAgent(ide);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
179
|
+
let complianceLogPath = parseComplianceLogArg(process.argv);
|
|
180
|
+
if (complianceLogPath) {
|
|
181
|
+
complianceLogPath = initComplianceSessionForGate(complianceLogPath);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
hookLogSessionBanner('compliance_prompt_gate (before submit)');
|
|
185
|
+
}
|
|
186
|
+
let status = runLocalRemediationComplianceCheck(agent);
|
|
187
|
+
// Post-restart verify + server reports BEFORE sync so a manifest UUID swap cannot delay
|
|
188
|
+
// verified / activity-log until the following prompt.
|
|
189
|
+
const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
|
|
190
|
+
if (postRestartVerify.outcomes.length > 0) {
|
|
191
|
+
await Promise.allSettled(postRestartVerify.reportPromises);
|
|
192
|
+
await flushRemediationOutcomeToServer(complianceLogPath);
|
|
193
|
+
hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
|
|
194
|
+
status = runLocalRemediationComplianceCheck(agent);
|
|
195
|
+
}
|
|
171
196
|
const hw = tryResolveHardwareUuid();
|
|
172
197
|
if (hw) {
|
|
173
198
|
try {
|
|
@@ -180,13 +205,7 @@ export async function runCompliancePromptGate() {
|
|
|
180
205
|
// Network or auth failure — fall back to local file state.
|
|
181
206
|
}
|
|
182
207
|
}
|
|
183
|
-
|
|
184
|
-
// Always process pending post-restart rows (even when compliance is ok — apply may have stuck).
|
|
185
|
-
const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
|
|
186
|
-
if (postRestartVerify.outcomes.length > 0) {
|
|
187
|
-
await Promise.allSettled(postRestartVerify.reportPromises);
|
|
188
|
-
hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
|
|
189
|
-
}
|
|
208
|
+
status = runLocalRemediationComplianceCheck(agent);
|
|
190
209
|
// Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
|
|
191
210
|
// Upload those files fire-and-forget so the backend can resolve the finding immediately.
|
|
192
211
|
for (const e of status.secondarySatisfied ?? []) {
|
|
@@ -255,6 +274,7 @@ export async function runCompliancePromptGate() {
|
|
|
255
274
|
if (immediateVerified) {
|
|
256
275
|
confirmAppliedAutofixVerified(appliedViolations, reportPromises);
|
|
257
276
|
await Promise.allSettled(reportPromises);
|
|
277
|
+
await flushRemediationOutcomeToServer(complianceLogPath);
|
|
258
278
|
}
|
|
259
279
|
const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
|
|
260
280
|
const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
|
|
@@ -7,9 +7,14 @@
|
|
|
7
7
|
import { readFileSync } from 'node:fs';
|
|
8
8
|
import { isThisCliModule } from './cli_invocation_match.js';
|
|
9
9
|
import { appendComplianceRunnerLine, hookRunLog } from './log_config_files/runtime/hook_logger.js';
|
|
10
|
+
import { setActiveComplianceLogPath } from './log_config_files/runtime/compliance_session_log.js';
|
|
10
11
|
import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
|
|
11
12
|
/** Invoked by dist entrypoint or by `npx log-llm-config@latest execute-trusted-restarts` (cli.js dispatches here). */
|
|
12
13
|
export function runExecuteTrustedRestartsFromStdin() {
|
|
14
|
+
const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim();
|
|
15
|
+
if (complianceLogPath) {
|
|
16
|
+
setActiveComplianceLogPath(complianceLogPath);
|
|
17
|
+
}
|
|
13
18
|
let raw;
|
|
14
19
|
try {
|
|
15
20
|
raw = readFileSync(0, 'utf8');
|
|
@@ -19,6 +19,109 @@ function extensionsInstallationsPath(home) {
|
|
|
19
19
|
const base = claudeAppSupportDir(home);
|
|
20
20
|
return base ? join(base, 'extensions-installations.json') : '';
|
|
21
21
|
}
|
|
22
|
+
function claudeExtensionSettingsDir(home) {
|
|
23
|
+
const base = claudeAppSupportDir(home);
|
|
24
|
+
return base ? join(base, 'Claude Extensions Settings') : '';
|
|
25
|
+
}
|
|
26
|
+
/** Per-connector toggle: Claude Extensions Settings/<extension-id>.json → { isEnabled: boolean }. */
|
|
27
|
+
function readClaudeDesktopExtensionEnableSettings(home) {
|
|
28
|
+
const settingsDir = claudeExtensionSettingsDir(home);
|
|
29
|
+
if (!settingsDir || !existsSync(settingsDir))
|
|
30
|
+
return new Map();
|
|
31
|
+
const out = new Map();
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
for (const dirent of entries) {
|
|
40
|
+
if (!dirent.name.endsWith('.json'))
|
|
41
|
+
continue;
|
|
42
|
+
const extId = dirent.name.slice(0, -'.json'.length);
|
|
43
|
+
if (!extId)
|
|
44
|
+
continue;
|
|
45
|
+
const raw = readJSONFile(join(settingsDir, dirent.name));
|
|
46
|
+
if (!raw || typeof raw !== 'object')
|
|
47
|
+
continue;
|
|
48
|
+
const isEnabled = raw.isEnabled;
|
|
49
|
+
if (typeof isEnabled === 'boolean') {
|
|
50
|
+
out.set(extId, isEnabled);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function applyExtensionEnableSettingsToRawContent(rawContent, settings) {
|
|
56
|
+
if (settings.size === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const extensions = rawContent.extensions;
|
|
59
|
+
if (!extensions || typeof extensions !== 'object')
|
|
60
|
+
return 0;
|
|
61
|
+
let applied = 0;
|
|
62
|
+
for (const [extId, enabled] of settings) {
|
|
63
|
+
const ext = extensions[extId];
|
|
64
|
+
if (!ext || typeof ext !== 'object')
|
|
65
|
+
continue;
|
|
66
|
+
ext.isEnabled = enabled;
|
|
67
|
+
applied += 1;
|
|
68
|
+
}
|
|
69
|
+
return applied;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Upload each Claude Extensions Settings/*.json as its own config file (raw on-disk JSON).
|
|
73
|
+
* Do not merge isEnabled into extensions-installations.json — that mutates the payload
|
|
74
|
+
* vs the source file and creates spurious inventory audit diffs.
|
|
75
|
+
*/
|
|
76
|
+
function collectClaudeDesktopExtensionSettingsFiles(home = homedir()) {
|
|
77
|
+
const settingsDir = claudeExtensionSettingsDir(home);
|
|
78
|
+
if (!settingsDir || !existsSync(settingsDir))
|
|
79
|
+
return [];
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = readdirSync(settingsDir, { withFileTypes: true }).filter((d) => d.isFile());
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const dirent of entries) {
|
|
89
|
+
if (!dirent.name.endsWith('.json'))
|
|
90
|
+
continue;
|
|
91
|
+
const filePath = join(settingsDir, dirent.name);
|
|
92
|
+
const raw = readJSONFile(filePath);
|
|
93
|
+
if (!raw || typeof raw !== 'object')
|
|
94
|
+
continue;
|
|
95
|
+
results.push({
|
|
96
|
+
file_type: 'claude_extension_settings',
|
|
97
|
+
file_path: filePath,
|
|
98
|
+
raw_content: raw,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @deprecated Do not call during collection — mutates extensions-installations upload payloads.
|
|
105
|
+
* isEnabled is resolved server-side from claude_extension_settings uploads.
|
|
106
|
+
*/
|
|
107
|
+
function mergeClaudeDesktopExtensionEnableSettings(configFiles, home = homedir()) {
|
|
108
|
+
const settings = readClaudeDesktopExtensionEnableSettings(home);
|
|
109
|
+
if (settings.size === 0) {
|
|
110
|
+
return { applied: 0, settingsCount: 0 };
|
|
111
|
+
}
|
|
112
|
+
let applied = 0;
|
|
113
|
+
for (const entry of configFiles) {
|
|
114
|
+
if (entry.file_type !== 'claude_extensions_installations')
|
|
115
|
+
continue;
|
|
116
|
+
if (!entry.raw_content || typeof entry.raw_content !== 'object')
|
|
117
|
+
continue;
|
|
118
|
+
const extensions = entry.raw_content.extensions;
|
|
119
|
+
if (!extensions || typeof extensions !== 'object')
|
|
120
|
+
continue;
|
|
121
|
+
applied += applyExtensionEnableSettingsToRawContent(entry.raw_content, settings);
|
|
122
|
+
}
|
|
123
|
+
return { applied, settingsCount: settings.size };
|
|
124
|
+
}
|
|
22
125
|
function normalizePathForMatch(path) {
|
|
23
126
|
return path.replace(/\\/g, '/');
|
|
24
127
|
}
|
|
@@ -142,4 +245,4 @@ function collectClaudeDesktopExtensionManifests(home = homedir()) {
|
|
|
142
245
|
}
|
|
143
246
|
return results;
|
|
144
247
|
}
|
|
145
|
-
export { claudeAppSupportDir, claudeDesktopExtensionsDir, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, };
|
|
248
|
+
export { claudeAppSupportDir, claudeDesktopExtensionsDir, claudeExtensionSettingsDir, collectClaudeDesktopExtensionSettingsFiles, collectClaudeDesktopExtensionManifests, collectDiskOnlyClaudeDesktopExtensions, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, readClaudeDesktopExtensionEnableSettings, };
|
|
@@ -1,13 +1,54 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { readJSONFile, readMCPConfig, readMarkdownFile, readInstalledExtensions } from '../readers/file_readers.js';
|
|
4
4
|
import { getExtensionsCachePath, getExtensionsCacheInstalledSuffix, getVscdbPath } from '../paths/path_constants_helpers.js';
|
|
5
5
|
import { resolvePatternToTargets, normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
6
6
|
import { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
7
|
+
import { applyCoworkSessionWhitelist } from './cowork_session_whitelist.js';
|
|
7
8
|
import { collectDirectoryEntries, collectDirectoryMetadata } from './directory_collector.js';
|
|
8
9
|
import { collectVscdbEntries } from '../readers/vscdb_config_builder.js';
|
|
9
10
|
import { pushDerivedFilesFromRecipe } from './openclaw_helpers.js';
|
|
10
11
|
import { collapseMetadataByFileType } from './metadata_merge.js';
|
|
12
|
+
function withOpencodeEnvPatterns(patterns) {
|
|
13
|
+
const out = [...patterns];
|
|
14
|
+
const seen = new Set(out.map((p) => `${p.type}:${p.file_type}:${p.path}`));
|
|
15
|
+
const add = (pattern) => {
|
|
16
|
+
const key = `${pattern.type}:${pattern.file_type}:${pattern.path}`;
|
|
17
|
+
if (seen.has(key))
|
|
18
|
+
return;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push(pattern);
|
|
21
|
+
};
|
|
22
|
+
const envConfig = (process.env.OPENCODE_CONFIG || '').trim();
|
|
23
|
+
if (envConfig) {
|
|
24
|
+
add({
|
|
25
|
+
path: envConfig,
|
|
26
|
+
type: 'file',
|
|
27
|
+
file_type: 'opencode_mcp',
|
|
28
|
+
content_format: 'json',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const envDirRaw = (process.env.OPENCODE_CONFIG_DIR || '').trim();
|
|
32
|
+
if (!envDirRaw)
|
|
33
|
+
return out;
|
|
34
|
+
const envDir = envDirRaw.replace(/[\\/]+$/, '');
|
|
35
|
+
add({ path: `${envDir}/opencode.json`, type: 'file', file_type: 'opencode_mcp', content_format: 'json' });
|
|
36
|
+
add({ path: `${envDir}/opencode.jsonc`, type: 'file', file_type: 'opencode_mcp', content_format: 'json' });
|
|
37
|
+
add({ path: `${envDir}/command-hooks.json`, type: 'file', file_type: 'opencode_command_hooks', content_format: 'json' });
|
|
38
|
+
add({ path: `${envDir}/command-hooks.jsonc`, type: 'file', file_type: 'opencode_command_hooks', content_format: 'json' });
|
|
39
|
+
add({ path: `${envDir}/AGENTS.md`, type: 'file', file_type: 'opencode_rule', content_format: 'markdown' });
|
|
40
|
+
add({ path: `${envDir}/agents/`, type: 'dir', file_type: 'opencode_subagent', content_format: 'markdown', dir_glob: '*.md' });
|
|
41
|
+
add({ path: `${envDir}/commands/`, type: 'dir', file_type: 'opencode_command', content_format: 'markdown', dir_glob: '*.md' });
|
|
42
|
+
add({ path: `${envDir}/skills/`, type: 'dir', file_type: 'opencode_skill', content_format: 'markdown', dir_glob: '*.md' });
|
|
43
|
+
add({ path: `${envDir}/mcp-auth.json`, type: 'file', file_type: 'opencode_mcp_auth', content_format: 'json' });
|
|
44
|
+
add({ path: `${envDir}/managed.json`, type: 'file', file_type: 'opencode_managed_preference', content_format: 'json' });
|
|
45
|
+
add({ path: `${envDir}/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
46
|
+
add({ path: `${envDir}/plugins/`, type: 'dir', file_type: 'opencode_plugin', content_format: 'text', dir_glob: '*' });
|
|
47
|
+
add({ path: `${envDir}/plugin/`, type: 'dir', file_type: 'opencode_plugin', content_format: 'text', dir_glob: '*' });
|
|
48
|
+
add({ path: `${envDir}/plugins/*/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
49
|
+
add({ path: `${envDir}/plugin/*/package.json`, type: 'file', file_type: 'opencode_plugin_manifest', content_format: 'json' });
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
11
52
|
function buildCollectionContext(patterns, projectRoot, home, homeRecurseSkipDirs = [], absolutePathPrefixes = [], mcpToolGlobSpec = null, clientPathConstants = null, pathResolutionSpecs = null) {
|
|
12
53
|
const seenPaths = new Set();
|
|
13
54
|
const targets = [];
|
|
@@ -69,11 +110,28 @@ function collectRegularFileEntry(t, enrichByFileType) {
|
|
|
69
110
|
const recipe = enrichByFileType[t.file_type];
|
|
70
111
|
if (recipe)
|
|
71
112
|
enrichRawFromRecipe(raw, recipe);
|
|
113
|
+
// Cowork session metadata: reduce to the PII whitelist (hash identity, drop
|
|
114
|
+
// server URLs, keep only the activity-signal keys) BEFORE it leaves the
|
|
115
|
+
// device. Drop the entry entirely if nothing whitelisted survives so raw
|
|
116
|
+
// session content can never be emitted (e.g. on a JSON parse failure).
|
|
117
|
+
if (t.file_type === 'cowork_session_metadata') {
|
|
118
|
+
const filtered = applyCoworkSessionWhitelist(raw);
|
|
119
|
+
if (filtered === null)
|
|
120
|
+
return null;
|
|
121
|
+
raw = filtered;
|
|
122
|
+
}
|
|
72
123
|
return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: raw };
|
|
73
124
|
}
|
|
74
125
|
function collectMetadataEntry(t) {
|
|
75
126
|
try {
|
|
76
127
|
const stats = statSync(t.path);
|
|
128
|
+
if (stats.isDirectory?.()) {
|
|
129
|
+
try {
|
|
130
|
+
if (readdirSync(t.path).length === 0)
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
catch { /* unreadable — assume non-empty, allow */ }
|
|
134
|
+
}
|
|
77
135
|
return { file_type: t.file_type, file_path: t.logicalFilePath ?? t.path, raw_content: { filename: t.path, last_modified: stats.mtime.toISOString(), source: 'file_metadata' }, collect_style: 'metadata' };
|
|
78
136
|
}
|
|
79
137
|
catch (err) {
|
|
@@ -99,7 +157,8 @@ function collectConfigFilesFromPatterns(patterns, projectRoot, onProgress, optio
|
|
|
99
157
|
const vscdbPath = getVscdbPath(home, pathConstants);
|
|
100
158
|
const extensionsCachePath = getExtensionsCachePath(home, pathConstants);
|
|
101
159
|
const extensionsInstalledSuffix = getExtensionsCacheInstalledSuffix(pathConstants);
|
|
102
|
-
const
|
|
160
|
+
const effectivePatterns = withOpencodeEnvPatterns(patterns);
|
|
161
|
+
const { targets, enrichByFileType, metadataOnlyFileTypes } = buildCollectionContext(effectivePatterns, projectRoot, home, options?.home_recurse_skip_dirs ?? [], options?.absolute_path_prefixes ?? [], options?.mcp_tool_glob_spec ?? null, pathConstants, options?.path_resolution_specs ?? null);
|
|
103
162
|
const configFiles = [];
|
|
104
163
|
const handledSpecialPaths = new Set();
|
|
105
164
|
const loggedFileTypes = new Set();
|
|
@@ -156,7 +215,7 @@ export { collectConfigFilesFromPatterns };
|
|
|
156
215
|
export { collectMcpToolFiles } from './mcp_tool_collector.js';
|
|
157
216
|
export { collectConfigFilesFromInstalledPlugins, collectPluginCacheMcpFiles } from './plugin_collector.js';
|
|
158
217
|
export { collectMcpFromClaudeJsonProjects } from './claude_known_projects_collector.js';
|
|
159
|
-
export { collectClaudeDesktopExtensionManifests, enrichClaudeDesktopExtensionsInstallationsUpload, } from './claude_desktop_extensions_collector.js';
|
|
218
|
+
export { collectClaudeDesktopExtensionManifests, collectClaudeDesktopExtensionSettingsFiles, enrichClaudeDesktopExtensionsInstallationsUpload, mergeClaudeDesktopExtensionEnableSettings, } from './claude_desktop_extensions_collector.js';
|
|
160
219
|
export { collectCursorProjectWorkspaceMcpConfigs } from './cursor_project_mcp_collector.js';
|
|
161
220
|
export { determineFileTypeFromPath } from './file_type_rules.js';
|
|
162
221
|
export { enrichRawFromRecipe } from './enrichment_helpers.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII boundary for Cowork session metadata (`local_<sessionId>.json`).
|
|
3
|
+
*
|
|
4
|
+
* Each Cowork session file carries ~47 KB of system prompt plus the user's
|
|
5
|
+
* conversation seed, selected folders, etc. We only need a small activity
|
|
6
|
+
* signal (which MCP servers were connected, recency, archived state). This
|
|
7
|
+
* reduces a parsed session object to a whitelist of top-level keys BEFORE it
|
|
8
|
+
* leaves the device — minimum-egress, matching DeepSecurityAndPrivacyEnhancer.
|
|
9
|
+
*
|
|
10
|
+
* - Only the keys in ALLOWED_KEYS survive; everything else is dropped.
|
|
11
|
+
* - `accountName` / `emailAddress` are sha256-hashed (cross-session joins
|
|
12
|
+
* without storing raw identity).
|
|
13
|
+
* - `remoteMcpServersConfig[]` keeps `{name, uuid, tools}` only; `url` is
|
|
14
|
+
* dropped (remote-MCP endpoint URLs can embed workspace ids / tokens).
|
|
15
|
+
*
|
|
16
|
+
* The sibling `local_<sessionId>/` directory (audit.jsonl, outputs/, uploads/)
|
|
17
|
+
* is never collected — see the collection glob in agents/cowork.py. Server-side
|
|
18
|
+
* ingest re-validates the shape and rejects any denied key as defense in depth.
|
|
19
|
+
*
|
|
20
|
+
* See design_notes/Phase0_5CoworkActivityEvidence.md.
|
|
21
|
+
*/
|
|
22
|
+
import crypto from 'node:crypto';
|
|
23
|
+
export const COWORK_SESSION_ALLOWED_KEYS = [
|
|
24
|
+
'sessionId',
|
|
25
|
+
'accountName', // hashed
|
|
26
|
+
'emailAddress', // hashed
|
|
27
|
+
'model',
|
|
28
|
+
'createdAt',
|
|
29
|
+
'lastActivityAt',
|
|
30
|
+
'isArchived',
|
|
31
|
+
'pluginsEnabled',
|
|
32
|
+
'skillsEnabled',
|
|
33
|
+
'memoryEnabled',
|
|
34
|
+
'hostLoopMode',
|
|
35
|
+
'cwd',
|
|
36
|
+
'enabledMcpTools', // dict "<serverUUID>:<tool>" -> bool
|
|
37
|
+
'remoteMcpServersConfig', // list of {name, uuid, tools} (url dropped)
|
|
38
|
+
];
|
|
39
|
+
const HASHED_KEYS = new Set(['accountName', 'emailAddress']);
|
|
40
|
+
const REMOTE_SERVER_KEEP = ['name', 'uuid', 'tools'];
|
|
41
|
+
function sha256Hex(value) {
|
|
42
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
function sanitizeRemoteServers(value) {
|
|
45
|
+
if (!Array.isArray(value))
|
|
46
|
+
return [];
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const entry of value) {
|
|
49
|
+
// Drop any non-object entry: a malformed/tampered remoteMcpServersConfig
|
|
50
|
+
// could carry a raw string (e.g. "https://…?token=…") that would otherwise
|
|
51
|
+
// survive the whitelist. Only keep whitelisted keys of well-formed objects.
|
|
52
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry))
|
|
53
|
+
continue;
|
|
54
|
+
const e = entry;
|
|
55
|
+
const kept = {};
|
|
56
|
+
for (const k of REMOTE_SERVER_KEEP) {
|
|
57
|
+
if (k in e)
|
|
58
|
+
kept[k] = e[k];
|
|
59
|
+
}
|
|
60
|
+
out.push(kept);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Reduce a parsed Cowork session object to the whitelisted, hashed shape.
|
|
66
|
+
* Returns `null` when nothing survives (e.g. a non-object or a file that
|
|
67
|
+
* failed to parse to the expected shape) so the caller can drop the entry
|
|
68
|
+
* rather than emit an empty row — and, critically, never emit raw content.
|
|
69
|
+
*/
|
|
70
|
+
export function applyCoworkSessionWhitelist(raw) {
|
|
71
|
+
if (raw === null || raw === undefined || typeof raw !== 'object')
|
|
72
|
+
return null;
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const key of COWORK_SESSION_ALLOWED_KEYS) {
|
|
75
|
+
if (!(key in raw))
|
|
76
|
+
continue;
|
|
77
|
+
if (HASHED_KEYS.has(key)) {
|
|
78
|
+
const v = raw[key];
|
|
79
|
+
out[key] = v === null || v === undefined || v === '' ? '' : sha256Hex(v);
|
|
80
|
+
}
|
|
81
|
+
else if (key === 'remoteMcpServersConfig') {
|
|
82
|
+
out[key] = sanitizeRemoteServers(raw[key]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
out[key] = raw[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
89
|
+
}
|
|
@@ -105,10 +105,45 @@ function isEphemeralCursorProjectDir(name) {
|
|
|
105
105
|
function isInternalCursorWorkspaceSlug(slug) {
|
|
106
106
|
return normalizeSlugPart(slug) === 'empty-window';
|
|
107
107
|
}
|
|
108
|
+
function readMcpToolCache(projectPath) {
|
|
109
|
+
const cachePath = join(projectPath, 'mcp-cache.json');
|
|
110
|
+
if (!existsSync(cachePath))
|
|
111
|
+
return null;
|
|
112
|
+
const cache = readJSONFile(cachePath);
|
|
113
|
+
if (cache && typeof cache === 'object' && !Array.isArray(cache)) {
|
|
114
|
+
return cache;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/** Tool schemas from mcp-cache.json for a connected mcps/ server (never creates inventory rows). */
|
|
119
|
+
function cursorMcpCacheToolsForServer(cache, label, serverDirName) {
|
|
120
|
+
if (!cache)
|
|
121
|
+
return undefined;
|
|
122
|
+
const aliases = new Set();
|
|
123
|
+
const addAlias = (s) => {
|
|
124
|
+
const t = s.trim();
|
|
125
|
+
if (t)
|
|
126
|
+
aliases.add(t.toLowerCase());
|
|
127
|
+
};
|
|
128
|
+
addAlias(label);
|
|
129
|
+
addAlias(serverDirName);
|
|
130
|
+
if (serverDirName.startsWith('user-'))
|
|
131
|
+
addAlias(serverDirName.slice('user-'.length));
|
|
132
|
+
else
|
|
133
|
+
addAlias(`user-${label}`);
|
|
134
|
+
for (const [key, val] of Object.entries(cache)) {
|
|
135
|
+
if (!key.trim() || !aliases.has(key.trim().toLowerCase()))
|
|
136
|
+
continue;
|
|
137
|
+
if (val && typeof val === 'object' && Array.isArray(val.tools)) {
|
|
138
|
+
return val.tools;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
108
143
|
/**
|
|
109
144
|
* Cursor stores per-workspace MCP state under ~/.cursor/projects/<slug>/ (mcp-cache.json, mcps/*).
|
|
110
145
|
* This is not the repo file at <repo>/.cursor/mcp.json. Upload one mcp_config per slug with
|
|
111
|
-
*
|
|
146
|
+
* mcpServers from mcps/ only; mcp-cache.json supplies supplementary tool schemas when names match.
|
|
112
147
|
*/
|
|
113
148
|
function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constants, workspaceVscdbSpec, home = homedir()) {
|
|
114
149
|
const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
|
|
@@ -136,18 +171,7 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
|
|
|
136
171
|
continue;
|
|
137
172
|
const mcpServers = {};
|
|
138
173
|
const cachePath = join(projectPath, 'mcp-cache.json');
|
|
139
|
-
|
|
140
|
-
const cache = readJSONFile(cachePath);
|
|
141
|
-
if (cache && typeof cache === 'object' && !Array.isArray(cache)) {
|
|
142
|
-
for (const [name, val] of Object.entries(cache)) {
|
|
143
|
-
if (!name.trim())
|
|
144
|
-
continue;
|
|
145
|
-
if (val && typeof val === 'object' && Array.isArray(val.tools)) {
|
|
146
|
-
mcpServers[name] = { cursor_source: 'mcp-cache.json' };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
174
|
+
const mcpToolCache = readMcpToolCache(projectPath);
|
|
151
175
|
const mcpsPath = join(projectPath, 'mcps');
|
|
152
176
|
if (existsSync(mcpsPath)) {
|
|
153
177
|
try {
|
|
@@ -158,12 +182,17 @@ function collectCursorProjectWorkspaceMcpConfigs(pathSkipPrefixes = [], constant
|
|
|
158
182
|
const label = mcpServerLabelFromMcpsDir(serverDir.name, metaObj);
|
|
159
183
|
if (!label)
|
|
160
184
|
continue;
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
if (label in mcpServers)
|
|
186
|
+
continue;
|
|
187
|
+
const entry = {
|
|
188
|
+
cursor_source: 'mcps',
|
|
189
|
+
server_identifier: serverDir.name,
|
|
190
|
+
};
|
|
191
|
+
const cacheTools = cursorMcpCacheToolsForServer(mcpToolCache, label, serverDir.name);
|
|
192
|
+
if (cacheTools !== undefined) {
|
|
193
|
+
entry.cursor_mcp_cache_tools = cacheTools;
|
|
166
194
|
}
|
|
195
|
+
mcpServers[label] = entry;
|
|
167
196
|
}
|
|
168
197
|
}
|
|
169
198
|
catch {
|