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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compliance check: types and check runner entry point.
|
|
3
|
+
*
|
|
4
|
+
* Compliance is in-memory only (no compliance.json): runLocalRemediationComplianceCheck returns
|
|
5
|
+
* ComplianceStatus; the prompt gate and tests use that return value. Autofix may write config files
|
|
6
|
+
* and remediation_instructions.json via applyAutofixViolations / enforceRemediation.
|
|
7
|
+
*
|
|
8
|
+
* Prompt gate: compliance_prompt_gate runs runLocalRemediationComplianceCheck then applyAutofixViolations.
|
|
9
|
+
* Background: compliance_check_runner runs syncRemediations (network) then the same local check.
|
|
10
|
+
* Apply (autofix) is intentionally left to the gate on the next prompt — the background pass only
|
|
11
|
+
* downloads the latest manifest so the gate has fresh data to act on.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { readVscdbItemTableJson } from '../readers/vscdb_reader.js';
|
|
15
|
+
import { readRemediationInstructionsFile, writeRemediationInstructionsFile } from './management_storage.js';
|
|
16
|
+
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
17
|
+
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
18
|
+
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
19
|
+
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
20
|
+
import { buildDeferredCursorRestartCommand, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
21
|
+
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
22
|
+
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* ItemTable keys use slashes (e.g. cursorai/donotchange/privacyMode) and do not contain dots.
|
|
28
|
+
* Compliance setting_path is `${itemKey}.${nested.path}` — take the segment before the first `.`.
|
|
29
|
+
*/
|
|
30
|
+
export function itemTableKeyFromSettingPath(settingPath) {
|
|
31
|
+
const i = settingPath.indexOf('.');
|
|
32
|
+
return i === -1 ? settingPath : settingPath.slice(0, i);
|
|
33
|
+
}
|
|
34
|
+
/** Traverse a JSON object using dot-notation path. Returns undefined if any segment is missing. */
|
|
35
|
+
export function getByPath(obj, path) {
|
|
36
|
+
const parts = path.split('.');
|
|
37
|
+
let current = obj;
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
if (current == null || typeof current !== 'object')
|
|
40
|
+
return undefined;
|
|
41
|
+
// Cursor composerState.modes4: non-numeric segment indexes by mode id (agent row is not always [0]).
|
|
42
|
+
if (Array.isArray(current) && !/^\d+$/.test(part)) {
|
|
43
|
+
const idx = current.findIndex((item) => item !== null &&
|
|
44
|
+
typeof item === 'object' &&
|
|
45
|
+
item.id === part);
|
|
46
|
+
current = idx >= 0 ? current[idx] : undefined;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
current = current[part];
|
|
50
|
+
}
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
/** Deep-equal comparison via JSON serialisation (handles booleans, strings, numbers, null). */
|
|
54
|
+
function deepEqual(a, b) {
|
|
55
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
56
|
+
}
|
|
57
|
+
function isStringArray(v) {
|
|
58
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
59
|
+
}
|
|
60
|
+
function verifyOpsApplied(configJson, settingPath, ops) {
|
|
61
|
+
const parts = settingPath.split('.');
|
|
62
|
+
const leafKey = parts[parts.length - 1] ?? '';
|
|
63
|
+
const parentPath = parts.slice(0, -1).join('.');
|
|
64
|
+
const set = ops.set ?? {};
|
|
65
|
+
const add = ops.add ?? {};
|
|
66
|
+
const remove = ops.remove ?? {};
|
|
67
|
+
const keys = new Set([...Object.keys(set), ...Object.keys(add), ...Object.keys(remove)]);
|
|
68
|
+
// If ops targets a single leaf key, check at settingPath; otherwise treat keys as subkeys under parent.
|
|
69
|
+
for (const k of keys) {
|
|
70
|
+
const targetPath = k === leafKey || (keys.size === 1 && (k === leafKey || k === ''))
|
|
71
|
+
? settingPath
|
|
72
|
+
: parentPath
|
|
73
|
+
? `${parentPath}.${k}`
|
|
74
|
+
: k;
|
|
75
|
+
if (Object.prototype.hasOwnProperty.call(set, k)) {
|
|
76
|
+
const cur = getByPath(configJson, targetPath);
|
|
77
|
+
const expected = set[k];
|
|
78
|
+
if (!deepEqual(cur, expected))
|
|
79
|
+
return { ok: false, expected: { op: 'set', path: targetPath, value: expected } };
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const cur = getByPath(configJson, targetPath);
|
|
83
|
+
const curArr = isStringArray(cur) ? cur : [];
|
|
84
|
+
const toAdd = add[k] ?? [];
|
|
85
|
+
const toRemove = remove[k] ?? [];
|
|
86
|
+
for (const item of toRemove) {
|
|
87
|
+
if (curArr.includes(item)) {
|
|
88
|
+
return { ok: false, expected: { op: 'remove', path: targetPath, value: item } };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const item of toAdd) {
|
|
92
|
+
if (!curArr.includes(item)) {
|
|
93
|
+
return { ok: false, expected: { op: 'add', path: targetPath, value: item } };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, expected: null };
|
|
98
|
+
}
|
|
99
|
+
/** Plain JSON file or virtual `…/state.vscdb#itemKey` path for ItemTable-backed settings. */
|
|
100
|
+
function loadRemediationConfigJson(configFilePath, checkSettingPaths = []) {
|
|
101
|
+
const resolvedPath = resolveRemediationConfigPath(configFilePath);
|
|
102
|
+
const hashIdx = resolvedPath.indexOf('#');
|
|
103
|
+
if (hashIdx >= 0) {
|
|
104
|
+
const dbPath = resolvedPath.slice(0, hashIdx);
|
|
105
|
+
const itemKeyFromPath = resolvedPath.slice(hashIdx + 1).trim();
|
|
106
|
+
if (!itemKeyFromPath)
|
|
107
|
+
return { ok: false, reason: 'empty_vscdb_key' };
|
|
108
|
+
if (!existsSync(dbPath))
|
|
109
|
+
return { ok: false, reason: 'db_not_found' };
|
|
110
|
+
const keys = new Set([itemKeyFromPath, ...checkSettingPaths.map(itemTableKeyFromSettingPath)].filter(Boolean));
|
|
111
|
+
const merged = {};
|
|
112
|
+
for (const k of keys) {
|
|
113
|
+
const wrapped = readVscdbItemTableJson(dbPath, k);
|
|
114
|
+
if (wrapped === null)
|
|
115
|
+
return { ok: false, reason: 'vscdb_read_failed' };
|
|
116
|
+
Object.assign(merged, wrapped);
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, json: merged };
|
|
119
|
+
}
|
|
120
|
+
if (!existsSync(resolvedPath))
|
|
121
|
+
return { ok: false, reason: 'file_not_found' };
|
|
122
|
+
try {
|
|
123
|
+
return { ok: true, json: JSON.parse(readFileSync(resolvedPath, 'utf8')) };
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return { ok: false, reason: 'parse_error' };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Check runner — Section 6: real per-check evaluation
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
/**
|
|
133
|
+
* Evaluate current on-disk configs against remediation_instructions.json only (no server).
|
|
134
|
+
* Returns status for prompt gating / callers; does not persist compliance.json.
|
|
135
|
+
*/
|
|
136
|
+
export function runLocalRemediationComplianceCheck() {
|
|
137
|
+
try {
|
|
138
|
+
const { remediations: rawEntries } = readRemediationInstructionsFile();
|
|
139
|
+
const entries = rawEntries;
|
|
140
|
+
if (entries.length === 0) {
|
|
141
|
+
hookRunLog('compliance_check: no remediation instructions present');
|
|
142
|
+
return { status: 'ok', checked_at: new Date().toISOString(), manifest_uuids: [], violations: [] };
|
|
143
|
+
}
|
|
144
|
+
const uuids = entries.map((e) => e.uuid);
|
|
145
|
+
const violations = [];
|
|
146
|
+
let skippedNoCompliance = 0;
|
|
147
|
+
let skippedNonJson = 0;
|
|
148
|
+
let skippedNoChecks = 0;
|
|
149
|
+
let skippedUnreadable = 0;
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const compliance = entry.fix ?? entry.compliance;
|
|
152
|
+
if (!compliance) {
|
|
153
|
+
skippedNoCompliance++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (compliance.file_format !== 'json') {
|
|
157
|
+
skippedNonJson++;
|
|
158
|
+
hookRunLog(`compliance_check: skipping non-json entry uuid=${entry.uuid}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const checks = compliance.checks ?? [];
|
|
162
|
+
if (checks.length === 0) {
|
|
163
|
+
skippedNoChecks++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const loaded = loadRemediationConfigJson(entry.config_file_path, checks.map((c) => c.setting_path));
|
|
167
|
+
if (!loaded.ok) {
|
|
168
|
+
skippedUnreadable++;
|
|
169
|
+
const msg = loaded.reason === 'file_not_found'
|
|
170
|
+
? `compliance_check: config file not found, skipping uuid=${entry.uuid}`
|
|
171
|
+
: loaded.reason === 'db_not_found'
|
|
172
|
+
? `compliance_check: vscdb file not found, skipping uuid=${entry.uuid}`
|
|
173
|
+
: loaded.reason === 'vscdb_read_failed'
|
|
174
|
+
? `compliance_check: could not read vscdb (sqlite3 missing or invalid JSON?), skipping uuid=${entry.uuid}`
|
|
175
|
+
: loaded.reason === 'empty_vscdb_key'
|
|
176
|
+
? `compliance_check: invalid vscdb path (empty # key), skipping uuid=${entry.uuid}`
|
|
177
|
+
: `compliance_check: could not parse config file, skipping uuid=${entry.uuid}`;
|
|
178
|
+
hookRunLog(msg);
|
|
179
|
+
logRemediationApplyFailure('compliance_check_config_unreadable', {
|
|
180
|
+
uuid: entry.uuid,
|
|
181
|
+
config_file_path: entry.config_file_path,
|
|
182
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
183
|
+
load_reason: loaded.reason,
|
|
184
|
+
reason: 'cannot read config for compliance verify — remediation may not run until path/db is valid',
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const configJson = loaded.json;
|
|
189
|
+
for (const check of checks) {
|
|
190
|
+
// Prefer ops-based verification (matches delta apply semantics; doesn't require full after snapshot).
|
|
191
|
+
if (check.ops) {
|
|
192
|
+
const { ok, expected } = verifyOpsApplied(configJson, check.setting_path, check.ops);
|
|
193
|
+
if (!ok) {
|
|
194
|
+
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} expected=${JSON.stringify(expected)}`);
|
|
195
|
+
violations.push({
|
|
196
|
+
uuid: entry.uuid,
|
|
197
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
198
|
+
setting_path: check.setting_path,
|
|
199
|
+
description: check.description,
|
|
200
|
+
finding_title: entry.finding_title,
|
|
201
|
+
finding_description: entry.finding_description,
|
|
202
|
+
severity: compliance.severity,
|
|
203
|
+
autofix_allowed: compliance.autofix_allowed,
|
|
204
|
+
config_file_path: entry.config_file_path,
|
|
205
|
+
expected_value: expected,
|
|
206
|
+
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 ${entry.config_file_path}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
// Backwards compat: old local files may still carry after snapshots.
|
|
212
|
+
const currentValue = getByPath(configJson, check.setting_path);
|
|
213
|
+
const leafKey = check.setting_path.split('.').pop();
|
|
214
|
+
const expectedValue = check.after?.[leafKey];
|
|
215
|
+
if (expectedValue === undefined)
|
|
216
|
+
continue;
|
|
217
|
+
if (!deepEqual(currentValue, expectedValue)) {
|
|
218
|
+
hookRunLog(`compliance_check: VIOLATION uuid=${entry.uuid} path=${check.setting_path} current=${JSON.stringify(currentValue)} expected=${JSON.stringify(expectedValue)}`);
|
|
219
|
+
violations.push({
|
|
220
|
+
uuid: entry.uuid,
|
|
221
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
222
|
+
setting_path: check.setting_path,
|
|
223
|
+
description: check.description,
|
|
224
|
+
finding_title: entry.finding_title,
|
|
225
|
+
finding_description: entry.finding_description,
|
|
226
|
+
severity: compliance.severity,
|
|
227
|
+
autofix_allowed: compliance.autofix_allowed,
|
|
228
|
+
config_file_path: entry.config_file_path,
|
|
229
|
+
expected_value: expectedValue,
|
|
230
|
+
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Set ${check.setting_path} to ${JSON.stringify(expectedValue)} in ${entry.config_file_path}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const status = {
|
|
236
|
+
status: violations.length > 0 ? 'fail' : 'ok',
|
|
237
|
+
checked_at: new Date().toISOString(),
|
|
238
|
+
manifest_uuids: uuids,
|
|
239
|
+
violations,
|
|
240
|
+
};
|
|
241
|
+
const skipParts = [];
|
|
242
|
+
if (skippedNoCompliance)
|
|
243
|
+
skipParts.push(`no_fix=${skippedNoCompliance}`);
|
|
244
|
+
if (skippedNonJson)
|
|
245
|
+
skipParts.push(`non_json=${skippedNonJson}`);
|
|
246
|
+
if (skippedNoChecks)
|
|
247
|
+
skipParts.push(`no_checks=${skippedNoChecks}`);
|
|
248
|
+
if (skippedUnreadable)
|
|
249
|
+
skipParts.push(`unreadable_config=${skippedUnreadable}`);
|
|
250
|
+
const skipSummary = skipParts.length > 0 ? ` skipped{${skipParts.join(' ')}}` : '';
|
|
251
|
+
hookRunLog(`compliance_check: done — status=${status.status} violations=${violations.length} manifest_entries=${uuids.length}${skipSummary}`);
|
|
252
|
+
if (skippedUnreadable > 0) {
|
|
253
|
+
complianceRunnerDiag(`compliance_check: ${skippedUnreadable} manifest row(s) skipped — vscdb/config unreadable (often DB locked while Cursor runs, or sqlite3/PATH). Autofix cannot evaluate until read succeeds.`);
|
|
254
|
+
}
|
|
255
|
+
return status;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
const emsg = err instanceof Error ? err.message : String(err);
|
|
259
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
260
|
+
hookRunLog(`compliance_check: unexpected error: ${emsg}`);
|
|
261
|
+
logRemediationApplyFailure('compliance_check_exception', {
|
|
262
|
+
reason: 'runLocalRemediationComplianceCheck threw',
|
|
263
|
+
error: emsg,
|
|
264
|
+
stack: stack ?? '',
|
|
265
|
+
});
|
|
266
|
+
const fallback = {
|
|
267
|
+
status: 'ok',
|
|
268
|
+
checked_at: new Date().toISOString(),
|
|
269
|
+
manifest_uuids: [],
|
|
270
|
+
violations: [],
|
|
271
|
+
};
|
|
272
|
+
return fallback;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
export function applyAutofixViolations(violations) {
|
|
276
|
+
for (const v of violations) {
|
|
277
|
+
if (!v.autofix_allowed) {
|
|
278
|
+
logRemediationApplyFailure('autofix_skipped_not_allowed', {
|
|
279
|
+
uuid: v.uuid,
|
|
280
|
+
finding_formatted_id: v.finding_formatted_id,
|
|
281
|
+
config_file_path: v.config_file_path,
|
|
282
|
+
setting_path: v.setting_path,
|
|
283
|
+
reason: 'autofix_allowed is false — policy/manual fix required',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const autofixable = violations.filter((v) => v.autofix_allowed);
|
|
288
|
+
if (autofixable.length === 0)
|
|
289
|
+
return {
|
|
290
|
+
fixed: 0,
|
|
291
|
+
appliedViolations: [],
|
|
292
|
+
restartCommands: [],
|
|
293
|
+
failedViolations: [],
|
|
294
|
+
reportPromises: [],
|
|
295
|
+
};
|
|
296
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
297
|
+
const byUuid = new Map(remediations.map((r) => [r.uuid, r]));
|
|
298
|
+
let fixed = 0;
|
|
299
|
+
const appliedViolations = [];
|
|
300
|
+
const seen = new Set();
|
|
301
|
+
const restartCommands = [];
|
|
302
|
+
const failedViolations = [];
|
|
303
|
+
const reportPromises = [];
|
|
304
|
+
const oneTimeAppliedUuids = new Set();
|
|
305
|
+
let deferredSqlitePending = false;
|
|
306
|
+
for (const violation of autofixable) {
|
|
307
|
+
if (seen.has(violation.uuid))
|
|
308
|
+
continue;
|
|
309
|
+
const instruction = byUuid.get(violation.uuid);
|
|
310
|
+
if (!instruction) {
|
|
311
|
+
hookRunLog(`autofix: no instruction found for uuid=${violation.uuid}, skipping`);
|
|
312
|
+
logRemediationApplyFailure('autofix_no_local_instruction', {
|
|
313
|
+
uuid: violation.uuid,
|
|
314
|
+
finding_formatted_id: violation.finding_formatted_id,
|
|
315
|
+
config_file_path: violation.config_file_path,
|
|
316
|
+
setting_path: violation.setting_path,
|
|
317
|
+
reason: 'violation UUID not present in remediation_instructions.json remediations[]',
|
|
318
|
+
});
|
|
319
|
+
failedViolations.push(violation);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const inst = instruction;
|
|
323
|
+
const configPathForDisk = resolveRemediationConfigPath(inst.config_file_path);
|
|
324
|
+
complianceRunnerDiag(`autofix: calling enforceRemediation uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
325
|
+
const er = enforceRemediation(inst);
|
|
326
|
+
if (!er.ok) {
|
|
327
|
+
failedViolations.push(violation);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (er.deferredSqlite)
|
|
331
|
+
deferredSqlitePending = true;
|
|
332
|
+
seen.add(violation.uuid);
|
|
333
|
+
fixed++;
|
|
334
|
+
appliedViolations.push(violation);
|
|
335
|
+
hookRunLog(`autofix: applied uuid=${inst.uuid} path=${configPathForDisk}`);
|
|
336
|
+
reportPromises.push(reportAutofixApplied(inst.uuid, 'success'));
|
|
337
|
+
const authKey = readStoredAuthKey();
|
|
338
|
+
if (authKey) {
|
|
339
|
+
if (er.deferredSqlite && configPathForDisk.includes('#')) {
|
|
340
|
+
hookRunLog(`autofix: skip immediate vscdb upload (deferred until after restart) uuid=${inst.uuid}`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
let updatedContent;
|
|
344
|
+
if (configPathForDisk.includes('#')) {
|
|
345
|
+
const hi = configPathForDisk.indexOf('#');
|
|
346
|
+
const dbPath = configPathForDisk.slice(0, hi);
|
|
347
|
+
const itemKey = configPathForDisk.slice(hi + 1).trim();
|
|
348
|
+
updatedContent =
|
|
349
|
+
itemKey ? (readVscdbItemTableJson(dbPath, itemKey) ?? undefined) : undefined;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
try {
|
|
353
|
+
updatedContent = JSON.parse(readFileSync(configPathForDisk, 'utf8'));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
updatedContent = undefined;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (updatedContent !== undefined) {
|
|
360
|
+
const fileType = (inst.file_type ?? '').trim();
|
|
361
|
+
if (fileType) {
|
|
362
|
+
const hw = tryResolveHardwareUuid();
|
|
363
|
+
if (hw) {
|
|
364
|
+
reportPromises.push(sendConfigFile({ file_type: fileType, file_path: configPathForDisk, raw_content: updatedContent }, hw, authKey).then((sentOk) => {
|
|
365
|
+
hookRunLog(`autofix: uploaded remediated file uuid=${inst.uuid} path=${configPathForDisk} ok=${sentOk}`);
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
hookRunLog(`autofix: skip upload uuid=${inst.uuid} (hardware UUID unavailable)`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
hookRunLog(`autofix: skip upload uuid=${inst.uuid} — remediation_instructions.json missing file_type (re-sync manifest)`);
|
|
374
|
+
logRemediationApplyFailure('autofix_post_apply_upload_skipped', {
|
|
375
|
+
uuid: inst.uuid,
|
|
376
|
+
config_file_path: inst.config_file_path,
|
|
377
|
+
reason: 'file_type missing on instruction — server sync/manifest will not see applied file',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
hookRunLog(`autofix: skip re-upload uuid=${inst.uuid} (no stored auth key)`);
|
|
385
|
+
}
|
|
386
|
+
const spec = remediationFixSpec(inst);
|
|
387
|
+
if (spec?.restart_required && spec.restart_command) {
|
|
388
|
+
if (!er.deferredSqlite) {
|
|
389
|
+
if (isTrustedRestartCommandForAutofix(spec.restart_command)) {
|
|
390
|
+
hookRunLog(`autofix: restart required uuid=${inst.uuid} command=${spec.restart_command}`);
|
|
391
|
+
restartCommands.push(spec.restart_command);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
hookRunLog(`autofix: restart command rejected (not an allowlisted template) uuid=${inst.uuid}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!inst.is_enforced) {
|
|
399
|
+
oneTimeAppliedUuids.add(inst.uuid);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (deferredSqlitePending) {
|
|
403
|
+
restartCommands.length = 0;
|
|
404
|
+
restartCommands.push(buildDeferredCursorRestartCommand());
|
|
405
|
+
hookRunLog('autofix: deferred vscdb — restart command runs apply_deferred_vscdb.js then open -a Cursor');
|
|
406
|
+
}
|
|
407
|
+
if (oneTimeAppliedUuids.size > 0) {
|
|
408
|
+
const remaining = remediations.filter((r) => !oneTimeAppliedUuids.has(r.uuid));
|
|
409
|
+
writeRemediationInstructionsFile({ remediations: remaining });
|
|
410
|
+
hookRunLog(`autofix: removed ${oneTimeAppliedUuids.size} one-time remediation(s) from local store`);
|
|
411
|
+
// Send a post-autofix heartbeat so the server sees the updated (reduced) UUID set immediately,
|
|
412
|
+
// without waiting for the background runner (which may be locked out).
|
|
413
|
+
const remainingUuids = remaining.map((r) => r.uuid);
|
|
414
|
+
const hwHeartbeat = tryResolveHardwareUuid();
|
|
415
|
+
if (hwHeartbeat) {
|
|
416
|
+
reportPromises.push(fetchSync(loadEndpointBase(), hwHeartbeat, remainingUuids)
|
|
417
|
+
.then(() => hookRunLog(`autofix: post-autofix heartbeat sent uuids=${remainingUuids.length}`))
|
|
418
|
+
.catch(() => undefined));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
hookRunLog('autofix: skip post-autofix heartbeat (hardware UUID unavailable)');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (fixed > 0) {
|
|
425
|
+
hookRunLog(`autofix: total_applied=${fixed}`);
|
|
426
|
+
}
|
|
427
|
+
return { fixed, appliedViolations, restartCommands, failedViolations, reportPromises, deferredSqlitePending };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Remove satisfied one-time remediations from local remediation_instructions.json.
|
|
431
|
+
*
|
|
432
|
+
* This handles the case where a user (or another tool) manually applied the change, so the
|
|
433
|
+
* instruction no longer needs to persist locally. Enforced remediations are never pruned.
|
|
434
|
+
*
|
|
435
|
+
* Returns number removed and any async report promises (heartbeat).
|
|
436
|
+
*/
|
|
437
|
+
export function pruneSatisfiedOneTimeRemediations() {
|
|
438
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
439
|
+
if (!Array.isArray(remediations) || remediations.length === 0)
|
|
440
|
+
return { removed: 0, reportPromises: [] };
|
|
441
|
+
const remaining = [];
|
|
442
|
+
let removed = 0;
|
|
443
|
+
for (const raw of remediations) {
|
|
444
|
+
const inst = raw;
|
|
445
|
+
if (inst.is_enforced) {
|
|
446
|
+
remaining.push(raw);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const spec = remediationFixSpec(inst);
|
|
450
|
+
const checks = spec?.file_format === 'json' ? (spec.checks ?? []) : [];
|
|
451
|
+
if (checks.length === 0) {
|
|
452
|
+
remaining.push(raw);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const prLoaded = loadRemediationConfigJson(inst.config_file_path, checks.map((c) => c.setting_path));
|
|
456
|
+
if (!prLoaded.ok) {
|
|
457
|
+
remaining.push(raw);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const configJson = prLoaded.json;
|
|
461
|
+
// Only prune when every check is ops-based and currently satisfied.
|
|
462
|
+
let okAll = true;
|
|
463
|
+
for (const check of checks) {
|
|
464
|
+
if (!check.ops) {
|
|
465
|
+
okAll = false;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
const res = verifyOpsApplied(configJson, check.setting_path, check.ops);
|
|
469
|
+
if (!res.ok) {
|
|
470
|
+
okAll = false;
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (okAll) {
|
|
475
|
+
removed++;
|
|
476
|
+
hookRunLog(`remediation_prune: satisfied one-time uuid=${inst.uuid} path=${inst.config_file_path}`);
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
remaining.push(raw);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (removed === 0)
|
|
483
|
+
return { removed: 0, reportPromises: [] };
|
|
484
|
+
writeRemediationInstructionsFile({ remediations: remaining });
|
|
485
|
+
hookRunLog(`remediation_prune: removed=${removed} remaining=${remaining.length}`);
|
|
486
|
+
const remainingUuids = remaining.map((r) => r.uuid);
|
|
487
|
+
const hw = tryResolveHardwareUuid();
|
|
488
|
+
const reportPromises = [];
|
|
489
|
+
if (hw) {
|
|
490
|
+
reportPromises.push(fetchSync(loadEndpointBase(), hw, remainingUuids)
|
|
491
|
+
.then(() => hookRunLog(`remediation_prune: post-prune heartbeat sent uuids=${remainingUuids.length}`))
|
|
492
|
+
.catch(() => undefined));
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
hookRunLog('remediation_prune: skip post-prune heartbeat (hardware UUID unavailable)');
|
|
496
|
+
}
|
|
497
|
+
return { removed, reportPromises };
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
|
|
501
|
+
* Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
|
|
502
|
+
* a fresh manifest so the gate has up-to-date data when it runs.
|
|
503
|
+
*/
|
|
504
|
+
export async function runComplianceCheck() {
|
|
505
|
+
try {
|
|
506
|
+
await syncRemediations(loadEndpointBase(), resolveHardwareUuid());
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
hookRunLog(`compliance_check: remediation_sync unexpected error: ${err instanceof Error ? err.message : String(err)}`);
|
|
510
|
+
}
|
|
511
|
+
const status = runLocalRemediationComplianceCheck();
|
|
512
|
+
if (status.status === 'ok' || status.violations.length === 0) {
|
|
513
|
+
const pruned = pruneSatisfiedOneTimeRemediations();
|
|
514
|
+
if (pruned.removed > 0) {
|
|
515
|
+
await Promise.allSettled(pruned.reportPromises);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
/** Resolve hardware UUID from environment or system commands. */
|
|
3
|
+
function resolveHardwareUuid() {
|
|
4
|
+
const envUuid = process.env.OPTIMUS_HARDWARE_UUID?.trim();
|
|
5
|
+
if (envUuid)
|
|
6
|
+
return envUuid;
|
|
7
|
+
try {
|
|
8
|
+
const ioregOutput = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { encoding: 'utf8' }).trim();
|
|
9
|
+
const ioregMatch = ioregOutput.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/i);
|
|
10
|
+
if (ioregMatch?.[1])
|
|
11
|
+
return ioregMatch[1];
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// try next method
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const profilerOutput = execSync('system_profiler SPHardwareDataType', { encoding: 'utf8' }).trim();
|
|
18
|
+
const profilerMatch = profilerOutput.match(/Hardware UUID:\s*([A-F0-9-]+)/i);
|
|
19
|
+
if (profilerMatch?.[1])
|
|
20
|
+
return profilerMatch[1];
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// both methods failed
|
|
24
|
+
}
|
|
25
|
+
throw new Error('Unable to determine hardware UUID via ioreg or system_profiler.');
|
|
26
|
+
}
|
|
27
|
+
/** Same as {@link resolveHardwareUuid} but returns null when the host exposes no stable ID (e.g. Linux CI). */
|
|
28
|
+
export function tryResolveHardwareUuid() {
|
|
29
|
+
try {
|
|
30
|
+
return resolveHardwareUuid();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export { resolveHardwareUuid };
|