log-llm-config-staging 1.3.44
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 +46 -0
- package/dist/apply_deferred_vscdb.js +8 -0
- package/dist/bootstrap_constants.js +5 -0
- package/dist/cli/bash_script_generator.js +95 -0
- package/dist/cli.js +103 -0
- package/dist/cli_invocation_match.js +28 -0
- package/dist/compliance_check_runner.js +17 -0
- package/dist/compliance_prompt_gate.js +197 -0
- package/dist/endpoint_client/http_transport.js +88 -0
- package/dist/endpoint_client/index.js +3 -0
- package/dist/endpoint_client/registry_api.js +41 -0
- package/dist/endpoint_client/startup_api.js +43 -0
- package/dist/endpoint_client/types.js +4 -0
- package/dist/execute_trusted_restarts.js +54 -0
- package/dist/log_config_files/auth/auth_flow.js +22 -0
- package/dist/log_config_files/auth/auth_key_store.js +14 -0
- package/dist/log_config_files/collection/config_collector.js +160 -0
- package/dist/log_config_files/collection/directory_collector.js +96 -0
- package/dist/log_config_files/collection/enrichment_helpers.js +53 -0
- package/dist/log_config_files/collection/file_type_rules.js +47 -0
- package/dist/log_config_files/collection/mcp_tool_collector.js +37 -0
- package/dist/log_config_files/collection/openclaw_helpers.js +55 -0
- package/dist/log_config_files/collection/plugin_collector.js +89 -0
- package/dist/log_config_files/collection/plugin_version_helpers.js +37 -0
- package/dist/log_config_files/index.js +19 -0
- package/dist/log_config_files/paths/path_constants_helpers.js +71 -0
- package/dist/log_config_files/paths/pattern_resolver.js +227 -0
- package/dist/log_config_files/readers/file_readers.js +69 -0
- package/dist/log_config_files/readers/vscdb_config_builder.js +146 -0
- package/dist/log_config_files/readers/vscdb_reader.js +247 -0
- package/dist/log_config_files/runtime/compliance_check.js +518 -0
- package/dist/log_config_files/runtime/hardware_uuid.js +36 -0
- package/dist/log_config_files/runtime/hook_logger.js +197 -0
- package/dist/log_config_files/runtime/main_runner.js +192 -0
- package/dist/log_config_files/runtime/management_storage.js +82 -0
- package/dist/log_config_files/runtime/remediation_config_path.js +90 -0
- package/dist/log_config_files/runtime/remediation_sync.js +1290 -0
- package/dist/log_config_files/runtime/sqlite_binary.js +92 -0
- package/dist/log_config_files/runtime/trusted_restarts.js +52 -0
- package/dist/log_config_files/sender/batch_sender.js +220 -0
- package/dist/log_config_files/sender/endpoint_config.js +24 -0
- package/dist/log_config_files/sender/signing.js +1 -0
- package/dist/log_sensitive_paths_audit.js +97 -0
- package/dist/log_uuid/auth_key_store.js +71 -0
- package/dist/log_uuid/hardware_uuid.js +35 -0
- package/dist/log_uuid/index.js +11 -0
- package/dist/log_uuid/log_uuid_helper.js +30 -0
- package/dist/log_uuid/startup_sender.js +74 -0
- package/dist/log_uuid/user_profile.js +178 -0
- package/dist/types/config_file_types.js +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
4
|
+
const HOOK_LOG_FILENAME = 'hook_log.txt';
|
|
5
|
+
const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
|
|
6
|
+
/** Append-only: remediation verify/apply failures (not cleared per hook session). */
|
|
7
|
+
const REMEDIATION_APPLY_FAILURES_FILENAME = 'remediation_apply_failures.log';
|
|
8
|
+
/** Hard cap so a single upload/sync session cannot grow hook_log.txt without bound. */
|
|
9
|
+
const MAX_HOOK_LOG_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
function getHookLogPath() {
|
|
11
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
12
|
+
if (!homeDir)
|
|
13
|
+
return null;
|
|
14
|
+
return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, HOOK_LOG_FILENAME);
|
|
15
|
+
}
|
|
16
|
+
function getComplianceRunnerLogPath() {
|
|
17
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
18
|
+
if (!homeDir)
|
|
19
|
+
return null;
|
|
20
|
+
return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_RUNNER_LOG_FILENAME);
|
|
21
|
+
}
|
|
22
|
+
function getRemediationApplyFailuresLogPath() {
|
|
23
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
24
|
+
if (!homeDir)
|
|
25
|
+
return null;
|
|
26
|
+
return path.join(homeDir, OPT_AI_SEC_MANAGEMENT_REL, REMEDIATION_APPLY_FAILURES_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Append one ISO-timestamped line to compliance_runner.log.
|
|
30
|
+
* `level` examples: INFO, RUNNER, RESTART, FAIL
|
|
31
|
+
*/
|
|
32
|
+
function appendComplianceRunnerLine(level, message) {
|
|
33
|
+
const logPath = getComplianceRunnerLogPath();
|
|
34
|
+
if (!logPath)
|
|
35
|
+
return;
|
|
36
|
+
try {
|
|
37
|
+
const dir = path.dirname(logPath);
|
|
38
|
+
if (!existsSync(dir))
|
|
39
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
const ts = new Date().toISOString();
|
|
41
|
+
appendFileSync(logPath, `${ts} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// best-effort
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Append-only diagnostics for remediation sync / compliance (structured).
|
|
49
|
+
*/
|
|
50
|
+
function complianceRunnerDiag(message) {
|
|
51
|
+
appendComplianceRunnerLine('INFO', message);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Background compliance_check_runner should use this so entries match the same format.
|
|
55
|
+
*/
|
|
56
|
+
function complianceRunnerRunnerLine(message) {
|
|
57
|
+
appendComplianceRunnerLine('RUNNER', message);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Loud, append-only record when a remediation cannot be verified or applied. Writes
|
|
61
|
+
* {@link REMEDIATION_APPLY_FAILURES_FILENAME} and mirrors the same block to compliance_runner.log;
|
|
62
|
+
* also appends a single summary line to hook_log.txt for the current session.
|
|
63
|
+
*/
|
|
64
|
+
function logRemediationApplyFailure(context, fields) {
|
|
65
|
+
const ts = new Date().toISOString();
|
|
66
|
+
const bodyLines = Object.entries(fields)
|
|
67
|
+
.filter(([, v]) => v !== undefined && v !== null && String(v) !== '')
|
|
68
|
+
.map(([k, v]) => ` ${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`);
|
|
69
|
+
const block = [
|
|
70
|
+
'',
|
|
71
|
+
'='.repeat(72),
|
|
72
|
+
`${ts} REMEDIATION APPLY FAILURE — ${context}`,
|
|
73
|
+
...bodyLines,
|
|
74
|
+
'='.repeat(72),
|
|
75
|
+
'',
|
|
76
|
+
].join('\n');
|
|
77
|
+
const summary = fields.reason != null
|
|
78
|
+
? String(fields.reason)
|
|
79
|
+
: fields.message != null
|
|
80
|
+
? String(fields.message)
|
|
81
|
+
: 'see fields above';
|
|
82
|
+
const writeFailAppend = (filePath) => {
|
|
83
|
+
const dir = path.dirname(filePath);
|
|
84
|
+
if (!existsSync(dir))
|
|
85
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
86
|
+
appendFileSync(filePath, block, 'utf8');
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
const failPath = getRemediationApplyFailuresLogPath();
|
|
90
|
+
if (failPath)
|
|
91
|
+
writeFailAppend(failPath);
|
|
92
|
+
const crPath = getComplianceRunnerLogPath();
|
|
93
|
+
if (crPath) {
|
|
94
|
+
const dir = path.dirname(crPath);
|
|
95
|
+
if (!existsSync(dir))
|
|
96
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
97
|
+
appendFileSync(crPath, block, 'utf8');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* best-effort */
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
hookRunLog(`REMEDIATION_APPLY_FAILURE [${context}] ${fields.uuid != null ? `uuid=${fields.uuid} ` : ''}${summary}`);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* best-effort */
|
|
108
|
+
}
|
|
109
|
+
appendComplianceRunnerLine('FAIL', `${context} — ${summary}`);
|
|
110
|
+
}
|
|
111
|
+
function ensureHookLogUnderCap() {
|
|
112
|
+
const logPath = getHookLogPath();
|
|
113
|
+
if (!logPath || !existsSync(logPath))
|
|
114
|
+
return;
|
|
115
|
+
try {
|
|
116
|
+
if (statSync(logPath).size <= MAX_HOOK_LOG_BYTES)
|
|
117
|
+
return;
|
|
118
|
+
const ts = new Date().toISOString();
|
|
119
|
+
writeFileSync(logPath, `${'='.repeat(72)}\n${ts} hook_log.txt truncated (exceeded ${MAX_HOOK_LOG_BYTES} bytes)\n${'='.repeat(72)}\n`, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// best-effort
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Start a new hook_log.txt session (replaces the file). Use once per logical run
|
|
127
|
+
* (e.g. compliance_prompt_gate, log_config_files upload via hookLogReplace).
|
|
128
|
+
*/
|
|
129
|
+
function hookLogSessionBanner(label) {
|
|
130
|
+
const logPath = getHookLogPath();
|
|
131
|
+
if (!logPath)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
const dir = path.dirname(logPath);
|
|
135
|
+
if (!existsSync(dir))
|
|
136
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
137
|
+
const ts = new Date().toISOString();
|
|
138
|
+
const banner = `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`;
|
|
139
|
+
writeFileSync(logPath, banner, 'utf8');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// best-effort
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Append a subsection after an existing session (e.g. compliance_check_runner after the gate)
|
|
147
|
+
* without replacing hook_log.txt.
|
|
148
|
+
*/
|
|
149
|
+
function hookLogAppendSection(label) {
|
|
150
|
+
const logPath = getHookLogPath();
|
|
151
|
+
if (!logPath)
|
|
152
|
+
return;
|
|
153
|
+
try {
|
|
154
|
+
ensureHookLogUnderCap();
|
|
155
|
+
const dir = path.dirname(logPath);
|
|
156
|
+
if (!existsSync(dir))
|
|
157
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
158
|
+
const ts = new Date().toISOString();
|
|
159
|
+
const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
|
|
160
|
+
appendFileSync(logPath, banner, 'utf8');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// best-effort
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Begins a log_config_files upload session (replaces hook_log.txt, same as hookLogSessionBanner). */
|
|
167
|
+
function hookLogReplace() {
|
|
168
|
+
hookLogSessionBanner('log_config_files (config upload)');
|
|
169
|
+
}
|
|
170
|
+
/** Append a timestamped line to hook_log.txt. */
|
|
171
|
+
function hookRunLog(message) {
|
|
172
|
+
const logPath = getHookLogPath();
|
|
173
|
+
if (!logPath)
|
|
174
|
+
return;
|
|
175
|
+
try {
|
|
176
|
+
ensureHookLogUnderCap();
|
|
177
|
+
const ts = new Date().toISOString();
|
|
178
|
+
appendFileSync(logPath, `${ts} ${message}\n`, 'utf8');
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// best-effort; don't break the run
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Append a line without timestamp (for multi-line blocks like manifests). */
|
|
185
|
+
function hookLogLine(message) {
|
|
186
|
+
const logPath = getHookLogPath();
|
|
187
|
+
if (!logPath)
|
|
188
|
+
return;
|
|
189
|
+
try {
|
|
190
|
+
ensureHookLogUnderCap();
|
|
191
|
+
appendFileSync(logPath, `${message}\n`, 'utf8');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// best-effort
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export { getHookLogPath, getComplianceRunnerLogPath, hookLogReplace, hookLogSessionBanner, hookLogAppendSection, hookRunLog, hookLogLine, complianceRunnerDiag, complianceRunnerRunnerLine, appendComplianceRunnerLine, logRemediationApplyFailure, };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { getFileCollectionPatterns, FILE_PATH_REGISTRY_FILE_PATTERNS_PATH } from '../../endpoint_client/index.js';
|
|
5
|
+
import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
6
|
+
import { runSensitivePathsAudit } from '../../log_sensitive_paths_audit.js';
|
|
7
|
+
import { loadEndpointBase, getEndpointSource } from '../sender/endpoint_config.js';
|
|
8
|
+
import { hookLogReplace, hookRunLog } from './hook_logger.js';
|
|
9
|
+
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
10
|
+
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
11
|
+
import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
12
|
+
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
13
|
+
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
14
|
+
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
|
|
15
|
+
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
16
|
+
import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
|
|
17
|
+
import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
|
|
18
|
+
import { createSignature, canonicalizePayload } from '../sender/signing.js';
|
|
19
|
+
const PROJECT_ROOT = process.cwd();
|
|
20
|
+
async function collectAllConfigFiles(endpointBase) {
|
|
21
|
+
const patternsUrl = `${endpointBase.replace(/\/+$/, '')}${FILE_PATH_REGISTRY_FILE_PATTERNS_PATH}`;
|
|
22
|
+
hookRunLog(`fetching patterns from ${patternsUrl}`);
|
|
23
|
+
const patternsResponse = await getFileCollectionPatterns(endpointBase);
|
|
24
|
+
const n = patternsResponse?.patterns?.length ?? 0;
|
|
25
|
+
hookRunLog(`patterns: ${n} pattern(s) received`);
|
|
26
|
+
if (!patternsResponse?.patterns?.length) {
|
|
27
|
+
const msg = 'File collection requires patterns from the API; none received. Check endpoint and file-path-registry.';
|
|
28
|
+
hookRunLog(msg);
|
|
29
|
+
throw new Error(msg);
|
|
30
|
+
}
|
|
31
|
+
persistVscdbComposerContractFromPatternsResponse(patternsResponse);
|
|
32
|
+
hookRunLog(`scanning config files`);
|
|
33
|
+
const configFiles = collectConfigFilesFromPatterns(patternsResponse.patterns, PROJECT_ROOT, hookRunLog, {
|
|
34
|
+
vscdbEntrySpecs: patternsResponse.vscdb_entry_specs,
|
|
35
|
+
vscdb_read_queries: patternsResponse.vscdb_read_queries,
|
|
36
|
+
vscdb_merge_from_composer_state: patternsResponse.vscdb_merge_from_composer_state,
|
|
37
|
+
client_path_constants: patternsResponse.client_path_constants,
|
|
38
|
+
path_resolution_specs: patternsResponse.path_resolution_specs,
|
|
39
|
+
home_recurse_skip_dirs: patternsResponse.home_recurse_skip_dirs,
|
|
40
|
+
absolute_path_prefixes: patternsResponse.absolute_path_prefixes,
|
|
41
|
+
mcp_tool_glob_spec: patternsResponse.mcp_tool_glob_spec ?? null,
|
|
42
|
+
});
|
|
43
|
+
hookRunLog(`scanning MCP tool cache`);
|
|
44
|
+
const mcpToolGlobPattern = patternsResponse.patterns.find((p) => p.path_resolution === 'mcp_tool_glob');
|
|
45
|
+
const mcpToolSkipPrefixes = normalizePathSkipPrefixes(mcpToolGlobPattern?.path_skip_prefixes);
|
|
46
|
+
const existingPaths = new Set(configFiles.map((c) => `${c.file_type}\t${c.file_path}`));
|
|
47
|
+
for (const m of collectMcpToolFiles(mcpToolSkipPrefixes, patternsResponse.client_path_constants)) {
|
|
48
|
+
const key = `${m.file_type}\t${m.file_path}`;
|
|
49
|
+
if (!existingPaths.has(key)) {
|
|
50
|
+
existingPaths.add(key);
|
|
51
|
+
configFiles.push(m);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
hookRunLog(`scanning installed plugins`);
|
|
55
|
+
if (!patternsResponse.client_path_constants)
|
|
56
|
+
throw new Error('client_path_constants required from API response');
|
|
57
|
+
configFiles.push(...collectConfigFilesFromInstalledPlugins(patternsResponse.client_path_constants));
|
|
58
|
+
return configFiles;
|
|
59
|
+
}
|
|
60
|
+
async function addSensitivePathsAudit(endpointBase, configFiles) {
|
|
61
|
+
hookRunLog(`running sensitive paths audit`);
|
|
62
|
+
const auditOutputDir = join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
|
|
63
|
+
try {
|
|
64
|
+
const auditResult = await runSensitivePathsAudit(endpointBase, auditOutputDir, PROJECT_ROOT);
|
|
65
|
+
hookRunLog(`sensitive_paths_audit: ${auditResult.paths.length} path(s)`);
|
|
66
|
+
configFiles.push({ file_type: 'sensitive_paths_audit', file_path: 'sensitive_paths_audit.txt', raw_content: { paths: auditResult.paths } });
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
hookRunLog(`sensitive_paths_audit_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
70
|
+
configFiles.push({ file_type: 'sensitive_paths_audit', file_path: 'sensitive_paths_audit.txt', raw_content: { paths: [] } });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function sendAllConfigFiles(configFiles, hardwareUuid, authKey) {
|
|
74
|
+
const hookTypeRaw = (process.env.OPTIMUS_HOOK_TYPE || 'claude').toLowerCase();
|
|
75
|
+
const hookType = hookTypeRaw === 'cursor' ? 'cursor' : 'claude';
|
|
76
|
+
const workspaceRepo = process.env.OPTIMUS_WORKSPACE_REPO || process.env.GITHUB_REPOSITORY || '';
|
|
77
|
+
const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
|
|
78
|
+
const hookRequestId = await sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo);
|
|
79
|
+
hookRunLog(`hook-request id=${hookRequestId ?? 'none'}`);
|
|
80
|
+
const ingestSessionId = await sendIngestSessionStart(hardwareUuid, authKey);
|
|
81
|
+
hookRunLog(`ingest-session id=${ingestSessionId ?? 'none'}`);
|
|
82
|
+
const estimatedRequests = Math.ceil(configFiles.length / BATCH_CHUNK_SIZE);
|
|
83
|
+
hookRunLog(`config send: batch ${configFiles.length} file(s) in ~${estimatedRequests} request(s)${hookRequestId != null ? ` hook_request_id=${hookRequestId}` : ''}${ingestSessionId ? ` ingest_session_id=${ingestSessionId}` : ''}`);
|
|
84
|
+
const batchResult = await sendConfigFilesBatch(configFiles, hardwareUuid, authKey, hookRequestId ?? undefined, ingestSessionId ?? undefined);
|
|
85
|
+
hookRunLog(`config files ${batchResult.accepted}/${configFiles.length} logged (batch)`);
|
|
86
|
+
if (batchResult.failed > 0)
|
|
87
|
+
hookRunLog(`config_failed: ${batchResult.failed} item(s) in batch`);
|
|
88
|
+
if (hookRequestId != null) {
|
|
89
|
+
const ok = await sendHookRequestUpdateManifest(hardwareUuid, authKey, hookRequestId, manifest);
|
|
90
|
+
hookRunLog(`hook-request update manifest result=${ok ? 'ok' : 'fail'}`);
|
|
91
|
+
}
|
|
92
|
+
if (ingestSessionId && batchResult.accepted > 0) {
|
|
93
|
+
const ok = await sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId);
|
|
94
|
+
hookRunLog(`ingest-session finish result=${ok ? 'ok' : 'fail'}`);
|
|
95
|
+
}
|
|
96
|
+
// Exit 0 if anything was persisted so the shell hook keeps last_log and throttles. Exit 1 only when
|
|
97
|
+
// every item failed (e.g. SQLite locked on server) — otherwise partial success used to return 1,
|
|
98
|
+
// the hook deleted last_log, and the next prompt re-uploaded all files every time.
|
|
99
|
+
const exitCode = batchResult.accepted > 0 ? 0 : 1;
|
|
100
|
+
hookRunLog(`summary: exit=${exitCode} collected=${configFiles.length} sent=${batchResult.accepted} failed=${configFiles.length - batchResult.accepted} hook_id=${hookRequestId ?? 'n/a'}`);
|
|
101
|
+
hookRunLog(`done`);
|
|
102
|
+
process.exit(exitCode);
|
|
103
|
+
}
|
|
104
|
+
async function main() {
|
|
105
|
+
hookLogReplace();
|
|
106
|
+
const hardwareUuid = resolveHardwareUuid();
|
|
107
|
+
const endpointBase = loadEndpointBase();
|
|
108
|
+
hookRunLog(`start endpoint=${endpointBase} hardware_uuid=${hardwareUuid}`);
|
|
109
|
+
hookRunLog(`endpoint_source=${getEndpointSource()} cwd=${process.cwd()}`);
|
|
110
|
+
hookRunLog(`env: OPTIMUS_HOOK_TYPE=${process.env.OPTIMUS_HOOK_TYPE ? 'set' : 'unset'} GITHUB_REPOSITORY=${process.env.GITHUB_REPOSITORY ? 'set' : 'unset'} OPTIMUS_ENDPOINT=${process.env.OPTIMUS_ENDPOINT ? 'set' : 'unset'}`);
|
|
111
|
+
let authKey;
|
|
112
|
+
try {
|
|
113
|
+
authKey = await ensureAuthentication(hardwareUuid);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
hookRunLog(`auth: failed ${err instanceof Error ? err.message : String(err)}`);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
let configFiles;
|
|
120
|
+
try {
|
|
121
|
+
configFiles = await collectAllConfigFiles(endpointBase);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
hookRunLog(`patterns_error: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
await addSensitivePathsAudit(endpointBase, configFiles);
|
|
128
|
+
const fileTypes = [...new Set(configFiles.map((c) => c.file_type))].join(', ');
|
|
129
|
+
hookRunLog(`collected ${configFiles.length} config file(s) file_types=${fileTypes}`);
|
|
130
|
+
for (const c of configFiles) {
|
|
131
|
+
if (!c.file_path.replace(/\\/g, '/').includes('#composerState'))
|
|
132
|
+
continue;
|
|
133
|
+
const tail = c.file_path.length > 120 ? `…${c.file_path.slice(-120)}` : c.file_path;
|
|
134
|
+
hookRunLog(`diag pre-upload #composerState ${summarizeComposerPayloadForDiagnostics(c.raw_content)} path=${tail}`);
|
|
135
|
+
}
|
|
136
|
+
const claudeSettings = configFiles.filter((c) => c.file_type === 'claude_settings');
|
|
137
|
+
if (claudeSettings.length > 0)
|
|
138
|
+
hookRunLog(`claude_settings in batch: ${claudeSettings.length} path(s): ${claudeSettings.map((c) => c.file_path).join(', ')}`);
|
|
139
|
+
else
|
|
140
|
+
hookRunLog(`claude_settings in batch: 0 (none collected)`);
|
|
141
|
+
if (configFiles.length === 0) {
|
|
142
|
+
hookRunLog('no config files found, exiting');
|
|
143
|
+
process.exit(0);
|
|
144
|
+
}
|
|
145
|
+
await sendAllConfigFiles(configFiles, hardwareUuid, authKey);
|
|
146
|
+
}
|
|
147
|
+
async function logSingleFile(filePath) {
|
|
148
|
+
const hardwareUuid = resolveHardwareUuid();
|
|
149
|
+
const authKey = await ensureAuthentication(hardwareUuid);
|
|
150
|
+
const patternsResponse = await getFileCollectionPatterns(loadEndpointBase());
|
|
151
|
+
const isVscdb = isVscdbVirtualPath(filePath, patternsResponse ?? null);
|
|
152
|
+
if (!existsSync(filePath) && !isVscdb) {
|
|
153
|
+
console.error(`File not found: ${filePath}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
const patterns = patternsResponse?.patterns ?? [];
|
|
157
|
+
let rawContent = null;
|
|
158
|
+
let fileType = determineFileTypeFromPath(filePath, patterns) ?? undefined;
|
|
159
|
+
if (isVscdb) {
|
|
160
|
+
const vscdbResult = tryReadVscdbVirtualFile(filePath, patternsResponse ?? null);
|
|
161
|
+
if (!vscdbResult) {
|
|
162
|
+
console.error(`Could not read vscdb virtual file: ${filePath}`);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
rawContent = vscdbResult.rawContent;
|
|
166
|
+
fileType = vscdbResult.fileType;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
rawContent = readJSONFile(filePath);
|
|
170
|
+
if (!rawContent) {
|
|
171
|
+
const markdown = readMarkdownFile(filePath);
|
|
172
|
+
if (markdown !== null) {
|
|
173
|
+
rawContent = { content: markdown, source: 'file' };
|
|
174
|
+
if (!fileType)
|
|
175
|
+
fileType = determineFileTypeFromPath(filePath, patterns) ?? undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!rawContent) {
|
|
180
|
+
console.error(`Could not read file: ${filePath}`);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (!fileType) {
|
|
184
|
+
fileType = determineFileTypeFromPath(filePath, patterns) ?? undefined;
|
|
185
|
+
if (!fileType) {
|
|
186
|
+
console.error(`Could not determine file type for: ${filePath}`);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return sendConfigFile({ file_type: fileType, file_path: filePath, raw_content: rawContent }, hardwareUuid, authKey);
|
|
191
|
+
}
|
|
192
|
+
export { main, logSingleFile, createSignature, canonicalizePayload, sendConfigFile, sendConfigFilesBatch, BATCH_CHUNK_SIZE };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths and JSON persistence for ~/opt-ai-sec/management:
|
|
3
|
+
* - remediation_instructions.json (remediations[])
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
9
|
+
export const REMEDIATION_INSTRUCTIONS_BASENAME = 'remediation_instructions.json';
|
|
10
|
+
/** Pending state.vscdb writes applied after Cursor exits (IDE holds the DB locked). */
|
|
11
|
+
export const DEFERRED_VSCDB_APPLY_BASENAME = 'optimus_deferred_vscdb_apply.json';
|
|
12
|
+
/** Shell stdout/stderr from the trusted deferred restart pipeline (apply_deferred_vscdb + open Cursor). */
|
|
13
|
+
export const DEFERRED_VSCDB_RESTART_LOG_BASENAME = 'deferred_vscdb_restart.log';
|
|
14
|
+
/**
|
|
15
|
+
* Cached subset of GET file-patterns: reactive ItemTable key + composer shadow keys from the backend.
|
|
16
|
+
* Written when patterns are fetched; remediations and vscdb reads use this so the npm package stays free
|
|
17
|
+
* of hardcoded Cursor paths.
|
|
18
|
+
*/
|
|
19
|
+
export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
|
|
20
|
+
export function getManagementDir() {
|
|
21
|
+
return join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
|
|
22
|
+
}
|
|
23
|
+
export function getRemediationInstructionsPath() {
|
|
24
|
+
return join(getManagementDir(), REMEDIATION_INSTRUCTIONS_BASENAME);
|
|
25
|
+
}
|
|
26
|
+
export function getDeferredVscdbApplyPath() {
|
|
27
|
+
return join(getManagementDir(), DEFERRED_VSCDB_APPLY_BASENAME);
|
|
28
|
+
}
|
|
29
|
+
export function getDeferredVscdbRestartLogPath() {
|
|
30
|
+
return join(getManagementDir(), DEFERRED_VSCDB_RESTART_LOG_BASENAME);
|
|
31
|
+
}
|
|
32
|
+
export function getFileCollectionVscdbContractPath() {
|
|
33
|
+
return join(getManagementDir(), FILE_COLLECTION_VSCDB_CONTRACT_BASENAME);
|
|
34
|
+
}
|
|
35
|
+
export function readFileCollectionVscdbContract() {
|
|
36
|
+
const path = getFileCollectionVscdbContractPath();
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
+
if (parsed?.version !== 1 || !Array.isArray(parsed.composer_shadow_keys))
|
|
42
|
+
return null;
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function writeFileCollectionVscdbContract(contract) {
|
|
50
|
+
atomicWriteJson(getFileCollectionVscdbContractPath(), contract);
|
|
51
|
+
}
|
|
52
|
+
export function atomicWriteJson(filePath, data) {
|
|
53
|
+
const dir = dirname(filePath);
|
|
54
|
+
if (!existsSync(dir))
|
|
55
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
56
|
+
const tmp = `${filePath}.tmp`;
|
|
57
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
58
|
+
renameSync(tmp, filePath);
|
|
59
|
+
}
|
|
60
|
+
function parseInstructionsRaw(raw) {
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(raw);
|
|
63
|
+
return Array.isArray(parsed.remediations) ? parsed : { remediations: [] };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { remediations: [] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function writeRemediationInstructionsFile(file) {
|
|
70
|
+
atomicWriteJson(getRemediationInstructionsPath(), file);
|
|
71
|
+
}
|
|
72
|
+
export function readRemediationInstructionsFile() {
|
|
73
|
+
const path = getRemediationInstructionsPath();
|
|
74
|
+
if (!existsSync(path))
|
|
75
|
+
return { remediations: [] };
|
|
76
|
+
try {
|
|
77
|
+
return parseInstructionsRaw(readFileSync(path, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { remediations: [] };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Portable key stored by the server for Cursor User globalStorage state.vscdb
|
|
6
|
+
* (see optimus_security/endpoint/log_config_file/handler._canonical_cursor_user_state_vscdb_path).
|
|
7
|
+
*/
|
|
8
|
+
export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state.vscdb';
|
|
9
|
+
function normalizeSlashes(p) {
|
|
10
|
+
return p.trim().replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Canonical `file_path` for Cursor User globalStorage state.vscdb uploads, matching
|
|
14
|
+
* `optimus_security.endpoint.log_config_file.handler._canonical_cursor_user_state_vscdb_path`.
|
|
15
|
+
* Always use before sending log-config-file(s) so findings dedupe regardless of absolute vs portable input.
|
|
16
|
+
*/
|
|
17
|
+
export function canonicalCursorUserStateVscdbPath(filePath) {
|
|
18
|
+
const s = normalizeSlashes(filePath);
|
|
19
|
+
const lower = s.toLowerCase();
|
|
20
|
+
const needle = 'cursor/user/globalstorage/state.vscdb';
|
|
21
|
+
const idx = lower.indexOf(needle);
|
|
22
|
+
if (idx >= 0) {
|
|
23
|
+
const tail = s.slice(idx + needle.length);
|
|
24
|
+
if (tail.startsWith('#')) {
|
|
25
|
+
return `${PORTABLE_CURSOR_USER_STATE_VSCDB}${tail}`;
|
|
26
|
+
}
|
|
27
|
+
if (tail === '') {
|
|
28
|
+
return PORTABLE_CURSOR_USER_STATE_VSCDB;
|
|
29
|
+
}
|
|
30
|
+
return filePath.trim();
|
|
31
|
+
}
|
|
32
|
+
const vscdb = 'state.vscdb';
|
|
33
|
+
const vlen = vscdb.length;
|
|
34
|
+
const slashKey = '/' + vscdb;
|
|
35
|
+
let start;
|
|
36
|
+
const pos = lower.lastIndexOf(slashKey);
|
|
37
|
+
if (pos >= 0) {
|
|
38
|
+
start = pos + 1;
|
|
39
|
+
}
|
|
40
|
+
else if (lower.startsWith(vscdb)) {
|
|
41
|
+
start = 0;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
return filePath.trim();
|
|
45
|
+
}
|
|
46
|
+
const tail = s.slice(start + vlen);
|
|
47
|
+
if (tail.startsWith('#') || tail === '') {
|
|
48
|
+
return `${PORTABLE_CURSOR_USER_STATE_VSCDB}${tail}`;
|
|
49
|
+
}
|
|
50
|
+
return filePath.trim();
|
|
51
|
+
}
|
|
52
|
+
function cursorStateVscdbAbsoluteBasePaths() {
|
|
53
|
+
const h = homedir();
|
|
54
|
+
if (process.platform === 'darwin') {
|
|
55
|
+
const support = join(h, 'Library', 'Application Support');
|
|
56
|
+
const variants = ['Cursor', 'Cursor - Insiders', 'Cursor Nightly', 'Cursor Next'];
|
|
57
|
+
return variants.map((name) => join(support, name, 'User', 'globalStorage', 'state.vscdb'));
|
|
58
|
+
}
|
|
59
|
+
if (process.platform === 'win32') {
|
|
60
|
+
const appData = process.env.APPDATA;
|
|
61
|
+
if (appData)
|
|
62
|
+
return [join(appData, 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
63
|
+
return [join(h, 'AppData', 'Roaming', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
64
|
+
}
|
|
65
|
+
return [join(h, '.config', 'Cursor', 'User', 'globalStorage', 'state.vscdb')];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Expand server-side portable Cursor state.vscdb paths to a local absolute path.
|
|
69
|
+
* No-op for absolute paths and for relative paths that are not the portable vscdb key.
|
|
70
|
+
*/
|
|
71
|
+
export function resolveRemediationConfigPath(configFilePath) {
|
|
72
|
+
const t = normalizeSlashes(configFilePath);
|
|
73
|
+
const hash = t.indexOf('#');
|
|
74
|
+
const base = hash >= 0 ? t.slice(0, hash) : t;
|
|
75
|
+
const frag = hash >= 0 ? t.slice(hash) : '';
|
|
76
|
+
const baseNorm = base.replace(/\/+$/, '');
|
|
77
|
+
if (baseNorm.startsWith('/') || /^[a-zA-Z]:\//.test(baseNorm)) {
|
|
78
|
+
return configFilePath;
|
|
79
|
+
}
|
|
80
|
+
const portable = PORTABLE_CURSOR_USER_STATE_VSCDB;
|
|
81
|
+
if (baseNorm !== portable && !baseNorm.endsWith('/' + portable)) {
|
|
82
|
+
return configFilePath;
|
|
83
|
+
}
|
|
84
|
+
const candidates = cursorStateVscdbAbsoluteBasePaths();
|
|
85
|
+
for (const abs of candidates) {
|
|
86
|
+
if (existsSync(abs))
|
|
87
|
+
return `${abs}${frag}`;
|
|
88
|
+
}
|
|
89
|
+
return `${candidates[0]}${frag}`;
|
|
90
|
+
}
|