log-llm-config 1.3.20 → 1.3.22
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/log_config_files/readers/vscdb_reader.js +6 -0
- package/dist/log_config_files/runtime/main_runner.js +2 -1
- package/dist/log_config_files/runtime/remediation_config_path.js +40 -0
- package/dist/log_config_files/runtime/remediation_sync.js +58 -18
- package/dist/log_config_files/sender/batch_sender.js +10 -4
- package/package.json +1 -1
|
@@ -110,6 +110,12 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
|
|
|
110
110
|
if (typeof parsed === 'number' && !Number.isNaN(parsed)) {
|
|
111
111
|
return { [itemKey]: { [field]: parsed !== 0 } };
|
|
112
112
|
}
|
|
113
|
+
if (typeof parsed === 'string') {
|
|
114
|
+
const lower = parsed.trim().toLowerCase();
|
|
115
|
+
return {
|
|
116
|
+
[itemKey]: { [field]: lower === 'true' || lower === '1' || lower === 'yes' },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
113
119
|
}
|
|
114
120
|
return { [itemKey]: parsed };
|
|
115
121
|
}
|
|
@@ -14,6 +14,7 @@ import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vsc
|
|
|
14
14
|
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
|
|
15
15
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
16
16
|
import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
|
|
17
|
+
import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
|
|
17
18
|
import { createSignature, canonicalizePayload } from '../sender/signing.js';
|
|
18
19
|
const PROJECT_ROOT = process.cwd();
|
|
19
20
|
async function collectAllConfigFiles(endpointBase) {
|
|
@@ -73,7 +74,7 @@ async function sendAllConfigFiles(configFiles, hardwareUuid, authKey) {
|
|
|
73
74
|
const hookTypeRaw = (process.env.OPTIMUS_HOOK_TYPE || 'claude').toLowerCase();
|
|
74
75
|
const hookType = hookTypeRaw === 'cursor' ? 'cursor' : 'claude';
|
|
75
76
|
const workspaceRepo = process.env.OPTIMUS_WORKSPACE_REPO || process.env.GITHUB_REPOSITORY || '';
|
|
76
|
-
const manifest = configFiles.map((c) => c.file_path);
|
|
77
|
+
const manifest = configFiles.map((c) => canonicalCursorUserStateVscdbPath(c.file_path));
|
|
77
78
|
const hookRequestId = await sendHookRequestCreate(hardwareUuid, authKey, hookType, workspaceRepo);
|
|
78
79
|
hookRunLog(`hook-request id=${hookRequestId ?? 'none'}`);
|
|
79
80
|
const ingestSessionId = await sendIngestSessionStart(hardwareUuid, authKey);
|
|
@@ -9,6 +9,46 @@ export const PORTABLE_CURSOR_USER_STATE_VSCDB = 'Cursor/User/globalStorage/state
|
|
|
9
9
|
function normalizeSlashes(p) {
|
|
10
10
|
return p.trim().replace(/\\/g, '/');
|
|
11
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
|
+
}
|
|
12
52
|
function cursorStateVscdbAbsoluteBasePaths() {
|
|
13
53
|
const h = homedir();
|
|
14
54
|
if (process.platform === 'darwin') {
|
|
@@ -476,25 +476,49 @@ function mergeSqliteOpIntoJson(currentJson, sqliteOp) {
|
|
|
476
476
|
const parentParts = parts.slice(0, -1);
|
|
477
477
|
let node = currentJson;
|
|
478
478
|
for (const p of parentParts) {
|
|
479
|
-
if (node == null || typeof node !== 'object')
|
|
479
|
+
if (node == null || typeof node !== 'object' || Array.isArray(node))
|
|
480
480
|
return;
|
|
481
|
-
|
|
481
|
+
const rec = node;
|
|
482
|
+
const child = rec[p];
|
|
483
|
+
if (child == null) {
|
|
484
|
+
rec[p] = {};
|
|
485
|
+
node = rec[p];
|
|
486
|
+
}
|
|
487
|
+
else if (typeof child === 'object' && !Array.isArray(child)) {
|
|
488
|
+
node = child;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
482
493
|
}
|
|
483
494
|
if (node == null || typeof node !== 'object' || Array.isArray(node))
|
|
484
495
|
return;
|
|
485
496
|
const container = node;
|
|
486
|
-
const
|
|
487
|
-
|
|
497
|
+
const existing = container[arrayKey];
|
|
498
|
+
let arr;
|
|
499
|
+
if (Array.isArray(existing)) {
|
|
500
|
+
arr = existing;
|
|
501
|
+
}
|
|
502
|
+
else if (existing == null) {
|
|
503
|
+
arr = [];
|
|
504
|
+
container[arrayKey] = arr;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
488
507
|
return;
|
|
508
|
+
}
|
|
489
509
|
const idx = arr.findIndex((item) => {
|
|
490
510
|
if (item === null || typeof item !== 'object')
|
|
491
511
|
return false;
|
|
492
512
|
const o = item;
|
|
493
513
|
return Object.entries(where).every(([k, v]) => o[k] === v);
|
|
494
514
|
});
|
|
495
|
-
if (idx
|
|
515
|
+
if (idx >= 0) {
|
|
516
|
+
Object.assign(arr[idx], sqliteOp.updates);
|
|
496
517
|
return;
|
|
497
|
-
|
|
518
|
+
}
|
|
519
|
+
const newItem = { ...where };
|
|
520
|
+
Object.assign(newItem, sqliteOp.updates);
|
|
521
|
+
arr.push(newItem);
|
|
498
522
|
return;
|
|
499
523
|
}
|
|
500
524
|
mergeJsonAtSqlitePath(currentJson, sqliteOp.json_path, sqliteOp.updates);
|
|
@@ -750,10 +774,17 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
750
774
|
const line = `sqlite_update: database not found at ${dbPath}`;
|
|
751
775
|
hookRunLog(line);
|
|
752
776
|
complianceRunnerDiag(line);
|
|
753
|
-
return
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
reason: `state.vscdb not on disk at ${dbPath} (Cursor may not have created globalStorage yet — open Cursor once).`,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
if (!assertSqlite3Available()) {
|
|
783
|
+
return {
|
|
784
|
+
ok: false,
|
|
785
|
+
reason: 'sqlite3 CLI not found in PATH (IDE hooks often inherit a minimal PATH; install Xcode CLT / sqlite3 or fix PATH).',
|
|
786
|
+
};
|
|
754
787
|
}
|
|
755
|
-
if (!assertSqlite3Available())
|
|
756
|
-
return false;
|
|
757
788
|
const resolvedOps = sqliteOps.map((op) => resolveCursorComposerSqliteOp(dbPath, op));
|
|
758
789
|
const groups = new Map();
|
|
759
790
|
for (const op of resolvedOps) {
|
|
@@ -771,7 +802,7 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
771
802
|
const line = `sqlite_update: rejected unsafe or empty target_key for deferred queue (refusing to write state.vscdb)`;
|
|
772
803
|
hookRunLog(line);
|
|
773
804
|
complianceRunnerDiag(`${line} target_key_preview=${first.target_key.slice(0, 80)}`);
|
|
774
|
-
return false;
|
|
805
|
+
return { ok: false, reason: 'unsafe or empty ItemTable target_key (see hook_log).' };
|
|
775
806
|
}
|
|
776
807
|
complianceRunnerDiag(`sqlite_update: deferred merge db=${dbPath} target_key=${first.target_key} operations=${ops.length}`);
|
|
777
808
|
let currentJson = {};
|
|
@@ -786,7 +817,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
786
817
|
const line = `sqlite_update: error querying database: ${e instanceof Error ? e.message : String(e)}`;
|
|
787
818
|
hookRunLog(line);
|
|
788
819
|
complianceRunnerDiag(line);
|
|
789
|
-
return
|
|
820
|
+
return {
|
|
821
|
+
ok: false,
|
|
822
|
+
reason: `sqlite read/parse failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
823
|
+
};
|
|
790
824
|
}
|
|
791
825
|
for (const op of ops) {
|
|
792
826
|
mergeSqliteOpIntoJson(currentJson, op);
|
|
@@ -797,7 +831,10 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
797
831
|
const line = `sqlite_update: deferred merge produced empty JSON — refusing to queue (would wipe ItemTable row)`;
|
|
798
832
|
hookRunLog(line);
|
|
799
833
|
complianceRunnerDiag(line);
|
|
800
|
-
return
|
|
834
|
+
return {
|
|
835
|
+
ok: false,
|
|
836
|
+
reason: 'merge produced empty JSON (no-op merge — often missing modes4/agent structure or outdated log-llm-config; publish latest package and retry).',
|
|
837
|
+
};
|
|
801
838
|
}
|
|
802
839
|
queueDeferredVscdbItem({
|
|
803
840
|
dbPath,
|
|
@@ -811,13 +848,16 @@ function queueDeferredSqliteOpsMerged(configPath, sqliteOps, postApplyUpload) {
|
|
|
811
848
|
hookRunLog(okLine);
|
|
812
849
|
complianceRunnerDiag(okLine);
|
|
813
850
|
}
|
|
814
|
-
return true;
|
|
851
|
+
return { ok: true };
|
|
815
852
|
}
|
|
816
853
|
catch (err) {
|
|
817
854
|
const line = `sqlite_update: unexpected error: ${err instanceof Error ? err.message : String(err)}`;
|
|
818
855
|
hookRunLog(line);
|
|
819
856
|
complianceRunnerDiag(line);
|
|
820
|
-
return
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
reason: `unexpected: ${err instanceof Error ? err.message : String(err)}`,
|
|
860
|
+
};
|
|
821
861
|
}
|
|
822
862
|
}
|
|
823
863
|
/**
|
|
@@ -932,11 +972,11 @@ export function enforceRemediation(instruction) {
|
|
|
932
972
|
const postApplyUpload = ft && inst.config_file_path.includes('#')
|
|
933
973
|
? { file_path: inst.config_file_path, file_type: ft }
|
|
934
974
|
: undefined;
|
|
935
|
-
const
|
|
936
|
-
if (!ok) {
|
|
937
|
-
return fail(
|
|
975
|
+
const q = queueDeferredSqliteOpsMerged(inst.config_file_path, ops, postApplyUpload);
|
|
976
|
+
if (!q.ok) {
|
|
977
|
+
return fail(`deferred state.vscdb queue failed — ${q.reason} (see sqlite_update lines in ~/opt-ai-sec/management/hook_log.txt)`, { config_file_path: inst.config_file_path });
|
|
938
978
|
}
|
|
939
|
-
return { ok, deferredSqlite:
|
|
979
|
+
return { ok: true, deferredSqlite: true };
|
|
940
980
|
}
|
|
941
981
|
let allSuccess = true;
|
|
942
982
|
for (const check of sqliteOps) {
|
|
@@ -2,6 +2,7 @@ import { postStartupPayload, patchPayload } from '../../endpoint_client/index.js
|
|
|
2
2
|
import { createSignature } from './signing.js';
|
|
3
3
|
import { loadEndpointBase } from './endpoint_config.js';
|
|
4
4
|
import { hookRunLog } from '../runtime/hook_logger.js';
|
|
5
|
+
import { canonicalCursorUserStateVscdbPath } from '../runtime/remediation_config_path.js';
|
|
5
6
|
/** Chunk size per batch request. */
|
|
6
7
|
export const BATCH_CHUNK_SIZE = 20;
|
|
7
8
|
const MAX_BATCH_SIZE_BYTES = 500 * 1024; // 500KB
|
|
@@ -51,7 +52,11 @@ function buildBatchChunks(configFiles, basePayloadSize) {
|
|
|
51
52
|
return chunks;
|
|
52
53
|
}
|
|
53
54
|
function buildChunkBody(chunk, hardwareUuid, authKey, hookRequestId, metadata) {
|
|
54
|
-
const config_files = chunk.map((c) => ({
|
|
55
|
+
const config_files = chunk.map((c) => ({
|
|
56
|
+
file_type: c.file_type,
|
|
57
|
+
file_path: canonicalCursorUserStateVscdbPath(c.file_path),
|
|
58
|
+
raw_content: c.raw_content,
|
|
59
|
+
}));
|
|
55
60
|
const payload = { hardware_uuid: hardwareUuid, metadata, config_files };
|
|
56
61
|
if (hookRequestId != null)
|
|
57
62
|
payload.hook_request_id = hookRequestId;
|
|
@@ -153,18 +158,19 @@ async function sendIngestSessionFinish(hardwareUuid, authKey, ingestSessionId) {
|
|
|
153
158
|
async function sendConfigFile(configFile, hardwareUuid, authKey) {
|
|
154
159
|
const endpoint = loadEndpointBase();
|
|
155
160
|
const apiUrl = `${resolveApiBase(endpoint)}/endpoint_security/log-config-file/`;
|
|
156
|
-
const
|
|
161
|
+
const uploadPath = canonicalCursorUserStateVscdbPath(configFile.file_path);
|
|
162
|
+
const payload = { hardware_uuid: hardwareUuid, file_type: configFile.file_type, file_path: uploadPath, raw_content: configFile.raw_content };
|
|
157
163
|
const signature = createSignature(payload, authKey.key);
|
|
158
164
|
const body = { ...payload, signature, key_id: authKey.key_id || '', metadata: { org_identifier: process.env.GITHUB_ORG || process.env.GH_ORG || '', repo_identifier: process.env.GITHUB_REPOSITORY || process.env.GH_REPOSITORY || '' } };
|
|
159
165
|
try {
|
|
160
166
|
const response = await postStartupPayload(apiUrl, body);
|
|
161
167
|
if (response.status !== 'accepted') {
|
|
162
|
-
hookRunLog(`sendConfigFile: rejected status=${response.status} error=${response.error ?? ''} path=${
|
|
168
|
+
hookRunLog(`sendConfigFile: rejected status=${response.status} error=${response.error ?? ''} path=${uploadPath}`);
|
|
163
169
|
}
|
|
164
170
|
return response.status === 'accepted';
|
|
165
171
|
}
|
|
166
172
|
catch (err) {
|
|
167
|
-
hookRunLog(`sendConfigFile: request failed path=${
|
|
173
|
+
hookRunLog(`sendConfigFile: request failed path=${uploadPath} err=${err instanceof Error ? err.message : String(err)}`);
|
|
168
174
|
return false;
|
|
169
175
|
}
|
|
170
176
|
}
|