log-llm-config 1.4.4 → 1.4.9
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 +34 -5
- package/dist/compliance_check_runner.js +37 -3
- package/dist/compliance_prompt_gate.js +55 -23
- package/dist/execute_trusted_restarts.js +5 -0
- package/dist/log_config_files/collection/skills_cli_collector.js +91 -43
- package/dist/log_config_files/paths/pattern_resolver.js +8 -0
- package/dist/log_config_files/runtime/compliance_check.js +66 -3
- package/dist/log_config_files/runtime/compliance_session_log.js +372 -0
- package/dist/log_config_files/runtime/hook_logger.js +69 -12
- package/dist/log_config_files/runtime/main_runner.js +3 -2
- package/dist/log_config_files/runtime/remediation_apply_tracking.js +4 -0
- package/dist/log_config_files/runtime/remediation_sync.js +4 -1
- package/dist/log_config_files/runtime/secret_regex_scan.js +126 -0
- package/dist/log_config_files/runtime/secretlint_scan.js +69 -0
- package/dist/log_config_files/runtime/trusted_restarts.js +4 -1
- package/dist/log_config_files/sender/batch_sender.js +3 -1
- package/package.json +1 -1
|
@@ -1,9 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
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) =>
|
|
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,11 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { complianceRunnerRunnerLine, hookLogAppendSection } from './log_config_files/runtime/hook_logger.js';
|
|
3
|
-
import { runComplianceCheck } from './log_config_files/runtime/compliance_check.js';
|
|
3
|
+
import { normalizeAgentToken, 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';
|
|
6
|
+
function parseAgentArg() {
|
|
7
|
+
const eq = process.argv.find((a) => a.startsWith('--agent='));
|
|
8
|
+
if (!eq)
|
|
9
|
+
return undefined;
|
|
10
|
+
const normalized = normalizeAgentToken(eq.slice('--agent='.length));
|
|
11
|
+
return normalized ? normalized : undefined;
|
|
12
|
+
}
|
|
4
13
|
(async () => {
|
|
5
|
-
|
|
14
|
+
let complianceLogPath = parseComplianceLogArg(process.argv);
|
|
15
|
+
if (isFinalizeComplianceSessionArgv(process.argv)) {
|
|
16
|
+
if (complianceLogPath) {
|
|
17
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
18
|
+
}
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
if (complianceLogPath) {
|
|
22
|
+
const resolved = resolveComplianceSessionLogPath(complianceLogPath);
|
|
23
|
+
if (existsSync(resolved) && isInflightComplianceLogPath(resolved)) {
|
|
24
|
+
complianceLogPath = initComplianceSessionForRunner(resolved);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Gate may have finalized+moved the session before this background runner started.
|
|
28
|
+
complianceLogPath = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
hookLogAppendSection('compliance_check_runner (background sync + check)');
|
|
33
|
+
}
|
|
6
34
|
complianceRunnerRunnerLine('compliance_check_runner: start');
|
|
7
35
|
try {
|
|
8
|
-
await runComplianceCheck();
|
|
36
|
+
await runComplianceCheck(parseAgentArg());
|
|
9
37
|
complianceRunnerRunnerLine('compliance_check_runner: finished ok');
|
|
10
38
|
}
|
|
11
39
|
catch (err) {
|
|
@@ -13,6 +41,12 @@ import { runComplianceCheck } from './log_config_files/runtime/compliance_check.
|
|
|
13
41
|
complianceRunnerRunnerLine(`compliance_check_runner: uncaught error — ${err instanceof Error ? err.message : String(err)}`);
|
|
14
42
|
complianceRunnerRunnerLine(`compliance_check_runner: stack_or_detail ${detail.slice(0, 4000)}`);
|
|
15
43
|
process.stderr.write(`compliance_check_runner error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
44
|
+
if (complianceLogPath) {
|
|
45
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath, 'error');
|
|
46
|
+
}
|
|
16
47
|
process.exit(1);
|
|
17
48
|
}
|
|
49
|
+
if (complianceLogPath) {
|
|
50
|
+
await finalizeAndUploadComplianceSessionLog(complianceLogPath);
|
|
51
|
+
}
|
|
18
52
|
})();
|
|
@@ -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,17 +20,34 @@ 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) {
|
|
25
37
|
const v = eq.slice('--ide='.length).toLowerCase();
|
|
26
|
-
|
|
38
|
+
if (v === 'claude')
|
|
39
|
+
return 'claude';
|
|
40
|
+
if (v === 'copilot')
|
|
41
|
+
return 'copilot';
|
|
42
|
+
return 'cursor';
|
|
27
43
|
}
|
|
28
44
|
if (process.argv.includes('--claude'))
|
|
29
45
|
return 'claude';
|
|
30
46
|
return 'cursor';
|
|
31
47
|
}
|
|
32
48
|
function defaultAgentFromIde(ide) {
|
|
49
|
+
if (ide === 'copilot')
|
|
50
|
+
return 'copilot';
|
|
33
51
|
return ide === 'claude' ? 'claude' : 'cursor';
|
|
34
52
|
}
|
|
35
53
|
function parseAgent(ide) {
|
|
@@ -42,13 +60,13 @@ function parseAgent(ide) {
|
|
|
42
60
|
return defaultAgentFromIde(ide);
|
|
43
61
|
}
|
|
44
62
|
function printAllow(ide) {
|
|
45
|
-
if (ide === 'claude')
|
|
63
|
+
if (ide === 'claude' || ide === 'copilot')
|
|
46
64
|
console.log('{}');
|
|
47
65
|
else
|
|
48
66
|
console.log(JSON.stringify({ continue: true }));
|
|
49
67
|
}
|
|
50
68
|
function printAllowWithAdvisory(ide, advisoryMessage) {
|
|
51
|
-
if (ide === 'claude') {
|
|
69
|
+
if (ide === 'claude' || ide === 'copilot') {
|
|
52
70
|
console.log(JSON.stringify({ __optimus_advisory: true, advisory_message: advisoryMessage }));
|
|
53
71
|
}
|
|
54
72
|
else {
|
|
@@ -62,7 +80,7 @@ function printAllowWithAdvisory(ide, advisoryMessage) {
|
|
|
62
80
|
function blockPayload(ide, violationMessage) {
|
|
63
81
|
const prefix = 'Prompt blocked by Optimus: ';
|
|
64
82
|
const text = prefix + violationMessage;
|
|
65
|
-
if (ide === 'claude') {
|
|
83
|
+
if (ide === 'claude' || ide === 'copilot') {
|
|
66
84
|
return JSON.stringify({ decision: 'block', reason: text, systemMessage: text });
|
|
67
85
|
}
|
|
68
86
|
return JSON.stringify({ continue: false, user_message: text });
|
|
@@ -116,6 +134,10 @@ function formatPreventiveAutofixDialog(appliedViolations, applyLine) {
|
|
|
116
134
|
export function formatClaudeAutofixDialog(appliedViolations) {
|
|
117
135
|
return formatPreventiveAutofixDialog(appliedViolations, 'Claude will now apply this policy to your environment.');
|
|
118
136
|
}
|
|
137
|
+
/** Copilot dialog after enforced/preventive remediation is applied locally (no restart). */
|
|
138
|
+
export function formatCopilotAutofixDialog(appliedViolations) {
|
|
139
|
+
return formatPreventiveAutofixDialog(appliedViolations, 'Copilot will now apply this policy to your environment.');
|
|
140
|
+
}
|
|
119
141
|
/** Cursor restart dialog after enforced/preventive remediation is applied locally. */
|
|
120
142
|
export function formatCursorRestartAutofixDialog(appliedViolations) {
|
|
121
143
|
return formatPreventiveAutofixDialog(appliedViolations, 'Cursor will now restart to apply this policy, and your context will be retained.');
|
|
@@ -164,10 +186,23 @@ export async function runCompliancePromptGateCli() {
|
|
|
164
186
|
export async function runCompliancePromptGate() {
|
|
165
187
|
const ide = parseIde();
|
|
166
188
|
const agent = parseAgent(ide);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
let complianceLogPath = parseComplianceLogArg(process.argv);
|
|
190
|
+
if (complianceLogPath) {
|
|
191
|
+
complianceLogPath = initComplianceSessionForGate(complianceLogPath);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
hookLogSessionBanner('compliance_prompt_gate (before submit)');
|
|
195
|
+
}
|
|
196
|
+
let status = runLocalRemediationComplianceCheck(agent);
|
|
197
|
+
// Post-restart verify + server reports BEFORE sync so a manifest UUID swap cannot delay
|
|
198
|
+
// verified / activity-log until the following prompt.
|
|
199
|
+
const postRestartVerify = reportPostRestartVerificationOutcomes(status.violations);
|
|
200
|
+
if (postRestartVerify.outcomes.length > 0) {
|
|
201
|
+
await Promise.allSettled(postRestartVerify.reportPromises);
|
|
202
|
+
await flushRemediationOutcomeToServer(complianceLogPath);
|
|
203
|
+
hookRunLog(`compliance_prompt_gate: post_restart_verification count=${postRestartVerify.outcomes.length} quarantined=${postRestartVerify.outcomes.filter((o) => o.status === 'quarantined').length}`);
|
|
204
|
+
status = runLocalRemediationComplianceCheck(agent);
|
|
205
|
+
}
|
|
171
206
|
const hw = tryResolveHardwareUuid();
|
|
172
207
|
if (hw) {
|
|
173
208
|
try {
|
|
@@ -180,13 +215,7 @@ export async function runCompliancePromptGate() {
|
|
|
180
215
|
// Network or auth failure — fall back to local file state.
|
|
181
216
|
}
|
|
182
217
|
}
|
|
183
|
-
|
|
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
|
-
}
|
|
218
|
+
status = runLocalRemediationComplianceCheck(agent);
|
|
190
219
|
// Secondary-satisfied: primary checks failed but settings.json (or equiv) has the fix.
|
|
191
220
|
// Upload those files fire-and-forget so the backend can resolve the finding immediately.
|
|
192
221
|
for (const e of status.secondarySatisfied ?? []) {
|
|
@@ -238,15 +267,15 @@ export async function runCompliancePromptGate() {
|
|
|
238
267
|
const recheck = runLocalRemediationComplianceCheck(agent);
|
|
239
268
|
const recheckOk = recheck.status === 'ok' || recheck.violations.length === 0;
|
|
240
269
|
// Cursor: tolerate a failing recheck only when SQLite updates are deferred (apply after restart).
|
|
241
|
-
// Claude
|
|
242
|
-
// in-process recheck red for the same UUID — allow in that case
|
|
270
|
+
// Claude / Copilot: JSON remediations are written immediately; merge/verify timing can still leave
|
|
271
|
+
// the in-process recheck red for the same UUID — allow in that case for immediate JSON agents.
|
|
243
272
|
const appliedUuids = new Set(appliedViolations.map((v) => v.uuid));
|
|
244
|
-
const claudeRecheckStaleAfterImmediateApply = ide === 'claude' &&
|
|
273
|
+
const claudeRecheckStaleAfterImmediateApply = (ide === 'claude' || ide === 'copilot') &&
|
|
245
274
|
!recheckOk &&
|
|
246
275
|
recheck.violations.length > 0 &&
|
|
247
276
|
recheck.violations.every((v) => appliedUuids.has(v.uuid));
|
|
248
277
|
if (claudeRecheckStaleAfterImmediateApply) {
|
|
249
|
-
hookRunLog(
|
|
278
|
+
hookRunLog(`compliance_prompt_gate: ${ide} — autofix wrote JSON; recheck still flags same UUID(s) — proceeding (immediate apply)`);
|
|
250
279
|
}
|
|
251
280
|
if (deferredSqlitePending || recheckOk || claudeRecheckStaleAfterImmediateApply) {
|
|
252
281
|
const immediateVerified = restartCommands.length === 0 &&
|
|
@@ -255,6 +284,7 @@ export async function runCompliancePromptGate() {
|
|
|
255
284
|
if (immediateVerified) {
|
|
256
285
|
confirmAppliedAutofixVerified(appliedViolations, reportPromises);
|
|
257
286
|
await Promise.allSettled(reportPromises);
|
|
287
|
+
await flushRemediationOutcomeToServer(complianceLogPath);
|
|
258
288
|
}
|
|
259
289
|
const changePreview = formatRemediationChangePreviewForApplied(appliedViolations);
|
|
260
290
|
const changePreviewSuffix = changePreview ? `\n\n${changePreview}` : '';
|
|
@@ -262,9 +292,11 @@ export async function runCompliancePromptGate() {
|
|
|
262
292
|
? formatCursorRestartAutofixDialog(appliedViolations)
|
|
263
293
|
: ide === 'claude'
|
|
264
294
|
? formatClaudeAutofixDialog(appliedViolations)
|
|
265
|
-
:
|
|
266
|
-
|
|
267
|
-
|
|
295
|
+
: ide === 'copilot'
|
|
296
|
+
? formatCopilotAutofixDialog(appliedViolations)
|
|
297
|
+
: `Optimus Labs auto-fixed ${fixed} ${fixed === 1 ? 'policy violation' : 'policy violations'}:\n\n${appliedViolations
|
|
298
|
+
.map((v) => autofixDialogLine(v))
|
|
299
|
+
.join('\n')}${changePreviewSuffix}`;
|
|
268
300
|
const payload = { __optimus_autofix: true, autofix_message: autofixMessage };
|
|
269
301
|
if (restartCommands.length > 0)
|
|
270
302
|
payload.restart_commands = restartCommands;
|
|
@@ -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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
}
|
|
@@ -293,6 +293,14 @@ function resolvePatternToTargets(pathPattern, pathType, fileType, projectRoot, h
|
|
|
293
293
|
targets.push({ path: basePath + suffix, file_type: fileType, content_format: contentFormat ?? 'extensions_cache' });
|
|
294
294
|
return targets;
|
|
295
295
|
}
|
|
296
|
+
if (spec?.type === 'env_dir_file') {
|
|
297
|
+
const configuredDir = process.env[spec.env_var]?.trim();
|
|
298
|
+
const baseDir = configuredDir
|
|
299
|
+
? configuredDir.replace(/^~\//, `${home}/`)
|
|
300
|
+
: join(home, spec.fallback_under_home.replace(/^~\//, ''));
|
|
301
|
+
pushTargetPaths(targets, join(baseDir, spec.filename), fileType, false, collectStyle, contentFormat, dirGlob);
|
|
302
|
+
return targets;
|
|
303
|
+
}
|
|
296
304
|
if (norm.includes('**'))
|
|
297
305
|
return expandRecursiveGlobPathPattern(pathPattern, fileType, home, contentFormat, dirGlob, homeRecurseSkipDirs);
|
|
298
306
|
if (norm.includes('*'))
|
|
@@ -19,6 +19,7 @@ import { resolveRemediationConfigPath } from './remediation_config_path.js';
|
|
|
19
19
|
import { resolveOpsTargetPath } from './ops_target_path.js';
|
|
20
20
|
import { isRemediationQuarantined, markRemediationApplyPendingVerification, markRemediationApplyVerified, processPendingPostRestartVerifications, readRemediationApplyTrackingFile, writeRemediationApplyTrackingFile, } from './remediation_apply_tracking.js';
|
|
21
21
|
import { complianceRunnerDiag, hookRunLog, logRemediationApplyFailure } from './hook_logger.js';
|
|
22
|
+
import { isEnvBackedSecretValue, scanJsonForHardcodedSecrets, } from './secret_regex_scan.js';
|
|
22
23
|
import { loadEndpointBase } from '../sender/endpoint_config.js';
|
|
23
24
|
import { resolveHardwareUuid, tryResolveHardwareUuid } from './hardware_uuid.js';
|
|
24
25
|
import { buildDeferredCursorRestartCommand, discoverAllWorkspaceVscdbs, enforceRemediation, fetchSync, isTrustedRestartCommandForAutofix, remediationFixSpec, reportAutofixApplied, syncRemediations, } from './remediation_sync.js';
|
|
@@ -188,6 +189,36 @@ function allowlistEntryMatchesRemoveToken(entry, token) {
|
|
|
188
189
|
const pattern = new RegExp(`(^|[^a-z0-9_|])${escapeRegExp(t)}([^a-z0-9_|]|$)`);
|
|
189
190
|
return pattern.test(valStr);
|
|
190
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* MCP secret remediations deploy ops.set to ${env:KEY}. Local compliance passes when the
|
|
194
|
+
* user picks any env reference, not only the exact string from the manifest.
|
|
195
|
+
*/
|
|
196
|
+
function remediationSetOpSatisfied(current, expected) {
|
|
197
|
+
if (deepEqual(current, expected))
|
|
198
|
+
return true;
|
|
199
|
+
if (typeof expected === 'string' &&
|
|
200
|
+
isEnvBackedSecretValue(expected) &&
|
|
201
|
+
isEnvBackedSecretValue(current)) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
function violationFromSecretScanFinding(entry, compliance, finding) {
|
|
207
|
+
return {
|
|
208
|
+
uuid: entry.uuid,
|
|
209
|
+
finding_formatted_id: compliance.finding_formatted_id,
|
|
210
|
+
setting_path: finding.path,
|
|
211
|
+
description: compliance.description,
|
|
212
|
+
finding_title: entry.finding_title,
|
|
213
|
+
finding_description: entry.finding_description,
|
|
214
|
+
policy_name: entry.policy_name,
|
|
215
|
+
severity: compliance.severity,
|
|
216
|
+
autofix_allowed: compliance.autofix_allowed,
|
|
217
|
+
config_file_path: entry.config_file_path,
|
|
218
|
+
expected_value: { op: 'secret_scan', path: finding.path, secret_type: finding.secretType },
|
|
219
|
+
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}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
191
222
|
function verifyOpsApplied(configJson, settingPath, ops) {
|
|
192
223
|
const parts = settingPath.split('.');
|
|
193
224
|
const leafKey = parts[parts.length - 1] ?? '';
|
|
@@ -201,8 +232,9 @@ function verifyOpsApplied(configJson, settingPath, ops) {
|
|
|
201
232
|
if (Object.prototype.hasOwnProperty.call(set, k)) {
|
|
202
233
|
const cur = getByPath(configJson, targetPath);
|
|
203
234
|
const expected = set[k];
|
|
204
|
-
if (!
|
|
235
|
+
if (!remediationSetOpSatisfied(cur, expected)) {
|
|
205
236
|
return { ok: false, expected: { op: 'set', path: targetPath, value: expected } };
|
|
237
|
+
}
|
|
206
238
|
continue;
|
|
207
239
|
}
|
|
208
240
|
const cur = getByPath(configJson, targetPath);
|
|
@@ -319,6 +351,14 @@ export function evaluateManifestEntryCompliance(entry) {
|
|
|
319
351
|
if (!loaded.ok)
|
|
320
352
|
return { violations: [] };
|
|
321
353
|
const configJson = loaded.json;
|
|
354
|
+
if (compliance.requires_secret_scan === true) {
|
|
355
|
+
const secretFindings = scanJsonForHardcodedSecrets(configJson);
|
|
356
|
+
return secretFindings.length === 0
|
|
357
|
+
? { violations: [] }
|
|
358
|
+
: {
|
|
359
|
+
violations: secretFindings.map((f) => violationFromSecretScanFinding(entry, compliance, f)),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
322
362
|
const entryViolations = [];
|
|
323
363
|
for (const check of checks) {
|
|
324
364
|
const effectivePath = canonicalComplianceSettingPath(entry.config_file_path, check);
|
|
@@ -523,6 +563,19 @@ export function reportPostRestartVerificationOutcomes(violations) {
|
|
|
523
563
|
});
|
|
524
564
|
return { outcomes, reportPromises };
|
|
525
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Run immediately after a deferred state.vscdb write lands on disk (post-restart, before the
|
|
568
|
+
* user's next prompt) so `pending_post_restart_verify` remediations are confirmed and reported
|
|
569
|
+
* to the server right away — the UI does not need to wait for another gate invocation.
|
|
570
|
+
*/
|
|
571
|
+
export async function runPostApplyVerification(agent = 'cursor') {
|
|
572
|
+
const status = runLocalRemediationComplianceCheck(agent);
|
|
573
|
+
const { outcomes, reportPromises } = reportPostRestartVerificationOutcomes(status.violations);
|
|
574
|
+
if (outcomes.length > 0) {
|
|
575
|
+
await Promise.allSettled(reportPromises);
|
|
576
|
+
}
|
|
577
|
+
return outcomes;
|
|
578
|
+
}
|
|
526
579
|
/**
|
|
527
580
|
* Immediate autofix succeeded (inline recheck OK or Claude stale-recheck tolerance).
|
|
528
581
|
* Clear pending verification locally and report verified so the next prompt does not POST
|
|
@@ -746,6 +799,16 @@ export function pruneSatisfiedOneTimeRemediations(agent = 'cursor') {
|
|
|
746
799
|
continue;
|
|
747
800
|
}
|
|
748
801
|
const configJson = prLoaded.json;
|
|
802
|
+
if (spec?.requires_secret_scan === true) {
|
|
803
|
+
if (scanJsonForHardcodedSecrets(configJson).length === 0) {
|
|
804
|
+
removed++;
|
|
805
|
+
hookRunLog(`remediation_prune: satisfied secret-scan uuid=${inst.uuid} path=${inst.config_file_path}`);
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
remaining.push(raw);
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
749
812
|
// Only prune when every check is ops-based and currently satisfied.
|
|
750
813
|
let okAll = true;
|
|
751
814
|
for (const check of checks) {
|
|
@@ -880,8 +943,8 @@ export function uploadSatisfiedManifestConfigs(agent = 'cursor') {
|
|
|
880
943
|
* Apply (autofix) is intentionally deferred to the gate on the next prompt — this pass only downloads
|
|
881
944
|
* a fresh manifest so the gate has up-to-date data when it runs.
|
|
882
945
|
*/
|
|
883
|
-
export async function runComplianceCheck() {
|
|
884
|
-
const agent = currentAgentFromEnv();
|
|
946
|
+
export async function runComplianceCheck(agentOverride) {
|
|
947
|
+
const agent = agentOverride ?? currentAgentFromEnv();
|
|
885
948
|
try {
|
|
886
949
|
await syncRemediations(loadEndpointBase(), resolveHardwareUuid());
|
|
887
950
|
}
|