log-llm-config 1.4.12 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apply_deferred_vscdb.js +2 -2
- package/dist/bootstrap_constants.js +10 -0
- package/dist/cli.js +31 -1
- package/dist/compliance_check_runner.js +7 -2
- package/dist/compliance_prompt_gate.js +46 -17
- package/dist/log_config_files/auth/auth_flow.js +1 -1
- package/dist/log_config_files/auth/auth_key_store.js +1 -1
- package/dist/log_config_files/collection/config_collector.js +12 -0
- package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
- package/dist/log_config_files/paths/pattern_resolver.js +1 -1
- package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
- package/dist/log_config_files/runtime/compliance_check.js +67 -20
- package/dist/log_config_files/runtime/compliance_session_log.js +22 -16
- package/dist/log_config_files/runtime/hook_logger.js +33 -20
- package/dist/log_config_files/runtime/log_metadata.js +122 -0
- package/dist/log_config_files/runtime/main_runner.js +25 -5
- package/dist/log_config_files/runtime/management_storage.js +36 -7
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +4 -2
- package/dist/log_config_files/runtime/remediation_config_path.js +12 -1
- package/dist/log_config_files/runtime/remediation_sync.js +76 -8
- package/dist/log_config_files/sender/batch_sender.js +1 -1
- package/dist/log_sensitive_paths_audit.js +3 -3
- package/dist/log_uuid/auth_key_store.js +1 -1
- package/dist/log_uuid/startup_sender.js +21 -3
- package/package.json +1 -1
- package/dist/log_config_files/readers/vscdb_item_table_registry.js +0 -82
- package/dist/log_config_files/runtime/secretlint_scan.js +0 -69
- package/dist/post_restart_verify.js +0 -48
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget client telemetry → POST /endpoint_security/api/client-event/.
|
|
3
|
+
*
|
|
4
|
+
* Reports HookRun / FunctionalRun / Flow so the backend has visibility into what ran and
|
|
5
|
+
* what failed — even when a unit fails locally before any happy-path object exists.
|
|
6
|
+
*
|
|
7
|
+
* Guarantees the event is never lost AND never breaks the hook:
|
|
8
|
+
* - All calls are best-effort and never throw.
|
|
9
|
+
* - On send failure (backend down / network / no auth) the event is appended to a durable
|
|
10
|
+
* JSONL buffer and flushed on the next successful send (atomic-rename claim, like tool-call).
|
|
11
|
+
* - The backend accepts uuid-keyed reports without a valid signature, so auth failures report too.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, mkdirSync, appendFileSync, readFileSync, renameSync, unlinkSync, statSync } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
|
|
17
|
+
import { executeBody } from '../../endpoint_client/http_transport.js';
|
|
18
|
+
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
19
|
+
import { createSignature } from '../sender/signing.js';
|
|
20
|
+
import { readStoredAuthKey } from '../auth/auth_key_store.js';
|
|
21
|
+
import { resolveHardwareUuid } from './hardware_uuid.js';
|
|
22
|
+
import { buildLogMetadataObject } from './log_metadata.js';
|
|
23
|
+
const CLIENT_EVENT_PATH = '/endpoint_security/api/client-event/';
|
|
24
|
+
const SEND_TIMEOUT_MS = 4000;
|
|
25
|
+
const BUFFER_BASENAME = 'client_events.jsonl';
|
|
26
|
+
const MAX_BUFFER_BYTES = 1 * 1024 * 1024;
|
|
27
|
+
const MAX_FLUSH_EVENTS = 50;
|
|
28
|
+
function bufferPath() {
|
|
29
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
30
|
+
if (!home)
|
|
31
|
+
return null;
|
|
32
|
+
return path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry', BUFFER_BASENAME);
|
|
33
|
+
}
|
|
34
|
+
/** Keep filename-safe chars only — must match optimus_sanitize_id in optimus-hook-common.sh. */
|
|
35
|
+
function sanitizeId(v) {
|
|
36
|
+
return v.replace(/[^A-Za-z0-9._-]/g, '');
|
|
37
|
+
}
|
|
38
|
+
/** File start-every-prompt overwrites each submit with this prompt's story id (short-lived handoff). */
|
|
39
|
+
function storyFilePath() {
|
|
40
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
41
|
+
if (!home)
|
|
42
|
+
return null;
|
|
43
|
+
const telemetryDir = path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry');
|
|
44
|
+
const session = sanitizeId((process.env.OPTIMUS_SESSION_ID || '').trim());
|
|
45
|
+
if (session) {
|
|
46
|
+
const agent = sanitizeId((process.env.OPTIMUS_HOOK_TYPE || process.env.OPTIMUS_AGENT || 'claude').trim()) || 'claude';
|
|
47
|
+
return path.join(telemetryDir, `current_story.${agent}.${session}.id`);
|
|
48
|
+
}
|
|
49
|
+
return path.join(telemetryDir, 'current_story.id');
|
|
50
|
+
}
|
|
51
|
+
/** Match shell OPTIMUS_STORY_FILE_MAX_AGE_SEC — ignore stale ids from a prior prompt. */
|
|
52
|
+
const STORY_FILE_MAX_AGE_MS = 180_000;
|
|
53
|
+
function readStoryIdFromFileIfFresh(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
const st = statSync(filePath);
|
|
56
|
+
if (Date.now() - st.mtimeMs > STORY_FILE_MAX_AGE_MS)
|
|
57
|
+
return null;
|
|
58
|
+
const fromFile = readFileSync(filePath, 'utf8').trim();
|
|
59
|
+
return fromFile || null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Map a functional unit to the shell hook that runs it (every npm command belongs to one hook). */
|
|
66
|
+
const UNIT_HOOK = {
|
|
67
|
+
inventory: 'start_every_prompt',
|
|
68
|
+
auth_version: 'start_every_prompt',
|
|
69
|
+
compliance: 'compliance_check',
|
|
70
|
+
tool_call: 'tool_call',
|
|
71
|
+
install: 'install',
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* story_id groups all hooks + npm of one prompt submit. Resolution order:
|
|
75
|
+
* 1. OPTIMUS_STORY_ID env (when the hook successfully exported it), then
|
|
76
|
+
* 2. current_story.id if mtime is fresh (same temp handoff the shell hooks use), then
|
|
77
|
+
* 3. a fresh id (standalone / manually-run command).
|
|
78
|
+
* Cached on the env for the rest of this process.
|
|
79
|
+
*/
|
|
80
|
+
export function getStoryId() {
|
|
81
|
+
const fromEnv = (process.env.OPTIMUS_STORY_ID || '').trim();
|
|
82
|
+
if (fromEnv)
|
|
83
|
+
return fromEnv;
|
|
84
|
+
const p = storyFilePath();
|
|
85
|
+
if (p) {
|
|
86
|
+
const fromFile = readStoryIdFromFileIfFresh(p);
|
|
87
|
+
if (fromFile) {
|
|
88
|
+
process.env.OPTIMUS_STORY_ID = fromFile;
|
|
89
|
+
return fromFile;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const id = randomUUID();
|
|
93
|
+
process.env.OPTIMUS_STORY_ID = id;
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
/** The shell hook this process runs under: env, else derived from the unit. */
|
|
97
|
+
function resolveHookName(unit) {
|
|
98
|
+
const fromEnv = (process.env.OPTIMUS_HOOK_NAME || '').trim();
|
|
99
|
+
if (fromEnv)
|
|
100
|
+
return fromEnv;
|
|
101
|
+
if (unit && UNIT_HOOK[unit])
|
|
102
|
+
return UNIT_HOOK[unit];
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
/** File written by optimus_hook_begin for this hook invocation (gate + runner share it). */
|
|
106
|
+
function hookRunFilePath() {
|
|
107
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
108
|
+
if (!home)
|
|
109
|
+
return null;
|
|
110
|
+
return path.join(home, OPT_AI_SEC_MANAGEMENT_REL, 'telemetry', 'current_hook_run.id');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* hook_run_id: one uuid per shell hook invocation.
|
|
114
|
+
* 1. OPTIMUS_HOOK_RUN_ID env
|
|
115
|
+
* 2. current_hook_run.id (same hook process tree)
|
|
116
|
+
* 3. `${story_id}::${hook_name}` legacy fallback only
|
|
117
|
+
*/
|
|
118
|
+
export function getHookRunId(unit) {
|
|
119
|
+
const fromEnv = (process.env.OPTIMUS_HOOK_RUN_ID || '').trim();
|
|
120
|
+
if (fromEnv)
|
|
121
|
+
return fromEnv;
|
|
122
|
+
const hp = hookRunFilePath();
|
|
123
|
+
if (hp) {
|
|
124
|
+
try {
|
|
125
|
+
const fromFile = readFileSync(hp, 'utf8').trim();
|
|
126
|
+
if (fromFile) {
|
|
127
|
+
process.env.OPTIMUS_HOOK_RUN_ID = fromFile;
|
|
128
|
+
return fromFile;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// fall through
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const hook = resolveHookName(unit);
|
|
136
|
+
return hook ? `${getStoryId()}::${hook}` : getStoryId();
|
|
137
|
+
}
|
|
138
|
+
function agentFromEnv() {
|
|
139
|
+
const v = (process.env.OPTIMUS_AGENT || process.env.OPTIMUS_HOOK_TYPE || '').trim().toLowerCase();
|
|
140
|
+
return v || 'unknown';
|
|
141
|
+
}
|
|
142
|
+
function enrich(kind, fields, unit) {
|
|
143
|
+
return {
|
|
144
|
+
kind,
|
|
145
|
+
story_id: fields.story_id ?? getStoryId(),
|
|
146
|
+
hook_run_id: fields.hook_run_id ?? getHookRunId(unit),
|
|
147
|
+
outcome: fields.outcome,
|
|
148
|
+
exit_code: fields.exit_code ?? null,
|
|
149
|
+
message: fields.message ?? '',
|
|
150
|
+
detail: fields.detail ?? '',
|
|
151
|
+
metadata: { ...buildLogMetadataObject(), ...(fields.metadata ?? {}) },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function appendToBuffer(events) {
|
|
155
|
+
const p = bufferPath();
|
|
156
|
+
if (!p)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
const dir = path.dirname(p);
|
|
160
|
+
if (!existsSync(dir))
|
|
161
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
162
|
+
if (existsSync(p) && statSync(p).size > MAX_BUFFER_BYTES)
|
|
163
|
+
return; // cap; drop rather than grow unbounded
|
|
164
|
+
appendFileSync(p, events.map((e) => JSON.stringify(e)).join('\n') + '\n', 'utf8');
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// best-effort
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/** Atomically claim + read buffered events so a concurrent hook won't double-send. */
|
|
171
|
+
function claimBufferedEvents() {
|
|
172
|
+
const p = bufferPath();
|
|
173
|
+
if (!p || !existsSync(p))
|
|
174
|
+
return [];
|
|
175
|
+
const claimed = `${p}.sending.${process.pid}`;
|
|
176
|
+
try {
|
|
177
|
+
renameSync(p, claimed);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const lines = readFileSync(claimed, 'utf8').split('\n').filter((l) => l.trim());
|
|
184
|
+
unlinkSync(claimed);
|
|
185
|
+
return lines
|
|
186
|
+
.slice(0, MAX_FLUSH_EVENTS)
|
|
187
|
+
.map((l) => {
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(l);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
.filter((e) => e !== null);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function postEvents(events) {
|
|
202
|
+
if (events.length === 0)
|
|
203
|
+
return true;
|
|
204
|
+
let hardwareUuid;
|
|
205
|
+
try {
|
|
206
|
+
hardwareUuid = resolveHardwareUuid();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
const base = loadEndpointBase().replace(/\/+$/, '');
|
|
212
|
+
const url = `${base}${CLIENT_EVENT_PATH}`;
|
|
213
|
+
const signingPayload = { hardware_uuid: hardwareUuid, events };
|
|
214
|
+
let signature = '';
|
|
215
|
+
try {
|
|
216
|
+
const authKey = readStoredAuthKey();
|
|
217
|
+
if (authKey?.key)
|
|
218
|
+
signature = createSignature(signingPayload, authKey.key);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// unsigned is fine — backend stores signature_valid=false and still records
|
|
222
|
+
}
|
|
223
|
+
const body = JSON.stringify({ ...signingPayload, signature });
|
|
224
|
+
try {
|
|
225
|
+
const res = await executeBody(url, 'POST', body, SEND_TIMEOUT_MS);
|
|
226
|
+
return res.statusCode >= 200 && res.statusCode < 300;
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Core: send these events + opportunistically flush the buffer; buffer on failure. Never throws. */
|
|
233
|
+
async function send(events) {
|
|
234
|
+
try {
|
|
235
|
+
const buffered = claimBufferedEvents();
|
|
236
|
+
const all = [...buffered, ...events];
|
|
237
|
+
const ok = await postEvents(all);
|
|
238
|
+
if (!ok)
|
|
239
|
+
appendToBuffer(all);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
appendToBuffer(events);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Each report* returns the send promise but never rejects. Callers on a hot path ignore it
|
|
247
|
+
* (fire-and-forget); callers on a process-exit path `await` it so the POST lands before exit.
|
|
248
|
+
*/
|
|
249
|
+
export function reportHookRun(fields) {
|
|
250
|
+
try {
|
|
251
|
+
const hook_name = fields.hook_name;
|
|
252
|
+
return send([
|
|
253
|
+
{
|
|
254
|
+
...enrich('hook_run', { ...fields, hook_run_id: fields.hook_run_id ?? getHookRunId(undefined) }),
|
|
255
|
+
agent: fields.agent ?? agentFromEnv(),
|
|
256
|
+
hook_name,
|
|
257
|
+
},
|
|
258
|
+
]);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return Promise.resolve();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export function reportFunctionalRun(fields) {
|
|
265
|
+
try {
|
|
266
|
+
return send([
|
|
267
|
+
{
|
|
268
|
+
...enrich('functional_run', fields, fields.unit),
|
|
269
|
+
unit: fields.unit,
|
|
270
|
+
command: fields.command ?? '',
|
|
271
|
+
hook_name: fields.hook_name ?? resolveHookName(fields.unit),
|
|
272
|
+
log_text: fields.log_text ?? '',
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return Promise.resolve();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Append a functional run to the durable buffer WITHOUT an inline POST. For latency-critical
|
|
282
|
+
* paths (the compliance gate blocks prompt submit) — the next background run flushes it.
|
|
283
|
+
*/
|
|
284
|
+
export function bufferFunctionalRun(fields) {
|
|
285
|
+
try {
|
|
286
|
+
appendToBuffer([
|
|
287
|
+
{
|
|
288
|
+
...enrich('functional_run', fields, fields.unit),
|
|
289
|
+
unit: fields.unit,
|
|
290
|
+
command: fields.command ?? '',
|
|
291
|
+
hook_name: fields.hook_name ?? resolveHookName(fields.unit),
|
|
292
|
+
log_text: fields.log_text ?? '',
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
/* never throw */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Convenience for catch blocks: report a functional-unit failure with stack. */
|
|
301
|
+
export function reportFunctionalFailure(unit, command, err, extra) {
|
|
302
|
+
const message = extra?.message ?? (err instanceof Error ? `${err.name}: ${err.message}` : String(err));
|
|
303
|
+
const detail = err instanceof Error && err.stack ? err.stack : String(err);
|
|
304
|
+
reportFunctionalRun({
|
|
305
|
+
unit,
|
|
306
|
+
command,
|
|
307
|
+
outcome: 'failed',
|
|
308
|
+
message,
|
|
309
|
+
detail,
|
|
310
|
+
exit_code: extra?.exit_code ?? null,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Awaitable failure report for the fatal-exit path (so the POST completes before process.exit).
|
|
315
|
+
* Falls back to the durable buffer if the send fails. Never throws.
|
|
316
|
+
*/
|
|
317
|
+
export async function reportFunctionalFailureAwait(unit, command, err, extra) {
|
|
318
|
+
try {
|
|
319
|
+
const message = extra?.message ?? (err instanceof Error ? `${err.name}: ${err.message}` : String(err));
|
|
320
|
+
const detail = err instanceof Error && err.stack ? err.stack : String(err);
|
|
321
|
+
const event = {
|
|
322
|
+
...enrich('functional_run', { outcome: 'failed', message, detail, exit_code: extra?.exit_code ?? null }, unit),
|
|
323
|
+
unit,
|
|
324
|
+
command,
|
|
325
|
+
hook_name: resolveHookName(unit),
|
|
326
|
+
log_text: '',
|
|
327
|
+
};
|
|
328
|
+
await send([event]);
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* never throw on the exit path */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/** Flush any buffered events (call opportunistically at the start of a run). Never throws. */
|
|
335
|
+
export async function flushBufferedClientEvents() {
|
|
336
|
+
try {
|
|
337
|
+
const buffered = claimBufferedEvents();
|
|
338
|
+
if (buffered.length === 0)
|
|
339
|
+
return;
|
|
340
|
+
const ok = await postEvents(buffered);
|
|
341
|
+
if (!ok)
|
|
342
|
+
appendToBuffer(buffered);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
/* best-effort */
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -16,14 +16,14 @@ import { join } from 'node:path';
|
|
|
16
16
|
import { parseJsonWithJsoncFallback } from '../readers/file_readers.js';
|
|
17
17
|
import { mergeComposerShadowKeysFromReactiveBlob, readVscdbItemTableJson, } from '../readers/vscdb_reader.js';
|
|
18
18
|
import { readRemediationInstructionsFile, writeRemediationInstructionsFile, } from './management_storage.js';
|
|
19
|
-
import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
19
|
+
import { resolveRemediationConfigPath, resolveRemediationUploadFileType } from './remediation_config_path.js';
|
|
20
20
|
import { resolveOpsTargetPath } from './ops_target_path.js';
|
|
21
21
|
import { isRemediationQuarantined, markRemediationApplyPendingVerification, markRemediationApplyVerified, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
|
|
22
22
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
23
23
|
import { isEnvBackedSecretValue, scanJsonForHardcodedSecrets, } from './secret_regex_scan.js';
|
|
24
24
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
25
25
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
26
|
-
import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
26
|
+
import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, globalCursorIgnoreListFromSettings, GLOBAL_CURSOR_IGNORE_SETTING_PATH, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
27
27
|
import { sendConfigFile } from '../sender/batch_sender.js';
|
|
28
28
|
import { ensureAuthentication } from '../auth/auth_flow.js';
|
|
29
29
|
/** Normalize manifest/env/CLI agent tokens to a known Agent, or '' if unrecognized. */
|
|
@@ -220,6 +220,16 @@ function violationFromSecretScanFinding(entry, compliance, finding) {
|
|
|
220
220
|
message: `[${compliance.finding_formatted_id}] ${entry.finding_title ?? compliance.description}\nDescription: ${entry.finding_description ?? compliance.description}\nHow to fix: Remove the hardcoded ${finding.secretType} from ${finding.path} in ${entry.config_file_path}`,
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
|
+
function currentArrayAtComplianceTarget(configJson, targetPath) {
|
|
224
|
+
if (configJson &&
|
|
225
|
+
typeof configJson === 'object' &&
|
|
226
|
+
!Array.isArray(configJson) &&
|
|
227
|
+
targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH) {
|
|
228
|
+
return globalCursorIgnoreListFromSettings(configJson);
|
|
229
|
+
}
|
|
230
|
+
const cur = getByPath(configJson, targetPath);
|
|
231
|
+
return Array.isArray(cur) ? cur : [];
|
|
232
|
+
}
|
|
223
233
|
function verifyOpsApplied(configJson, settingPath, ops) {
|
|
224
234
|
const parts = settingPath.split('.');
|
|
225
235
|
const leafKey = parts[parts.length - 1] ?? '';
|
|
@@ -231,15 +241,16 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
231
241
|
for (const k of keys) {
|
|
232
242
|
const targetPath = resolveOpsTargetPath(settingPath, k);
|
|
233
243
|
if (Object.prototype.hasOwnProperty.call(set, k)) {
|
|
234
|
-
const cur = getByPath(configJson, targetPath);
|
|
235
244
|
const expected = set[k];
|
|
236
|
-
|
|
245
|
+
const currentForSet = targetPath === GLOBAL_CURSOR_IGNORE_SETTING_PATH
|
|
246
|
+
? globalCursorIgnoreListFromSettings(configJson)
|
|
247
|
+
: getByPath(configJson, targetPath);
|
|
248
|
+
if (!remediationSetOpSatisfied(currentForSet, expected)) {
|
|
237
249
|
return { ok: false, expected: { op: 'set', path: targetPath, value: expected } };
|
|
238
250
|
}
|
|
239
251
|
continue;
|
|
240
252
|
}
|
|
241
|
-
const
|
|
242
|
-
const curArr = Array.isArray(cur) ? cur : [];
|
|
253
|
+
const curArr = currentArrayAtComplianceTarget(configJson, targetPath);
|
|
243
254
|
const toAdd = add[k] ?? [];
|
|
244
255
|
const toRemove = remove[k] ?? [];
|
|
245
256
|
const isYoloAllowlist = leafKey === 'yoloCommandAllowlist' || targetPath.endsWith('.yoloCommandAllowlist');
|
|
@@ -565,9 +576,8 @@ export function reportPostRestartVerificationOutcomes(violations) {
|
|
|
565
576
|
return { outcomes, reportPromises };
|
|
566
577
|
}
|
|
567
578
|
/**
|
|
568
|
-
*
|
|
569
|
-
* user's next prompt
|
|
570
|
-
* to the server right away — the UI does not need to wait for another gate invocation.
|
|
579
|
+
* After deferred vscdb apply (`apply_deferred_vscdb.ts`): confirm pending remediations and POST
|
|
580
|
+
* outcomes before the user's next prompt.
|
|
571
581
|
*/
|
|
572
582
|
export async function runPostApplyVerification(agent = 'cursor') {
|
|
573
583
|
const status = runLocalRemediationComplianceCheck(agent);
|
|
@@ -683,7 +693,7 @@ export function applyAutofixViolations(violations, agent = 'cursor') {
|
|
|
683
693
|
parseJsonWithJsoncFallback(readFileSync(configPathForDisk, 'utf8')) ?? undefined;
|
|
684
694
|
}
|
|
685
695
|
if (updatedContent !== undefined) {
|
|
686
|
-
const fileType = (inst.file_type ??
|
|
696
|
+
const fileType = resolveRemediationUploadFileType(inst.config_file_path, inst.file_type ?? undefined);
|
|
687
697
|
if (fileType) {
|
|
688
698
|
const hw = tryResolveHardwareUuid();
|
|
689
699
|
if (hw) {
|
|
@@ -888,8 +898,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
888
898
|
if (violations.length > 0)
|
|
889
899
|
continue;
|
|
890
900
|
const inst = entry;
|
|
891
|
-
const
|
|
892
|
-
if (!
|
|
901
|
+
const uploadFileType = resolveRemediationUploadFileType(entry.config_file_path, inst.file_type ?? undefined);
|
|
902
|
+
if (!uploadFileType)
|
|
893
903
|
continue;
|
|
894
904
|
const diskPath = resolveRemediationConfigPath(entry.config_file_path);
|
|
895
905
|
if (diskPath.includes('#'))
|
|
@@ -914,16 +924,24 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
914
924
|
continue;
|
|
915
925
|
}
|
|
916
926
|
const uploadPath = entry.config_file_path.trim() || diskPath;
|
|
927
|
+
const shouldReportVerified = !prev?.pending_post_restart_verify && !prev?.server_verified_at;
|
|
917
928
|
promises.push(ensureAuthentication(hw)
|
|
918
|
-
.then((authKey) => sendConfigFile({ file_type:
|
|
919
|
-
.then((sentOk) => {
|
|
929
|
+
.then((authKey) => sendConfigFile({ file_type: uploadFileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
|
|
930
|
+
.then(async (sentOk) => {
|
|
920
931
|
hookRunLog(`satisfied_upload: uuid=${entry.uuid} path=${uploadPath} ok=${sentOk}`);
|
|
921
|
-
if (sentOk)
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
932
|
+
if (!sentOk)
|
|
933
|
+
return;
|
|
934
|
+
const file = readRemediationApplyTrackingFile();
|
|
935
|
+
const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
|
|
936
|
+
ent.last_satisfied_upload_at = new Date().toISOString();
|
|
937
|
+
file.entries[entry.uuid] = ent;
|
|
938
|
+
writeRemediationApplyTrackingFile(file);
|
|
939
|
+
if (shouldReportVerified) {
|
|
940
|
+
await reportAutofixApplied(entry.uuid, 'verified', {
|
|
941
|
+
config_snapshot_after: rawContent,
|
|
942
|
+
});
|
|
943
|
+
markRemediationApplyVerified(entry.uuid);
|
|
944
|
+
hookRunLog(`satisfied_upload: reported verified uuid=${entry.uuid}`);
|
|
927
945
|
}
|
|
928
946
|
})
|
|
929
947
|
.catch((err) => {
|
|
@@ -932,6 +950,35 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
932
950
|
}
|
|
933
951
|
return promises;
|
|
934
952
|
}
|
|
953
|
+
/**
|
|
954
|
+
* When disk already matches remediation ops but autofix never ran, POST verified so the UI
|
|
955
|
+
* leaves Pending (EnforcementLog) even if satisfied_upload is throttled.
|
|
956
|
+
*/
|
|
957
|
+
export function reportCompliantRemediationVerifiedStatus(agent = 'cursor') {
|
|
958
|
+
const { remediations } = readRemediationInstructionsFile();
|
|
959
|
+
const entries = remediations.filter((e) => targetsCurrentAgent(e, agent));
|
|
960
|
+
const promises = [];
|
|
961
|
+
for (const entry of entries) {
|
|
962
|
+
const { violations } = evaluateManifestEntryCompliance(entry);
|
|
963
|
+
if (violations.length > 0)
|
|
964
|
+
continue;
|
|
965
|
+
const tracking = readRemediationApplyTrackingFile();
|
|
966
|
+
const prev = tracking.entries[entry.uuid];
|
|
967
|
+
if (prev?.pending_post_restart_verify || prev?.server_verified_at)
|
|
968
|
+
continue;
|
|
969
|
+
const diskPath = resolveRemediationConfigPath(entry.config_file_path);
|
|
970
|
+
if (diskPath.includes('#'))
|
|
971
|
+
continue;
|
|
972
|
+
const rawContent = parseJsonWithJsoncFallback(readFileSync(diskPath, 'utf8'));
|
|
973
|
+
if (rawContent === null)
|
|
974
|
+
continue;
|
|
975
|
+
promises.push(reportAutofixApplied(entry.uuid, 'verified', { config_snapshot_after: rawContent }).then(() => {
|
|
976
|
+
markRemediationApplyVerified(entry.uuid);
|
|
977
|
+
hookRunLog(`compliance_check: reported verified (already compliant) uuid=${entry.uuid}`);
|
|
978
|
+
}));
|
|
979
|
+
}
|
|
980
|
+
return promises;
|
|
981
|
+
}
|
|
935
982
|
/**
|
|
936
983
|
* Background refresh: server sync for latest instructions, then the same local evaluation as the hook.
|
|
937
984
|
* Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, readFileSync, } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { OPT_AI_SEC_LOGS_REL, LOG_GROUP_COMPLIANCE } from '../../bootstrap_constants.js';
|
|
5
|
+
import { buildLogMetadataBlock } from './log_metadata.js';
|
|
6
|
+
const COMPLIANCE_SUBDIR = LOG_GROUP_COMPLIANCE;
|
|
6
7
|
const TIER_NOOP = 'noop';
|
|
7
8
|
const TIER_SUCCESS = 'success';
|
|
8
|
-
const
|
|
9
|
+
const TIER_FAILURES = 'failures';
|
|
9
10
|
const TIER_IN_PROCESS = 'in_process';
|
|
10
|
-
const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS,
|
|
11
|
+
const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS, TIER_FAILURES];
|
|
12
|
+
/** Server upload contract still uses "error" for failed remediation sessions. */
|
|
13
|
+
const UPLOAD_OUTCOME_ERROR = 'error';
|
|
11
14
|
const NOOP_MAX_AGE_MS = 30 * 60 * 1000;
|
|
12
15
|
const SUCCESS_ERROR_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
13
16
|
/** In-flight logs in compliance/in_process/ before finalize. */
|
|
@@ -32,7 +35,7 @@ export function isFinalizeComplianceSessionArgv(argv) {
|
|
|
32
35
|
return argv.includes('--finalize-compliance-session');
|
|
33
36
|
}
|
|
34
37
|
export function getComplianceLogDir() {
|
|
35
|
-
return path.join(homedir(),
|
|
38
|
+
return path.join(homedir(), OPT_AI_SEC_LOGS_REL, COMPLIANCE_SUBDIR);
|
|
36
39
|
}
|
|
37
40
|
export function getActiveComplianceLogPath() {
|
|
38
41
|
return activeComplianceLogPath;
|
|
@@ -109,7 +112,7 @@ export function pruneComplianceSessionLogs(exceptPath) {
|
|
|
109
112
|
let removed = 0;
|
|
110
113
|
removed += pruneLogFilesInDir(path.join(base, TIER_NOOP), NOOP_MAX_AGE_MS, keep);
|
|
111
114
|
removed += pruneLogFilesInDir(path.join(base, TIER_SUCCESS), SUCCESS_ERROR_MAX_AGE_MS, keep);
|
|
112
|
-
removed += pruneLogFilesInDir(path.join(base,
|
|
115
|
+
removed += pruneLogFilesInDir(path.join(base, TIER_FAILURES), SUCCESS_ERROR_MAX_AGE_MS, keep);
|
|
113
116
|
removed += pruneLogFilesInDir(path.join(base, TIER_IN_PROCESS), INFLIGHT_MAX_AGE_MS, keep);
|
|
114
117
|
if (!existsSync(base))
|
|
115
118
|
return removed;
|
|
@@ -137,7 +140,7 @@ export function pruneComplianceSessionLogs(exceptPath) {
|
|
|
137
140
|
/**
|
|
138
141
|
* Classify a compliance session for finalize + optional server upload.
|
|
139
142
|
*
|
|
140
|
-
* Only two outcomes are uploaded (success / error): remediation **succeeded** or **failed**.
|
|
143
|
+
* Only two outcomes are uploaded (success / error on the wire): remediation **succeeded** or **failed**.
|
|
141
144
|
* Policy violations, in-flight restart, and sync-only sessions are noop (local retention only).
|
|
142
145
|
*/
|
|
143
146
|
export function classifyComplianceSessionLog(content) {
|
|
@@ -155,7 +158,7 @@ export function classifyComplianceSessionLog(content) {
|
|
|
155
158
|
/enforceRemediation.*failed/i,
|
|
156
159
|
];
|
|
157
160
|
if (remediationFailedPatterns.some((p) => p.test(text))) {
|
|
158
|
-
return
|
|
161
|
+
return TIER_FAILURES;
|
|
159
162
|
}
|
|
160
163
|
return TIER_NOOP;
|
|
161
164
|
}
|
|
@@ -212,7 +215,7 @@ export function complianceLogSessionBanner(logPath, label, mode) {
|
|
|
212
215
|
if (!existsSync(dir))
|
|
213
216
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
214
217
|
if (mode === 'replace') {
|
|
215
|
-
writeFileSync(resolved, banner, 'utf8');
|
|
218
|
+
writeFileSync(resolved, banner + buildLogMetadataBlock(), 'utf8');
|
|
216
219
|
}
|
|
217
220
|
else {
|
|
218
221
|
appendFileSync(resolved, banner, 'utf8');
|
|
@@ -235,7 +238,7 @@ export function initComplianceSessionForGate(logPath) {
|
|
|
235
238
|
writeLatestPointer(resolved);
|
|
236
239
|
}
|
|
237
240
|
if (removed > 0) {
|
|
238
|
-
appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/
|
|
241
|
+
appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/failures 24h)\n`);
|
|
239
242
|
}
|
|
240
243
|
return resolved;
|
|
241
244
|
}
|
|
@@ -247,12 +250,12 @@ export function initComplianceSessionForRunner(logPath) {
|
|
|
247
250
|
setActiveComplianceLogPath(resolved);
|
|
248
251
|
complianceLogSessionBanner(resolved, 'compliance_check_runner (background sync + check)', 'append');
|
|
249
252
|
if (removed > 0) {
|
|
250
|
-
appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/
|
|
253
|
+
appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/failures 24h)\n`);
|
|
251
254
|
}
|
|
252
255
|
return resolved;
|
|
253
256
|
}
|
|
254
257
|
/**
|
|
255
|
-
* Move session log from compliance/ root into noop|success|
|
|
258
|
+
* Move session log from compliance/ root into noop|success|failures/ and prune tiers.
|
|
256
259
|
* No-op if file missing or already finalized.
|
|
257
260
|
*/
|
|
258
261
|
export function finalizeComplianceSessionLog(logPath, forcedTier) {
|
|
@@ -263,6 +266,9 @@ export function finalizeComplianceSessionLog(logPath, forcedTier) {
|
|
|
263
266
|
if (isTierDirName(parentName)) {
|
|
264
267
|
return { tier: parentName, logPath: resolved, moved: false };
|
|
265
268
|
}
|
|
269
|
+
if (parentName === 'error') {
|
|
270
|
+
return { tier: TIER_FAILURES, logPath: resolved, moved: false };
|
|
271
|
+
}
|
|
266
272
|
const content = readFileSync(resolved, 'utf8');
|
|
267
273
|
if (isRemediationCycleInflight(content)) {
|
|
268
274
|
return null;
|
|
@@ -310,10 +316,10 @@ function appendLineToSessionLogFile(logPath, message) {
|
|
|
310
316
|
}
|
|
311
317
|
}
|
|
312
318
|
/**
|
|
313
|
-
* Upload full session log to the server (success and
|
|
319
|
+
* Upload full session log to the server (success and failures tiers only, first finalize only).
|
|
314
320
|
*/
|
|
315
321
|
export async function uploadComplianceSessionLogFromFinalize(result) {
|
|
316
|
-
if (result.tier !== TIER_SUCCESS && result.tier !==
|
|
322
|
+
if (result.tier !== TIER_SUCCESS && result.tier !== TIER_FAILURES)
|
|
317
323
|
return;
|
|
318
324
|
if (!result.moved)
|
|
319
325
|
return;
|
|
@@ -338,7 +344,7 @@ export async function uploadComplianceSessionLogFromFinalize(result) {
|
|
|
338
344
|
return;
|
|
339
345
|
}
|
|
340
346
|
const sessionFilename = path.basename(result.logPath);
|
|
341
|
-
const outcome = result.tier;
|
|
347
|
+
const outcome = result.tier === TIER_FAILURES ? UPLOAD_OUTCOME_ERROR : result.tier;
|
|
342
348
|
const signingPayload = {
|
|
343
349
|
hardware_uuid: hardwareUuid,
|
|
344
350
|
outcome,
|
|
@@ -362,7 +368,7 @@ export async function uploadComplianceSessionLogFromFinalize(result) {
|
|
|
362
368
|
appendLineToSessionLogFile(result.logPath, `compliance_session_upload: failed ${msg}`);
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
|
-
/** Finalize session file into success|
|
|
371
|
+
/** Finalize session file into success|failures|noop tier, then upload when success or failures. */
|
|
366
372
|
export async function finalizeAndUploadComplianceSessionLog(logPath, forcedTier) {
|
|
367
373
|
const result = finalizeComplianceSessionLog(logPath, forcedTier);
|
|
368
374
|
if (result) {
|