log-llm-config 1.3.61 → 1.3.63
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/compliance_prompt_gate.js +41 -1
- package/dist/log_config_files/collection/mcp_tool_collector.js +64 -2
- package/dist/log_config_files/readers/vscdb_reader.js +9 -4
- package/dist/log_config_files/runtime/compliance_check.js +152 -8
- package/dist/log_config_files/runtime/main_runner.js +11 -0
- package/dist/log_config_files/runtime/management_storage.js +32 -4
- package/dist/log_config_files/runtime/remediation_config_path.js +5 -0
- package/dist/log_config_files/runtime/remediation_sync.js +121 -11
- package/dist/tofu.js +1 -1
- package/package.json +2 -2
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
|
|
8
8
|
*/
|
|
9
9
|
import { applyAutofixViolations, normalizeAgentToken, pruneSatisfiedOneTimeRemediations, runLocalRemediationComplianceCheck, } from './log_config_files/runtime/compliance_check.js';
|
|
10
|
-
import { existsSync, statSync } from 'node:fs';
|
|
10
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
11
11
|
import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
|
|
12
12
|
import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
|
|
13
13
|
import { isThisCliModule } from './cli_invocation_match.js';
|
|
14
|
+
import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
|
|
15
|
+
import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
|
|
16
|
+
import { tryResolveHardwareUuid } from './log_config_files/runtime/hardware_uuid.js';
|
|
14
17
|
const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
15
18
|
function parseIde() {
|
|
16
19
|
const eq = process.argv.find((a) => a.startsWith('--ide='));
|
|
@@ -87,6 +90,38 @@ function autofixDialogLine(v) {
|
|
|
87
90
|
const short = d.length > 160 ? `${d.slice(0, 157)}…` : d;
|
|
88
91
|
return `• [${v.finding_formatted_id}] ${short}`;
|
|
89
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Upload a secondary compliance file to the backend so the server can resolve the finding.
|
|
95
|
+
* Fire-and-forget: upload runs in background; any failure is logged but does not block the gate.
|
|
96
|
+
*/
|
|
97
|
+
async function _uploadSecondaryFile(entry) {
|
|
98
|
+
const { uuid, config_file_path, file_type } = entry;
|
|
99
|
+
if (!file_type) {
|
|
100
|
+
hookRunLog(`secondary_upload: skipping uuid=${uuid} — no file_type on secondary group`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let rawContent;
|
|
104
|
+
try {
|
|
105
|
+
rawContent = JSON.parse(readFileSync(config_file_path, 'utf8'));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
hookRunLog(`secondary_upload: could not read file uuid=${uuid} path=${config_file_path}`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const hw = tryResolveHardwareUuid();
|
|
112
|
+
if (!hw) {
|
|
113
|
+
hookRunLog(`secondary_upload: hardware UUID unavailable, skipping uuid=${uuid}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const authKey = await ensureAuthentication(hw);
|
|
118
|
+
const sent = await sendConfigFile({ file_type, file_path: config_file_path, raw_content: rawContent }, hw, authKey);
|
|
119
|
+
hookRunLog(`secondary_upload: uuid=${uuid} path=${config_file_path} sent=${sent}`);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
hookRunLog(`secondary_upload: upload failed uuid=${uuid} path=${config_file_path} err=${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
90
125
|
/**
|
|
91
126
|
* Entry when the default npm bin (`log-llm-config` / cli.js) dispatches
|
|
92
127
|
* `compliance_prompt_gate` — npx does not execute compliance_prompt_gate.js as argv[1], so
|
|
@@ -101,6 +136,11 @@ export async function runCompliancePromptGate() {
|
|
|
101
136
|
const agent = parseAgent(ide);
|
|
102
137
|
hookLogSessionBanner('compliance_prompt_gate (before submit)');
|
|
103
138
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
139
|
+
// Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
|
|
140
|
+
// Upload those files fire-and-forget so the backend can resolve the finding immediately.
|
|
141
|
+
for (const e of status.secondarySatisfied ?? []) {
|
|
142
|
+
_uploadSecondaryFile(e).catch(() => undefined);
|
|
143
|
+
}
|
|
104
144
|
if (status.status === 'fail' && status.violations.length > 0) {
|
|
105
145
|
const staleMs = getManifestStalenessMs();
|
|
106
146
|
if (staleMs !== null && staleMs > MANIFEST_STALE_MS) {
|
|
@@ -4,10 +4,72 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { readJSONFile } from '../readers/file_readers.js';
|
|
5
5
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
6
6
|
import { getCursorProjectsPath } from '../paths/path_constants_helpers.js';
|
|
7
|
+
import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
8
|
+
/**
|
|
9
|
+
* Read workspaceMetadata.entries from the Cursor global state.vscdb and emit one
|
|
10
|
+
* cursor_workspace_vscdb ConfigFileData per workspace entry.
|
|
11
|
+
*
|
|
12
|
+
* Cursor maintains a canonical mapping of workspace hash → display path in the global
|
|
13
|
+
* state.vscdb under "workspaceMetadata.entries". Each entry has:
|
|
14
|
+
* { workspaceId: "<hash>", displayPath: "~/my-project", folderUri: "file:///Users/..." }
|
|
15
|
+
*
|
|
16
|
+
* The per-workspace workspaceStorage/<hash>/state.vscdb holds cursor/disabledMcpServers.
|
|
17
|
+
* Together they give us: which servers are disabled per workspace + a human-readable label.
|
|
18
|
+
*/
|
|
19
|
+
function collectWorkspaceVscdbs(spec, home = homedir()) {
|
|
20
|
+
if (!spec.global_vscdb_path_segments?.length || !spec.global_workspace_list_key) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const globalDbPath = join(home, ...spec.global_vscdb_path_segments);
|
|
24
|
+
if (!existsSync(globalDbPath))
|
|
25
|
+
return [];
|
|
26
|
+
const globalResult = readVscdbItemTableJson(globalDbPath, spec.global_workspace_list_key);
|
|
27
|
+
if (!globalResult)
|
|
28
|
+
return [];
|
|
29
|
+
const rawValue = globalResult[spec.global_workspace_list_key];
|
|
30
|
+
// The stored value is {"entries": [...]}; unwrap it.
|
|
31
|
+
const entries = Array.isArray(rawValue)
|
|
32
|
+
? rawValue
|
|
33
|
+
: Array.isArray(rawValue?.entries)
|
|
34
|
+
? rawValue.entries
|
|
35
|
+
: [];
|
|
36
|
+
if (!entries.length)
|
|
37
|
+
return [];
|
|
38
|
+
const workspaceStoragePath = join(home, ...spec.workspace_storage_path_segments);
|
|
39
|
+
const output = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry || typeof entry !== 'object')
|
|
42
|
+
continue;
|
|
43
|
+
const e = entry;
|
|
44
|
+
const workspaceId = typeof e.workspaceId === 'string' ? e.workspaceId : null;
|
|
45
|
+
if (!workspaceId)
|
|
46
|
+
continue;
|
|
47
|
+
const dbPath = join(workspaceStoragePath, workspaceId, spec.vscdb_filename);
|
|
48
|
+
if (!existsSync(dbPath))
|
|
49
|
+
continue;
|
|
50
|
+
const perWorkspaceResult = readVscdbItemTableJson(dbPath, spec.item_table_key);
|
|
51
|
+
const disabled = perWorkspaceResult?.[spec.item_table_key];
|
|
52
|
+
const rawContent = {
|
|
53
|
+
[spec.item_table_key]: Array.isArray(disabled) ? disabled : [],
|
|
54
|
+
};
|
|
55
|
+
if (typeof e.displayPath === 'string')
|
|
56
|
+
rawContent['displayPath'] = e.displayPath;
|
|
57
|
+
if (typeof e.folderUri === 'string')
|
|
58
|
+
rawContent['folderUri'] = e.folderUri;
|
|
59
|
+
rawContent['workspaceId'] = workspaceId;
|
|
60
|
+
output.push({
|
|
61
|
+
file_type: spec.file_type,
|
|
62
|
+
file_path: `${dbPath}#${spec.item_table_key}`,
|
|
63
|
+
raw_content: rawContent,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
7
68
|
function collectMcpToolFiles(pathSkipPrefixes = [], constants) {
|
|
8
69
|
const result = [];
|
|
9
70
|
const skipPrefixes = normalizePathSkipPrefixes(pathSkipPrefixes);
|
|
10
|
-
const
|
|
71
|
+
const home = homedir();
|
|
72
|
+
const cursorProjectsPath = getCursorProjectsPath(home, constants);
|
|
11
73
|
try {
|
|
12
74
|
if (!existsSync(cursorProjectsPath))
|
|
13
75
|
return result;
|
|
@@ -34,4 +96,4 @@ function collectMcpToolFiles(pathSkipPrefixes = [], constants) {
|
|
|
34
96
|
}
|
|
35
97
|
return result;
|
|
36
98
|
}
|
|
37
|
-
export { collectMcpToolFiles };
|
|
99
|
+
export { collectMcpToolFiles, collectWorkspaceVscdbs };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { resolveSqlite3Binary } from '../runtime/sqlite_binary.js';
|
|
4
|
-
import { readFileCollectionVscdbContract, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
|
|
4
|
+
import { readFileCollectionVscdbContract, sanitizeFileCollectionVscdbContract, sanitizeReactiveStorageItemKey, writeFileCollectionVscdbContract, } from '../runtime/management_storage.js';
|
|
5
5
|
/**
|
|
6
6
|
* ItemTable keys that store a bare JSON boolean; compliance paths use `${key}.${field}`.
|
|
7
7
|
* Kept in sync with `coerceItemTableValueToObjectRoot` / `serializeItemTableValueForWrite` in remediation_sync.
|
|
@@ -52,11 +52,11 @@ export function normalizeVscdbComposerContractFromPatternsResponse(resp) {
|
|
|
52
52
|
((typeof c.reactive_storage_item_key === 'string' && c.reactive_storage_item_key.trim() !== '') ||
|
|
53
53
|
(c.composer_shadow_keys?.length ?? 0) > 0);
|
|
54
54
|
if (hasKey && c) {
|
|
55
|
-
return {
|
|
55
|
+
return sanitizeFileCollectionVscdbContract({
|
|
56
56
|
version: 1,
|
|
57
57
|
reactive_storage_item_key: c.reactive_storage_item_key ?? undefined,
|
|
58
58
|
composer_shadow_keys: [...(c.composer_shadow_keys ?? [])],
|
|
59
|
-
};
|
|
59
|
+
}) ?? { version: 1, reactive_storage_item_key: undefined, composer_shadow_keys: [] };
|
|
60
60
|
}
|
|
61
61
|
return parseVscdbComposerContractFromReadQueries(resp.vscdb_read_queries);
|
|
62
62
|
}
|
|
@@ -88,7 +88,7 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
|
|
|
88
88
|
try {
|
|
89
89
|
const raw = querySqlite(dbPath, itemKey);
|
|
90
90
|
const contract = readFileCollectionVscdbContract();
|
|
91
|
-
const reactiveKey =
|
|
91
|
+
const reactiveKey = sanitizeReactiveStorageItemKey(contract?.reactive_storage_item_key) ?? '';
|
|
92
92
|
if (itemKey === 'composerState' && (!raw || raw === '{}') && reactiveKey) {
|
|
93
93
|
const reactive = querySqlite(dbPath, reactiveKey);
|
|
94
94
|
if (reactive) {
|
|
@@ -106,6 +106,11 @@ export function readVscdbItemTableJson(dbPath, itemKey) {
|
|
|
106
106
|
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
107
107
|
return { [itemKey]: parsed };
|
|
108
108
|
}
|
|
109
|
+
// Bare JSON arrays (e.g. cursor/disabledMcpServers) — wrap under itemKey so ops-based
|
|
110
|
+
// compliance checks can locate the array via getByPath(itemKey).
|
|
111
|
+
if (Array.isArray(parsed)) {
|
|
112
|
+
return { [itemKey]: parsed };
|
|
113
|
+
}
|
|
109
114
|
// Bare JSON primitives. Scalar toggles must be wrapped so getByPath(key.field) works in compliance checks.
|
|
110
115
|
// Match coerceScalarForItemTableField in remediation_sync: Cursor may store toggles as JSON 0/1, not only booleans.
|
|
111
116
|
if (typeof parsed === 'boolean' || typeof parsed === 'number' || typeof parsed === 'string') {
|
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
* downloads the latest manifest so the gate has fresh data to act on.
|
|
12
12
|
*/
|
|
13
13
|
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
14
16
|
import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
15
17
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
|
|
16
18
|
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
17
19
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
18
20
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
19
21
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
20
|
-
import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
22
|
+
import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
21
23
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
22
24
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
23
25
|
/** Normalize manifest/env/CLI agent tokens to a known Agent, or '' if unrecognized. */
|
|
@@ -101,9 +103,34 @@ export function getByPath(obj, path) {
|
|
|
101
103
|
}
|
|
102
104
|
return current;
|
|
103
105
|
}
|
|
104
|
-
/**
|
|
106
|
+
/** Key-order-independent deep-equal comparison (handles primitives, arrays, plain objects). */
|
|
105
107
|
function deepEqual(a, b) {
|
|
106
|
-
|
|
108
|
+
if (a === b)
|
|
109
|
+
return true;
|
|
110
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object')
|
|
111
|
+
return false;
|
|
112
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
113
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
114
|
+
return false;
|
|
115
|
+
for (let i = 0; i < a.length; i++) {
|
|
116
|
+
if (!deepEqual(a[i], b[i]))
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const ao = a;
|
|
122
|
+
const bo = b;
|
|
123
|
+
const aKeys = Object.keys(ao);
|
|
124
|
+
const bKeys = Object.keys(bo);
|
|
125
|
+
if (aKeys.length !== bKeys.length)
|
|
126
|
+
return false;
|
|
127
|
+
for (const k of aKeys) {
|
|
128
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k))
|
|
129
|
+
return false;
|
|
130
|
+
if (!deepEqual(ao[k], bo[k]))
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
107
134
|
}
|
|
108
135
|
function isStringArray(v) {
|
|
109
136
|
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
@@ -131,16 +158,16 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
131
158
|
continue;
|
|
132
159
|
}
|
|
133
160
|
const cur = getByPath(configJson, targetPath);
|
|
134
|
-
const curArr =
|
|
161
|
+
const curArr = Array.isArray(cur) ? cur : [];
|
|
135
162
|
const toAdd = add[k] ?? [];
|
|
136
163
|
const toRemove = remove[k] ?? [];
|
|
137
164
|
for (const item of toRemove) {
|
|
138
|
-
if (curArr.
|
|
165
|
+
if (curArr.some((curItem) => deepEqual(curItem, item))) {
|
|
139
166
|
return { ok: false, expected: { op: 'remove', path: targetPath, value: item } };
|
|
140
167
|
}
|
|
141
168
|
}
|
|
142
169
|
for (const item of toAdd) {
|
|
143
|
-
if (!curArr.
|
|
170
|
+
if (!curArr.some((curItem) => deepEqual(curItem, item))) {
|
|
144
171
|
return { ok: false, expected: { op: 'add', path: targetPath, value: item } };
|
|
145
172
|
}
|
|
146
173
|
}
|
|
@@ -177,6 +204,25 @@ function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
|
177
204
|
return { ok: false, reason: 'parse_error' };
|
|
178
205
|
}
|
|
179
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Evaluate all checks in a secondary group against the group's config file.
|
|
209
|
+
* Returns true only if every check passes (ops-based). Non-ops checks are treated as failing.
|
|
210
|
+
*/
|
|
211
|
+
function evaluateSecondaryGroup(group) {
|
|
212
|
+
const loaded = loadRemediationConfigJson(group.config_file_path, group.checks.map((c) => c.setting_path));
|
|
213
|
+
if (!loaded.ok) {
|
|
214
|
+
hookRunLog(`evaluateSecondaryGroup: could not load secondary file path=${group.config_file_path} reason=${loaded.reason}`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
for (const check of group.checks) {
|
|
218
|
+
if (!check.ops)
|
|
219
|
+
return false;
|
|
220
|
+
const { ok } = verifyOpsApplied(loaded.json, check.setting_path, check.ops);
|
|
221
|
+
if (!ok)
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
180
226
|
// ---------------------------------------------------------------------------
|
|
181
227
|
// Check runner — Section 6: real per-check evaluation
|
|
182
228
|
// ---------------------------------------------------------------------------
|
|
@@ -194,6 +240,7 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
194
240
|
}
|
|
195
241
|
const uuids = entries.map((e) => e.uuid);
|
|
196
242
|
const violations = [];
|
|
243
|
+
const secondarySatisfied = [];
|
|
197
244
|
let skippedNoCompliance = 0;
|
|
198
245
|
let skippedNonJson = 0;
|
|
199
246
|
let skippedNoChecks = 0;
|
|
@@ -237,13 +284,63 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
237
284
|
continue;
|
|
238
285
|
}
|
|
239
286
|
const configJson = loaded.json;
|
|
287
|
+
const entryViolations = [];
|
|
240
288
|
for (const check of checks) {
|
|
289
|
+
// When the check carries a sqlite_op with apply_to_all_workspaces, the authoritative data
|
|
290
|
+
// lives in workspace state.vscdb files — not in config_file_path (which may be mcp.json in
|
|
291
|
+
// the fallback case where no workspace vscdb has been collected yet). Verify directly against
|
|
292
|
+
// every discovered workspace vscdb; fail as a violation if any is missing the required entry.
|
|
293
|
+
if (check.sqlite_op?.apply_to_all_workspaces && check.ops) {
|
|
294
|
+
const segments = check.sqlite_op.workspace_storage_path_segments ?? [
|
|
295
|
+
'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage',
|
|
296
|
+
];
|
|
297
|
+
const wsPath = join(homedir(), ...segments);
|
|
298
|
+
const vscdbPaths = discoverAllWorkspaceVscdbs(wsPath);
|
|
299
|
+
if (vscdbPaths.length === 0) {
|
|
300
|
+
hookRunLog(`compliance_check: apply_to_all_workspaces — no workspace vscdbs found at ${wsPath}, skipping uuid=${entry.uuid}`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
let violated = false;
|
|
304
|
+
let expectedForViolation = null;
|
|
305
|
+
for (const dbPath of vscdbPaths) {
|
|
306
|
+
const wrapped = readVscdbItemTableJson(dbPath, check.sqlite_op.target_key ?? '');
|
|
307
|
+
if (wrapped === null) {
|
|
308
|
+
violated = true;
|
|
309
|
+
expectedForViolation = { op: 'read_failed', db: dbPath };
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
const { ok, expected } = verifyOpsApplied(wrapped, check.setting_path, check.ops);
|
|
313
|
+
if (!ok) {
|
|
314
|
+
violated = true;
|
|
315
|
+
expectedForViolation = expected;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (violated) {
|
|
320
|
+
hookRunLog(`compliance_check: VIOLATION (vscdb) uuid=${entry.uuid} path=${check.setting_path} expected=${JSON.stringify(expectedForViolation)}`);
|
|
321
|
+
entryViolations.push({
|
|
322
|
+
uuid: entry.uuid,
|
|
323
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
324
|
+
setting_path: check.setting_path,
|
|
325
|
+
description: check.description,
|
|
326
|
+
finding_title: entry.finding_title,
|
|
327
|
+
finding_description: entry.finding_description,
|
|
328
|
+
severity: compliance.severity,
|
|
329
|
+
autofix_allowed: compliance.autofix_allowed,
|
|
330
|
+
config_file_path: entry.config_file_path,
|
|
331
|
+
expected_value: expectedForViolation,
|
|
332
|
+
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Apply remediation ops for ${check.setting_path} in workspace state.vscdb files`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
241
338
|
// Prefer ops-based verification (matches delta apply semantics; doesn't require full after snapshot).
|
|
242
339
|
if (check.ops) {
|
|
243
340
|
const { ok, expected } = verifyOpsApplied(configJson, check.setting_path, check.ops);
|
|
244
341
|
if (!ok) {
|
|
245
342
|
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} expected=${JSON.stringify(expected)}`);
|
|
246
|
-
|
|
343
|
+
entryViolations.push({
|
|
247
344
|
uuid: entry.uuid,
|
|
248
345
|
finding_formatted_id: compliance.finding_formatted_id,
|
|
249
346
|
setting_path: check.setting_path,
|
|
@@ -267,7 +364,7 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
267
364
|
continue;
|
|
268
365
|
if (!deepEqual(currentValue, expectedValue)) {
|
|
269
366
|
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} current=${JSON.stringify(currentValue)} expected=${JSON.stringify(expectedValue)}`);
|
|
270
|
-
|
|
367
|
+
entryViolations.push({
|
|
271
368
|
uuid: entry.uuid,
|
|
272
369
|
finding_formatted_id: compliance.finding_formatted_id,
|
|
273
370
|
setting_path: check.setting_path,
|
|
@@ -282,12 +379,30 @@ export function runLocalRemediationComplianceCheck(agent = 'cursor') {
|
|
|
282
379
|
});
|
|
283
380
|
}
|
|
284
381
|
}
|
|
382
|
+
if (entryViolations.length > 0) {
|
|
383
|
+
const secondaryGroups = compliance.secondary_checks ?? [];
|
|
384
|
+
const passingGroup = secondaryGroups.length > 0
|
|
385
|
+
? secondaryGroups.find((g) => evaluateSecondaryGroup(g))
|
|
386
|
+
: undefined;
|
|
387
|
+
if (passingGroup) {
|
|
388
|
+
hookRunLog(`compliance_check: secondary check satisfied uuid=${entry.uuid} — skipping ${entryViolations.length} primary violation(s)`);
|
|
389
|
+
secondarySatisfied.push({
|
|
390
|
+
uuid: entry.uuid,
|
|
391
|
+
config_file_path: passingGroup.config_file_path,
|
|
392
|
+
file_type: passingGroup.file_type,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
violations.push(...entryViolations);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
285
399
|
}
|
|
286
400
|
const status = {
|
|
287
401
|
status: violations.length > 0 ? 'fail' : 'ok',
|
|
288
402
|
checked_at: new Date().toISOString(),
|
|
289
403
|
manifest_uuids: uuids,
|
|
290
404
|
violations,
|
|
405
|
+
secondarySatisfied: secondarySatisfied.length > 0 ? secondarySatisfied : undefined,
|
|
291
406
|
};
|
|
292
407
|
const skipParts = [];
|
|
293
408
|
if (skippedNoCompliance)
|
|
@@ -527,12 +642,41 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
|
|
|
527
642
|
okAll = false;
|
|
528
643
|
break;
|
|
529
644
|
}
|
|
645
|
+
// apply_to_all_workspaces: verify against workspace vscdbs, not the loaded config file.
|
|
646
|
+
if (check.sqlite_op?.apply_to_all_workspaces) {
|
|
647
|
+
const segments = check.sqlite_op.workspace_storage_path_segments ?? [
|
|
648
|
+
'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage',
|
|
649
|
+
];
|
|
650
|
+
const wsPath = join(homedir(), ...segments);
|
|
651
|
+
const vscdbPaths = discoverAllWorkspaceVscdbs(wsPath);
|
|
652
|
+
if (vscdbPaths.length === 0) {
|
|
653
|
+
okAll = false;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
for (const dbPath of vscdbPaths) {
|
|
657
|
+
const wrapped = readVscdbItemTableJson(dbPath, check.sqlite_op.target_key ?? '');
|
|
658
|
+
if (wrapped === null || !verifyOpsApplied(wrapped, check.setting_path, check.ops).ok) {
|
|
659
|
+
okAll = false;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!okAll)
|
|
664
|
+
break;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
530
667
|
const res = verifyOpsApplied(configJson, check.setting_path, check.ops);
|
|
531
668
|
if (!res.ok) {
|
|
532
669
|
okAll = false;
|
|
533
670
|
break;
|
|
534
671
|
}
|
|
535
672
|
}
|
|
673
|
+
if (!okAll) {
|
|
674
|
+
const secondaryGroups = spec?.secondary_checks ?? [];
|
|
675
|
+
if (secondaryGroups.length > 0 && secondaryGroups.some((g) => evaluateSecondaryGroup(g))) {
|
|
676
|
+
okAll = true;
|
|
677
|
+
hookRunLog(`remediation_prune: secondary check satisfied uuid=${inst.uuid}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
536
680
|
if (okAll) {
|
|
537
681
|
removed++;
|
|
538
682
|
hookRunLog(`remediation_prune: satisfied one-time uuid=${inst.uuid} path=${inst.config_file_path}`);
|
|
@@ -12,6 +12,7 @@ import { readJSONFile, readMarkdownFile } from '../readers/file_readers.js';
|
|
|
12
12
|
import { isVscdbVirtualPath, tryReadVscdbVirtualFile, summarizeComposerPayloadForDiagnostics, } from '../readers/vscdb_config_builder.js';
|
|
13
13
|
import { persistVscdbComposerContractFromPatternsResponse } from '../readers/vscdb_reader.js';
|
|
14
14
|
import { collectConfigFilesFromPatterns, collectMcpToolFiles, collectConfigFilesFromInstalledPlugins, determineFileTypeFromPath } from '../collection/config_collector.js';
|
|
15
|
+
import { collectWorkspaceVscdbs } from '../collection/mcp_tool_collector.js';
|
|
15
16
|
import { normalizePathSkipPrefixes } from '../paths/pattern_resolver.js';
|
|
16
17
|
import { sendConfigFile, sendConfigFilesBatch, sendHookRequestCreate, sendHookRequestUpdateManifest, sendIngestSessionStart, sendIngestSessionFinish, BATCH_CHUNK_SIZE } from '../sender/batch_sender.js';
|
|
17
18
|
import { canonicalCursorUserStateVscdbPath } from './remediation_config_path.js';
|
|
@@ -51,6 +52,16 @@ async function collectAllConfigFiles(endpointBase) {
|
|
|
51
52
|
configFiles.push(m);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
if (patternsResponse.workspace_vscdb_spec) {
|
|
56
|
+
hookRunLog(`scanning Cursor workspace vscdb files`);
|
|
57
|
+
for (const m of collectWorkspaceVscdbs(patternsResponse.workspace_vscdb_spec)) {
|
|
58
|
+
const key = `${m.file_type}\t${m.file_path}`;
|
|
59
|
+
if (!existingPaths.has(key)) {
|
|
60
|
+
existingPaths.add(key);
|
|
61
|
+
configFiles.push(m);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
54
65
|
hookRunLog(`scanning installed plugins`);
|
|
55
66
|
if (!patternsResponse.client_path_constants)
|
|
56
67
|
throw new Error('client_path_constants required from API response');
|
|
@@ -17,6 +17,33 @@ export const DEFERRED_VSCDB_RESTART_LOG_BASENAME = 'deferred_vscdb_restart.log';
|
|
|
17
17
|
* of hardcoded Cursor paths.
|
|
18
18
|
*/
|
|
19
19
|
export const FILE_COLLECTION_VSCDB_CONTRACT_BASENAME = 'file_collection_vscdb_contract.json';
|
|
20
|
+
export function sanitizeReactiveStorageItemKey(raw) {
|
|
21
|
+
if (typeof raw !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
const trimmed = raw.trim();
|
|
24
|
+
if (!trimmed || trimmed.length > 512)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (/['"\\]/.test(trimmed))
|
|
27
|
+
return undefined;
|
|
28
|
+
return trimmed;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeComposerShadowKeys(raw) {
|
|
31
|
+
if (!Array.isArray(raw))
|
|
32
|
+
return [];
|
|
33
|
+
return raw
|
|
34
|
+
.filter((entry) => typeof entry === 'string')
|
|
35
|
+
.map((entry) => entry.trim())
|
|
36
|
+
.filter((entry) => entry.length > 0);
|
|
37
|
+
}
|
|
38
|
+
export function sanitizeFileCollectionVscdbContract(contract) {
|
|
39
|
+
if (!contract || contract.version !== 1)
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
version: 1,
|
|
43
|
+
reactive_storage_item_key: sanitizeReactiveStorageItemKey(contract.reactive_storage_item_key),
|
|
44
|
+
composer_shadow_keys: sanitizeComposerShadowKeys(contract.composer_shadow_keys),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
20
47
|
export function getManagementDir() {
|
|
21
48
|
return join(homedir(), OPT_AI_SEC_MANAGEMENT_REL);
|
|
22
49
|
}
|
|
@@ -38,16 +65,17 @@ export function readFileCollectionVscdbContract() {
|
|
|
38
65
|
return null;
|
|
39
66
|
try {
|
|
40
67
|
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
41
|
-
|
|
42
|
-
return null;
|
|
43
|
-
return parsed;
|
|
68
|
+
return sanitizeFileCollectionVscdbContract(parsed);
|
|
44
69
|
}
|
|
45
70
|
catch {
|
|
46
71
|
return null;
|
|
47
72
|
}
|
|
48
73
|
}
|
|
49
74
|
export function writeFileCollectionVscdbContract(contract) {
|
|
50
|
-
|
|
75
|
+
const sanitized = sanitizeFileCollectionVscdbContract(contract);
|
|
76
|
+
if (!sanitized)
|
|
77
|
+
return;
|
|
78
|
+
atomicWriteJson(getFileCollectionVscdbContractPath(), sanitized);
|
|
51
79
|
}
|
|
52
80
|
export function atomicWriteJson(filePath, data) {
|
|
53
81
|
const dir = dirname(filePath);
|
|
@@ -29,6 +29,11 @@ export function canonicalCursorUserStateVscdbPath(filePath) {
|
|
|
29
29
|
}
|
|
30
30
|
return filePath.trim();
|
|
31
31
|
}
|
|
32
|
+
// Per-workspace vscdbs live under workspaceStorage/<hash>/state.vscdb and must NOT be
|
|
33
|
+
// normalized to the global path — each workspace is a distinct ConfigurationFile row.
|
|
34
|
+
if (lower.includes('workspacestorage')) {
|
|
35
|
+
return filePath.trim();
|
|
36
|
+
}
|
|
32
37
|
const vscdb = 'state.vscdb';
|
|
33
38
|
const vlen = vscdb.length;
|
|
34
39
|
const slashKey = '/' + vscdb;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import { delimiter, dirname } from 'node:path';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { executeGet, executeBody } from '../../endpoint_client/http_transport.js';
|
|
5
6
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
@@ -209,6 +210,34 @@ function isPlainObject(v) {
|
|
|
209
210
|
function isStringArray(v) {
|
|
210
211
|
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
211
212
|
}
|
|
213
|
+
function deepEqual(a, b) {
|
|
214
|
+
if (a === b)
|
|
215
|
+
return true;
|
|
216
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object')
|
|
217
|
+
return false;
|
|
218
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
219
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
220
|
+
return false;
|
|
221
|
+
for (let i = 0; i < a.length; i++) {
|
|
222
|
+
if (!deepEqual(a[i], b[i]))
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
const ao = a;
|
|
228
|
+
const bo = b;
|
|
229
|
+
const aKeys = Object.keys(ao);
|
|
230
|
+
const bKeys = Object.keys(bo);
|
|
231
|
+
if (aKeys.length !== bKeys.length)
|
|
232
|
+
return false;
|
|
233
|
+
for (const k of aKeys) {
|
|
234
|
+
if (!Object.prototype.hasOwnProperty.call(bo, k))
|
|
235
|
+
return false;
|
|
236
|
+
if (!deepEqual(ao[k], bo[k]))
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
212
241
|
function applyStringArrayDelta(current, before, after) {
|
|
213
242
|
const cur = isStringArray(current) ? [...current] : [];
|
|
214
243
|
const b = isStringArray(before) ? before : [];
|
|
@@ -253,16 +282,13 @@ function applyCheck(configJson, check) {
|
|
|
253
282
|
continue;
|
|
254
283
|
}
|
|
255
284
|
const curVal = getByPath(configJson, targetPath);
|
|
256
|
-
const cur =
|
|
285
|
+
const cur = Array.isArray(curVal) ? [...curVal] : [];
|
|
257
286
|
const toRemove = (remove && remove[k]) ?? [];
|
|
258
287
|
const toAdd = (add && add[k]) ?? [];
|
|
259
|
-
const
|
|
260
|
-
const next = cur.filter((x) => !rmSet.has(x));
|
|
261
|
-
const nextSet = new Set(next);
|
|
288
|
+
const next = cur.filter((x) => !toRemove.some((item) => deepEqual(item, x)));
|
|
262
289
|
for (const x of toAdd) {
|
|
263
|
-
if (!
|
|
290
|
+
if (!next.some((item) => deepEqual(item, x))) {
|
|
264
291
|
next.push(x);
|
|
265
|
-
nextSet.add(x);
|
|
266
292
|
}
|
|
267
293
|
}
|
|
268
294
|
setByPath(configJson, targetPath, next);
|
|
@@ -468,6 +494,11 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
|
|
|
468
494
|
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
469
495
|
return parsed;
|
|
470
496
|
}
|
|
497
|
+
// Bare JSON array (e.g. cursor/disabledMcpServers): wrap under target_key so array_union_add
|
|
498
|
+
// can locate and mutate it via mergeSqliteOpIntoJson.
|
|
499
|
+
if (Array.isArray(parsed)) {
|
|
500
|
+
return { [targetKey]: parsed };
|
|
501
|
+
}
|
|
471
502
|
const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
|
|
472
503
|
if (field) {
|
|
473
504
|
const b = coerceScalarForItemTableField(parsed);
|
|
@@ -481,6 +512,12 @@ function coerceItemTableValueToObjectRoot(targetKey, parsed) {
|
|
|
481
512
|
* object can be ignored or overwritten on launch; match the native primitive shape when disabling.
|
|
482
513
|
*/
|
|
483
514
|
function serializeItemTableValueForWrite(targetKey, root) {
|
|
515
|
+
// Bare-array keys: root was wrapped as { [targetKey]: array } by coerceItemTableValueToObjectRoot.
|
|
516
|
+
// Unwrap back to a bare JSON array before writing.
|
|
517
|
+
const keys = Object.keys(root);
|
|
518
|
+
if (keys.length === 1 && keys[0] === targetKey && Array.isArray(root[targetKey])) {
|
|
519
|
+
return JSON.stringify(root[targetKey]);
|
|
520
|
+
}
|
|
484
521
|
const field = CURSOR_SCALAR_ITEMTABLE_FIELDS[targetKey];
|
|
485
522
|
if (field) {
|
|
486
523
|
const keys = Object.keys(root);
|
|
@@ -495,6 +532,22 @@ function serializeItemTableValueForWrite(targetKey, root) {
|
|
|
495
532
|
return JSON.stringify(root);
|
|
496
533
|
}
|
|
497
534
|
function mergeSqliteOpIntoJson(currentJson, sqliteOp) {
|
|
535
|
+
// Array union: target_key holds a bare JSON array (e.g. cursor/disabledMcpServers).
|
|
536
|
+
// currentJson here is the coerced object root — the raw array is stored under target_key.
|
|
537
|
+
if (sqliteOp.array_union_add?.length) {
|
|
538
|
+
const key = sqliteOp.target_key;
|
|
539
|
+
const existing = currentJson[key];
|
|
540
|
+
const arr = Array.isArray(existing) ? existing : [];
|
|
541
|
+
const lowerSet = new Set(arr.map((v) => String(v).toLowerCase()));
|
|
542
|
+
for (const item of sqliteOp.array_union_add) {
|
|
543
|
+
if (!lowerSet.has(item.toLowerCase())) {
|
|
544
|
+
arr.push(item);
|
|
545
|
+
lowerSet.add(item.toLowerCase());
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
currentJson[key] = arr;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
498
551
|
const where = sqliteOp.array_item_where;
|
|
499
552
|
const jp = sqliteOp.json_path ?? '';
|
|
500
553
|
if (where && typeof where === 'object' && Object.keys(where).length > 0 && jp) {
|
|
@@ -769,10 +822,10 @@ export async function applyDeferredVscdbFromDisk() {
|
|
|
769
822
|
}
|
|
770
823
|
const safeJson = it.new_value_json.replace(/'/g, "''");
|
|
771
824
|
const safeName = it.target_key.replace(/'/g, "''");
|
|
772
|
-
const sql = `
|
|
825
|
+
const sql = `INSERT OR REPLACE INTO ${it.table} (${it.key_column}, ${it.value_column}) VALUES ('${safeName}', '${safeJson}');`;
|
|
773
826
|
const changed = sqliteRunUpdateReturningChanges(it.dbPath, sql);
|
|
774
827
|
if (changed < 1) {
|
|
775
|
-
hookRunLog(`deferred_vscdb:
|
|
828
|
+
hookRunLog(`deferred_vscdb: INSERT OR REPLACE changed 0 rows key=${it.target_key} db=${it.dbPath} — keeping queue file`);
|
|
776
829
|
return false;
|
|
777
830
|
}
|
|
778
831
|
}
|
|
@@ -1015,6 +1068,24 @@ function applyOrQueueSqliteJsonUpdate(configPath, sqliteOp, deferred) {
|
|
|
1015
1068
|
return false;
|
|
1016
1069
|
}
|
|
1017
1070
|
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Return paths to every state.vscdb found under workspaceStoragePath/<hash>/state.vscdb.
|
|
1073
|
+
* Used for the apply_to_all_workspaces fallback when no specific workspace vscdb was collected.
|
|
1074
|
+
* Exported so compliance_check.ts can verify workspace vscdb state during ops-based checks.
|
|
1075
|
+
*/
|
|
1076
|
+
export function discoverAllWorkspaceVscdbs(workspaceStoragePath) {
|
|
1077
|
+
if (!existsSync(workspaceStoragePath))
|
|
1078
|
+
return [];
|
|
1079
|
+
try {
|
|
1080
|
+
return readdirSync(workspaceStoragePath, { withFileTypes: true })
|
|
1081
|
+
.filter((e) => e.isDirectory())
|
|
1082
|
+
.map((e) => join(workspaceStoragePath, e.name, 'state.vscdb'))
|
|
1083
|
+
.filter((p) => existsSync(p));
|
|
1084
|
+
}
|
|
1085
|
+
catch {
|
|
1086
|
+
return [];
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1018
1089
|
export function enforceRemediation(instruction) {
|
|
1019
1090
|
const resolvedPath = resolveRemediationConfigPath(instruction.config_file_path);
|
|
1020
1091
|
const inst = resolvedPath === instruction.config_file_path
|
|
@@ -1045,9 +1116,48 @@ export function enforceRemediation(instruction) {
|
|
|
1045
1116
|
});
|
|
1046
1117
|
if (sqliteOps.length > 0) {
|
|
1047
1118
|
complianceRunnerDiag(`remediation_enforce: sqlite path uuid=${inst.uuid} checks_with_sqlite=${sqliteOps.length}`);
|
|
1119
|
+
const ops = sqliteOps.map((c) => c.sqlite_op);
|
|
1120
|
+
const firstOp = ops[0];
|
|
1121
|
+
// apply_to_all_workspaces: no specific vscdb was collected — discover and patch every
|
|
1122
|
+
// workspaceStorage/<hash>/state.vscdb on disk (fallback for mcp_config-linked findings).
|
|
1123
|
+
if (firstOp?.apply_to_all_workspaces) {
|
|
1124
|
+
const segments = firstOp.workspace_storage_path_segments ?? [
|
|
1125
|
+
'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage',
|
|
1126
|
+
];
|
|
1127
|
+
const wsPath = join(homedir(), ...segments);
|
|
1128
|
+
const vscdbPaths = discoverAllWorkspaceVscdbs(wsPath);
|
|
1129
|
+
hookRunLog(`remediation_enforce: apply_to_all_workspaces uuid=${inst.uuid} wsPath=${wsPath} found=${vscdbPaths.length}`);
|
|
1130
|
+
if (vscdbPaths.length === 0) {
|
|
1131
|
+
return fail(`no workspace vscdbs found at ${wsPath} (open Cursor in at least one project first)`);
|
|
1132
|
+
}
|
|
1133
|
+
let anyOk = false;
|
|
1134
|
+
for (const vscdbPath of vscdbPaths) {
|
|
1135
|
+
// Queue a post-apply upload for each workspace vscdb so that after the deferred
|
|
1136
|
+
// SQLite write is applied at restart time, the updated cursor/disabledMcpServers
|
|
1137
|
+
// value is immediately sent to the server. This closes the race condition where
|
|
1138
|
+
// the user re-enables the server between the enforcement write and the next
|
|
1139
|
+
// scheduled log-config run: the upload after the deferred apply gives the server
|
|
1140
|
+
// the authoritative disabled state without waiting for a full log-config cycle.
|
|
1141
|
+
const postApplyUpload = {
|
|
1142
|
+
file_path: `${vscdbPath}#${firstOp.target_key}`,
|
|
1143
|
+
file_type: 'cursor_workspace_vscdb',
|
|
1144
|
+
};
|
|
1145
|
+
const q = queueDeferredSqliteOpsMerged(vscdbPath, ops, postApplyUpload);
|
|
1146
|
+
if (q.ok) {
|
|
1147
|
+
anyOk = true;
|
|
1148
|
+
hookRunLog(`remediation_enforce: queued apply_to_all_workspaces op for ${vscdbPath}`);
|
|
1149
|
+
}
|
|
1150
|
+
else {
|
|
1151
|
+
hookRunLog(`remediation_enforce: skip ${vscdbPath}: ${q.reason}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (!anyOk) {
|
|
1155
|
+
return fail('all workspace vscdb deferred writes failed (see sqlite_update lines above)');
|
|
1156
|
+
}
|
|
1157
|
+
return { ok: true, deferredSqlite: true };
|
|
1158
|
+
}
|
|
1048
1159
|
const restartRequired = !!fixSpec?.restart_required;
|
|
1049
1160
|
if (restartRequired) {
|
|
1050
|
-
const ops = sqliteOps.map((c) => c.sqlite_op);
|
|
1051
1161
|
const ft = inst.file_type?.trim();
|
|
1052
1162
|
const postApplyUpload = ft && inst.config_file_path.includes('#')
|
|
1053
1163
|
? { file_path: inst.config_file_path, file_type: ft }
|
package/dist/tofu.js
CHANGED
|
@@ -30,7 +30,7 @@ function readEnvironment() {
|
|
|
30
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const isDevelopment = readEnvironment() === "development";
|
|
32
32
|
// In development: resolve local optimus-tofu dist relative to this file.
|
|
33
|
-
// dist/tofu.js → ../.. → npx_packages/
|
|
33
|
+
// dist/tofu.js → ../.. → npx_packages/staging/ → optimus-tofu/dist/index.js
|
|
34
34
|
const localTofuPath = path.join(__dirname, "../../optimus-tofu/dist/index.js");
|
|
35
35
|
const tofu = (isDevelopment
|
|
36
36
|
? await import(localTofuPath)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "log-llm-config",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.63",
|
|
4
4
|
"description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -57,6 +57,6 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"axios": "^1.15.0",
|
|
59
59
|
"canonicalize": "^2.1.0",
|
|
60
|
-
"optimus-tofu": "
|
|
60
|
+
"optimus-tofu": "^0.1.14"
|
|
61
61
|
}
|
|
62
62
|
}
|