log-llm-config-staging 1.4.5 → 1.4.8

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.
@@ -1,9 +1,38 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Runs after Cursor SIGKILL: applies queued state.vscdb patches from
4
- * ~/opt-ai-sec/management/optimus_deferred_vscdb_apply.json (see remediation_sync).
5
- */
2
+ // Runs after Cursor SIGKILL: applies queued state.vscdb patches, then immediately runs
3
+ // post-restart verification so the server gets `verified` without waiting for the next prompt.
6
4
  import { applyDeferredVscdbFromDisk } from './log_config_files/runtime/remediation_sync.js';
5
+ import { runPostApplyVerification } from './log_config_files/runtime/compliance_check.js';
6
+ import { hookRunLog } from './log_config_files/runtime/hook_logger.js';
7
+ import { finalizeAndUploadComplianceSessionLog, setActiveComplianceLogPath, } from './log_config_files/runtime/compliance_session_log.js';
8
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim() || null;
9
+ if (complianceLogPath) {
10
+ setActiveComplianceLogPath(complianceLogPath);
11
+ }
7
12
  applyDeferredVscdbFromDisk()
8
- .then((ok) => process.exit(ok ? 0 : 1))
13
+ .then(async (ok) => {
14
+ if (ok) {
15
+ try {
16
+ const outcomes = await runPostApplyVerification();
17
+ if (outcomes.length > 0) {
18
+ hookRunLog(`apply_deferred_vscdb: post_apply_verification count=${outcomes.length} ` +
19
+ `verified=${outcomes.filter((o) => o.status === 'verified').length}`);
20
+ }
21
+ if (complianceLogPath) {
22
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
23
+ }
24
+ }
25
+ catch (e) {
26
+ hookRunLog(`apply_deferred_vscdb: post_apply_verification error: ${e instanceof Error ? e.message : String(e)}`);
27
+ if (complianceLogPath) {
28
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
29
+ }
30
+ }
31
+ }
32
+ else if (complianceLogPath) {
33
+ hookRunLog('apply_deferred_vscdb: apply failed');
34
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
35
+ }
36
+ process.exit(ok ? 0 : 1);
37
+ })
9
38
  .catch(() => process.exit(1));
@@ -1,8 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  import { complianceRunnerRunnerLine, hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
3
3
  import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
4
+ import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForRunner, isFinalizeComplianceSessionArgv, isInflightComplianceLogPath, parseComplianceLogArg, resolveComplianceSessionLogPath, } from './log_config_files/runtime/compliance_session_log.js';
5
+ import { existsSync } from 'node:fs';
4
6
  (async () => {
5
- hookLogAppendSection('compliance_check_runner (background sync + check)');
7
+ let complianceLogPath = parseComplianceLogArg(process.argv);
8
+ if (isFinalizeComplianceSessionArgv(process.argv)) {
9
+ if (complianceLogPath) {
10
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
11
+ }
12
+ process.exit(0);
13
+ }
14
+ if (complianceLogPath) {
15
+ const resolved = resolveComplianceSessionLogPath(complianceLogPath);
16
+ if (existsSync(resolved) && isInflightComplianceLogPath(resolved)) {
17
+ complianceLogPath = initComplianceSessionForRunner(resolved);
18
+ }
19
+ else {
20
+ complianceLogPath = null;
21
+ }
22
+ }
23
+ else {
24
+ hookLogAppendSection('compliance_check_runner (background sync + check)');
25
+ }
6
26
  complianceRunnerRunnerLine('compliance_check_runner: start');
7
27
  try {
8
28
  await runComplianceCheck();
@@ -13,6 +33,12 @@ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.
13
33
  complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
14
34
  complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
15
35
  process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
36
+ if (complianceLogPath) {
37
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
38
+ }
16
39
  process.exit(1);
17
40
  }
41
+ if (complianceLogPath) {
42
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
43
+ }
18
44
  })();
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Synchronous pre-prompt gate: local compliance only (stdout = single JSON line for IDE hooks).
4
- * stderr must stay clean; logs go via hookRunLog (file).
4
+ * stderr must stay clean; logs go via hookRunLog (compliance session file when --compliance-log is set).
5
5
  *
6
6
  * When autofix returns restart_commands, this process does not spawn them — the shell hook pipes
7
7
  * the same JSON line to execute_trusted_restarts (TS allowlist + spawn).
@@ -11,6 +11,7 @@ import { isRemediationQuarantined } from './log_config_files/runtime/remediation
11
11
  import { existsSync, readFileSync, statSync } from 'node:fs';
12
12
  import { getRemediationInstructionsPath } from './log_config_files/runtime/management_storage.js';
13
13
  import { hookLogSessionBanner, hookRunLog, logRemediationApplyFailure } from './log_config_files/runtime/hook_logger.js';
14
+ import { finalizeAndUploadComplianceSessionLog, initComplianceSessionForGate, parseComplianceLogArg, } from './log_config_files/runtime/compliance_session_log.js';
14
15
  import { isThisCliModule } from './cli_invocation_match.js';
15
16
  import { ensureAuthentication } from './log_config_files/auth/auth_flow.js';
16
17
  import { sendConfigFile } from './log_config_files/sender/batch_sender.js';
@@ -19,6 +20,17 @@ import { syncRemediations } from './log_config_files/runtime/remediation_sync.js
19
20
  import { loadEndpointBase } from './log_config_files/sender/endpoint_config.js';
20
21
  import { formatRemediationChangePreviewForApplied } from './remediation_change_preview.js';
21
22
  const MANIFEST_STALE_MS = 7 * 24 * 60 * 60 * 1000;
23
+ /** After verified (or failed verify), push enforcement + session log immediately — do not wait for background runner. */
24
+ async function flushRemediationOutcomeToServer(complianceLogPath) {
25
+ if (!complianceLogPath)
26
+ return;
27
+ try {
28
+ await finalizeAndUploadComplianceSessionLog(complianceLogPath);
29
+ }
30
+ catch (err) {
31
+ hookRunLog(`compliance_prompt_gate: session finalize/upload failed: ${err instanceof Error ? err.message : String(err)}`);
32
+ }
33
+ }
22
34
  function parseIde() {
23
35
  const eq = process.argv.find((a) => a.startsWith('--ide='));
24
36
  if (eq) {
@@ -164,10 +176,21 @@ export async function runCompliancePromptGateCli() {
164
176
  export async function runCompliancePromptGate() {
165
177
  const ide = parseIde();
166
178
  const agent = parseAgent(ide);
167
- hookLogSessionBanner('compliance_prompt_gate (before submit)');
168
- // Sync before checking so server-side changes (enforce/unenforce) propagate on every prompt
169
- // without waiting for the background runner. Race against a 3 s timeout so a slow or
170
- // unavailable server never delays the gate significantly.
179
+ let complianceLogPath = parseComplianceLogArg(process.argv);
180
+ if (complianceLogPath) {
181
+ complianceLogPath = initComplianceSessionForGate(complianceLogPath);
182
+ }
183
+ else {
184
+ hookLogSessionBanner('compliance_prompt_gate (before submit)');
185
+ }
186
+ let status = runLocalRemediationComplianceCheck(agent);
187
+ const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
188
+ if (postRestartVerify.outcomes.length > 0) {
189
+ await Promise.allSettled(postRestartVerify.reportPromises);
190
+ await flushRemediationOutcomeToServer(complianceLogPath);
191
+ hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
192
+ status = runLocalRemediationComplianceCheck(agent);
193
+ }
171
194
  const hw = tryResolveHardwareUuid();
172
195
  if (hw) {
173
196
  try {
@@ -180,13 +203,7 @@ export async function runCompliancePromptGate() {
180
203
  // Network or auth failure — fall back to local file state.
181
204
  }
182
205
  }
183
- const status = runLocalRemediationComplianceCheck(agent);
184
- // Always process pending post-restart rows (even when compliance is ok — apply may have stuck).
185
- const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
186
- if (postRestartVerify.outcomes.length > 0) {
187
- await Promise.allSettled(postRestartVerify.reportPromises);
188
- hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
189
- }
206
+ status = runLocalRemediationComplianceCheck(agent);
190
207
  // Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
191
208
  // Upload those files fire-and-forget so the backend can resolve the finding immediately.
192
209
  for (const e of status.secondarySatisfied ?? []) {
@@ -255,6 +272,7 @@ export async function runCompliancePromptGate() {
255
272
  if (immediateVerified) {
256
273
  confirmAppliedAutofixVerified(appliedViolations, reportPromises);
257
274
  await Promise.allSettled(reportPromises);
275
+ await flushRemediationOutcomeToServer(complianceLogPath);
258
276
  }
259
277
  const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
260
278
  const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
@@ -7,9 +7,14 @@
7
7
  import { readFileSync } from 'node:fs';
8
8
  import { isThisCliModule } from './cli_invocation_match.js';
9
9
  import { appendComplianceRunnerLine, hookRunLog } from './log_config_files/runtime/hook_logger.js';
10
+ import { setActiveComplianceLogPath } from './log_config_files/runtime/compliance_session_log.js';
10
11
  import { executeTrustedRestartCommands } from './log_config_files/runtime/trusted_restarts.js';
11
12
  /** Invoked by dist entrypoint or by `npx log-llm-config@latest execute-trusted-restarts` (cli.js dispatches here). */
12
13
  export function runExecuteTrustedRestartsFromStdin() {
14
+ const complianceLogPath = process.env.OPTIMUS_COMPLIANCE_LOG?.trim();
15
+ if (complianceLogPath) {
16
+ setActiveComplianceLogPath(complianceLogPath);
17
+ }
13
18
  let raw;
14
19
  try {
15
20
  raw = readFileSync(0, 'utf8');
@@ -1,11 +1,14 @@
1
- import { execFileSync } from 'node:child_process';
2
- import { homedir } from 'node:os';
1
+ import { spawnSync } from 'node:child_process';
2
+ import { closeSync, mkdtempSync, openSync, readFileSync, rmSync } from 'node:fs';
3
+ import { homedir, tmpdir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
  export const SKILLS_CLI_FILE_TYPE = 'skills_cli_installed';
5
6
  export const SKILLS_CLI_INSTALLED_PATH = join(homedir(), '.agents', '.skills-cli-installed.json');
6
7
  /** Override the skills package spec, e.g. `skills@1.5.10`. Default uses whatever is on the machine. */
7
8
  export const SKILLS_CLI_NPX_PACKAGE_ENV = 'SKILLS_CLI_NPX_PACKAGE';
8
9
  const LIST_TIMEOUT_MS = 120_000;
10
+ /** stderr from npx/npm only; stdout is streamed to a temp file (avoids pipe/maxBuffer truncation). */
11
+ const LIST_STDERR_MAX_BUFFER = 16 * 1024 * 1024;
9
12
  function listExecEnv() {
10
13
  return {
11
14
  ...process.env,
@@ -16,20 +19,61 @@ function npxPackageSpec() {
16
19
  const fromEnv = (process.env[SKILLS_CLI_NPX_PACKAGE_ENV] || '').trim();
17
20
  return fromEnv || 'skills';
18
21
  }
19
- export function runSkillsListJson(args, cwd) {
20
- const out = execFileSync('npx', [npxPackageSpec(), 'list', ...args, '--json'], {
21
- encoding: 'utf8',
22
- cwd,
23
- timeout: LIST_TIMEOUT_MS,
24
- env: listExecEnv(),
25
- });
26
- const trimmed = out.trim();
22
+ /** Strip leading npx/npm noise and parse the skills `list --json` array payload. */
23
+ export function parseSkillsListJsonStdout(stdout) {
24
+ const trimmed = stdout.trim();
27
25
  if (!trimmed)
28
26
  return [];
29
- const parsed = JSON.parse(trimmed);
30
- if (!Array.isArray(parsed))
31
- return [];
32
- return parsed;
27
+ const start = trimmed.indexOf('[');
28
+ if (start < 0) {
29
+ throw new SyntaxError('skills list --json: no JSON array in stdout');
30
+ }
31
+ let end = trimmed.lastIndexOf(']');
32
+ let lastErr;
33
+ while (end > start) {
34
+ const slice = trimmed.slice(start, end + 1);
35
+ try {
36
+ const parsed = JSON.parse(slice);
37
+ if (!Array.isArray(parsed))
38
+ return [];
39
+ return parsed;
40
+ }
41
+ catch (err) {
42
+ lastErr = err instanceof SyntaxError ? err : new SyntaxError(String(err));
43
+ end = trimmed.lastIndexOf(']', end - 1);
44
+ }
45
+ }
46
+ throw lastErr ?? new SyntaxError('skills list --json: could not parse stdout');
47
+ }
48
+ export function runSkillsListJson(args, cwd) {
49
+ const tmpDir = mkdtempSync(join(tmpdir(), 'optimus-skills-list-'));
50
+ const outPath = join(tmpDir, 'stdout.json');
51
+ const outFd = openSync(outPath, 'w');
52
+ try {
53
+ const child = spawnSync('npx', [npxPackageSpec(), 'list', ...args, '--json'], {
54
+ cwd,
55
+ timeout: LIST_TIMEOUT_MS,
56
+ env: listExecEnv(),
57
+ stdio: ['ignore', outFd, 'pipe'],
58
+ maxBuffer: LIST_STDERR_MAX_BUFFER,
59
+ });
60
+ if (child.error)
61
+ throw child.error;
62
+ if (child.status !== 0) {
63
+ const stderr = (child.stderr ?? '').toString().trim();
64
+ throw new Error(stderr || `npx skills list exited ${child.status ?? 'unknown'}`);
65
+ }
66
+ }
67
+ finally {
68
+ closeSync(outFd);
69
+ }
70
+ try {
71
+ const out = readFileSync(outPath, 'utf8');
72
+ return parseSkillsListJsonStdout(out);
73
+ }
74
+ finally {
75
+ rmSync(tmpDir, { recursive: true, force: true });
76
+ }
33
77
  }
34
78
  function normalizeListRows(rows, scope) {
35
79
  const out = [];
@@ -60,39 +104,43 @@ export function formatSkillsListScopeForHookLog(scopeLabel, entries) {
60
104
  .join(' | ');
61
105
  return `skills_cli list ${scopeLabel}: ${entries.length} skill(s) — ${detail}`;
62
106
  }
107
+ function runSkillsListForScope(scopeLabel, args, projectRoot, logLine) {
108
+ try {
109
+ return runSkillsListJson(args, projectRoot);
110
+ }
111
+ catch (err) {
112
+ const msg = err instanceof Error ? err.message : String(err);
113
+ logLine(`skills_cli: list ${scopeLabel} --json failed: ${msg}`);
114
+ return [];
115
+ }
116
+ }
63
117
  export function collectSkillsCliInstalled(projectRoot, log) {
64
118
  const logLine = (message) => {
65
119
  log?.(message);
66
120
  };
67
- try {
68
- const packageSpec = npxPackageSpec();
69
- logLine(`skills_cli: npx ${packageSpec} list -g --json && list --json (projectRoot=${projectRoot})`);
70
- const globalRows = runSkillsListJson(['-g'], projectRoot);
71
- const projectRows = runSkillsListJson([], projectRoot);
72
- const global = normalizeListRows(globalRows, 'global');
73
- const project = normalizeListRows(projectRows, 'project');
74
- logLine(formatSkillsListScopeForHookLog('-g', global));
75
- logLine(formatSkillsListScopeForHookLog('project', project));
76
- const payload = {
77
- version: 1,
78
- skills_cli_version: packageSpec,
79
- generated_at: new Date().toISOString(),
80
- global,
81
- project,
82
- };
83
- if (payload.global.length === 0 && payload.project.length === 0) {
84
- logLine('skills_cli_installed: not uploaded (no global or project skills)');
85
- return null;
86
- }
87
- logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
88
- return {
89
- file_type: SKILLS_CLI_FILE_TYPE,
90
- file_path: SKILLS_CLI_INSTALLED_PATH,
91
- raw_content: payload,
92
- };
93
- }
94
- catch (err) {
95
- logLine(`skills_cli: list --json failed: ${err instanceof Error ? err.message : String(err)}`);
121
+ const packageSpec = npxPackageSpec();
122
+ logLine(`skills_cli: npx ${packageSpec} list -g --json && list --json (projectRoot=${projectRoot})`);
123
+ const globalRows = runSkillsListForScope('-g', ['-g'], projectRoot, logLine);
124
+ const projectRows = runSkillsListForScope('project', [], projectRoot, logLine);
125
+ const global = normalizeListRows(globalRows, 'global');
126
+ const project = normalizeListRows(projectRows, 'project');
127
+ logLine(formatSkillsListScopeForHookLog('-g', global));
128
+ logLine(formatSkillsListScopeForHookLog('project', project));
129
+ const payload = {
130
+ version: 1,
131
+ skills_cli_version: packageSpec,
132
+ generated_at: new Date().toISOString(),
133
+ global,
134
+ project,
135
+ };
136
+ if (payload.global.length === 0 && payload.project.length === 0) {
137
+ logLine('skills_cli_installed: not uploaded (no global or project skills)');
96
138
  return null;
97
139
  }
140
+ logLine(`skills_cli_installed: upload ${SKILLS_CLI_INSTALLED_PATH} global=${global.length} project=${project.length}`);
141
+ return {
142
+ file_type: SKILLS_CLI_FILE_TYPE,
143
+ file_path: SKILLS_CLI_INSTALLED_PATH,
144
+ raw_content: payload,
145
+ };
98
146
  }
@@ -523,6 +523,19 @@ export function reportPostRestartVerificationOutcomes(violations) {
523
523
  });
524
524
  return { outcomes, reportPromises };
525
525
  }
526
+ /**
527
+ * Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
528
+ * user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
529
+ * to the server right away — the UI does not need to wait for another gate invocation.
530
+ */
531
+ export async function runPostApplyVerification(agent = 'cursor') {
532
+ const status = runLocalRemediationComplianceCheck(agent);
533
+ const { outcomes, reportPromises } = reportPostRestartVerificationOutcomes(status.violations);
534
+ if (outcomes.length > 0) {
535
+ await Promise.allSettled(reportPromises);
536
+ }
537
+ return outcomes;
538
+ }
526
539
  /**
527
540
  * Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
528
541
  * Clear pending verification locally and report verified so the next prompt does not POST
@@ -0,0 +1,372 @@
1
+ import { existsSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, appendFileSync, readFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
5
+ const COMPLIANCE_SUBDIR = 'compliance';
6
+ const TIER_NOOP = 'noop';
7
+ const TIER_SUCCESS = 'success';
8
+ const TIER_ERROR = 'error';
9
+ const TIER_IN_PROCESS = 'in_process';
10
+ const TIER_DIRS = [TIER_NOOP, TIER_SUCCESS, TIER_ERROR];
11
+ const NOOP_MAX_AGE_MS = 30 * 60 * 1000;
12
+ const SUCCESS_ERROR_MAX_AGE_MS = 24 * 60 * 60 * 1000;
13
+ /** In-flight logs in compliance/in_process/ before finalize. */
14
+ const INFLIGHT_MAX_AGE_MS = 2 * 60 * 60 * 1000;
15
+ const LATEST_POINTER = 'latest';
16
+ const MAX_SESSION_LOG_UPLOAD_BYTES = 5 * 1024 * 1024;
17
+ let activeComplianceLogPath = null;
18
+ export function parseComplianceLogArg(argv) {
19
+ const eq = argv.find((a) => a.startsWith('--compliance-log='));
20
+ if (eq) {
21
+ const v = eq.slice('--compliance-log='.length).trim();
22
+ return v || null;
23
+ }
24
+ const idx = argv.indexOf('--compliance-log');
25
+ if (idx >= 0 && argv[idx + 1]) {
26
+ const v = argv[idx + 1].trim();
27
+ return v || null;
28
+ }
29
+ return null;
30
+ }
31
+ export function isFinalizeComplianceSessionArgv(argv) {
32
+ return argv.includes('--finalize-compliance-session');
33
+ }
34
+ export function getComplianceLogDir() {
35
+ return path.join(homedir(), OPT_AI_SEC_MANAGEMENT_REL, COMPLIANCE_SUBDIR);
36
+ }
37
+ export function getActiveComplianceLogPath() {
38
+ return activeComplianceLogPath;
39
+ }
40
+ export function setActiveComplianceLogPath(logPath) {
41
+ activeComplianceLogPath = logPath ? path.resolve(logPath) : null;
42
+ }
43
+ function isTierDirName(name) {
44
+ return TIER_DIRS.includes(name);
45
+ }
46
+ function isManagedDirName(name) {
47
+ return isTierDirName(name) || name === TIER_IN_PROCESS;
48
+ }
49
+ function getInProcessComplianceLogDir() {
50
+ return path.join(getComplianceLogDir(), TIER_IN_PROCESS);
51
+ }
52
+ /**
53
+ * Convert legacy/root session paths into the explicit in_process/ location.
54
+ *
55
+ * Hooks may still pass ~/.../compliance/<timestamp>.log; future writes should live
56
+ * at ~/.../compliance/in_process/<timestamp>.log until finalize classifies them.
57
+ */
58
+ export function resolveComplianceSessionLogPath(logPath) {
59
+ const resolved = path.resolve(logPath);
60
+ if (path.dirname(resolved) === getComplianceLogDir()) {
61
+ return path.join(getInProcessComplianceLogDir(), path.basename(resolved));
62
+ }
63
+ return resolved;
64
+ }
65
+ /** Session log still in compliance/in_process/ (not yet finalized into a tier folder). */
66
+ export function isInflightComplianceLogPath(logPath) {
67
+ const resolved = resolveComplianceSessionLogPath(logPath);
68
+ return path.dirname(resolved) === getInProcessComplianceLogDir();
69
+ }
70
+ export function readLatestComplianceLogPointer() {
71
+ const pointer = path.join(getComplianceLogDir(), LATEST_POINTER);
72
+ if (!existsSync(pointer))
73
+ return null;
74
+ try {
75
+ const p = readFileSync(pointer, 'utf8').trim();
76
+ return p || null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ function pruneLogFilesInDir(dir, maxAgeMs, exceptPath) {
83
+ if (!existsSync(dir))
84
+ return 0;
85
+ const cutoff = Date.now() - maxAgeMs;
86
+ let removed = 0;
87
+ for (const name of readdirSync(dir)) {
88
+ if (!name.endsWith('.log'))
89
+ continue;
90
+ const full = path.join(dir, name);
91
+ if (exceptPath && full === exceptPath)
92
+ continue;
93
+ try {
94
+ const st = statSync(full);
95
+ if (st.mtimeMs < cutoff) {
96
+ unlinkSync(full);
97
+ removed += 1;
98
+ }
99
+ }
100
+ catch {
101
+ // best-effort
102
+ }
103
+ }
104
+ return removed;
105
+ }
106
+ export function pruneComplianceSessionLogs(exceptPath) {
107
+ const base = getComplianceLogDir();
108
+ const keep = exceptPath ? path.resolve(exceptPath) : null;
109
+ let removed = 0;
110
+ removed += pruneLogFilesInDir(path.join(base, TIER_NOOP), NOOP_MAX_AGE_MS, keep);
111
+ 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);
113
+ removed += pruneLogFilesInDir(path.join(base, TIER_IN_PROCESS), INFLIGHT_MAX_AGE_MS, keep);
114
+ if (!existsSync(base))
115
+ return removed;
116
+ for (const name of readdirSync(base)) {
117
+ if (name === LATEST_POINTER || isManagedDirName(name))
118
+ continue;
119
+ if (!name.endsWith('.log'))
120
+ continue;
121
+ const full = path.join(base, name);
122
+ if (keep && full === keep)
123
+ continue;
124
+ try {
125
+ const st = statSync(full);
126
+ if (st.mtimeMs < Date.now() - INFLIGHT_MAX_AGE_MS) {
127
+ unlinkSync(full);
128
+ removed += 1;
129
+ }
130
+ }
131
+ catch {
132
+ // best-effort
133
+ }
134
+ }
135
+ return removed;
136
+ }
137
+ /**
138
+ * Classify a compliance session for finalize + optional server upload.
139
+ *
140
+ * Only two outcomes are uploaded (success / error): remediation **succeeded** or **failed**.
141
+ * Policy violations, in-flight restart, and sync-only sessions are noop (local retention only).
142
+ */
143
+ export function classifyComplianceSessionLog(content) {
144
+ const text = content.trim();
145
+ if (!text)
146
+ return TIER_NOOP;
147
+ if (/remediation_tracking: verified uuid=/.test(text)) {
148
+ return TIER_SUCCESS;
149
+ }
150
+ const remediationFailedPatterns = [
151
+ /REMEDIATION APPLY FAILURE/,
152
+ /compliance_check_runner: uncaught error/,
153
+ /remediation_tracking: verification_failed uuid=/,
154
+ /post_restart_verification count=\d+ quarantined=[1-9]/,
155
+ /enforceRemediation.*failed/i,
156
+ ];
157
+ if (remediationFailedPatterns.some((p) => p.test(text))) {
158
+ return TIER_ERROR;
159
+ }
160
+ return TIER_NOOP;
161
+ }
162
+ /** Keep at compliance/ root until verify or remediation failure is recorded in the session. */
163
+ export function isRemediationCycleInflight(content) {
164
+ if (/remediation_tracking: verified uuid=/.test(content))
165
+ return false;
166
+ if (/remediation_tracking: verification_failed uuid=/.test(content))
167
+ return false;
168
+ if (/REMEDIATION APPLY FAILURE/.test(content))
169
+ return false;
170
+ if (/post_restart_verification count=\d+ quarantined=[1-9]/.test(content))
171
+ return false;
172
+ return /remediation_tracking: pending_post_restart_verify uuid=/.test(content);
173
+ }
174
+ function ensureComplianceLogDir() {
175
+ const dir = getComplianceLogDir();
176
+ if (!existsSync(dir))
177
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
178
+ for (const tier of [...TIER_DIRS, TIER_IN_PROCESS]) {
179
+ const sub = path.join(dir, tier);
180
+ if (!existsSync(sub))
181
+ mkdirSync(sub, { recursive: true, mode: 0o700 });
182
+ }
183
+ }
184
+ function writeLatestPointer(logPath) {
185
+ try {
186
+ ensureComplianceLogDir();
187
+ writeFileSync(path.join(getComplianceLogDir(), LATEST_POINTER), `${logPath}\n`, 'utf8');
188
+ }
189
+ catch {
190
+ // best-effort
191
+ }
192
+ }
193
+ function appendComplianceLine(logPath, line) {
194
+ try {
195
+ const dir = path.dirname(logPath);
196
+ if (!existsSync(dir))
197
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
198
+ appendFileSync(logPath, line, 'utf8');
199
+ }
200
+ catch {
201
+ // best-effort
202
+ }
203
+ }
204
+ export function complianceLogSessionBanner(logPath, label, mode) {
205
+ const resolved = path.resolve(logPath);
206
+ const ts = new Date().toISOString();
207
+ const banner = mode === 'replace'
208
+ ? `\n${'='.repeat(72)}\n${ts} session: ${label}\n${'='.repeat(72)}\n`
209
+ : `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
210
+ try {
211
+ const dir = path.dirname(resolved);
212
+ if (!existsSync(dir))
213
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
214
+ if (mode === 'replace') {
215
+ writeFileSync(resolved, banner, 'utf8');
216
+ }
217
+ else {
218
+ appendFileSync(resolved, banner, 'utf8');
219
+ }
220
+ writeLatestPointer(resolved);
221
+ }
222
+ catch {
223
+ // best-effort
224
+ }
225
+ }
226
+ /** Gate: prune old files, bind path, start or resume an inflight session file. */
227
+ export function initComplianceSessionForGate(logPath) {
228
+ const resolved = resolveComplianceSessionLogPath(logPath);
229
+ ensureComplianceLogDir();
230
+ const removed = pruneComplianceSessionLogs(resolved);
231
+ setActiveComplianceLogPath(resolved);
232
+ const resume = existsSync(resolved) && statSync(resolved).size > 0 && isInflightComplianceLogPath(resolved);
233
+ complianceLogSessionBanner(resolved, 'compliance_prompt_gate (before submit)', resume ? 'append' : 'replace');
234
+ if (!resume) {
235
+ writeLatestPointer(resolved);
236
+ }
237
+ if (removed > 0) {
238
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
239
+ }
240
+ return resolved;
241
+ }
242
+ /** Runner: prune old files, bind path, append runner section to the gate session file. */
243
+ export function initComplianceSessionForRunner(logPath) {
244
+ const resolved = resolveComplianceSessionLogPath(logPath);
245
+ ensureComplianceLogDir();
246
+ const removed = pruneComplianceSessionLogs(resolved);
247
+ setActiveComplianceLogPath(resolved);
248
+ complianceLogSessionBanner(resolved, 'compliance_check_runner (background sync + check)', 'append');
249
+ if (removed > 0) {
250
+ appendComplianceLine(resolved, `${new Date().toISOString()} compliance_session: pruned ${removed} log(s) (in_process 2h, noop 30m, success/error 24h)\n`);
251
+ }
252
+ return resolved;
253
+ }
254
+ /**
255
+ * Move session log from compliance/ root into noop|success|error/ and prune tiers.
256
+ * No-op if file missing or already finalized.
257
+ */
258
+ export function finalizeComplianceSessionLog(logPath, forcedTier) {
259
+ const resolved = resolveComplianceSessionLogPath(logPath);
260
+ if (!existsSync(resolved))
261
+ return null;
262
+ const parentName = path.basename(path.dirname(resolved));
263
+ if (isTierDirName(parentName)) {
264
+ return { tier: parentName, logPath: resolved, moved: false };
265
+ }
266
+ const content = readFileSync(resolved, 'utf8');
267
+ if (isRemediationCycleInflight(content)) {
268
+ return null;
269
+ }
270
+ const tier = forcedTier ?? classifyComplianceSessionLog(content);
271
+ ensureComplianceLogDir();
272
+ const dest = path.join(getComplianceLogDir(), tier, path.basename(resolved));
273
+ if (resolved !== dest) {
274
+ if (existsSync(dest))
275
+ unlinkSync(dest);
276
+ renameSync(resolved, dest);
277
+ }
278
+ setActiveComplianceLogPath(null);
279
+ writeLatestPointer(dest);
280
+ pruneComplianceSessionLogs(dest);
281
+ return { tier, logPath: dest, moved: true };
282
+ }
283
+ export function appendToActiveComplianceLog(message) {
284
+ const logPath = activeComplianceLogPath;
285
+ if (!logPath)
286
+ return;
287
+ try {
288
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
289
+ }
290
+ catch {
291
+ // best-effort
292
+ }
293
+ }
294
+ export function readComplianceSessionLog(logPath) {
295
+ try {
296
+ if (!existsSync(logPath))
297
+ return '';
298
+ return readFileSync(logPath, 'utf8');
299
+ }
300
+ catch {
301
+ return '';
302
+ }
303
+ }
304
+ function appendLineToSessionLogFile(logPath, message) {
305
+ try {
306
+ appendComplianceLine(logPath, `${new Date().toISOString()} ${message}\n`);
307
+ }
308
+ catch {
309
+ // best-effort
310
+ }
311
+ }
312
+ /**
313
+ * Upload full session log to the server (success and error tiers only, first finalize only).
314
+ */
315
+ export async function uploadComplianceSessionLogFromFinalize(result) {
316
+ if (result.tier !== TIER_SUCCESS && result.tier !== TIER_ERROR)
317
+ return;
318
+ if (!result.moved)
319
+ return;
320
+ const logText = readComplianceSessionLog(result.logPath);
321
+ if (!logText.trim())
322
+ return;
323
+ const logBytes = Buffer.byteLength(logText, 'utf8');
324
+ if (logBytes > MAX_SESSION_LOG_UPLOAD_BYTES) {
325
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: skipped (${logBytes} bytes exceeds ${MAX_SESSION_LOG_UPLOAD_BYTES})`);
326
+ return;
327
+ }
328
+ const { readStoredAuthKey } = await import('../auth/auth_key_store.js');
329
+ const { tryResolveHardwareUuid } = await import('./hardware_uuid.js');
330
+ const { loadEndpointBase } = await import('../sender/endpoint_config.js');
331
+ const { createSignature } = await import('../sender/signing.js');
332
+ const { buildApiUrl } = await import('../../endpoint_client/registry_api.js');
333
+ const { executeBody } = await import('../../endpoint_client/http_transport.js');
334
+ const authKey = readStoredAuthKey();
335
+ const hardwareUuid = tryResolveHardwareUuid();
336
+ if (!authKey || !hardwareUuid) {
337
+ appendLineToSessionLogFile(result.logPath, 'compliance_session_upload: skipped (auth key or hardware UUID unavailable)');
338
+ return;
339
+ }
340
+ const sessionFilename = path.basename(result.logPath);
341
+ const outcome = result.tier;
342
+ const signingPayload = {
343
+ hardware_uuid: hardwareUuid,
344
+ outcome,
345
+ session_filename: sessionFilename,
346
+ log_text: logText,
347
+ };
348
+ const signature = createSignature(signingPayload, authKey.key);
349
+ const body = JSON.stringify({ ...signingPayload, signature });
350
+ const url = buildApiUrl(loadEndpointBase(), '/endpoint_security/api/remediation-activity-log/');
351
+ try {
352
+ const { statusCode } = await executeBody(url, 'POST', body, 30_000);
353
+ if (statusCode === 200) {
354
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: ok outcome=${outcome} bytes=${logBytes}`);
355
+ }
356
+ else {
357
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: server status=${statusCode}`);
358
+ }
359
+ }
360
+ catch (err) {
361
+ const msg = err instanceof Error ? err.message : String(err);
362
+ appendLineToSessionLogFile(result.logPath, `compliance_session_upload: failed ${msg}`);
363
+ }
364
+ }
365
+ /** Finalize session file into success|error|noop tier, then upload when success or error. */
366
+ export async function finalizeAndUploadComplianceSessionLog(logPath, forcedTier) {
367
+ const result = finalizeComplianceSessionLog(logPath, forcedTier);
368
+ if (result) {
369
+ await uploadComplianceSessionLogFromFinalize(result);
370
+ }
371
+ return result;
372
+ }
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, appendFileSync, writeFileSync, statSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { OPT_AI_SEC_MANAGEMENT_REL } from '../../bootstrap_constants.js';
4
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
4
5
  const HOOK_LOG_FILENAME = 'hook_log.txt';
5
6
  const COMPLIANCE_RUNNER_LOG_FILENAME = 'compliance_runner.log';
6
7
  /** Append-only: remediation verify/apply failures (not cleared per hook session). */
@@ -30,6 +31,16 @@ function getRemediationApplyFailuresLogPath() {
30
31
  * `level` examples: INFO, RUNNER, RESTART, FAIL
31
32
  */
32
33
  function appendComplianceRunnerLine(level, message) {
34
+ const sessionPath = getActiveComplianceLogPath();
35
+ if (sessionPath) {
36
+ try {
37
+ appendFileSync(sessionPath, `${new Date().toISOString()} [${level}] pid=${process.pid} ${message}\n`, 'utf8');
38
+ }
39
+ catch {
40
+ // best-effort
41
+ }
42
+ return;
43
+ }
33
44
  const logPath = getComplianceRunnerLogPath();
34
45
  if (!logPath)
35
46
  return;
@@ -147,6 +158,18 @@ function hookLogSessionBanner(label) {
147
158
  * without replacing hook_log.txt.
148
159
  */
149
160
  function hookLogAppendSection(label) {
161
+ const compliancePath = getActiveComplianceLogPath();
162
+ if (compliancePath) {
163
+ try {
164
+ const ts = new Date().toISOString();
165
+ const banner = `\n${'-'.repeat(72)}\n${ts} section: ${label}\n${'-'.repeat(72)}\n`;
166
+ appendFileSync(compliancePath, banner, 'utf8');
167
+ }
168
+ catch {
169
+ // best-effort
170
+ }
171
+ return;
172
+ }
150
173
  const logPath = getHookLogPath();
151
174
  if (!logPath)
152
175
  return;
@@ -167,8 +190,19 @@ function hookLogAppendSection(label) {
167
190
  function hookLogReplace() {
168
191
  hookLogSessionBanner('log_config_files (config upload)');
169
192
  }
170
- /** Append a timestamped line to hook_log.txt. */
193
+ /** Append a timestamped line to hook_log.txt or the active compliance session log. */
171
194
  function hookRunLog(message) {
195
+ const compliancePath = getActiveComplianceLogPath();
196
+ if (compliancePath) {
197
+ try {
198
+ const ts = new Date().toISOString();
199
+ appendFileSync(compliancePath, `${ts} ${message}\n`, 'utf8');
200
+ }
201
+ catch {
202
+ // best-effort
203
+ }
204
+ return;
205
+ }
172
206
  const logPath = getHookLogPath();
173
207
  if (!logPath)
174
208
  return;
@@ -58,6 +58,10 @@ export function clearRemediationApplyQuarantine(uuid) {
58
58
  writeRemediationApplyTrackingFile(file);
59
59
  hookRunLog(`remediation_tracking: quarantine_cleared uuid=${uuid}`);
60
60
  }
61
+ export function hasPendingPostRestartVerification() {
62
+ const file = readRemediationApplyTrackingFile();
63
+ return Object.values(file.entries).some((e) => e.pending_post_restart_verify === true);
64
+ }
61
65
  export function markRemediationApplyPendingVerification(uuid) {
62
66
  const file = readRemediationApplyTrackingFile();
63
67
  const prev = file.entries[uuid] ?? defaultEntry();
@@ -1460,7 +1460,10 @@ export function reportAutofixApplied(remediationUuid, result, details) {
1460
1460
  const body = JSON.stringify({ ...payload, signature });
1461
1461
  return executeBody(url, 'POST', body, 8000)
1462
1462
  .then(({ statusCode }) => {
1463
- if (statusCode !== 200) {
1463
+ if (statusCode === 200) {
1464
+ hookRunLog(`autofix_report: ok result=${result} uuid=${remediationUuid}`);
1465
+ }
1466
+ else {
1464
1467
  hookRunLog(`autofix_report: server returned ${statusCode} for uuid=${remediationUuid}`);
1465
1468
  }
1466
1469
  })
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
6
  import { appendComplianceRunnerLine, hookRunLog } from './hook_logger.js';
7
+ import { getActiveComplianceLogPath } from './compliance_session_log.js';
7
8
  import { getDeferredVscdbRestartLogPath } from './management_storage.js';
8
9
  import { normalizeAgentToken } from './compliance_check.js';
9
10
  import { isClaudeRestartCommand, isCursorRestartCommand, isTrustedRestartCommandForAutofix } from './remediation_sync.js';
@@ -88,6 +89,7 @@ export function executeTrustedRestartCommands(commands) {
88
89
  appendComplianceRunnerLine('RESTART', 'spawn trusted_restart sh -c (non-deferred)');
89
90
  }
90
91
  const projectRoot = resolveProjectRoot();
92
+ const complianceLogPath = getActiveComplianceLogPath();
91
93
  const child = spawn('sh', ['-c', t], {
92
94
  detached: true,
93
95
  stdio: 'ignore',
@@ -95,6 +97,7 @@ export function executeTrustedRestartCommands(commands) {
95
97
  env: envForRestartSpawn({
96
98
  ...process.env,
97
99
  CURSOR_PROJECT_DIR: process.env.CURSOR_PROJECT_DIR || projectRoot,
100
+ ...(complianceLogPath ? { OPTIMUS_COMPLIANCE_LOG: complianceLogPath } : {}),
98
101
  PATH: process.env.PATH && String(process.env.PATH).trim().length > 0
99
102
  ? process.env.PATH
100
103
  : FALLBACK_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "log-llm-config-staging",
3
- "version": "1.4.5",
3
+ "version": "1.4.8",
4
4
  "description": "CLI helpers for logging hardware UUIDs and posting startup payloads to Optimus Security.",
5
5
  "type": "module",
6
6
  "bin": {