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.
Files changed (28) hide show
  1. package/dist/apply_deferred_vscdb.js +2 -2
  2. package/dist/bootstrap_constants.js +10 -0
  3. package/dist/cli.js +31 -1
  4. package/dist/compliance_check_runner.js +7 -2
  5. package/dist/compliance_prompt_gate.js +46 -17
  6. package/dist/log_config_files/auth/auth_flow.js +1 -1
  7. package/dist/log_config_files/auth/auth_key_store.js +1 -1
  8. package/dist/log_config_files/collection/config_collector.js +12 -0
  9. package/dist/log_config_files/collection/ensure_cursor_user_settings_snapshot.js +50 -0
  10. package/dist/log_config_files/paths/pattern_resolver.js +1 -1
  11. package/dist/log_config_files/runtime/client_event_reporter.js +347 -0
  12. package/dist/log_config_files/runtime/compliance_check.js +67 -20
  13. package/dist/log_config_files/runtime/compliance_session_log.js +22 -16
  14. package/dist/log_config_files/runtime/hook_logger.js +33 -20
  15. package/dist/log_config_files/runtime/log_metadata.js +122 -0
  16. package/dist/log_config_files/runtime/main_runner.js +25 -5
  17. package/dist/log_config_files/runtime/management_storage.js +36 -7
  18. package/dist/log_config_files/runtime/remediation_apply_tracking.js +4 -2
  19. package/dist/log_config_files/runtime/remediation_config_path.js +12 -1
  20. package/dist/log_config_files/runtime/remediation_sync.js +76 -8
  21. package/dist/log_config_files/sender/batch_sender.js +1 -1
  22. package/dist/log_sensitive_paths_audit.js +3 -3
  23. package/dist/log_uuid/auth_key_store.js +1 -1
  24. package/dist/log_uuid/startup_sender.js +21 -3
  25. package/package.json +1 -1
  26. package/dist/log_config_files/readers/vscdb_item_table_registry.js +0 -82
  27. package/dist/log_config_files/runtime/secretlint_scan.js +0 -69
  28. 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
- if (!remediationSetOpSatisfied(cur, expected)) {
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 cur = getByPath(configJson, targetPath);
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
- * Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
569
- * user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
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 ?? '').trim();
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 fileType = (inst.file_type ?? '').trim();
892
- if (!fileType)
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: fileType, file_path: uploadPath, raw_content: rawContent }, hw, authKey))
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
- const file = readRemediationApplyTrackingFile();
923
- const ent = file.entries[entry.uuid] ?? { consecutive_verify_failures: 0, quarantined: false };
924
- ent.last_satisfied_upload_at = new Date().toISOString();
925
- file.entries[entry.uuid] = ent;
926
- writeRemediationApplyTrackingFile(file);
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 { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
5
- const COMPLIANCE_SUBDIR = 'compliance';
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 TIER_ERROR = 'error';
9
+ const TIER_FAILURES = 'failures';
9
10
  const TIER_IN_PROCESS = 'in_process';
10
- const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS, TIER_ERROR];
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(), OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR);
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, TIER_ERROR), SUCCESS_ERROR_MAX_AGE_MS, keep);
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 TIER_ERROR;
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/error 24h)\n`);
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/error 24h)\n`);
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|error/ and prune tiers.
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 error tiers only, first finalize only).
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 !== TIER_ERROR)
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|error|noop tier, then upload when success or error. */
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) {